diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..849f79e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 410df31..16b3022 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,7 +28,8 @@ "adm-zip": "^0.5.10", "colorette": "^2.0.19", "react-scripts": "5.0.1", - "ts-node": "^10.9.1" + "ts-node": "^10.9.1", + "wasm-oci": "^0.1.3" } }, "node_modules/@adobe/css-tools": { @@ -15551,6 +15552,15 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -15611,6 +15621,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-rest-client": { + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.9.tgz", + "integrity": "sha512-uSmjE38B80wjL85UFX3sTYEUlvZ1JgCRhsWj/fJ4rZ0FqDUFoIuodtiVeE+cUqiVTOKPdKrp/sdftD15MDek6g==", + "dev": true, + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -15647,6 +15668,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "dev": true + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -15883,6 +15910,15 @@ "makeerror": "1.0.12" } }, + "node_modules/wasm-oci": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/wasm-oci/-/wasm-oci-0.1.3.tgz", + "integrity": "sha512-NaEP1xa0KtyJ4xVAaaRVBZjHWLWlRraON/p5R8c9ObbRgp42VgSCriR7uDUCZ1UOZIEh8HEVs3Imgb4Avw2GLA==", + "dev": true, + "dependencies": { + "typed-rest-client": "^1.8.9" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 951fdbd..d6a1567 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,7 +26,8 @@ "adm-zip": "^0.5.10", "colorette": "^2.0.19", "react-scripts": "5.0.1", - "ts-node": "^10.9.1" + "ts-node": "^10.9.1", + "wasm-oci": "^0.1.3" }, "dependencies": { "@types/node": "^18.15.0", diff --git a/frontend/scripts/webjar.ts b/frontend/scripts/webjar.ts index 1212e43..a51991d 100644 --- a/frontend/scripts/webjar.ts +++ b/frontend/scripts/webjar.ts @@ -1,47 +1,134 @@ import AdmZip from 'adm-zip' -import fs from 'fs' -import { green, yellow } from 'colorette' +import fs from 'fs/promises' +import os from 'os' +import { green, yellow, cyan, magenta, Color, blue, white } from 'colorette' +import { WasmRegistry, Image } from 'wasm-oci' -const coords = { +type Packager = (packer: Packer, log: Logger) => Promise + +interface Packer { + /** + * Adds a file from the disk to the archive. + * @param localPath Path to a file on disk. + * @param zipPath Path to a directory in the archive. Defaults to the empty + * string. + * @param zipName Name for the file. + * @param comment Comment to be attached to the file + */ + addLocalFile(localPath: string, zipPath?: string, zipName?: string, comment?: string): void + /** + * Adds a local directory and all its nested files and directories to the + * archive. + * @param localPath Path to a folder on disk. + * @param zipPath Path to a folder in the archive. Default: `""`. + * @param filter RegExp or Function if files match will be included. + */ + addLocalFolder(localPath: string, zipPath?: string, filter?: (path: string) => boolean): void + /** + * Allows you to create a entry (file or directory) in the zip file. + * If you want to create a directory the `entryName` must end in `"/"` and a `null` + * buffer should be provided. + * @param entryName Entry path. + * @param content Content to add to the entry; must be a 0-length buffer + * for a directory. + * @param comment Comment to add to the entry. + * @param attr Attribute to add to the entry. + */ + addFile(entryName: string, content: Buffer, comment?: string, attr?: number): void +} + +interface Webjar { + group: string + artifact: string + version: string + color: Color, + build: Packager +} + +const webjars: Webjar[] = [{ group: 'com.redhat.openshift.knative.showcase', artifact: 'frontend', version: 'main', + color: cyan, + build: async p => { + p.addLocalFolder('./build', 'META-INF/resources', (path) => { + return !path.includes('index.html') + }) + p.addLocalFile('./build/index.html', 'META-INF/resources', 'home.html') + } +}, { + group: 'com.redhat.openshift.knative.showcase', + artifact: 'cloudevents-pp-wasm', + version: 'main', + color: magenta, + build: async (p, log) => { + const tmpDir = os.tmpdir() + const tmp = await fs.mkdtemp(`${tmpDir}/wasm-oci-`) + const reg = new WasmRegistry(tmp) + const image = Image.parse('quay.io/cardil/cloudevents-pretty-print@sha256:01b30983dda5eb42a8baefb523eb50d7d0e539fb10d7ab9498a2a59f35036afb') + log(`Pulling image: ${green(image.toString())}`) + const wasm = await reg.pull(image) + p.addFile('META-INF/cloudevents-pretty-print.wasm', await fs.readFile(wasm.file), 'Wasm') + await fs.rm(tmp, { recursive: true }) + } +}] + +type Logger = (message?: any, ...optionalParams: any[]) => void + +function createLogger(name: string): Logger { + return (msg) => { + console.log(`[${name}] ${msg}`) + } } -const jarDir = `${process.env.HOME}/.m2/repository/` + - `${coords.group.replace(/\./g, '/')}/` + - `${coords.artifact}/` + coords.version -const jarPath = `${jarDir}/${coords.artifact}-${coords.version}.jar` -const pomPath = `${jarDir}/${coords.artifact}-${coords.version}.pom` - -const pom = ` - - 4.0.0 - ${coords.group} - ${coords.artifact} - ${coords.version} - jar - // Build by: forntend/scripts/webjar.ts script +async function buildWebjar(webjar: Webjar) { + const log = createLogger(webjar.color(webjar.artifact)) + const jarDir = `${process.env.HOME}/.m2/repository/` + + `${webjar.group.replace(/\./g, '/')}/` + + `${webjar.artifact}/` + webjar.version + const jarPath = `${jarDir}/${webjar.artifact}-${webjar.version}.jar` + const pomPath = `${jarDir}/${webjar.artifact}-${webjar.version}.pom` + + const pom = ` + +4.0.0 +${webjar.group} +${webjar.artifact} +${webjar.version} +jar +// Build by: forntend/scripts/webjar.ts script ` + const zip = new AdmZip() + await webjar.build(zip, log) + zip.addFile(`META-INF/maven/${webjar.group}/${webjar.artifact}/pom.xml`, Buffer.from(pom)) + zip.writeZip(jarPath) + log(`Created webjar: ${yellow(jarPath)}`) + await fs.writeFile(pomPath, pom) + log(`Created webjar POM: ${yellow(pomPath)}`) + log('To use it, add following to your pom.xml file:\n' + blue( + ` + + ${white(webjar.group)} + ${white(webjar.artifact)} + ${white(webjar.version)} + + `)) +} + +async function build() { + const ps : Promise[] = [] + for (const webjar of webjars) { + ps.push(buildWebjar(webjar)) + } + await Promise.all(ps) +} -const zip = new AdmZip() -zip.addLocalFolder('./build', 'META-INF/resources', (path) => { - return !path.includes('index.html') -}) -zip.addLocalFile('./build/index.html', 'META-INF/resources', 'home.html') -zip.addFile(`META-INF/maven/${coords.group}/${coords.artifact}/pom.xml`, Buffer.from(pom)) -zip.writeZip(jarPath) -console.log(`Created webjar: ${yellow(jarPath)}`) -fs.writeFileSync(pomPath, pom) -console.log(`Created webjar POM: ${yellow(pomPath)}`) -console.log('\nTo use it, add following to your pom.xml file:\n\n' + green( - ` - - ${coords.group} - ${coords.artifact} - ${coords.version} - -`)) +build() + .catch(e => { + console.error(e) + process.exit(1) + }) + .then(() => process.exit(0)) diff --git a/quarkus/pom.xml b/quarkus/pom.xml index fb54eaa..32cd545 100644 --- a/quarkus/pom.xml +++ b/quarkus/pom.xml @@ -105,6 +105,11 @@ frontend main + + com.redhat.openshift.knative.showcase + cloudevents-pp-wasm + main + io.github.kawamuray.wasmtime wasmtime-java diff --git a/quarkus/src/main/java/com/redhat/openshift/knative/showcase/events/Presenter.java b/quarkus/src/main/java/com/redhat/openshift/knative/showcase/events/Presenter.java index da283d9..04bf311 100644 --- a/quarkus/src/main/java/com/redhat/openshift/knative/showcase/events/Presenter.java +++ b/quarkus/src/main/java/com/redhat/openshift/knative/showcase/events/Presenter.java @@ -1,6 +1,5 @@ package com.redhat.openshift.knative.showcase.events; -import com.redhat.openshift.oci.registry.WasmDownloader; import com.redhat.openshift.wasm.c.CString; import io.cloudevents.CloudEvent; import io.cloudevents.jackson.JsonFormat; @@ -8,17 +7,11 @@ import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Observes; -import javax.inject.Inject; @ApplicationScoped class Presenter { - private final PrettyPrintWasm wasm; - - @Inject - Presenter(WasmDownloader downloader) { - this.wasm = new PrettyPrintWasm(downloader); - } + private final PrettyPrintWasm wasm = new PrettyPrintWasm(); void onStop(@Observes ShutdownEvent ignored) { wasm.close(); diff --git a/quarkus/src/main/java/com/redhat/openshift/knative/showcase/events/PrettyPrintWasm.java b/quarkus/src/main/java/com/redhat/openshift/knative/showcase/events/PrettyPrintWasm.java index ee490ef..3501887 100644 --- a/quarkus/src/main/java/com/redhat/openshift/knative/showcase/events/PrettyPrintWasm.java +++ b/quarkus/src/main/java/com/redhat/openshift/knative/showcase/events/PrettyPrintWasm.java @@ -1,7 +1,5 @@ package com.redhat.openshift.knative.showcase.events; -import com.redhat.openshift.oci.registry.ContainerRegistry; -import com.redhat.openshift.oci.registry.WasmDownloader; import com.redhat.openshift.wasm.c.CString; import io.github.kawamuray.wasmtime.Engine; import io.github.kawamuray.wasmtime.Func; @@ -13,8 +11,7 @@ import io.github.kawamuray.wasmtime.wasi.WasiCtx; import io.github.kawamuray.wasmtime.wasi.WasiCtxBuilder; -import java.nio.file.Files; -import java.nio.file.Path; +import java.util.Objects; class PrettyPrintWasm implements AutoCloseable { public static final String MODULE_NAME = "wasm"; @@ -22,10 +19,9 @@ class PrettyPrintWasm implements AutoCloseable { private final Store store; private final Linker linker; private final Engine engine; - private final WasmDownloader downloader; + private Module module; - PrettyPrintWasm(WasmDownloader downloader) { - this.downloader = downloader; + PrettyPrintWasm() { this.wasi = new WasiCtxBuilder() .inheritStdout() .inheritStderr() @@ -38,18 +34,37 @@ class PrettyPrintWasm implements AutoCloseable { } CString execute(CString input) { - Path wasm = ensureWasmModuleIsDownloaded(); - try (var module = Module.fromFile(engine, wasm.toString())) { - if (!linker.modules(store).contains(MODULE_NAME)) { - linker.module(store, MODULE_NAME, module); - } - try(var mem = linker.get(store, MODULE_NAME, "memory").orElseThrow().memory()) { - return executeOnSharedMemory(input, mem); - } + loadWasmModule(); + try(var mem = linker.get(store, MODULE_NAME, "memory").orElseThrow().memory()) { + return executeUsingSharedMemory(input, mem); } } - private CString executeOnSharedMemory(CString input, Memory mem) { + private synchronized Module loadWasmModule() { + if (module != null) { + return module; + } + byte[] wasm = loadWasmBinary(); + module = Module.fromBinary(engine, wasm); + if (!linker.modules(store).contains(MODULE_NAME)) { + linker.module(store, MODULE_NAME, module); + } + return module; + } + + private byte[] loadWasmBinary() { + try (var steam = PrettyPrintWasm.class.getResourceAsStream( + "/META-INF/cloudevents-pretty-print.wasm")) { + Objects.requireNonNull(steam, + "cloudevents-pretty-print.wasm not found"); + return steam.readAllBytes(); + } catch (Exception ex) { + throw new RuntimeException( + "Failed to load cloudevents-pretty-print.wasm", ex); + } + } + + private CString executeUsingSharedMemory(CString input, Memory mem) { var buf = mem.buffer(store); var offset = 0; input.writeOn(buf, offset); @@ -65,20 +80,11 @@ private CString executeOnSharedMemory(CString input, Memory mem) { } } - private Path ensureWasmModuleIsDownloaded() { - var repo = new WasmDownloader.Repository("cardil/cloudevents-pretty-print"); - var target = new WasmDownloader.Target(Path.of( - System.getProperty("java.io.tmpdir"), ContainerRegistry.USER_AGENT - )); - var wasm = downloader.computeDownloadPath(repo, target); - if (!Files.exists(wasm)) { - downloader.download(repo, wasm); - } - return wasm; - } - @Override public void close() { + if (module != null) { + module.close(); + } linker.close(); engine.close(); store.close(); diff --git a/quarkus/src/main/java/com/redhat/openshift/oci/registry/Authorization.java b/quarkus/src/main/java/com/redhat/openshift/oci/registry/Authorization.java deleted file mode 100644 index 1564cb4..0000000 --- a/quarkus/src/main/java/com/redhat/openshift/oci/registry/Authorization.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.redhat.openshift.oci.registry; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class Authorization { - public final String token; - - @JsonCreator - public Authorization(@JsonProperty("token") String token) { - this.token = token; - } - - @Override - public String toString() { - return "Bearer " + token; - } -} diff --git a/quarkus/src/main/java/com/redhat/openshift/oci/registry/Config.java b/quarkus/src/main/java/com/redhat/openshift/oci/registry/Config.java deleted file mode 100644 index 739144b..0000000 --- a/quarkus/src/main/java/com/redhat/openshift/oci/registry/Config.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.redhat.openshift.oci.registry; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class Config { - public final String digest; - public final Long size; - public final String mediaType; - - @JsonCreator - public Config( - @JsonProperty("digest") String digest, - @JsonProperty("size") Long size, - @JsonProperty("mediaType") String mediaType - ) { - this.digest = digest; - this.size = size; - this.mediaType = mediaType; - } -} diff --git a/quarkus/src/main/java/com/redhat/openshift/oci/registry/ContainerRegistry.java b/quarkus/src/main/java/com/redhat/openshift/oci/registry/ContainerRegistry.java deleted file mode 100644 index 2d82172..0000000 --- a/quarkus/src/main/java/com/redhat/openshift/oci/registry/ContainerRegistry.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.redhat.openshift.oci.registry; - -import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; - -import javax.ws.rs.GET; -import javax.ws.rs.HeaderParam; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MultivaluedHashMap; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import java.net.URI; -import java.net.http.HttpRequest; -import java.util.Collections; - -@RegisterClientHeaders(ContainerRegistry.ClientHeadersFactory.class) -public interface ContainerRegistry { - - String USER_AGENT = "OpenShift-OciRegistry/1.0"; - String API_VERSION = "registry/2.0"; - - /** - * TODO: replace the owner and repository parameters with a single repository - * name parameter, when the {@link javax.ws.rs.Encoded} annotation is - * supported by Quarkus Reactive REST Client. - * - * The currenct workaround is to split the repository name into owner and - * repository name. This prevents the use of slashes in the repository name, - * and now supported nested repository names. - * - * The parameter should have been annotated as follows: - *
-   * {@code @Encoded @PathParam("repository") String repository}
-   * 
- * - * And the method call should have been annotated with: - *
-   * {@code @GET() @Path("/v2/{repository}/manifests/{reference}")}
-   * 
- */ - @GET() - @Path("/v2/{owner}/{repository}/manifests/{reference}") - Manifest getManifest( - @PathParam("owner") String owner, - @PathParam("repository") String repository, - @PathParam("reference") String reference, - @HeaderParam("Authorization") String token, - @HeaderParam("Accept") String accept - ); - - default Manifest getManifest(String owner, String repository, String reference, String token) { - return getManifest(owner, repository, reference, token, - "application/vnd.oci.image.manifest.v1+json"); - } - - @GET() - @Path("/v2/auth") - Authorization authorize( - @QueryParam("scope") String scope, - @QueryParam("service") String service, - @HeaderParam("Authorization") String token, - @QueryParam("account") String account - ); - - default Authorization authorize(Scope scope, String service) { - return authorize(scope.toString(), service, null, null); - } - - default Authorization authorize(Scope scope) { - return authorize(scope, service()); - } - - String service(); - - @GET() - @Path("/v2/") - Response checkVersion(@HeaderParam("Authorization") String token); - - default Response checkVersion() { - return checkVersion(null); - } - - default HttpRequest.Builder getDigestContentRequest( - String repository, - String digest, - String token - ) { - URI uri = URI.create(String.format( - "https://%s/v2/%s/blobs/%s", - service(), repository, digest - )); - var req = HttpRequest.newBuilder(uri); - var chf = new ClientHeadersFactory(); - var outgoingHeaders = new MultivaluedHashMap<>(Collections.singletonMap( - "Authorization", token - )); - var headers = chf.update(new MultivaluedHashMap<>(), outgoingHeaders); - headers.forEach((k, v) -> v.forEach(h -> req.header(k, h))); - return req; - } - - class ClientHeadersFactory implements org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory { - @Override - public MultivaluedMap update( - MultivaluedMap incomingHeaders, - MultivaluedMap clientOutgoingHeaders - ) { - MultivaluedMap headers = new MultivaluedHashMap<>(); - headers.putAll(incomingHeaders); - headers.putAll(clientOutgoingHeaders); - headers.putSingle("Docker-Distribution-API-Version", API_VERSION); - headers.putSingle("User-Agent", USER_AGENT); - return headers; - } - } -} diff --git a/quarkus/src/main/java/com/redhat/openshift/oci/registry/Layer.java b/quarkus/src/main/java/com/redhat/openshift/oci/registry/Layer.java deleted file mode 100644 index e0f9741..0000000 --- a/quarkus/src/main/java/com/redhat/openshift/oci/registry/Layer.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.redhat.openshift.oci.registry; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.HashMap; -import java.util.Map; - -public class Layer { - public final String digest; - public final Long size; - public final String mediaType; - public final Map annotations; - - @JsonCreator - public Layer( - @JsonProperty("digest") String digest, - @JsonProperty("size") Long size, - @JsonProperty("mediaType") String mediaType, - @JsonProperty("annotations") Map annotations - ) { - this.digest = digest; - this.size = size; - this.mediaType = mediaType; - this.annotations = new HashMap<>(annotations); - } -} diff --git a/quarkus/src/main/java/com/redhat/openshift/oci/registry/Manifest.java b/quarkus/src/main/java/com/redhat/openshift/oci/registry/Manifest.java deleted file mode 100644 index a9add1b..0000000 --- a/quarkus/src/main/java/com/redhat/openshift/oci/registry/Manifest.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.redhat.openshift.oci.registry; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.ArrayList; -import java.util.List; - -public class Manifest { - public final Integer schemaVersion; - public final List layers; - public final Config config; - - @JsonCreator - public Manifest( - @JsonProperty("schemaVersion") Integer schemaVersion, - @JsonProperty("layers") List layers, - @JsonProperty("config") Config config - ) { - this.schemaVersion = schemaVersion; - this.layers = new ArrayList<>(layers); - this.config = config; - } -} diff --git a/quarkus/src/main/java/com/redhat/openshift/oci/registry/Quay.java b/quarkus/src/main/java/com/redhat/openshift/oci/registry/Quay.java deleted file mode 100644 index 22db648..0000000 --- a/quarkus/src/main/java/com/redhat/openshift/oci/registry/Quay.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.redhat.openshift.oci.registry; - -import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; - -@RegisterRestClient(baseUri = "https://quay.io") -public interface Quay extends ContainerRegistry { - - default String service() { - return "quay.io"; - } -} diff --git a/quarkus/src/main/java/com/redhat/openshift/oci/registry/Scope.java b/quarkus/src/main/java/com/redhat/openshift/oci/registry/Scope.java deleted file mode 100644 index 4758dca..0000000 --- a/quarkus/src/main/java/com/redhat/openshift/oci/registry/Scope.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.redhat.openshift.oci.registry; - -public class Scope { - public final String repository; - public final String action; - - public Scope(String repository) { - this(repository, "pull"); - } - - public Scope(String repository, String action) { - this.repository = repository; - this.action = action; - } - - @Override - public String toString() { - return "repository:" + repository + ":" + action; - } -} diff --git a/quarkus/src/main/java/com/redhat/openshift/oci/registry/WasmDownloader.java b/quarkus/src/main/java/com/redhat/openshift/oci/registry/WasmDownloader.java deleted file mode 100644 index f983567..0000000 --- a/quarkus/src/main/java/com/redhat/openshift/oci/registry/WasmDownloader.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.redhat.openshift.oci.registry; - -import org.eclipse.microprofile.rest.client.inject.RestClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response.Status; -import java.io.IOException; -import java.net.http.HttpClient; -import java.net.http.HttpResponse.BodyHandlers; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Objects; - -@ApplicationScoped -public class WasmDownloader { - - private static final Logger LOGGER = LoggerFactory.getLogger(WasmDownloader.class); - - public static final String WASM_MEDIA_TYPE = "application/vnd.wasm.content.layer.v1+wasm"; - private final Quay quay; - - @Inject - WasmDownloader(@RestClient Quay quay) { - this.quay = quay; - } - - public Path computeDownloadPath(Repository repository, Target target) { - return target.path.resolve( - repository.name.replace('/', '-') + ".wasm"); - } - - public void download(Repository repository, Path target) { - LOGGER.debug("Downloading quay.io/{} to {}", repository, target); - Authorization auth = null; - //noinspection EmptyTryBlock - try (var ignored = quay.checkVersion()) { - // noop - } catch (WebApplicationException e) { - if (e.getResponse().getStatusInfo().toEnum() == Status.UNAUTHORIZED) { - auth = quay.authorize(new Scope(repository.name, "pull")); - } else { - throw e; - } - } - var token = auth != null ? auth.toString() : null; - // TODO: remove the split when @Encoded is supported in Quarkus Reactive REST Client - var repoCoords = repository.name.split("/", 2); - assert repoCoords.length == 2; - var manifest = quay.getManifest(repoCoords[0], repoCoords[1], repository.version, token); - assert manifest.layers.size() == 1; - var layer = manifest.layers.get(0); - assert Objects.equals(layer.mediaType, WASM_MEDIA_TYPE); - HttpClient client = HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.ALWAYS) - .build(); - - var req = quay.getDigestContentRequest(repository.name, layer.digest, token) - .build(); - try { - Files.createDirectories(target.getParent()); - var res = client.send(req, BodyHandlers.ofFile(target)); - var status = Status.fromStatusCode(res.statusCode()); - if (status.getFamily() != Status.Family.SUCCESSFUL) { - throw new IllegalStateException("Unexpected status code: " + res.statusCode()); - } - var actualSize = Files.size(target); - if (actualSize != layer.size) { - Files.delete(target); - throw new IllegalStateException("Unexpected file size: " + actualSize); - } - LOGGER.debug("Successfully downloaded quay.io/{}", repository); - } catch (IOException e) { - throw new IllegalStateException(e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IllegalStateException(e); - } - } - - public static final class Repository { - public final String name; - public final String version; - - public Repository(String name) { - this(name, "latest"); - } - - public Repository(String name, String version) { - this.name = name; - this.version = version; - } - - @Override - public String toString() { - if (version.startsWith("sha256")) { - return name + "@" + version; - } - return name + ":" + version; - } - } - - public static final class Target { - public final Path path; - - public Target(Path path) { - this.path = path; - } - } -}