Feature/web gui (#12)

Basic GUI functions (start/stop).
This commit is contained in:
Michael N. Lipp 2023-10-21 22:16:10 +02:00 committed by GitHub
parent 6491742eb0
commit ae3941707a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
86 changed files with 12225 additions and 514 deletions

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<fileset-config file-format-version="1.2.0" simple-config="false" sync-formatter="false">
<local-check-config name="Project Checks" location="/VM-Operator/checkstyle.xml" type="project" description="">
<local-check-config name="Project Checks" location="/VM-Operator/checkstyle.xml" type="project" description="">
<additional-data name="protect-config-file" value="false"/>
</local-check-config>
<fileset name="all" enabled="true" check-config-name="Project Checks" local="true">
<file-match-pattern match-pattern="^src/" include-pattern="true"/>
<file-match-pattern match-pattern="." include-pattern="true"/>
</fileset>
</fileset-config>

View file

@ -9,16 +9,30 @@ plugins {
}
dependencies {
implementation project(':org.jdrupes.vmoperator.manager.events')
implementation 'commons-cli:commons-cli:1.5.0'
implementation 'org.jgrapes:org.jgrapes.core:[1.19.0,2)'
implementation 'org.jgrapes:org.jgrapes.io:[2.7.0,3)'
implementation 'org.jgrapes:org.jgrapes.http:[3.1.0,4)'
implementation 'org.jgrapes:org.jgrapes.util:[1.31.0,2)'
implementation project(':org.jdrupes.vmoperator.util')
implementation 'commons-cli:commons-cli:1.5.0'
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.2.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.3.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.0.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconlet.locallogin:[1.0.0,2)'
runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.locallogin:[0.1.0,2)'
runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.sysinfo:[1.2.0,2)'
runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.logviewer:[0.1.0-SNAPSHOT,2)'
runtimeOnly 'com.electronwill.night-config:yaml:[3.6.7,3.7)'
runtimeOnly 'org.eclipse.angus:angus-activation:[1.0.0,2.0.0)'
runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)'
runtimeOnly 'org.apache.logging.log4j:log4j-to-jul:2.20.0'
runtimeOnly project(':org.jdrupes.vmoperator.vmconlet')
testImplementation 'io.fabric8:kubernetes-client:[6.8.1,6.9)'
}
@ -97,3 +111,8 @@ test {
? project.getProperty("k8s.testCluster") : null
}
// Update favicon:
// # Convert with inkscape to png because convert cannot handle svg
// # background transparency, then
// convert VM-Operator.png -background transparent \
// -define icon:auto-resize=256,64,48,32,16 favicon.ico

View file

