Skip to content

Commit

Permalink
Merge pull request #31435 from cescoffier/devui-container-image
Browse files Browse the repository at this point in the history
Implement the container image card
  • Loading branch information
cescoffier authored Mar 1, 2023
2 parents df56851 + 23407ea commit c0b9b32
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<String, Function<Map<String, String>, ?>> 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 <T> void register(String name, Function<Map<String, String>, 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> T invoke(String name, Map<String, String> params) {
var function = actions.get(name);
if (function == null) {
throw new NoSuchElementException(name);
} else {
return (T) function.apply(params);
}
}
}
8 changes: 8 additions & 0 deletions extensions/container-image/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mutiny-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-http-dev-console-spi</artifactId>
Expand All @@ -25,6 +29,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-container-image-spi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-http-dev-ui-spi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-container-image-util</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AvailableContainerImageExtensionBuildItem> 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<Map<String, String>, String> build() {
return (map -> {
QuarkusBootstrap existing = (QuarkusBootstrap) DevConsoleManager.getQuarkusBootstrap();
try (TempSystemProperties properties = new TempSystemProperties()) {
properties.set("quarkus.container-image.build", "true");
for (Map.Entry<String, String> 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<ArtifactResult> 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();
}
}
});
}
}
Original file line number Diff line number Diff line change
@@ -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`<span>Loading...</span>`)}`;
}

_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`
<vaadin-select
label="Image Builder"
.items="${_builders}"
.value="${_defaultBuilder}"
?disabled="${_builders.length === 1 || this.build_in_progress}"
@value-changed="${e => this.selected_builder = e.target.value}"
></vaadin-select>`;

let progress;
if (this.build_in_progress) {
progress = html`
<div class="report">
<div>Generating container images...</div>
<vaadin-progress-bar indeterminate theme="contrast"></vaadin-progress-bar>
</div>`;
} else if (this.build_complete) {
progress = html`
<div class="report">
<div>${this.result}</div>
<vaadin-progress-bar value="1"
theme="${this.build_error} ? 'error' : 'success'"></vaadin-progress-bar>
</div>`;
} else {
progress = html`
<div class="report"></div>`;
}

return html`
<p>Select the type of build (jar, native...) and the container image builder.</p>
<vaadin-select
label="Build Type"
.items="${_types}"
.value="${_defaultType}"
?disabled="${this.build_in_progress}"
@value-changed="${e => this.selected_type = e.target.value}"
></vaadin-select>
${_builderPicker}
<vaadin-button @click="${this._build}" ?disabled="${this.build_in_progress}">Build Container</vaadin-button>
${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);
4 changes: 4 additions & 0 deletions extensions/container-image/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mutiny</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> build(String type, String builder) {
Map<String, String> 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<String> build = Uni.createFrom().item(() -> DevConsoleManager
.<String> 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);

}

}

0 comments on commit c0b9b32

Please sign in to comment.