From 23407ea77039ab83ed71898f643b2384449501f7 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Mon, 27 Feb 2023 10:07:46 +0100 Subject: [PATCH] Implement the container image card --- .../dev/console/DevConsoleManager.java | 39 +++++ extensions/container-image/deployment/pom.xml | 8 + .../ContainerImageDevUiProcessor.java | 75 +++++++++ .../qwc-container-image-build.js | 144 ++++++++++++++++++ extensions/container-image/runtime/pom.xml | 4 + .../devui/ContainerBuilderJsonRpcService.java | 29 ++++ 6 files changed, 299 insertions(+) create mode 100644 extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/devconsole/ContainerImageDevUiProcessor.java create mode 100644 extensions/container-image/deployment/src/main/resources/dev-ui/container-image/qwc-container-image-build.js create mode 100644 extensions/container-image/runtime/src/main/java/io/quarkus/container/image/runtime/devui/ContainerBuilderJsonRpcService.java diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/console/DevConsoleManager.java b/core/devmode-spi/src/main/java/io/quarkus/dev/console/DevConsoleManager.java index b591e7d684185..44c413466984f 100644 --- a/core/devmode-spi/src/main/java/io/quarkus/dev/console/DevConsoleManager.java +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/console/DevConsoleManager.java @@ -1,9 +1,12 @@ package io.quarkus.dev.console; import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import java.util.function.Function; import io.quarkus.dev.spi.HotReplacementContext; @@ -94,6 +97,42 @@ public static void close() { templateInfo = null; hotReplacementContext = null; quarkusBootstrap = null; + actions.clear(); globals.clear(); } + + /** + * A list of action that can be executed. + * The action registered here should be used with the Dev UI / JSON RPC services. + */ + private static final Map, ?>> actions = new HashMap<>(); + + /** + * Registers an action that will be called by a JSON RPC service at runtime + * + * @param name the name of the action, should be namespaced to avoid conflicts + * @param action the action. The function receives a Map as parameters (named parameters) and returns an object of type + * {@code T}. + * Note that the type {@code T} must be a class shared by both the deployment and the runtime. + */ + public static void register(String name, Function, T> action) { + actions.put(name, action); + } + + /** + * Invokes a registered action + * + * @param name the name of the action + * @param params the named parameters + * @return the result of the invocation. An empty map is returned for action not returning any result. + */ + @SuppressWarnings("unchecked") + public static T invoke(String name, Map params) { + var function = actions.get(name); + if (function == null) { + throw new NoSuchElementException(name); + } else { + return (T) function.apply(params); + } + } } diff --git a/extensions/container-image/deployment/pom.xml b/extensions/container-image/deployment/pom.xml index f6dd3f9da0772..4eab095b553f5 100644 --- a/extensions/container-image/deployment/pom.xml +++ b/extensions/container-image/deployment/pom.xml @@ -17,6 +17,10 @@ io.quarkus quarkus-core-deployment + + io.quarkus + quarkus-mutiny-deployment + io.quarkus quarkus-vertx-http-dev-console-spi @@ -25,6 +29,10 @@ io.quarkus quarkus-container-image-spi + + io.quarkus + quarkus-vertx-http-dev-ui-spi + io.quarkus quarkus-container-image-util diff --git a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/devconsole/ContainerImageDevUiProcessor.java b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/devconsole/ContainerImageDevUiProcessor.java new file mode 100644 index 0000000000000..6baf87b9549f9 --- /dev/null +++ b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/devconsole/ContainerImageDevUiProcessor.java @@ -0,0 +1,75 @@ +package io.quarkus.container.image.deployment.devconsole; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import io.quarkus.bootstrap.BootstrapException; +import io.quarkus.bootstrap.app.ArtifactResult; +import io.quarkus.bootstrap.app.AugmentResult; +import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.bootstrap.app.QuarkusBootstrap; +import io.quarkus.container.image.runtime.devui.ContainerBuilderJsonRpcService; +import io.quarkus.container.spi.AvailableContainerImageExtensionBuildItem; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.dev.console.TempSystemProperties; +import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.Page; +import io.vertx.core.json.JsonArray; + +public class ContainerImageDevUiProcessor { + + @BuildStep(onlyIf = IsDevelopment.class) + CardPageBuildItem create(List extensions) { + // Get the list of builders + JsonArray array = extensions.stream().map(AvailableContainerImageExtensionBuildItem::getName).sorted() + .collect(JsonArray::new, JsonArray::add, JsonArray::addAll); + + CardPageBuildItem card = new CardPageBuildItem("Container Image"); + card.addBuildTimeData("builderTypes", array); + card.addPage(Page.webComponentPageBuilder() + .title("Build Container") + .componentLink("qwc-container-image-build.js") + .icon("font-awesome-solid:box")); + return card; + } + + @BuildStep(onlyIf = IsDevelopment.class) + JsonRPCProvidersBuildItem createJsonRPCServiceForContainerBuild() { + DevConsoleManager.register("container-image-build-action", build()); + return new JsonRPCProvidersBuildItem("ContainerImage", ContainerBuilderJsonRpcService.class); + } + + private Function, String> build() { + return (map -> { + QuarkusBootstrap existing = (QuarkusBootstrap) DevConsoleManager.getQuarkusBootstrap(); + try (TempSystemProperties properties = new TempSystemProperties()) { + properties.set("quarkus.container-image.build", "true"); + for (Map.Entry arg : map.entrySet()) { + properties.set(arg.getKey(), arg.getValue()); + } + + QuarkusBootstrap quarkusBootstrap = existing.clonedBuilder() + .setMode(QuarkusBootstrap.Mode.PROD) + .setIsolateDeployment(true).build(); + try (CuratedApplication bootstrap = quarkusBootstrap.bootstrap()) { + AugmentResult augmentResult = bootstrap + .createAugmentor().createProductionApplication(); + List containerArtifactResults = augmentResult + .resultsMatchingType((s) -> s.contains("container")); + if (containerArtifactResults.size() >= 1) { + return "Container image: " + containerArtifactResults.get(0).getMetadata().get("container-image") + + " created."; + } else { + return "Unknown error (image not created)"; + } + } catch (BootstrapException e) { + return e.getMessage(); + } + } + }); + } +} diff --git a/extensions/container-image/deployment/src/main/resources/dev-ui/container-image/qwc-container-image-build.js b/extensions/container-image/deployment/src/main/resources/dev-ui/container-image/qwc-container-image-build.js new file mode 100644 index 0000000000000..cef131afc6127 --- /dev/null +++ b/extensions/container-image/deployment/src/main/resources/dev-ui/container-image/qwc-container-image-build.js @@ -0,0 +1,144 @@ +import {LitElement, html, css, render} from 'lit'; +import {JsonRpc} from 'jsonrpc'; +import '@vaadin/icon'; +import '@vaadin/button'; +import {until} from 'lit/directives/until.js'; +import '@vaadin/grid'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; +import {builderTypes} from 'container-image-data'; +import '@vaadin/text-field'; +import '@vaadin/text-area'; +import '@vaadin/form-layout'; +import '@vaadin/progress-bar'; +import '@vaadin/checkbox'; +import '@vaadin/select'; +import '@vaadin/item'; +import '@vaadin/list-box'; + + +export class QwcContainerImageBuild extends LitElement { + + jsonRpc = new JsonRpc("ContainerImage"); + + static properties = { + builders: {type: Array}, + types: {type: Array}, + selected_builder: {type: String}, + selected_type: {type: String}, + build_in_progress: {state: true, type: Boolean}, + build_complete: {state: true, type: Boolean}, + build_error: {state: true, type: Boolean}, + result: {type: String} + } + + static styles = css` + .report { + margin-top: 1em; + width: 80%; + } + `; + + + connectedCallback() { + super.connectedCallback(); + this.build_in_progress = false; + this.build_complete = false; + this.build_error = false; + this.result = ""; + + this.builders = builderTypes.list; + + this.types = []; + this.types.push({name: "Default", value: ""}); + this.types.push({name: "Jar", value: "jar"}); + this.types.push({name: "Mutable Jar", value: "mutable-jar"}); + this.types.push({name: "Native", value: "native"}); + } + + /** + * Called when it needs to render the components + * @returns {*} + */ + render() { + return html`${until(this._renderForm(), html`Loading...`)}`; + } + + _renderForm() { + const _builders = []; + this.builders.map(item => _builders.push({'label': item, 'value': item})); + const _defaultBuilder = _builders[0].label; + + const _types = []; + this.types.map(item => _types.push({'label': item.name, 'value': item.value})); + const _defaultType = "jar"; + + const _builderPicker = html` + `; + + let progress; + if (this.build_in_progress) { + progress = html` +
+
Generating container images...
+ +
`; + } else if (this.build_complete) { + progress = html` +
+
${this.result}
+ +
`; + } else { + progress = html` +
`; + } + + return html` +

Select the type of build (jar, native...) and the container image builder.

+ + ${_builderPicker} + Build Container + ${progress} + `; + } + + _build() { + this.build_complete = false; + this.build_in_progress = true; + this.build_error = false; + this.result = ""; + this.jsonRpc.build({'type': this.selected_type, 'builder': this.selected_builder}) + .onNext(jsonRpcResponse => { + const msg = jsonRpcResponse.result; + if (msg === "started") { + this.build_complete = false; + this.build_in_progress = true; + this.build_error = false; + } else if (msg.includes("created.")) { + this.result = msg; + this.build_complete = true; + this.build_in_progress = false; + } else { + this.build_complete = true; + this.build_in_progress = false; + this.build_error = true; + } + }); + } + +} + +customElements.define('qwc-container-image-build', QwcContainerImageBuild); diff --git a/extensions/container-image/runtime/pom.xml b/extensions/container-image/runtime/pom.xml index 317824b883e54..a180709fc207d 100644 --- a/extensions/container-image/runtime/pom.xml +++ b/extensions/container-image/runtime/pom.xml @@ -25,6 +25,10 @@ io.quarkus quarkus-core
+ + io.quarkus + quarkus-mutiny + diff --git a/extensions/container-image/runtime/src/main/java/io/quarkus/container/image/runtime/devui/ContainerBuilderJsonRpcService.java b/extensions/container-image/runtime/src/main/java/io/quarkus/container/image/runtime/devui/ContainerBuilderJsonRpcService.java new file mode 100644 index 0000000000000..1acc266f997ff --- /dev/null +++ b/extensions/container-image/runtime/src/main/java/io/quarkus/container/image/runtime/devui/ContainerBuilderJsonRpcService.java @@ -0,0 +1,29 @@ +package io.quarkus.container.image.runtime.devui; + +import java.util.Map; + +import io.quarkus.dev.console.DevConsoleManager; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; + +public class ContainerBuilderJsonRpcService { + + public Multi build(String type, String builder) { + Map params = Map.of( + "quarkus.container-image.builder", builder, + "quarkus.build.package-type", type); + + // For now, the JSON RPC are called on the event loop, but the action is blocking, + // So, work around this by invoking the action on a worker thread. + Multi build = Uni.createFrom().item(() -> DevConsoleManager + . invoke("container-image-build-action", params)) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) // It's a blocking action. + .toMulti(); + + return Multi.createBy().concatenating() + .streams(Multi.createFrom().item("started"), build); + + } + +}