@ -16,7 +16,8 @@
# with this program; if not, see <http://www.gnu.org/licenses/>.
#
handlers=java.util.logging.ConsoleHandler
handlers=java.util.logging.ConsoleHandler, \
org.jgrapes.webconlet.logviewer.LogViewerHandler
org.jgrapes.level=FINE
org.jgrapes.core.handlerTracking.level=FINER
@ -26,3 +27,5 @@ org.jdrupes.vmoperator.manager.level=FINE
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format=%1$tb %1$td %1$tT %4$s %5$s%6$s%n
org.jgrapes.webconlet.logviewer.LogViewerHandler.level=CONFIG

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,184 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="93.557968mm"
height="91.220795mm"
viewBox="0 0 331.50461 323.22329"
id="svg2"
version="1.1"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="VM-Operator.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="245"
inkscape:cy="145.71429"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(799.83239,410.74206)">
<g
id="path300"
inkscape:transform-center-x="-0.49891951"
inkscape:transform-center-y="-10.814906"
transform="matrix(0.93749998,0,0,0.93749998,-364.15225,128.12438)">
<path
sodipodi:type="star"
style="fill:#326de6;fill-opacity:1;stroke:#ffffff;stroke-linecap:square;stroke-miterlimit:0;paint-order:fill markers stroke"
id="path1033"
inkscape:flatsided="false"
sodipodi:sides="7"
sodipodi:cx="-790.008"
sodipodi:cy="-357.15076"
sodipodi:r1="221.23064"
sodipodi:r2="199.10757"
sodipodi:arg1="1.1215879"
sodipodi:arg2="1.5605315"
inkscape:rounded="0"
inkscape:randomized="0"
d="m -693.93801,-157.86816 -94.02622,-0.18551 -97.95052,0.26412 -58.47935,-73.62832 -61.2776,-76.41613 21.10362,-91.6275 21.53854,-95.55347 84.79518,-40.6293 88.13578,-42.7371 84.6342,40.96358 88.36497,42.26117 20.74194,91.71007 22.05354,95.43592 -58.76943,73.39699 z"
transform="matrix(0.81788201,0,0,0.81788201,358.19384,-101.37507)"
inkscape:transform-center-x="1.2804791"
inkscape:transform-center-y="-8.9686433" />
</g>
<g
aria-label="VM"
id="text300"
style="font-size:16.6665px;-inkscape-font-specification:'sans-serif, Normal';letter-spacing:0px;word-spacing:0px;fill:none;stroke:#ffffff;stroke-width:1.04165">
<g
id="path305">
<path
style="color:#000000;-inkscape-font-specification:'Nimbus Sans Narrow, Bold Semi-Condensed';fill:#ff6600;stroke:none;-inkscape-stroke:none"
d="m -698.0792,-217.35754 -25.39961,-109.59835 h -26.39961 l 39.59941,143.59784 h 23.39965 l 39.99939,-143.59784 h -25.59961 z"
id="path312" />
<path
style="color:#000000;-inkscape-font-specification:'Nimbus Sans Narrow, Bold Semi-Condensed';fill:#ffffff;stroke:none;-inkscape-stroke:none"
d="m -750.5625,-327.47656 39.88672,144.63867 h 24.19141 l 40.29101,-144.63867 h -26.69922 l -25.18554,107.82226 -24.98633,-107.82226 z m 1.36719,1.04101 h 25.30273 l 25.30664,109.19532 1.01367,0.002 25.50586,-109.19727 h 24.50196 l -39.71094,142.55664 h -22.60742 z"
id="path325" />
</g>
<g
id="path307">
<path
style="color:#000000;-inkscape-font-specification:'Nimbus Sans Narrow, Bold Semi-Condensed';fill:#ff6600;stroke:none;-inkscape-stroke:none"
d="m -518.28172,-326.95589 h -35.39947 l -21.19968,113.99829 -21.59968,-113.99829 h -35.79946 v 143.59784 h 22.79966 v -121.79817 l 21.79967,121.79817 h 24.19963 l 22.39967,-121.79817 v 121.79817 h 22.79966 z"
id="path318" />
<path
style="color:#000000;-inkscape-font-specification:'Nimbus Sans Narrow, Bold Semi-Condensed';fill:#ffffff;stroke:none;-inkscape-stroke:none"
d="m -632.80078,-327.47656 v 144.63867 h 23.8418 v -116.45313 l 20.84179,116.45313 h 25.07032 l 21.44531,-116.60742 v 116.60742 h 23.83984 v -144.63867 h -0.51953 -35.83203 l -20.77149,111.69726 -21.16406,-111.69726 z m 1.04101,1.04101 h 34.84766 l 21.51953,113.57422 1.02344,-0.002 21.12109,-113.57227 h 34.44532 v 142.55664 h -21.75782 v -121.27734 l -1.0332,-0.0937 -22.32031,121.37109 h -23.33008 l -21.72266,-121.36914 -1.03515,0.0918 v 121.27734 h -21.75782 z"
id="path320" />
</g>
</g>
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136"
cx="-691.58337"
cy="-161.52753"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-8"
cx="-671.12665"
cy="-161.52753"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-1"
cx="-650.66992"
cy="-161.52753"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-2"
cx="-630.2132"
cy="-161.52753"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-8-8"
cx="-681.66187"
cy="-144.03705"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-1-9"
cx="-661.20514"
cy="-144.03705"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-2-3"
cx="-640.74841"
cy="-144.03705"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-8-6"
cx="-671.53577"
cy="-126.23969"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-1-8"
cx="-651.07904"
cy="-126.23969"
r="9.2055216" />
<circle
style="fill:#6606b5;fill-opacity:1;stroke:#ffffff;stroke-width:1.07634;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4136-2-0"
cx="-661.61426"
cy="-109.05605"
r="9.2055216" />
<g
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:180px;line-height:125%;font-family:Arial;-inkscape-font-specification:'Arial Bold';letter-spacing:0px;word-spacing:0px;fill:#238220;fill-opacity:1;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="flowRoot4814"
transform="matrix(0.25165808,0,0,0.24991064,-709.96916,-218.52595)">
<path
d="m 210.10352,57.161102 h 25.92773 V 138.7236 q 0,15.9961 -2.8125,24.60938 -3.7793,11.25 -13.71094,18.10547 -9.93164,6.76757 -26.1914,6.76757 -19.07227,0 -29.35547,-10.63476 -10.28321,-10.72266 -10.3711,-31.37696 l 24.52149,-2.8125 q 0.43945,11.07422 3.25195,15.64454 4.21875,6.94336 12.83203,6.94336 8.70117,0 12.30469,-4.92188 3.60352,-5.00977 3.60352,-20.6543 z"
style="fill:#238220;fill-opacity:1;stroke:#ffffff;stroke-opacity:1"
id="path4823"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.5 KiB

View file

@ -0,0 +1,2 @@
<a class="navbar-brand" href="#"><img style="height: 1.25em; padding-right: 0.25em;"
src="${renderSupport.consoleResource('VM-Operator.svg')}"><span>${_("consoleTitle")}<span></a>

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -0,0 +1,19 @@
#
# VM-Operator
# Copyright (C) 2023 Michael N. Lipp
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
# for more details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>.
#
consoleTitle = VM-Operator

