Skip to content

Commit c0b9b32

Browse files
authored
Merge pull request #31435 from cescoffier/devui-container-image
Implement the container image card
2 parents df56851 + 23407ea commit c0b9b32

File tree

6 files changed

+299
-0
lines changed

6 files changed

+299
-0
lines changed

core/devmode-spi/src/main/java/io/quarkus/dev/console/DevConsoleManager.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package io.quarkus.dev.console;
22

33
import java.util.Collections;
4+
import java.util.HashMap;
45
import java.util.Map;
6+
import java.util.NoSuchElementException;
57
import java.util.concurrent.ConcurrentHashMap;
68
import java.util.function.Consumer;
9+
import java.util.function.Function;
710

811
import io.quarkus.dev.spi.HotReplacementContext;
912

@@ -94,6 +97,42 @@ public static void close() {
9497
templateInfo = null;
9598
hotReplacementContext = null;
9699
quarkusBootstrap = null;
100+
actions.clear();
97101
globals.clear();
98102
}
103+
104+
/**
105+
* A list of action that can be executed.
106+
* The action registered here should be used with the Dev UI / JSON RPC services.
107+
*/
108+
private static final Map<String, Function<Map<String, String>, ?>> actions = new HashMap<>();
109+
110+
/**
111+
* Registers an action that will be called by a JSON RPC service at runtime
112+
*
113+
* @param name the name of the action, should be namespaced to avoid conflicts
114+
* @param action the action. The function receives a Map as parameters (named parameters) and returns an object of type
115+
* {@code T}.
116+
* Note that the type {@code T} must be a class shared by both the deployment and the runtime.
117+
*/
118+
public static <T> void register(String name, Function<Map<String, String>, T> action) {
119+
actions.put(name, action);
120+
}
121+
122+
/**
123+
* Invokes a registered action
124+
*
125+
* @param name the name of the action
126+
* @param params the named parameters
127+
* @return the result of the invocation. An empty map is returned for action not returning any result.
128+
*/
129+
@SuppressWarnings("unchecked")
130+
public static <T> T invoke(String name, Map<String, String> params) {
131+
var function = actions.get(name);
132+
if (function == null) {
133+
throw new NoSuchElementException(name);
134+
} else {
135+
return (T) function.apply(params);
136+
}
137+
}
99138
}

