Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add oci registry plugin #2276

Draft
wants to merge 8 commits into
base: 4.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.reposilite.packages


interface PackageRepository {


}

This file was deleted.

Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright (c) 2023 dzikoysk
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.reposilite.packages.oci

import com.reposilite.journalist.Journalist
import com.reposilite.journalist.Logger
import com.reposilite.packages.oci.api.BlobResponse
import com.reposilite.packages.oci.api.ManifestResponse
import com.reposilite.packages.oci.api.SaveManifestRequest
import com.reposilite.packages.oci.api.UploadState
import com.reposilite.plugin.api.Facade
import com.reposilite.shared.ErrorResponse
import com.reposilite.shared.badRequestError
import com.reposilite.shared.notFound
import com.reposilite.shared.notFoundError
import com.reposilite.storage.StorageProvider
import com.reposilite.storage.api.toLocation
import panda.std.Result
import panda.std.asSuccess
import java.security.MessageDigest
import java.util.*

class OciFacade(
private val journalist: Journalist,
private val storageProvider: StorageProvider,
private val ociRepositoryProvider: OciRepositoryProvider
) : Journalist, Facade {

private val sessions = mutableMapOf<String, UploadState>()
private val sha256Hash = MessageDigest.getInstance("SHA-256")

fun saveManifest(namespace: String, digest: String, saveManifestRequest: SaveManifestRequest): Result<ManifestResponse, ErrorResponse> {
storageProvider.putFile("manifests/${namespace}/${digest}".toLocation(), saveManifestRequest.toString().toByteArray().inputStream())
return saveManifestRequest.let { ManifestResponse(it.schemaVersion, it.mediaType, it.config, it.layers) }.asSuccess()
}

fun saveTaggedManifest(namespace: String, tag: String, saveManifestRequest: SaveManifestRequest): Result<ManifestResponse, ErrorResponse> {
val digest = sha256Hash.digest(saveManifestRequest.toString().toByteArray()).joinToString("") { "%02x".format(it) }

storageProvider.putFile("manifests/${namespace}/${tag}/manifest".toLocation(), saveManifestRequest.toString().toByteArray().inputStream())
storageProvider.putFile("manifests/${namespace}/${tag}/manifest.sha256".toLocation(), digest.toByteArray().inputStream())
return saveManifestRequest.let { ManifestResponse(it.schemaVersion, it.mediaType, it.config, it.layers) }.asSuccess()
}

fun retrieveBlobUploadSessionId(namespace: String): Result<String, ErrorResponse> {
val sessionId = UUID.randomUUID().toString()

sessions[sessionId] = UploadState(
sessionId = sessionId,
name = namespace,
uploadedData = ByteArray(0),
bytesReceived = 0,
createdAt = System.currentTimeMillis().toString()
)

return sessionId.asSuccess()
}

fun uploadBlobStreamPart(sessionId: String, part: ByteArray): Result<UploadState, ErrorResponse> {
val session = sessions[sessionId] ?: return notFoundError("Session not found")

session.uploadedData += part
session.bytesReceived += part.size

return session.asSuccess()
}

fun finalizeBlobUpload(namespace: String, digest: String, sessionId: String, lastPart: ByteArray?): Result<BlobResponse, ErrorResponse> {
val session = sessions[sessionId] ?: return notFoundError("Session not found")

if (lastPart != null) {
session.bytesReceived += lastPart.size
}

storageProvider.putFile("blobs/$namespace/$digest".toLocation(), session.uploadedData.inputStream())

sessions.remove(sessionId)

return findBlobByDigest(namespace, digest)
}

fun findBlobByDigest(namespace: String, digest: String): Result<BlobResponse, ErrorResponse> =
storageProvider.getFile("blobs/${namespace}/${digest}".toLocation())
.map {
BlobResponse(
digest = digest,
length = it.available(),
content = it
)
}
.mapErr { notFound("Could not find blob with specified digest") }

fun findManifestChecksumByDigest(namespace: String, digest: String): Result<String, ErrorResponse> {
val location = "manifests/${namespace}/${digest}".toLocation()
return storageProvider.getFile(location)
.map { it.readAllBytes().joinToString("") { "%02x".format(it) } }
}

fun findManifestChecksumByTag(namespace: String, tag: String): Result<String, ErrorResponse> {
val location = "manifests/${namespace}/${tag}/manifest.sha256".toLocation()

return storageProvider.getFile(location)
.map { it.readAllBytes().joinToString("") { "%02x".format(it) } }
}

fun findManifestTagByDigest(namespace: String, digest: String): Result<String, ErrorResponse> {
val tagsDirectory = "manifests/${namespace}".toLocation()

// todo replace with exposed (digest to tag mapping)
return storageProvider.getFiles(tagsDirectory)
.flatMap { files ->
files
.map { storageProvider.getFile(it.resolve("manifest.sha256")) }
.map { it.map { it.readAllBytes().joinToString("") { "%02x".format(it) } } }
.first()
}
}

fun validateDigest(digest: String): Result<String, ErrorResponse> {
if (!digest.startsWith("sha256:")) {
return badRequestError("Invalid digest format")
}

return digest.asSuccess()
}

fun getRepositories(): Collection<OciRepository> =
ociRepositoryProvider.getRepositories()

override fun getLogger(): Logger =
journalist.logger

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.reposilite.packages.oci

data class OciRepository(
val name: String,
val type: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.reposilite.packages.oci

class OciRepositoryProvider {

private val repositories = mutableMapOf<String, OciRepository>()

fun getRepositories(): Collection<OciRepository> = repositories.values

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.reposilite.packages.oci.api

import java.io.InputStream

data class BlobResponse(
val length: Int,
val content: InputStream,
val digest: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 dzikoysk
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.reposilite.packages.oci.api

data class SaveManifestRequest(
val schemaVersion: Int,
val mediaType: String,
val config: ManifestConfig,
val layers: List<ManifestLayer>,
)

data class ManifestResponse(
val schemaVersion: Int,
val mediaType: String,
val config: ManifestConfig,
val layers: List<ManifestLayer>,
)

data class ManifestConfig(
val mediaType: String,
val size: Int,
val digest: String,
)

data class ManifestLayer(
val mediaType: String,
val size: Int,
val digest: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.reposilite.packages.oci.api

data class UploadState(
val sessionId: String,
val name: String,
var uploadedData: ByteArray,
var bytesReceived: Int,
val createdAt: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2023 dzikoysk
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.reposilite.packages.oci.application

import com.reposilite.packages.oci.OciFacade
import com.reposilite.packages.oci.OciRepositoryProvider
import com.reposilite.packages.oci.infrastructure.OciEndpoints
import com.reposilite.plugin.api.Plugin
import com.reposilite.plugin.api.ReposilitePlugin
import com.reposilite.plugin.event
import com.reposilite.plugin.facade
import com.reposilite.token.infrastructure.AccessTokenApiEndpoints
import com.reposilite.web.api.RoutingSetupEvent

@Plugin(
name = "oci",
dependencies = ["failure", "local-configuration", "shared-configuration", "statistics", "authentication", "access-token", "storage"]
)
internal class OciPlugin : ReposilitePlugin() {

override fun initialize(): OciFacade {
val ociRepositoryProvider = OciRepositoryProvider()

val ociFacade = OciFacade(
journalist = this,
storageProvider = facade(),
ociRepositoryProvider = ociRepositoryProvider
)

// register endpoints
event { event: RoutingSetupEvent ->
ociFacade.getRepositories().forEach {
when (it.type) {
"oci" -> {
val ociEndpoints = OciEndpoints(ociFacade)

event.register(ociEndpoints.saveManifest(it.name))
event.register(ociEndpoints.retrieveBlobUploadSessionId(it.name))
event.register(ociEndpoints.findManifestChecksumByReference(it.name))
event.register(ociEndpoints.findBlobByDigest(it.name))
event.register(ociEndpoints.uploadBlobStreamPart(it.name))
event.register(ociEndpoints.finalizeBlobUpload(it.name))
}
}
}
}

return ociFacade
}

}
Loading
Loading