View file

@ -32,9 +32,11 @@ import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;
import java.util.logging.Logger;
import org.jdrupes.vmoperator.common.K8s;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.util.K8s;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;

View file

@ -21,7 +21,7 @@ package org.jdrupes.vmoperator.manager;
/**
* Some constants.
*/
public class Constants extends org.jdrupes.vmoperator.util.Constants {
public class Constants extends org.jdrupes.vmoperator.common.Constants {
/** The Constant APP_NAME. */
public static final String APP_NAME = "vm-runner";

View file

@ -18,15 +18,28 @@
package org.jdrupes.vmoperator.manager;
import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.Configuration;
import io.kubernetes.client.util.Config;
import io.kubernetes.client.util.generic.options.PatchOptions;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_KIND_VM;
import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.manager.events.StartVm;
import org.jdrupes.vmoperator.manager.events.StopVm;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Start;
import org.jgrapes.core.events.Stop;
import org.jgrapes.util.events.ConfigurationUpdate;
/**
* Implements a controller as defined in the
@ -66,6 +79,8 @@ import org.jgrapes.core.events.Start;
*/
public class Controller extends Component {
private String namespace;
/**
* Creates a new instance.
*/
@ -90,6 +105,20 @@ public class Controller extends Component {
}
}
/**
* Configure the component.
*
* @param event the event
*/
@Handler
public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured(componentPath()).ifPresent(c -> {
if (c.containsKey("namespace")) {
namespace = (String) c.get("namespace");
}
});
}
/**
* Handle the start event. Has higher priority because it configures
* the default Kubernetes client.
@ -103,5 +132,73 @@ public class Controller extends Component {
// Make sure to use thread specific client
// https://github.com/kubernetes-client/java/issues/100
Configuration.setDefaultApiClient(null);
// Verify that a namespace has been configured
if (namespace == null) {
var path = Path
.of("/var/run/secrets/kubernetes.io/serviceaccount/namespace");
if (Files.isReadable(path)) {
namespace = Files.lines(path).findFirst().orElse(null);
}
}
if (namespace == null) {
logger.severe(() -> "Namespace to control not configured and"
+ " no file in kubernetes directory.");
event.cancel(true);
fire(new Stop());
return;
}
logger.fine(() -> "Controlling namespace \"" + namespace + "\".");
}
/**
* On start vm.
*
* @param event the event
* @throws ApiException the api exception
* @throws IOException Signals that an I/O exception has occurred.
*/
@Handler
public void onStartVm(StartVm event) throws ApiException, IOException {
patchRunning(event.name(), true);
}
/**
* On stop vm.
*
* @param event the event
* @throws ApiException the api exception
* @throws IOException Signals that an I/O exception has occurred.
*/
@Handler
public void onStopVm(StopVm event) throws ApiException, IOException {
patchRunning(event.name(), false);
}
private void patchRunning(String name, boolean running)
throws ApiException, IOException {
var crApi = K8s.crApi(Config.defaultClient(), VM_OP_GROUP,
VM_OP_KIND_VM, namespace, name);
if (crApi.isEmpty()) {
logger.warning(() -> "Trying to patch " + namespace + "/" + name
+ " which does not exist.");
return;
}
// Patch running
PatchOptions patchOpts = new PatchOptions();
patchOpts.setFieldManager("kubernetes-java-kubectl-apply");
var res = crApi.get().patch(namespace, name,
V1Patch.PATCH_FORMAT_JSON_PATCH,
new V1Patch("[{\"op\": \"replace\", \"path\": "
+ "\"/spec/vm/state\", "
+ "\"value\": \"" + (running ? "Running" : "Stopped")
+ "\"}]"),
patchOpts);
if (!res.isSuccess()) {
logger.warning(
() -> "Cannot patch pod annotations: " + res.getStatus());
}
}
}

View file

@ -33,8 +33,10 @@ import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Logger;
import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jdrupes.vmoperator.util.K8s;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
@ -120,7 +122,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
= json.deserialize(json.serialize(asmData), JsonObject.class);
// Get metadata from VM definition
var vmMeta = GsonPtr.to(channel.vmDefinition()).to("spec")
var vmMeta = GsonPtr.to(channel.vmDefinition().getRaw()).to("spec")
.get(JsonObject.class, LOAD_BALANCER_SERVICE)
.map(JsonObject::deepCopy).orElseGet(() -> new JsonObject());

View file