extensions/container-image/deployment/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
<groupId>io.quarkus</groupId>
1818
<artifactId>quarkus-core-deployment</artifactId>
1919
</dependency>
20+
<dependency>
21+
<groupId>io.quarkus</groupId>
22+
<artifactId>quarkus-mutiny-deployment</artifactId>
23+
</dependency>
2024
<dependency>
2125
<groupId>io.quarkus</groupId>
2226
<artifactId>quarkus-vertx-http-dev-console-spi</artifactId>
@@ -25,6 +29,10 @@
2529
<groupId>io.quarkus</groupId>
2630
<artifactId>quarkus-container-image-spi</artifactId>
2731
</dependency>
32+
<dependency>
33+
<groupId>io.quarkus</groupId>
34+
<artifactId>quarkus-vertx-http-dev-ui-spi</artifactId>
35+
</dependency>
2836
<dependency>
2937
<groupId>io.quarkus</groupId>
3038
<artifactId>quarkus-container-image-util</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package io.quarkus.container.image.deployment.devconsole;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import java.util.function.Function;
6+
7+
import io.quarkus.bootstrap.BootstrapException;
8+
import io.quarkus.bootstrap.app.ArtifactResult;
9+
import io.quarkus.bootstrap.app.AugmentResult;
10+
import io.quarkus.bootstrap.app.CuratedApplication;
11+
import io.quarkus.bootstrap.app.QuarkusBootstrap;
12+
import io.quarkus.container.image.runtime.devui.ContainerBuilderJsonRpcService;
13+
import io.quarkus.container.spi.AvailableContainerImageExtensionBuildItem;
14+
import io.quarkus.deployment.IsDevelopment;
15+
import io.quarkus.deployment.annotations.BuildStep;
16+
import io.quarkus.dev.console.DevConsoleManager;
17+
import io.quarkus.dev.console.TempSystemProperties;
18+
import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
19+
import io.quarkus.devui.spi.page.CardPageBuildItem;
20+
import io.quarkus.devui.spi.page.Page;
21+
import io.vertx.core.json.JsonArray;
22+
23+
public class ContainerImageDevUiProcessor {
24+
25+
@BuildStep(onlyIf = IsDevelopment.class)
26+
CardPageBuildItem create(List<AvailableContainerImageExtensionBuildItem> extensions) {
27+
// Get the list of builders
28+
JsonArray array = extensions.stream().map(AvailableContainerImageExtensionBuildItem::getName).sorted()
29+
.collect(JsonArray::new, JsonArray::add, JsonArray::addAll);
30+
31+
CardPageBuildItem card = new CardPageBuildItem("Container Image");
32+
card.addBuildTimeData("builderTypes", array);
33+
card.addPage(Page.webComponentPageBuilder()
34+
.title("Build Container")
35+
.componentLink("qwc-container-image-build.js")
36+
.icon("font-awesome-solid:box"));
37+
return card;
38+
}
39+
40+
@BuildStep(onlyIf = IsDevelopment.class)
41+
JsonRPCProvidersBuildItem createJsonRPCServiceForContainerBuild() {
42+
DevConsoleManager.register("container-image-build-action", build());
43+
return new JsonRPCProvidersBuildItem("ContainerImage", ContainerBuilderJsonRpcService.class);
44+
}
45+
46+
private Function<Map<String, String>, String> build() {
47+
return (map -> {
48+
QuarkusBootstrap existing = (QuarkusBootstrap) DevConsoleManager.getQuarkusBootstrap();
49+
try (TempSystemProperties properties = new TempSystemProperties()) {
50+
properties.set("quarkus.container-image.build", "true");
51+
for (Map.Entry<String, String> arg : map.entrySet()) {
52+
properties.set(arg.getKey(), arg.getValue());
53+
}
54+
55+
QuarkusBootstrap quarkusBootstrap = existing.clonedBuilder()
56+
.setMode(QuarkusBootstrap.Mode.PROD)
57+
.setIsolateDeployment(true).build();
58+
try (CuratedApplication bootstrap = quarkusBootstrap.bootstrap()) {
59+
AugmentResult augmentResult = bootstrap
60+
.createAugmentor().createProductionApplication();
61+
List<ArtifactResult> containerArtifactResults = augmentResult
62+
.resultsMatchingType((s) -> s.contains("container"));
63+
if (containerArtifactResults.size() >= 1) {
64+
return "Container image: " + containerArtifactResults.get(0).getMetadata().get("container-image")
65+
+ " created.";
66+
} else {
67+
return "Unknown error (image not created)";
68+
}
69+
} catch (BootstrapException e) {
70+
return e.getMessage();
71+
}
72+
}
73+
});
74+
}
75+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import {LitElement, html, css, render} from 'lit';
2+
import {JsonRpc} from 'jsonrpc';
3+
import '@vaadin/icon';
4+
import '@vaadin/button';
5+
import {until} from 'lit/directives/until.js';
6+
import '@vaadin/grid';
7+
import '@vaadin/grid/vaadin-grid-sort-column.js';
8+
import {builderTypes} from 'container-image-data';
9+
import '@vaadin/text-field';
10+
import '@vaadin/text-area';
11+
import '@vaadin/form-layout';
12+
import '@vaadin/progress-bar';
13+
import '@vaadin/checkbox';
14+
import '@vaadin/select';
15+
import '@vaadin/item';
16+
import '@vaadin/list-box';
17+
18+
19+
export class QwcContainerImageBuild extends LitElement {
20+
21+
jsonRpc = new JsonRpc("ContainerImage");
22+
23+
static properties = {
24+
builders: {type: Array},
25+
types: {type: Array},
26+
selected_builder: {type: String},
27+
selected_type: {type: String},
28+
build_in_progress: {state: true, type: Boolean},
29+
build_complete: {state: true, type: Boolean},
30+
build_error: {state: true, type: Boolean},
31+
result: {type: String}
32+
}
33+
34+
static styles = css`
35+
.report {
36+
margin-top: 1em;
37+
width: 80%;
38+
}
39+
`;
40+
41+
42+
connectedCallback() {
43+
super.connectedCallback();
44+
this.build_in_progress = false;
45+
this.build_complete = false;
46+
this.build_error = false;
47+
this.result = "";
48+
49+
this.builders = builderTypes.list;
50+
51+
this.types = [];
52+
this.types.push({name: "Default", value: ""});
53+
this.types.push({name: "Jar", value: "jar"});
54+
this.types.push({name: "Mutable Jar", value: "mutable-jar"});
55+
this.types.push({name: "Native", value: "native"});
56+
}
57+
58+
/**
59+
* Called when it needs to render the components
60+
* @returns {*}
61+
*/
62+
render() {
63+
return html`${until(this._renderForm(), html`<span>Loading...</span>`)}`;
64+
}
65+
66+
_renderForm() {
67+
const _builders = [];
68+
this.builders.map(item => _builders.push({'label': item, 'value': item}));
69+
const _defaultBuilder = _builders[0].label;
70+
71+
const _types = [];
72+
this.types.map(item => _types.push({'label': item.name, 'value': item.value}));
73+
const _defaultType = "jar";
74+
75+
const _builderPicker = html`
76+
<vaadin-select
77+
label="Image Builder"
78+
.items="${_builders}"
79+
.value="${_defaultBuilder}"
80+
?disabled="${_builders.length === 1 || this.build_in_progress}"
81+
@value-changed="${e => this.selected_builder = e.target.value}"
82+
></vaadin-select>`;
83+
84+
let progress;
85+
if (this.build_in_progress) {
86+
progress = html`
87+
<div class="report">
88+
<div>Generating container images...</div>
89+
<vaadin-progress-bar indeterminate theme="contrast"></vaadin-progress-bar>
90+
</div>`;
91+
} else if (this.build_complete) {
92+
progress = html`
93+
<div class="report">
94+
<div>${this.result}</div>
95+
<vaadin-progress-bar value="1"
96+
theme="${this.build_error} ? 'error' : 'success'"></vaadin-progress-bar>
97+
</div>`;
98+
} else {
99+
progress = html`
100+
<div class="report"></div>`;
101+
}
102+
103+
return html`
104+
<p>Select the type of build (jar, native...) and the container image builder.</p>
105+
<vaadin-select
106+
label="Build Type"
107+
.items="${_types}"
108+
.value="${_defaultType}"
109+
?disabled="${this.build_in_progress}"
110+
@value-changed="${e => this.selected_type = e.target.value}"
111+
></vaadin-select>
112+
${_builderPicker}
113+
<vaadin-button @click="${this._build}" ?disabled="${this.build_in_progress}">Build Container</vaadin-button>
114+
${progress}
115+
`;
116+
}
117+
118+
_build() {
119+
this.build_complete = false;
120+
this.build_in_progress = true;
121+
this.build_error = false;
122+
this.result = "";
123+
this.jsonRpc.build({'type': this.selected_type, 'builder': this.selected_builder})
124+
.onNext(jsonRpcResponse => {
125+
const msg = jsonRpcResponse.result;
126+
if (msg === "started") {
127+
this.build_complete = false;
128+
this.build_in_progress = true;
129+
this.build_error = false;
130+
} else if (msg.includes("created.")) {
131+
this.result = msg;
132+
this.build_complete = true;
133+
this.build_in_progress = false;
134+
} else {
135+
this.build_complete = true;
136+
this.build_in_progress = false;
137+
this.build_error = true;
138+
}
139+
});
140+
}
141+
142+
}
143+
144+
customElements.define('qwc-container-image-build', QwcContainerImageBuild);

extensions/container-image/runtime/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
<groupId>io.quarkus</groupId>
2626
<artifactId>quarkus-core</artifactId>
2727
</dependency>
28+
<dependency>
29+
<groupId>io.quarkus</groupId>
30+
<artifactId>quarkus-mutiny</artifactId>
31+
</dependency>
2832
</dependencies>
2933
<build>
3034
<plugins>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.quarkus.container.image.runtime.devui;
2+
3+
import java.util.Map;
4+
5+
import io.quarkus.dev.console.DevConsoleManager;
6+
import io.smallrye.mutiny.Multi;
7+
import io.smallrye.mutiny.Uni;
8+
import io.smallrye.mutiny.infrastructure.Infrastructure;
9+
10+
public class ContainerBuilderJsonRpcService {
11+
12+
public Multi<String> build(String type, String builder) {
13+
Map<String, String> params = Map.of(
14+
"quarkus.container-image.builder", builder,
15+
"quarkus.build.package-type", type);
16+
17+
// For now, the JSON RPC are called on the event loop, but the action is blocking,
18+
// So, work around this by invoking the action on a worker thread.
19+
Multi<String> build = Uni.createFrom().item(() -> DevConsoleManager
20+
.<String> invoke("container-image-build-action", params))
21+
.runSubscriptionOn(Infrastructure.getDefaultExecutor()) // It's a blocking action.
22+
.toMulti();
23+
24+
return Multi.createBy().concatenating()
25+
.streams(Multi.createFrom().item("started"), build);
26+
27+
}
28+
29+
}

0 commit comments

Comments
 (0)