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

Implement the container image card #31435

Merged
merged 1 commit into from
Mar 1, 2023
Merged
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
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>
Comment on lines +28 to +31
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to add a runtime dependency just for the sake of the Dev UI?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I need to stream, and there is not much I can do without Mutiny. I don't believe it's a problem, as mutiny will likely be there.

</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);

}

}