@ -21,7 +21,12 @@ package org.jdrupes.vmoperator.manager;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
@ -35,39 +40,34 @@ import org.jdrupes.vmoperator.util.FsdUtils;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.Components;
import org.jgrapes.core.NamedChannel;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.HandlingError;
import org.jgrapes.core.events.Stop;
import org.jgrapes.http.HttpServer;
import org.jgrapes.http.InMemorySessionManager;
import org.jgrapes.http.LanguageSelector;
import org.jgrapes.http.events.Request;
import org.jgrapes.io.NioDispatcher;
import org.jgrapes.io.util.PermitsPool;
import org.jgrapes.net.SocketServer;
import org.jgrapes.util.ComponentCollector;
import org.jgrapes.util.FileSystemWatcher;
import org.jgrapes.util.YamlConfigurationStore;
import org.jgrapes.util.events.WatchFile;
import org.jgrapes.webconlet.locallogin.LoginConlet;
import org.jgrapes.webconsole.base.BrowserLocalBackedKVStore;
import org.jgrapes.webconsole.base.ConletComponentFactory;
import org.jgrapes.webconsole.base.ConsoleWeblet;
import org.jgrapes.webconsole.base.KVStoreBasedConsolePolicy;
import org.jgrapes.webconsole.base.PageResourceProviderFactory;
import org.jgrapes.webconsole.base.WebConsole;
import org.jgrapes.webconsole.rbac.RoleConfigurator;
import org.jgrapes.webconsole.rbac.RoleConletFilter;
import org.jgrapes.webconsole.vuejs.VueJsConsoleWeblet;
/**
* The application class. In framework term, this is the root component.
* Two of its child components, the {@link Controller} and the WebGui
* implement user-visible functions. The others are used internally.
*
* ![Manager components](manager-components.svg)
*
* @startuml manager-components.svg
* skinparam component {
* BackGroundColor #FEFECE
* BorderColor #A80036
* BorderThickness 1.25
* BackgroundColor<<internal>> #F1F1F1
* BorderColor<<internal>> #181818
* BorderThickness<<internal>> 1
* }
*
* [Manager]
* [Manager] *--> [Controller]
* [Manager] *--> [WebGui]
* [NioDispatcher] <<internal>>
* [Manager] *--> [NioDispatcher] <<internal>>
* [FileSystemWatcher] <<internal>>
* [Manager] *--> [FileSystemWatcher] <<internal>>
* @enduml
* The application class.
*/
public class Manager extends Component {
@ -79,22 +79,78 @@ public class Manager extends Component {
*
* @throws IOException Signals that an I/O exception has occurred.
*/
@SuppressWarnings("PMD.TooFewBranchesForASwitchStatement")
public Manager(CommandLine cmdLine) throws IOException {
// Prepare component tree
attach(new NioDispatcher());
attach(new FileSystemWatcher(channel()));
attach(new Controller(channel()));
Channel mgrChannel = new NamedChannel("manager");
attach(new FileSystemWatcher(mgrChannel));
attach(new Controller(mgrChannel));
// Configuration store with file in /etc/opt (default)
File config = new File(cmdLine.getOptionValue('c',
"/etc/opt/" + VM_OP_NAME.replace("-", "") + "/config.yaml"));
File cfgFile = new File(cmdLine.getOptionValue('c',
"/etc/opt/" + VM_OP_NAME.replace("-", "") + "/config.yaml"))
.getCanonicalFile();
logger.config(() -> "Using configuration from: " + cfgFile.getPath());
// Don't rely on night config to produce a good exception
// for this simple case
if (!Files.isReadable(config.toPath())) {
throw new IOException("Cannot read configuration file " + config);
if (!Files.isReadable(cfgFile.toPath())) {
throw new IOException("Cannot read configuration file " + cfgFile);
}
attach(new YamlConfigurationStore(channel(), config, false));
fire(new WatchFile(config.toPath()));
attach(new YamlConfigurationStore(mgrChannel, cfgFile, false));
fire(new WatchFile(cfgFile.toPath()));
// Prepare GUI
Channel httpTransport = new NamedChannel("guiTransport");
attach(new SocketServer(httpTransport)
.setConnectionLimiter(new PermitsPool(300))
.setMinimalPurgeableTime(1000)
.setServerAddress(new InetSocketAddress(8080))
.setName("GuiSocketServer"));
// Create an HTTP server as converter between transport and application
// layer.
Channel httpChannel = new NamedChannel("guiHttp");
HttpServer guiHttpServer = attach(new HttpServer(httpChannel,
httpTransport, Request.In.Get.class, Request.In.Post.class));
guiHttpServer.setName("GuiHttpServer");
// Build HTTP application layer
guiHttpServer.attach(new InMemorySessionManager(httpChannel));
guiHttpServer.attach(new LanguageSelector(httpChannel));
URI rootUri;
try {
rootUri = new URI("/");
} catch (URISyntaxException e) {
// Cannot happen
return;
}
ConsoleWeblet consoleWeblet = guiHttpServer
.attach(new VueJsConsoleWeblet(httpChannel, Channel.SELF, rootUri))
.prependClassTemplateLoader(getClass())
.prependResourceBundleProvider(getClass())
.prependConsoleResourceProvider(getClass());
consoleWeblet.setName("ConsoleWeblet");
WebConsole console = consoleWeblet.console();
console.attach(new BrowserLocalBackedKVStore(
console.channel(), consoleWeblet.prefix().getPath()));
console.attach(new KVStoreBasedConsolePolicy(console.channel()));
console.attach(new RoleConfigurator(console.channel()));
console.attach(new RoleConletFilter(console.channel()));
console.attach(new LoginConlet(console.channel()));
// Add all available page resource providers
console.attach(new ComponentCollector<>(
PageResourceProviderFactory.class, console.channel()));
// Add all available conlets
console.attach(new ComponentCollector<>(
ConletComponentFactory.class, console.channel(), type -> {
if (LoginConlet.class.getName().equals(type)) {
// Explicitly added, see above
return Collections.emptyList();
} else {
return Arrays.asList(Collections.emptyMap());
}
}));
}
/**
@ -148,10 +204,12 @@ public class Manager extends Component {
@SuppressWarnings("PMD.SignatureDeclareThrowsException")
public static void main(String[] args) {
try {
// Instance logger is not available yet.
var logger = Logger.getLogger(Manager.class.getName());
logger.fine(() -> "Version: "
logger.config(() -> "Version: "
+ Manager.class.getPackage().getImplementationVersion());
logger.fine(() -> "running on " + System.getProperty("java.vm.name")
logger.config(() -> "running on "
+ System.getProperty("java.vm.name")
+ " (" + System.getProperty("java.vm.version") + ")"
+ " from " + System.getProperty("java.vm.vendor"));
@ -165,7 +223,7 @@ public class Manager extends Component {
// The Manager is the root component
app = new Manager(cmd);
// Prepare Stop
// Prepare generation of Stop event
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
app.fire(new Stop());

View file

@ -33,7 +33,7 @@ import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNotFoundException;
import io.kubernetes.client.custom.Quantity;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
@ -43,12 +43,12 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_GROUP;
import org.jdrupes.vmoperator.manager.VmDefChanged.Type;
import org.jdrupes.vmoperator.util.Convertions;
import org.jdrupes.vmoperator.common.Convertions;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jdrupes.vmoperator.util.K8s;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.annotation.Handler;
@ -129,7 +129,7 @@ public class Reconciler extends Component {
@SuppressWarnings("PMD.SingularField")
private final Configuration fmConfig;
private final ConfigMapReconciler cmReconciler;
private final StatefuleSetReconciler stsReconciler;
private final StatefulSetReconciler stsReconciler;
private final LoadBalancerReconciler lbReconciler;
@SuppressWarnings("PMD.UseConcurrentHashMap")
private final Map<String, Object> config = new HashMap<>();
@ -153,7 +153,7 @@ public class Reconciler extends Component {
fmConfig.setClassForTemplateLoading(Reconciler.class, "");
cmReconciler = new ConfigMapReconciler(fmConfig);
stsReconciler = new StatefuleSetReconciler(fmConfig);
stsReconciler = new StatefulSetReconciler(fmConfig);
lbReconciler = new LoadBalancerReconciler(fmConfig);
}
@ -186,38 +186,65 @@ public class Reconciler extends Component {
@SuppressWarnings("PMD.ConfusingTernary")
public void onVmDefChanged(VmDefChanged event, VmChannel channel)
throws ApiException, TemplateException, IOException {
var defMeta = event.object().getMetadata();
// We're only interested in "spec" changes.
if (!event.specChanged()) {
return;
}
// Ownership relationships takes care of deletions
var defMeta = event.vmDefinition().getMetadata();
if (event.type() == Type.DELETED) {
logger.fine(() -> "VM \"" + defMeta.getName() + "\" deleted");
return;
}
// Get full definition and associate with channel
var apiVersion = K8s.version(event.object().getApiVersion());
DynamicKubernetesApi vmCrApi = new DynamicKubernetesApi(VM_OP_GROUP,
apiVersion, event.crd().getName(), channel.client());
K8s.get(vmCrApi, defMeta).ifPresent(def -> channel
.setVmDefinition(patchCr(def.getRaw().deepCopy())));
// Reconcile
Map<String, Object> model = prepareModel(channel.vmDefinition());
// Reconcile, use "augmented" vm definition for model
Map<String, Object> model = prepareModel(patchCr(event.vmDefinition()));
var configMap = cmReconciler.reconcile(event, model, channel);
model.put("cm", configMap.getRaw());
stsReconciler.reconcile(event, model, channel);
lbReconciler.reconcile(event, model, channel);
}
private DynamicKubernetesObject patchCr(DynamicKubernetesObject vmDef) {
var json = vmDef.getRaw().deepCopy();
// Adjust cdromImage path
var disks
= GsonPtr.to(json).to("spec", "vm", "disks").get(JsonArray.class);
for (var disk : disks) {
var cdrom = (JsonObject) ((JsonObject) disk).get("cdrom");
if (cdrom == null) {
continue;
}
String image = cdrom.get("image").getAsString();
if (image.isEmpty()) {
continue;
}
try {
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
var imageUri = new URI("file://" + Constants.IMAGE_REPO_PATH
+ "/").resolve(image);
if ("file".equals(imageUri.getScheme())) {
cdrom.addProperty("image", imageUri.getPath());
} else {
cdrom.addProperty("image", imageUri.toString());
}
} catch (URISyntaxException e) {
logger.warning(() -> "Invalid CDROM image: " + image);
}
}
return new DynamicKubernetesObject(json);
}
@SuppressWarnings("PMD.CognitiveComplexity")
private Map<String, Object> prepareModel(JsonObject vmDef)
private Map<String, Object> prepareModel(DynamicKubernetesObject vmDef)
throws TemplateModelException {
@SuppressWarnings("PMD.UseConcurrentHashMap")
Map<String, Object> model = new HashMap<>();
model.put("managerVersion",
Optional.ofNullable(Reconciler.class.getPackage()
.getImplementationVersion()).orElse("(Unknown)"));
model.put("cr", vmDef);
model.put("cr", vmDef.getRaw());
model.put("constants",
(TemplateHashModel) new DefaultObjectWrapperBuilder(
Configuration.VERSION_2_3_32)
@ -274,33 +301,4 @@ public class Reconciler extends Component {
return model;
}
private JsonObject patchCr(JsonObject vmDef) {
// Adjust cdromImage path
var disks
= GsonPtr.to(vmDef).to("spec", "vm", "disks").get(JsonArray.class);
for (var disk : disks) {
var cdrom = (JsonObject) ((JsonObject) disk).get("cdrom");
if (cdrom == null) {
continue;
}
String image = cdrom.get("image").getAsString();
if (image.isEmpty()) {
continue;
}
try {
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
var imageUri = new URI("file://" + Constants.IMAGE_REPO_PATH
+ "/").resolve(image);
if ("file".equals(imageUri.getScheme())) {
cdrom.addProperty("image", imageUri.getPath());
} else {
cdrom.addProperty("image", imageUri.toString());
}
} catch (URISyntaxException e) {
logger.warning(() -> "Invalid CDROM image: " + image);
}
}
return vmDef;
}
}

View file

@ -29,8 +29,10 @@ import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;
import java.util.logging.Logger;
import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.jdrupes.vmoperator.util.K8s;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
@ -39,7 +41,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
* Delegee for reconciling the stateful set (effectively the pod).
*/
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
/* default */ class StatefuleSetReconciler {
/* default */ class StatefulSetReconciler {
protected final Logger logger = Logger.getLogger(getClass().getName());
private final Configuration fmConfig;
@ -49,7 +51,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
*
* @param fmConfig the fm config
*/
public StatefuleSetReconciler(Configuration fmConfig) {
public StatefulSetReconciler(Configuration fmConfig) {
this.fmConfig = fmConfig;
}
@ -68,7 +70,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
throws IOException, TemplateException, ApiException {
DynamicKubernetesApi stsApi = new DynamicKubernetesApi("apps", "v1",
"statefulsets", channel.client());
var metadata = event.object().getMetadata();
var metadata = event.vmDefinition().getMetadata();
// Combine template and data and parse result
var fmTemplate = fmConfig.getTemplate("runnerSts.ftl.yaml");

View file

@ -1,115 +0,0 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager;
import com.google.gson.JsonObject;
import io.kubernetes.client.openapi.ApiClient;
import org.jgrapes.core.Channel;
import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.Subchannel.DefaultSubchannel;
/**
* A subchannel used to send the events related to a specific VM.
*/
@SuppressWarnings("PMD.DataClass")
public class VmChannel extends DefaultSubchannel {
private final EventPipeline pipeline;
private final ApiClient client;
private JsonObject vmDefinition;
private long generation = -1;
/**
* Instantiates a new watch channel.
*
* @param mainChannel the main channel
* @param pipeline the pipeline
* @param client the client
*/
public VmChannel(Channel mainChannel, EventPipeline pipeline,
ApiClient client) {
super(mainChannel);
this.pipeline = pipeline;
this.client = client;
}
/**
* Sets the last known definition of the resource.
*
* @param definition the definition
* @return the watch channel
*/
@SuppressWarnings("PMD.LinguisticNaming")
public VmChannel setVmDefinition(JsonObject definition) {
this.vmDefinition = definition;
return this;
}
/**
* Returns the last known definition of the VM.
*
* @return the json object
*/
public JsonObject vmDefinition() {
return vmDefinition;
}
/**
* Gets the last processed generation. Returns -1 if no
* definition has been processed yet.
*
* @return the generation
*/
public long generation() {
return generation;
}
/**
* Sets the last processed generation.
*
* @param generation the generation to set
* @return true if value has changed
*/
@SuppressWarnings("PMD.LinguisticNaming")
public boolean setGeneration(long generation) {
if (this.generation == generation) {
return false;
}
this.generation = generation;
return true;
}
/**
* Returns the pipeline.
*
* @return the event pipeline
*/
public EventPipeline pipeline() {
return pipeline;
}
/**
* Returns the API client.
*
* @return the API client
*/
public ApiClient client() {
return client;
}
}

View file

@ -1,95 +0,0 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager;
import io.kubernetes.client.openapi.models.V1APIResource;
import io.kubernetes.client.openapi.models.V1Namespace;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
/**
* Indicates a change in a VM definition.
*/
public class VmDefChanged extends Event<Void> {
/**
* The type of change.
*/
public enum Type {
ADDED, MODIFIED, DELETED
}
private final Type type;
private final V1APIResource crd;
private final V1Namespace object;
/**
* Instantiates a new VM changed event.
*
* @param type the type
* @param crd the crd
* @param object the object
*/
public VmDefChanged(Type type, V1APIResource crd, V1Namespace object) {
this.type = type;
this.crd = crd;
this.object = object;
}
/**
* Returns the type.
*
* @return the type
*/
public Type type() {
return type;
}
/**
* Returns the Crd.
*
* @return the v 1 API resource
*/
public V1APIResource crd() {
return crd;
}
/**
* Returns the object.
*
* @return the object.
*/
public V1Namespace object() {
return object;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(Components.objectName(this)).append(" [")
.append(object.getMetadata().getName()).append(' ').append(type);
if (channels() != null) {
builder.append(", channels=");
builder.append(Channel.toString(channels()));
}
builder.append(']');
return builder.toString();
}
}

View file

@ -44,11 +44,14 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import static org.jdrupes.vmoperator.common.Constants.VM_OP_GROUP;
import org.jdrupes.vmoperator.common.K8s;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_GROUP;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_KIND_VM;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.manager.VmDefChanged.Type;
import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged;
import org.jdrupes.vmoperator.manager.events.VmDefChanged.Type;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.Components;
@ -107,15 +110,8 @@ public class VmWatcher extends Component {
namespaceToWatch = Files.lines(path).findFirst().orElse(null);
}
}
if (namespaceToWatch == null) {
logger.severe(() -> "Namespace to watch not configured and"
+ " no file in kubernetes directory.");
event.cancel(true);
fire(new Stop());
return;
}
logger
.fine(() -> "Controlling namespace \"" + namespaceToWatch + "\".");
// Availability already checked by Controller.onStart
logger.fine(() -> "Watching namespace \"" + namespaceToWatch + "\".");
// Get all our API versions
var client = Config.defaultClient();
@ -140,8 +136,7 @@ public class VmWatcher extends Component {
}
}
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
"PMD.CognitiveComplexity" })
@SuppressWarnings("PMD.CognitiveComplexity")
private void purge(ApiClient client, CustomObjectsApi coa,
List<String> vmOpApiVersions) throws ApiException {
// Get existing CRs (VMs)
@ -161,6 +156,8 @@ public class VmWatcher extends Component {
+ "app.kubernetes.io/name=" + APP_NAME);
for (String resource : List.of("apps/v1/statefulsets",
"v1/configmaps", "v1/secrets")) {
@SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
"PMD.AvoidDuplicateLiterals" })
var resParts = new LinkedList<>(List.of(resource.split("/")));
var group = resParts.size() == 3 ? resParts.poll() : "";
var version = resParts.poll();
@ -271,13 +268,20 @@ public class VmWatcher extends Component {
return;
}
// Filter duplicates
if (!"DELETED".equals(item.type) && !channel
.setGeneration(item.object.getMetadata().getGeneration())) {
return;
}
// if (event.type() == Type.DELETED) {
// Get full definition and associate with channel as backup
var apiVersion = K8s.version(item.object.getApiVersion());
DynamicKubernetesApi vmCrApi = new DynamicKubernetesApi(VM_OP_GROUP,
apiVersion, vmsCrd.getName(), channel.client());
var vmDef = K8s.get(vmCrApi, metadata);
vmDef.ifPresent(def -> channel.setVmDefinition(def));
// Create and fire event
channel.pipeline().fire(new VmDefChanged(VmDefChanged.Type
.valueOf(item.type), vmsCrd, item.object), channel);
.valueOf(item.type),
channel.setGeneration(item.object.getMetadata().getGeneration()),
vmsCrd, vmDef.orElse(channel.vmDefinition())), channel);
}
/**
@ -289,7 +293,7 @@ public class VmWatcher extends Component {
@Handler(priority = -10_000)
public void onVmDefChanged(VmDefChanged event, VmChannel channel) {
if (event.type() == Type.DELETED) {
channels.remove(event.object().getMetadata().getName());
channels.remove(event.vmDefinition().getMetadata().getName());
}
}

View file

@ -16,4 +16,151 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager;
/**
* The following diagram shows the components of the manager application.
*
* In framework terms, the {@link org.jdrupes.vmoperator.manager.Manager}
* is the root component of the application. Two of its child components,
* the {@link org.jdrupes.vmoperator.manager.Controller} and the WebGui
* provide the functions that are apparent to the user.
*
* The position of the components in the component tree is important
* when writing the configuration file for the manager and therefore
* shown below. In order to keep the diagram readable, the
* components attached to the
* {@link org.jgrapes.webconsole.base.WebConsole} are shown in a
* separate diagram further down.
*
* ![Manager components](manager-components.svg)
*
* Component hierarchy of the web console:
*
* ![Web console components](console-components.svg)
*
* The components marked as "&lt;&lt;internal&gt;&gt;" have no
* configuration options or use their default values when used
* in this application.
*
* As an example, the following YAML configures a different port for the
* GUI and some users. The relationship to the component tree should
* be obvious.
* ```
* "/Manager":
* "/GuiSocketServer":
* port: 8888
* "/GuiHttpServer":
* "/ConsoleWeblet":
* "/WebConsole":
* "/LoginConlet":
* users:
* ...
* ```
*
* Developers may also be interested in the usage of channels
* by the application's component:
*
* ![Main channels](app-channels.svg)
*
* @startuml manager-components.svg
* skinparam component {
* BackGroundColor #FEFECE
* BorderColor #A80036
* BorderThickness 1.25
* BackgroundColor<<internal>> #F1F1F1
* BorderColor<<internal>> #181818
* BorderThickness<<internal>> 1
* }
* skinparam packageStyle rectangle
*
* Component NioDispatcher as NioDispatcher <<internal>>
* [Manager] *-up- [NioDispatcher]
* Component FileSystemWatcher as FileSystemWatcher <<internal>>
* [Manager] *-up- [FileSystemWatcher]
* Component YamlConfigurationStore as YamlConfigurationStore <<internal>>
* [Manager] *-left- [YamlConfigurationStore]
* [YamlConfigurationStore] *-right[hidden]- [Controller]
*
* [Manager] *-- [Controller]
* [Controller] *-- [VmWatcher]
* [Controller] *-- [Reconciler]
* [Controller] -right[hidden]- [GuiHttpServer]
*
* [Manager] *-down- [GuiSocketServer:8080]
* [Manager] *-- [GuiHttpServer]
* Component PreferencesStore as PreferencesStore <<internal>>
* [GuiHttpServer] *-up- [PreferencesStore]
* Component InMemorySessionManager as InMemorySessionManager <<internal>>
* [GuiHttpServer] *-up- [InMemorySessionManager]
* Component LanguageSelector as LanguageSelector <<internal>>
* [GuiHttpServer] *-right- [LanguageSelector]
*
* package "Conceptual WebConsole" {
* [ConsoleWeblet] *-- [WebConsole]
* }
* [GuiHttpServer] *-- [ConsoleWeblet]
* @enduml
*
* @startuml console-components.svg
* skinparam component {
* BackGroundColor #FEFECE
* BorderColor #A80036
* BorderThickness 1.25
* BackgroundColor<<internal>> #F1F1F1
* BorderColor<<internal>> #181818
* BorderThickness<<internal>> 1
* }
* skinparam packageStyle rectangle
*
* Component BrowserLocalBackedKVStore as BrowserLocalBackedKVStore <<internal>>
* [WebConsole] *-up- [BrowserLocalBackedKVStore]
* Component KVStoreBasedConsolePolicy as KVStoreBasedConsolePolicy <<internal>>
* [WebConsole] *-up- [KVStoreBasedConsolePolicy]
*
* [WebConsole] *-- [RoleConfigurator]
* [WebConsole] *-- [RoleConletFilter]
* [WebConsole] *-left- [LoginConlet]
*
* Component "ComponentCollector\nfor page resources" as cpr <<internal>>
* [WebConsole] *-- [cpr]
* Component "ComponentCollector\nfor conlets" as cc <<internal>>
* [WebConsole] *-- [cc]
*
* package "Providers and Conlets" {
* [Some component]
* }
*
* [cpr] *-- [Some component]
* [cc] *-- [Some component]
* @enduml
*
* @startuml app-channels.svg
* skinparam packageStyle rectangle
*
* () "manager" as mgr
* mgr .left. [FileSystemWatcher]
* mgr .right. [YamlConfigurationStore]
* mgr .. [Controller]
* mgr .up. [VmWatcher]
* mgr .. [Reconciler]
*
* () "guiTransport" as hT
* hT .up. [GuiSocketServer:8080]
* hT .down. [GuiHttpServer]
*
* [YamlConfigurationStore] -right[hidden]- hT
*
* () "guiHttp" as http
* http .up. [GuiHttpServer]
*
* [PreferencesStore] .right. http
* [InMemorySessionManager] .up. http
* [LanguageSelector] .up. http
*
* package "Conceptual WebConsole" {
* [ConsoleWeblet] .left. http
* [ConsoleWeblet] *-down- [WebConsole]
* }
*
* @enduml
*/
package org.jdrupes.vmoperator.manager;