mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-06-22 22:40:01 -04:00
Squash merge branch 14-release into master
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
package io.xpipe.ext.system.incus;
|
||||
|
||||
import io.xpipe.app.ext.ContainerStoreState;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.CommandViewBase;
|
||||
import io.xpipe.core.process.*;
|
||||
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class IncusCommandView extends CommandViewBase {
|
||||
|
||||
private static ElevationFunction requiresElevation() {
|
||||
return new ElevationFunction() {
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
return "Incus";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSpecified() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(ShellControl shellControl) throws Exception {
|
||||
// This is not perfect as it does not respect custom locations for the Incus socket
|
||||
// Sadly the socket location changes based on the installation type, and we can't dynamically query the
|
||||
// path
|
||||
return !shellControl
|
||||
.command("test -S /var/lib/incus/unix.socket && test -w /var/lib/incus/unix.socket || "
|
||||
+ "test -S /var/snap/incus/common/incus/unix.socket && test -w /var/snap/incus/common/incus/unix.socket || "
|
||||
+ "test -S /var/snap/incus/common/incus/unix.socket.user && test -w /var/snap/incus/common/incus/unix.socket.user || "
|
||||
+ "test -S /var/lib/incus/unix.socket.user && test -w /var/lib/incus/unix.socket.user")
|
||||
.executeAndCheck();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public IncusCommandView(ShellControl shellControl) {
|
||||
super(shellControl);
|
||||
}
|
||||
|
||||
private static String formatErrorMessage(String s) {
|
||||
return s;
|
||||
}
|
||||
|
||||
private static <T extends Throwable> T convertException(T s) {
|
||||
return ErrorEvent.expectedIfContains(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CommandControl build(Consumer<CommandBuilder> builder) {
|
||||
var cmd = CommandBuilder.of().add("incus");
|
||||
builder.accept(cmd);
|
||||
return shellControl
|
||||
.command(cmd)
|
||||
.withErrorFormatter(IncusCommandView::formatErrorMessage)
|
||||
.withExceptionConverter(IncusCommandView::convertException)
|
||||
.elevated(requiresElevation());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IncusCommandView start() throws Exception {
|
||||
shellControl.start();
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isSupported() throws Exception {
|
||||
return shellControl
|
||||
.command("incus --help")
|
||||
.withErrorFormatter(IncusCommandView::formatErrorMessage)
|
||||
.withExceptionConverter(IncusCommandView::convertException)
|
||||
.executeAndCheck();
|
||||
}
|
||||
|
||||
public String version() throws Exception {
|
||||
return build(commandBuilder -> commandBuilder.add("version")).readStdoutOrThrow();
|
||||
}
|
||||
|
||||
public void start(String containerName) throws Exception {
|
||||
build(commandBuilder -> commandBuilder.add("start").addQuoted(containerName))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public void stop(String containerName) throws Exception {
|
||||
build(commandBuilder -> commandBuilder.add("stop").addQuoted(containerName))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public void pause(String containerName) throws Exception {
|
||||
build(commandBuilder -> commandBuilder.add("pause").addQuoted(containerName))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public CommandControl console(String containerName) {
|
||||
return build(commandBuilder -> commandBuilder.add("console").addQuoted(containerName));
|
||||
}
|
||||
|
||||
public CommandControl configEdit(String containerName) {
|
||||
return build(commandBuilder -> commandBuilder.add("config", "edit").addQuoted(containerName));
|
||||
}
|
||||
|
||||
public List<DataStoreEntryRef<IncusContainerStore>> listContainers(DataStoreEntryRef<IncusInstallStore> store)
|
||||
throws Exception {
|
||||
return listContainersAndStates().entrySet().stream()
|
||||
.map(s -> {
|
||||
boolean running = s.getValue().toLowerCase(Locale.ROOT).equals("running");
|
||||
var c = new IncusContainerStore(store, s.getKey(), null);
|
||||
var entry = DataStoreEntry.createNew(c.getContainerName(), c);
|
||||
entry.setStorePersistentState(ContainerStoreState.builder()
|
||||
.containerState(s.getValue())
|
||||
.running(running)
|
||||
.build());
|
||||
return Optional.of(entry.<IncusContainerStore>ref());
|
||||
})
|
||||
.flatMap(Optional::stream)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public String queryContainerState(String containerName) throws Exception {
|
||||
var states = listContainersAndStates();
|
||||
return states.getOrDefault(containerName, "?");
|
||||
}
|
||||
|
||||
private Map<String, String> listContainersAndStates() throws Exception {
|
||||
try (var c = build(commandBuilder -> commandBuilder.add("list", "-f", "csv", "-c", "ns"))
|
||||
.start()) {
|
||||
var output = c.readStdoutOrThrow();
|
||||
return output.lines()
|
||||
.collect(Collectors.toMap(
|
||||
s -> s.trim().split(",")[0], s -> s.trim().split(",")[1], (x, y) -> y, LinkedHashMap::new));
|
||||
}
|
||||
}
|
||||
|
||||
public ShellControl exec(String container, String user) {
|
||||
return shellControl
|
||||
.subShell(createOpenFunction(container, user, false), createOpenFunction(container, user, true))
|
||||
.withErrorFormatter(IncusCommandView::formatErrorMessage)
|
||||
.withExceptionConverter(IncusCommandView::convertException)
|
||||
.elevated(requiresElevation());
|
||||
}
|
||||
|
||||
private ShellOpenFunction createOpenFunction(String containerName, String user, boolean terminal) {
|
||||
return new ShellOpenFunction() {
|
||||
@Override
|
||||
public CommandBuilder prepareWithoutInitCommand() {
|
||||
var b = execCommand(containerName, terminal).add("su", "-l");
|
||||
if (user != null) {
|
||||
b.addQuoted(user);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandBuilder prepareWithInitCommand(@NonNull String command) {
|
||||
var b = execCommand(containerName, terminal).add("su", "-l");
|
||||
if (user != null) {
|
||||
b.addQuoted(user);
|
||||
}
|
||||
return b.add("--session-command").addLiteral(command);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public CommandBuilder execCommand(String containerName, boolean terminal) {
|
||||
var c = CommandBuilder.of().add("incus", "exec", terminal ? "-t" : "-T");
|
||||
return c.addQuoted(containerName).add("--");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.xpipe.ext.system.incus;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.ext.base.store.StorePauseAction;
|
||||
import io.xpipe.ext.base.store.StoreRestartAction;
|
||||
import io.xpipe.ext.base.store.StoreStartAction;
|
||||
import io.xpipe.ext.base.store.StoreStopAction;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IncusContainerActionMenu implements ActionProvider {
|
||||
|
||||
@Override
|
||||
public BranchDataStoreCallSite<?> getBranchDataStoreCallSite() {
|
||||
return new BranchDataStoreCallSite<IncusContainerStore>() {
|
||||
|
||||
@Override
|
||||
public Class<IncusContainerStore> getApplicableClass() {
|
||||
return IncusContainerStore.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMajor(DataStoreEntryRef<IncusContainerStore> o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(DataStoreEntryRef<IncusContainerStore> store) {
|
||||
return AppI18n.observable("containerActions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(DataStoreEntryRef<IncusContainerStore> store) {
|
||||
return "mdi2p-package-variant-closed";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ActionProvider> getChildren(DataStoreEntryRef<IncusContainerStore> store) {
|
||||
return List.of(
|
||||
new StoreStartAction(),
|
||||
new StoreStopAction(),
|
||||
new StorePauseAction(),
|
||||
new StoreRestartAction(),
|
||||
new IncusContainerConsoleAction(),
|
||||
new IncusContainerEditConfigAction(),
|
||||
new IncusContainerEditRunConfigAction());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package io.xpipe.ext.system.incus;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.terminal.TerminalLauncher;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
public class IncusContainerConsoleAction implements ActionProvider {
|
||||
|
||||
@Override
|
||||
public LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
|
||||
return new LeafDataStoreCallSite<IncusContainerStore>() {
|
||||
|
||||
@Override
|
||||
public ActionProvider.Action createAction(DataStoreEntryRef<IncusContainerStore> store) {
|
||||
return new Action(store.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<IncusContainerStore> getApplicableClass() {
|
||||
return IncusContainerStore.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(DataStoreEntryRef<IncusContainerStore> store) {
|
||||
return AppI18n.observable("serialConsole");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(DataStoreEntryRef<IncusContainerStore> store) {
|
||||
return "mdi2c-console";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresValidStore() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Value
|
||||
static class Action implements ActionProvider.Action {
|
||||
|
||||
DataStoreEntry store;
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
var d = (IncusContainerStore) store.getStore();
|
||||
var view = new IncusCommandView(
|
||||
d.getInstall().getStore().getHost().getStore().getOrStartSession());
|
||||
TerminalLauncher.open(store.getName(), view.console(d.getContainerName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package io.xpipe.ext.system.incus;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.terminal.TerminalLauncher;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
public class IncusContainerEditConfigAction implements ActionProvider {
|
||||
|
||||
@Override
|
||||
public LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
|
||||
return new LeafDataStoreCallSite<IncusContainerStore>() {
|
||||
|
||||
@Override
|
||||
public ActionProvider.Action createAction(DataStoreEntryRef<IncusContainerStore> store) {
|
||||
return new Action(store.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<IncusContainerStore> getApplicableClass() {
|
||||
return IncusContainerStore.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(DataStoreEntryRef<IncusContainerStore> store) {
|
||||
return AppI18n.observable("editConfiguration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(DataStoreEntryRef<IncusContainerStore> store) {
|
||||
return "mdi2f-file-document-edit";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresValidStore() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Value
|
||||
static class Action implements ActionProvider.Action {
|
||||
|
||||
DataStoreEntry store;
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
var d = (IncusContainerStore) store.getStore();
|
||||
var view = new IncusCommandView(
|
||||
d.getInstall().getStore().getHost().getStore().getOrStartSession());
|
||||
TerminalLauncher.open(store.getName(), view.configEdit(d.getContainerName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package io.xpipe.ext.system.incus;
|
||||
|
||||
import io.xpipe.app.browser.BrowserFullSessionModel;
|
||||
import io.xpipe.app.browser.file.BrowserFileOpener;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
public class IncusContainerEditRunConfigAction implements ActionProvider {
|
||||
|
||||
@Override
|
||||
public LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
|
||||
return new LeafDataStoreCallSite<IncusContainerStore>() {
|
||||
|
||||
@Override
|
||||
public ActionProvider.Action createAction(DataStoreEntryRef<IncusContainerStore> store) {
|
||||
return new Action(store.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<IncusContainerStore> getApplicableClass() {
|
||||
return IncusContainerStore.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(DataStoreEntryRef<IncusContainerStore> store) {
|
||||
return AppI18n.observable("editRunConfiguration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(DataStoreEntryRef<IncusContainerStore> store) {
|
||||
return "mdi2m-movie-edit";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresValidStore() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Value
|
||||
static class Action implements ActionProvider.Action {
|
||||
|
||||
DataStoreEntry store;
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
var d = (IncusContainerStore) store.getStore();
|
||||
var elevatedRef = ProcessControlProvider.get()
|
||||
.elevated(d.getInstall().getStore().getHost().get().ref());
|
||||
var file = new FilePath("/run/incus/" + d.getContainerName() + "/lxc.conf");
|
||||
var model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(
|
||||
elevatedRef, m -> file.getParent().toString(), null, true);
|
||||
var found = model.findFile(file.toString());
|
||||
if (found.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
AppLayoutModel.get().selectBrowser();
|
||||
BrowserFileOpener.openInTextEditor(model, found.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package io.xpipe.ext.system.incus;
|
||||
|
||||
import io.xpipe.app.ext.ContainerStoreState;
|
||||
import io.xpipe.app.ext.ShellControlFunction;
|
||||
import io.xpipe.app.ext.ShellControlParentStoreFunction;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import io.xpipe.core.store.FixedChildStore;
|
||||
import io.xpipe.core.store.StatefulDataStore;
|
||||
import io.xpipe.ext.base.identity.IdentityValue;
|
||||
import io.xpipe.ext.base.store.PauseableStore;
|
||||
import io.xpipe.ext.base.store.StartableStore;
|
||||
import io.xpipe.ext.base.store.StoppableStore;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
@JsonTypeName("incusContainer")
|
||||
@SuperBuilder
|
||||
@Jacksonized
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@Value
|
||||
public class IncusContainerStore
|
||||
implements ShellStore,
|
||||
FixedChildStore,
|
||||
StatefulDataStore<ContainerStoreState>,
|
||||
StartableStore,
|
||||
StoppableStore,
|
||||
PauseableStore {
|
||||
|
||||
DataStoreEntryRef<IncusInstallStore> install;
|
||||
String containerName;
|
||||
IdentityValue identity;
|
||||
|
||||
@Override
|
||||
public Class<ContainerStoreState> getStateClass() {
|
||||
return ContainerStoreState.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws Throwable {
|
||||
Validators.nonNull(install);
|
||||
Validators.isType(install, IncusInstallStore.class);
|
||||
install.checkComplete();
|
||||
Validators.nonNull(containerName);
|
||||
if (identity != null) {
|
||||
identity.checkComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt getFixedId() {
|
||||
return OptionalInt.of(Objects.hash(containerName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShellControlFunction shellFunction() {
|
||||
return new ShellControlParentStoreFunction() {
|
||||
|
||||
@Override
|
||||
public ShellStore getParentStore() {
|
||||
return getInstall().getStore().getHost().getStore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShellControl control(ShellControl parent) {
|
||||
var user = identity != null ? identity.unwrap().getUsername() : null;
|
||||
var sc = new IncusCommandView(parent).exec(containerName, user);
|
||||
sc.withSourceStore(IncusContainerStore.this);
|
||||
if (identity != null && identity.unwrap().getPassword() != null) {
|
||||
sc.setElevationHandler(new BaseElevationHandler(
|
||||
IncusContainerStore.this, identity.unwrap().getPassword())
|
||||
.orElse(sc.getElevationHandler()));
|
||||
}
|
||||
sc.withShellStateInit(IncusContainerStore.this);
|
||||
sc.onStartupFail(throwable -> {
|
||||
if (throwable instanceof LicenseRequiredException) {
|
||||
return;
|
||||
}
|
||||
|
||||
var s = getState().toBuilder()
|
||||
.running(false)
|
||||
.containerState("Connection failed")
|
||||
.build();
|
||||
setState(s);
|
||||
});
|
||||
|
||||
return sc;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void refreshContainerState(ShellControl sc) throws Exception {
|
||||
var state = getState();
|
||||
var view = new IncusCommandView(sc);
|
||||
var displayState = view.queryContainerState(containerName);
|
||||
var running = "RUNNING".equals(displayState);
|
||||
var newState =
|
||||
state.toBuilder().containerState(displayState).running(running).build();
|
||||
setState(newState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
var sc = getInstall().getStore().getHost().getStore().getOrStartSession();
|
||||
var view = new IncusCommandView(sc);
|
||||
view.start(containerName);
|
||||
refreshContainerState(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
var sc = getInstall().getStore().getHost().getStore().getOrStartSession();
|
||||
var view = new IncusCommandView(sc);
|
||||
view.stop(containerName);
|
||||
refreshContainerState(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() throws Exception {
|
||||
var sc = getInstall().getStore().getHost().getStore().getOrStartSession();
|
||||
var view = new IncusCommandView(sc);
|
||||
view.pause(containerName);
|
||||
refreshContainerState(sc);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package io.xpipe.ext.system.incus;
|
||||
|
||||
import io.xpipe.app.comp.store.StoreChoiceComp;
|
||||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.comp.store.StoreSection;
|
||||
import io.xpipe.app.comp.store.StoreViewState;
|
||||
import io.xpipe.app.ext.ContainerStoreState;
|
||||
import io.xpipe.app.ext.GuiDialog;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
import io.xpipe.ext.base.identity.IdentityChoice;
|
||||
import io.xpipe.ext.base.store.ShellStoreProvider;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IncusContainerStoreProvider implements ShellStoreProvider {
|
||||
|
||||
@Override
|
||||
public String getDisplayIconFileName(DataStore store) {
|
||||
return "system:lxd_icon.svg";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShow(StoreEntryWrapper w) {
|
||||
IncusContainerStore s = w.getEntry().getStore().asNeeded();
|
||||
var state = s.getState();
|
||||
return Boolean.TRUE.equals(state.getRunning())
|
||||
|| s.getInstall().getStore().getState().isShowNonRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShowScan() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String createInsightsMarkdown(DataStore store) {
|
||||
var c = (IncusContainerStore) store;
|
||||
return String.format(
|
||||
"""
|
||||
XPipe will execute:
|
||||
```
|
||||
%s
|
||||
```
|
||||
in a host shell of `%s` to open a shell into the container.
|
||||
""",
|
||||
new IncusCommandView(null)
|
||||
.execCommand(c.getContainerName(), true)
|
||||
.buildSimple(),
|
||||
c.getInstall().getStore().getHost().get().getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataStoreEntry getDisplayParent(DataStoreEntry store) {
|
||||
IncusContainerStore s = store.getStore().asNeeded();
|
||||
return s.getInstall().get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {
|
||||
IncusContainerStore st = (IncusContainerStore) store.getValue();
|
||||
var identity = new SimpleObjectProperty<>(st.getIdentity());
|
||||
|
||||
var q = new OptionsBuilder()
|
||||
.name("host")
|
||||
.description("lxdHostDescription")
|
||||
.addComp(StoreChoiceComp.host(
|
||||
new SimpleObjectProperty<>(st.getInstall().getStore().getHost()),
|
||||
StoreViewState.get().getAllConnectionsCategory()))
|
||||
.disable()
|
||||
.name("container")
|
||||
.description("lxdContainerDescription")
|
||||
.addString(new SimpleObjectProperty<>(st.getContainerName()), false)
|
||||
.disable()
|
||||
.sub(IdentityChoice.container(identity), identity)
|
||||
.bind(
|
||||
() -> {
|
||||
return IncusContainerStore.builder()
|
||||
.containerName(st.getContainerName())
|
||||
.install(st.getInstall())
|
||||
.identity(identity.getValue())
|
||||
.build();
|
||||
},
|
||||
store)
|
||||
.buildDialog();
|
||||
return q;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String summaryString(StoreEntryWrapper wrapper) {
|
||||
IncusContainerStore s = wrapper.getEntry().getStore().asNeeded();
|
||||
return DataStoreFormatter.toApostropheName(
|
||||
s.getInstall().getStore().getHost().get()) + " container";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> informationString(StoreSection section) {
|
||||
return ShellStoreFormat.shellStore(
|
||||
section, (ContainerStoreState s) -> DataStoreFormatter.capitalize(s.getContainerState()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleNames() {
|
||||
return List.of("incusContainer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<?>> getStoreClasses() {
|
||||
return List.of(IncusContainerStore.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package io.xpipe.ext.system.incus;
|
||||
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.CommandSupport;
|
||||
import io.xpipe.app.util.FixedHierarchyStore;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.store.DataStoreState;
|
||||
import io.xpipe.core.store.FixedChildStore;
|
||||
import io.xpipe.core.store.StatefulDataStore;
|
||||
import io.xpipe.ext.base.SelfReferentialStore;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@JsonTypeName("incusInstall")
|
||||
@SuperBuilder
|
||||
@Jacksonized
|
||||
@Getter
|
||||
@Value
|
||||
public class IncusInstallStore
|
||||
implements FixedHierarchyStore, StatefulDataStore<IncusInstallStore.State>, SelfReferentialStore {
|
||||
|
||||
DataStoreEntryRef<ShellStore> host;
|
||||
|
||||
public IncusInstallStore(DataStoreEntryRef<ShellStore> host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws Throwable {
|
||||
Validators.nonNull(host);
|
||||
Validators.isType(host, ShellStore.class);
|
||||
host.checkComplete();
|
||||
}
|
||||
|
||||
private void updateState() throws Exception {
|
||||
var sc = getHost().getStore().getOrStartSession();
|
||||
var view = new IncusCommandView(sc);
|
||||
var out = view.version();
|
||||
var namePattern = Pattern.compile("Server version:\\s+(.+)");
|
||||
var nameMatcher = namePattern.matcher(out);
|
||||
var v = nameMatcher.find() ? nameMatcher.group(1) : null;
|
||||
var reachable = v != null && !"unreachable".equals(v);
|
||||
setState(getState().toBuilder()
|
||||
.serverVersion(reachable ? v : null)
|
||||
.reachable(reachable)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends DataStoreEntryRef<? extends FixedChildStore>> listChildren() throws Exception {
|
||||
var sc = getHost().getStore().getOrStartSession();
|
||||
var view = new IncusCommandView(sc);
|
||||
CommandSupport.isSupported(() -> view.isSupported(), "Incus CLI client (incus)", host.get());
|
||||
updateState();
|
||||
return view.listContainers(getSelfEntry().ref());
|
||||
}
|
||||
|
||||
@Value
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Jacksonized
|
||||
public static class State extends DataStoreState {
|
||||
String serverVersion;
|
||||
boolean reachable;
|
||||
boolean showNonRunning;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package io.xpipe.ext.system.incus;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.store.*;
|
||||
import io.xpipe.app.ext.DataStoreProvider;
|
||||
import io.xpipe.app.ext.DataStoreUsageCategory;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.DataStoreFormatter;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IncusInstallStoreProvider implements DataStoreProvider {
|
||||
|
||||
@Override
|
||||
public DataStoreUsageCategory getUsageCategory() {
|
||||
return DataStoreUsageCategory.GROUP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
|
||||
var nonRunning = StoreToggleComp.<IncusInstallStore>childrenToggle(
|
||||
null, true, sec, s -> s.getState().isShowNonRunning(), (s, aBoolean) -> {
|
||||
var state =
|
||||
s.getState().toBuilder().showNonRunning(aBoolean).build();
|
||||
s.setState(state);
|
||||
});
|
||||
return StoreEntryComp.create(sec, nonRunning, preferLarge);
|
||||
}
|
||||
|
||||
public Comp<?> stateDisplay(StoreEntryWrapper w) {
|
||||
return new SystemStateComp(BindingsHelper.map(w.getPersistentState(), o -> {
|
||||
var state = (IncusInstallStore.State) o;
|
||||
if (state.isReachable()) {
|
||||
return SystemStateComp.State.SUCCESS;
|
||||
}
|
||||
|
||||
return SystemStateComp.State.FAILURE;
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataStoreEntry getDisplayParent(DataStoreEntry store) {
|
||||
IncusInstallStore s = store.getStore().asNeeded();
|
||||
return s.getHost().get();
|
||||
}
|
||||
|
||||
public String summaryString(StoreEntryWrapper wrapper) {
|
||||
IncusInstallStore s = wrapper.getEntry().getStore().asNeeded();
|
||||
return DataStoreFormatter.toApostropheName(s.getHost().get()) + " containers";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> informationString(StoreSection section) {
|
||||
return BindingsHelper.map(section.getWrapper().getPersistentState(), o -> {
|
||||
var state = (IncusInstallStore.State) o;
|
||||
return state.isReachable() ? "incus v" + state.getServerVersion() : "Connection failed";
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayIconFileName(DataStore store) {
|
||||
return "system:lxd_icon.svg";
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataStore defaultStore() {
|
||||
return new IncusInstallStore(DataStorage.get().local().ref());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleNames() {
|
||||
return List.of("incusInstall");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<?>> getStoreClasses() {
|
||||
return List.of(IncusInstallStore.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package io.xpipe.ext.system.incus;
|
||||
|
||||
import io.xpipe.app.ext.ScanProvider;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
|
||||
public class IncusScanProvider extends ScanProvider {
|
||||
|
||||
@Override
|
||||
public ScanOpportunity create(DataStoreEntry entry, ShellControl sc) throws Exception {
|
||||
if (sc.getOsType() != OsType.LINUX) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScanOpportunity("system.incusContainers", !new IncusCommandView(sc).isSupported(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scan(DataStoreEntry entry, ShellControl sc) {
|
||||
var e = DataStorage.get()
|
||||
.addStoreIfNotPresent(
|
||||
entry,
|
||||
"Incus containers",
|
||||
IncusInstallStore.builder().host(entry.ref()).build());
|
||||
DataStorage.get().refreshChildren(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package io.xpipe.ext.system.lxd;
|
||||
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.CommandSupport;
|
||||
import io.xpipe.app.util.FixedHierarchyStore;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.store.DataStoreState;
|
||||
import io.xpipe.core.store.FixedChildStore;
|
||||
import io.xpipe.core.store.StatefulDataStore;
|
||||
import io.xpipe.ext.base.SelfReferentialStore;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@JsonTypeName("lxdCmd")
|
||||
@SuperBuilder
|
||||
@Jacksonized
|
||||
@Value
|
||||
public class LxdCmdStore implements FixedHierarchyStore, StatefulDataStore<LxdCmdStore.State>, SelfReferentialStore {
|
||||
|
||||
DataStoreEntryRef<ShellStore> host;
|
||||
|
||||
public LxdCmdStore(DataStoreEntryRef<ShellStore> host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws Throwable {
|
||||
Validators.nonNull(host);
|
||||
Validators.isType(host, ShellStore.class);
|
||||
host.checkComplete();
|
||||
}
|
||||
|
||||
private void updateState(LxdCommandView view) throws Exception {
|
||||
var out = view.version();
|
||||
var namePattern = Pattern.compile("Server version:\\s+(.+)");
|
||||
var nameMatcher = namePattern.matcher(out);
|
||||
var v = nameMatcher.find() ? nameMatcher.group(1) : null;
|
||||
var reachable = v != null && !"unreachable".equals(v);
|
||||
setState(getState().toBuilder()
|
||||
.serverVersion(reachable ? v : null)
|
||||
.reachable(reachable)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends DataStoreEntryRef<? extends FixedChildStore>> listChildren() throws Exception {
|
||||
var sc = getHost().getStore().getOrStartSession();
|
||||
var view = new LxdCommandView(sc);
|
||||
CommandSupport.isSupported(() -> view.isSupported(), "LXD CLI client (lxc)", host.get());
|
||||
updateState(view);
|
||||
return view.listContainers(getSelfEntry().ref());
|
||||
}
|
||||
|
||||
@Value
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Jacksonized
|
||||
public static class State extends DataStoreState {
|
||||
String serverVersion;
|
||||
boolean reachable;
|
||||
boolean showNonRunning;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package io.xpipe.ext.system.lxd;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.store.*;
|
||||
import io.xpipe.app.ext.DataStoreProvider;
|
||||
import io.xpipe.app.ext.DataStoreUsageCategory;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.DataStoreFormatter;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class LxdCmdStoreProvider implements DataStoreProvider {
|
||||
|
||||
@Override
|
||||
public DataStoreUsageCategory getUsageCategory() {
|
||||
return DataStoreUsageCategory.GROUP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
|
||||
var nonRunning = StoreToggleComp.<LxdCmdStore>childrenToggle(
|
||||
null, true, sec, s -> s.getState().isShowNonRunning(), (s, aBoolean) -> {
|
||||
var state =
|
||||
s.getState().toBuilder().showNonRunning(aBoolean).build();
|
||||
s.setState(state);
|
||||
});
|
||||
return StoreEntryComp.create(sec, nonRunning, preferLarge);
|
||||
}
|
||||
|
||||
public Comp<?> stateDisplay(StoreEntryWrapper w) {
|
||||
return new SystemStateComp(BindingsHelper.map(w.getPersistentState(), o -> {
|
||||
var state = (LxdCmdStore.State) o;
|
||||
if (state.isReachable()) {
|
||||
return SystemStateComp.State.SUCCESS;
|
||||
}
|
||||
|
||||
return SystemStateComp.State.FAILURE;
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataStoreEntry getDisplayParent(DataStoreEntry store) {
|
||||
LxdCmdStore s = store.getStore().asNeeded();
|
||||
return s.getHost().get();
|
||||
}
|
||||
|
||||
public String summaryString(StoreEntryWrapper wrapper) {
|
||||
LxdCmdStore s = wrapper.getEntry().getStore().asNeeded();
|
||||
return DataStoreFormatter.toApostropheName(s.getHost().get()) + " containers";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> informationString(StoreSection section) {
|
||||
return BindingsHelper.map(section.getWrapper().getPersistentState(), o -> {
|
||||
var state = (LxdCmdStore.State) o;
|
||||
return state.isReachable() ? "lxd v" + state.getServerVersion() : "Connection failed";
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayIconFileName(DataStore store) {
|
||||
return "system:lxd_icon.svg";
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataStore defaultStore() {
|
||||
return new LxdCmdStore(DataStorage.get().local().ref());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleNames() {
|
||||
return List.of("lxdCmd");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<?>> getStoreClasses() {
|
||||
return List.of(LxdCmdStore.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package io.xpipe.ext.system.lxd;
|
||||
|
||||
import io.xpipe.app.ext.ContainerStoreState;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.CommandViewBase;
|
||||
import io.xpipe.core.process.*;
|
||||
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class LxdCommandView extends CommandViewBase {
|
||||
|
||||
private static ElevationFunction requiresElevation() {
|
||||
return new ElevationFunction() {
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
return "LXD";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSpecified() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(ShellControl shellControl) throws Exception {
|
||||
// This is not perfect as it does not respect custom locations for the LXD socket
|
||||
// Sadly the socket location changes based on the installation type, and we can't dynamically query the
|
||||
// path
|
||||
return !shellControl
|
||||
.command(
|
||||
"test -S /var/lib/lxd/unix.socket && test -w /var/lib/lxd/unix.socket || test -S /var/snap/lxd/common/lxd/unix.socket && test -w /var/snap/lxd/common/lxd/unix.socket")
|
||||
.executeAndCheck();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public LxdCommandView(ShellControl shellControl) {
|
||||
super(shellControl);
|
||||
}
|
||||
|
||||
private static String formatErrorMessage(String s) {
|
||||
return s;
|
||||
}
|
||||
|
||||
private static <T extends Throwable> T convertException(T s) {
|
||||
return ErrorEvent.expectedIfContains(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CommandControl build(Consumer<CommandBuilder> builder) {
|
||||
var cmd = CommandBuilder.of().add("lxc");
|
||||
builder.accept(cmd);
|
||||
return shellControl
|
||||
.command(cmd)
|
||||
.withErrorFormatter(LxdCommandView::formatErrorMessage)
|
||||
.withExceptionConverter(LxdCommandView::convertException)
|
||||
.elevated(requiresElevation());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LxdCommandView start() throws Exception {
|
||||
shellControl.start();
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isSupported() throws Exception {
|
||||
return shellControl
|
||||
.command("lxc --help")
|
||||
.withErrorFormatter(LxdCommandView::formatErrorMessage)
|
||||
.withExceptionConverter(LxdCommandView::convertException)
|
||||
.executeAndCheck();
|
||||
}
|
||||
|
||||
public String version() throws Exception {
|
||||
return shellControl
|
||||
.command("lxc version")
|
||||
.withErrorFormatter(LxdCommandView::formatErrorMessage)
|
||||
.withExceptionConverter(LxdCommandView::convertException)
|
||||
.elevated(requiresElevation())
|
||||
.readStdoutOrThrow();
|
||||
}
|
||||
|
||||
public String queryContainerState(String containerName) throws Exception {
|
||||
var states = listContainersAndStates();
|
||||
return states.getOrDefault(containerName, "?");
|
||||
}
|
||||
|
||||
public void start(String containerName) throws Exception {
|
||||
build(commandBuilder -> commandBuilder.add("start").addQuoted(containerName))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public void stop(String containerName) throws Exception {
|
||||
build(commandBuilder -> commandBuilder.add("stop").addQuoted(containerName))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public void pause(String containerName) throws Exception {
|
||||
build(commandBuilder -> commandBuilder.add("pause").addQuoted(containerName))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public CommandControl console(String containerName) {
|
||||
return build(commandBuilder -> commandBuilder.add("console").addQuoted(containerName));
|
||||
}
|
||||
|
||||
public CommandControl configEdit(String containerName) {
|
||||
return build(commandBuilder -> commandBuilder.add("config", "edit").addQuoted(containerName));
|
||||
}
|
||||
|
||||
public List<DataStoreEntryRef<LxdContainerStore>> listContainers(DataStoreEntryRef<LxdCmdStore> store)
|
||||
throws Exception {
|
||||
return listContainersAndStates().entrySet().stream()
|
||||
.map(s -> {
|
||||
boolean running = s.getValue().toLowerCase(Locale.ROOT).equals("running");
|
||||
var c = LxdContainerStore.builder()
|
||||
.cmd(store)
|
||||
.containerName(s.getKey())
|
||||
.build();
|
||||
var entry = DataStoreEntry.createNew(c.getContainerName(), c);
|
||||
entry.setStorePersistentState(ContainerStoreState.builder()
|
||||
.containerState(s.getValue())
|
||||
.running(running)
|
||||
.build());
|
||||
return Optional.of(entry.<LxdContainerStore>ref());
|
||||
})
|
||||
.flatMap(Optional::stream)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Map<String, String> listContainersAndStates() throws Exception {
|
||||
try (var c = build(commandBuilder -> commandBuilder.add("list", "-f", "csv", "-c", "ns"))
|
||||
.start()) {
|
||||
var output = c.readStdoutOrThrow();
|
||||
return output.lines()
|
||||
.collect(Collectors.toMap(
|
||||
s -> s.trim().split(",")[0], s -> s.trim().split(",")[1], (x, y) -> y, LinkedHashMap::new));
|
||||
} catch (ProcessOutputException ex) {
|
||||
if (ex.getOutput().contains("Error: unknown shorthand flag: 'f' in -f")) {
|
||||
throw ErrorEvent.expected(ProcessOutputException.withParagraph("Unsupported legacy LXD version", ex));
|
||||
} else {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ShellControl exec(String container, String user) {
|
||||
return shellControl
|
||||
.subShell(createOpenFunction(container, user, false), createOpenFunction(container, user, true))
|
||||
.withErrorFormatter(LxdCommandView::formatErrorMessage)
|
||||
.withExceptionConverter(LxdCommandView::convertException)
|
||||
.elevated(requiresElevation());
|
||||
}
|
||||
|
||||
private ShellOpenFunction createOpenFunction(String containerName, String user, boolean terminal) {
|
||||
return new ShellOpenFunction() {
|
||||
@Override
|
||||
public CommandBuilder prepareWithoutInitCommand() {
|
||||
var b = execCommand(containerName, terminal).add("su", "-l");
|
||||
if (user != null) {
|
||||
b.addQuoted(user);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandBuilder prepareWithInitCommand(@NonNull String command) {
|
||||
var b = execCommand(containerName, terminal).add("su", "-l");
|
||||
if (user != null) {
|
||||
b.addQuoted(user);
|
||||
}
|
||||
return b.add("--session-command").addLiteral(command);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public CommandBuilder execCommand(String containerName, boolean terminal) {
|
||||
var c = CommandBuilder.of().add("lxc", "exec", terminal ? "-t" : "-T");
|
||||
return c.addQuoted(containerName).add("--");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.xpipe.ext.system.lxd;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.ext.base.store.StorePauseAction;
|
||||
import io.xpipe.ext.base.store.StoreRestartAction;
|
||||
import io.xpipe.ext.base.store.StoreStartAction;
|
||||
import io.xpipe.ext.base.store.StoreStopAction;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class LxdContainerActionMenu implements ActionProvider {
|
||||
|
||||
@Override
|
||||
public BranchDataStoreCallSite<?> getBranchDataStoreCallSite() {
|
||||
return new BranchDataStoreCallSite<LxdContainerStore>() {
|
||||
|
||||
@Override
|
||||
public Class<LxdContainerStore> getApplicableClass() {
|
||||
return LxdContainerStore.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMajor(DataStoreEntryRef<LxdContainerStore> o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(DataStoreEntryRef<LxdContainerStore> store) {
|
||||
return AppI18n.observable("containerActions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(DataStoreEntryRef<LxdContainerStore> store) {
|
||||
return "mdi2p-package-variant-closed";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ActionProvider> getChildren(DataStoreEntryRef<LxdContainerStore> store) {
|
||||
return List.of(
|
||||
new StoreStartAction(),
|
||||
new StoreStopAction(),
|
||||
new StorePauseAction(),
|
||||
new StoreRestartAction(),
|
||||
new LxdContainerConsoleAction(),
|
||||
new LxdContainerEditConfigAction(),
|
||||
new LxdContainerEditRunConfigAction());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package io.xpipe.ext.system.lxd;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.terminal.TerminalLauncher;
|
||||
import io.xpipe.ext.system.incus.IncusCommandView;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
public class LxdContainerConsoleAction implements ActionProvider {
|
||||
|
||||
@Override
|
||||
public LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
|
||||
return new LeafDataStoreCallSite<LxdContainerStore>() {
|
||||
|
||||
@Override
|
||||
public ActionProvider.Action createAction(DataStoreEntryRef<LxdContainerStore> store) {
|
||||
return new Action(store.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<LxdContainerStore> getApplicableClass() {
|
||||
return LxdContainerStore.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(DataStoreEntryRef<LxdContainerStore> store) {
|
||||
return AppI18n.observable("serialConsole");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(DataStoreEntryRef<LxdContainerStore> store) {
|
||||
return "mdi2c-console";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresValidStore() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Value
|
||||
static class Action implements ActionProvider.Action {
|
||||
|
||||
DataStoreEntry store;
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
var d = (LxdContainerStore) store.getStore();
|
||||
var view = new IncusCommandView(
|
||||
d.getCmd().getStore().getHost().getStore().getOrStartSession());
|
||||
TerminalLauncher.open(store.getName(), view.console(d.getContainerName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package io.xpipe.ext.system.lxd;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.terminal.TerminalLauncher;
|
||||
import io.xpipe.ext.system.incus.IncusCommandView;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
public class LxdContainerEditConfigAction implements ActionProvider {
|
||||
|
||||
@Override
|
||||
public LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
|
||||
return new LeafDataStoreCallSite<LxdContainerStore>() {
|
||||
|
||||
@Override
|
||||
public ActionProvider.Action createAction(DataStoreEntryRef<LxdContainerStore> store) {
|
||||
return new Action(store.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<LxdContainerStore> getApplicableClass() {
|
||||
return LxdContainerStore.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(DataStoreEntryRef<LxdContainerStore> store) {
|
||||
return AppI18n.observable("editConfiguration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(DataStoreEntryRef<LxdContainerStore> store) {
|
||||
return "mdi2f-file-document-edit";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresValidStore() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Value
|
||||
static class Action implements ActionProvider.Action {
|
||||
|
||||
DataStoreEntry store;
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
var d = (LxdContainerStore) store.getStore();
|
||||
var view = new IncusCommandView(
|
||||
d.getCmd().getStore().getHost().getStore().getOrStartSession());
|
||||
TerminalLauncher.open(store.getName(), view.configEdit(d.getContainerName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package io.xpipe.ext.system.lxd;
|
||||
|
||||
import io.xpipe.app.browser.BrowserFullSessionModel;
|
||||
import io.xpipe.app.browser.file.BrowserFileOpener;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
public class LxdContainerEditRunConfigAction implements ActionProvider {
|
||||
|
||||
@Override
|
||||
public LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
|
||||
return new LeafDataStoreCallSite<LxdContainerStore>() {
|
||||
|
||||
@Override
|
||||
public ActionProvider.Action createAction(DataStoreEntryRef<LxdContainerStore> store) {
|
||||
return new Action(store.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<LxdContainerStore> getApplicableClass() {
|
||||
return LxdContainerStore.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(DataStoreEntryRef<LxdContainerStore> store) {
|
||||
return AppI18n.observable("editRunConfiguration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(DataStoreEntryRef<LxdContainerStore> store) {
|
||||
return "mdi2m-movie-edit";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresValidStore() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Value
|
||||
static class Action implements ActionProvider.Action {
|
||||
|
||||
DataStoreEntry store;
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
var d = (LxdContainerStore) store.getStore();
|
||||
var elevatedRef = ProcessControlProvider.get()
|
||||
.elevated(d.getCmd().getStore().getHost().get().ref());
|
||||
var file = new FilePath("/run/lxd/" + d.getContainerName() + "/lxc.conf");
|
||||
var model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(
|
||||
elevatedRef, m -> file.getParent().toString(), null, true);
|
||||
var found = model.findFile(file.toString());
|
||||
if (found.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
AppLayoutModel.get().selectBrowser();
|
||||
BrowserFileOpener.openInTextEditor(model, found.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package io.xpipe.ext.system.lxd;
|
||||
|
||||
import io.xpipe.app.ext.ContainerStoreState;
|
||||
import io.xpipe.app.ext.ShellControlFunction;
|
||||
import io.xpipe.app.ext.ShellControlParentStoreFunction;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import io.xpipe.core.store.FixedChildStore;
|
||||
import io.xpipe.core.store.StatefulDataStore;
|
||||
import io.xpipe.ext.base.identity.IdentityValue;
|
||||
import io.xpipe.ext.base.store.PauseableStore;
|
||||
import io.xpipe.ext.base.store.StartableStore;
|
||||
import io.xpipe.ext.base.store.StoppableStore;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
@JsonTypeName("lxd")
|
||||
@SuperBuilder
|
||||
@Jacksonized
|
||||
@Value
|
||||
@AllArgsConstructor
|
||||
public class LxdContainerStore
|
||||
implements ShellStore,
|
||||
FixedChildStore,
|
||||
StatefulDataStore<ContainerStoreState>,
|
||||
StartableStore,
|
||||
StoppableStore,
|
||||
PauseableStore {
|
||||
|
||||
DataStoreEntryRef<LxdCmdStore> cmd;
|
||||
String containerName;
|
||||
IdentityValue identity;
|
||||
|
||||
@Override
|
||||
public Class<ContainerStoreState> getStateClass() {
|
||||
return ContainerStoreState.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws Throwable {
|
||||
Validators.nonNull(cmd);
|
||||
Validators.isType(cmd, LxdCmdStore.class);
|
||||
cmd.checkComplete();
|
||||
Validators.nonNull(containerName);
|
||||
if (identity != null) {
|
||||
identity.checkComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt getFixedId() {
|
||||
return OptionalInt.of(Objects.hash(containerName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShellControlFunction shellFunction() {
|
||||
return new ShellControlParentStoreFunction() {
|
||||
|
||||
@Override
|
||||
public ShellStore getParentStore() {
|
||||
return getCmd().getStore().getHost().getStore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShellControl control(ShellControl parent) {
|
||||
var user = identity != null ? identity.unwrap().getUsername() : null;
|
||||
var base = new LxdCommandView(parent).exec(containerName, user);
|
||||
if (identity != null && identity.unwrap().getPassword() != null) {
|
||||
base.setElevationHandler(new BaseElevationHandler(
|
||||
LxdContainerStore.this, identity.unwrap().getPassword())
|
||||
.orElse(base.getElevationHandler()));
|
||||
}
|
||||
return base.withSourceStore(LxdContainerStore.this)
|
||||
.onInit(shellControl -> {
|
||||
var s = getState().toBuilder()
|
||||
.osType(shellControl.getOsType())
|
||||
.shellDialect(shellControl.getShellDialect())
|
||||
.ttyState(shellControl.getTtyState())
|
||||
.running(true)
|
||||
.osName(shellControl.getOsName())
|
||||
.build();
|
||||
setState(s);
|
||||
})
|
||||
.onStartupFail(throwable -> {
|
||||
if (throwable instanceof LicenseRequiredException) {
|
||||
return;
|
||||
}
|
||||
|
||||
var s = getState().toBuilder()
|
||||
.running(false)
|
||||
.containerState("Connection failed")
|
||||
.build();
|
||||
setState(s);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void refreshContainerState(ShellControl sc) throws Exception {
|
||||
var state = getState();
|
||||
var view = new LxdCommandView(sc);
|
||||
var displayState = view.queryContainerState(containerName);
|
||||
var running = "RUNNING".equals(displayState);
|
||||
var newState =
|
||||
state.toBuilder().containerState(displayState).running(running).build();
|
||||
setState(newState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
var sc = getCmd().getStore().getHost().getStore().getOrStartSession();
|
||||
var view = new LxdCommandView(sc);
|
||||
view.start(containerName);
|
||||
refreshContainerState(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
var sc = getCmd().getStore().getHost().getStore().getOrStartSession();
|
||||
var view = new LxdCommandView(sc);
|
||||
view.stop(containerName);
|
||||
refreshContainerState(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() throws Exception {
|
||||
var sc = getCmd().getStore().getHost().getStore().getOrStartSession();
|
||||
var view = new LxdCommandView(sc);
|
||||
view.pause(containerName);
|
||||
refreshContainerState(sc);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package io.xpipe.ext.system.lxd;
|
||||
|
||||
import io.xpipe.app.comp.store.StoreChoiceComp;
|
||||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.comp.store.StoreSection;
|
||||
import io.xpipe.app.comp.store.StoreViewState;
|
||||
import io.xpipe.app.ext.ContainerStoreState;
|
||||
import io.xpipe.app.ext.GuiDialog;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.DataStoreFormatter;
|
||||
import io.xpipe.app.util.OptionsBuilder;
|
||||
import io.xpipe.app.util.ShellStoreFormat;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
import io.xpipe.ext.base.identity.IdentityChoice;
|
||||
import io.xpipe.ext.base.store.ShellStoreProvider;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class LxdContainerStoreProvider implements ShellStoreProvider {
|
||||
|
||||
@Override
|
||||
public boolean shouldShow(StoreEntryWrapper w) {
|
||||
LxdContainerStore s = w.getEntry().getStore().asNeeded();
|
||||
var state = s.getState();
|
||||
return Boolean.TRUE.equals(state.getRunning())
|
||||
|| s.getCmd().getStore().getState().isShowNonRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShowScan() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String createInsightsMarkdown(DataStore store) {
|
||||
var lxd = (LxdContainerStore) store;
|
||||
return String.format(
|
||||
"""
|
||||
XPipe will execute:
|
||||
```
|
||||
%s
|
||||
```
|
||||
in a host shell of `%s` to open a shell into the container.
|
||||
""",
|
||||
new LxdCommandView(null)
|
||||
.execCommand(lxd.getContainerName(), true)
|
||||
.buildSimple(),
|
||||
lxd.getCmd().getStore().getHost().get().getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataStoreEntry getDisplayParent(DataStoreEntry store) {
|
||||
LxdContainerStore s = store.getStore().asNeeded();
|
||||
return s.getCmd().get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {
|
||||
LxdContainerStore st = (LxdContainerStore) store.getValue();
|
||||
var identity = new SimpleObjectProperty<>(st.getIdentity());
|
||||
|
||||
var q = new OptionsBuilder()
|
||||
.name("host")
|
||||
.description("lxdHostDescription")
|
||||
.addComp(StoreChoiceComp.host(
|
||||
new SimpleObjectProperty<>(st.getCmd().getStore().getHost()),
|
||||
StoreViewState.get().getAllConnectionsCategory()))
|
||||
.disable()
|
||||
.name("container")
|
||||
.description("lxdContainerDescription")
|
||||
.addString(new SimpleObjectProperty<>(st.getContainerName()), false)
|
||||
.disable()
|
||||
.sub(IdentityChoice.container(identity), identity)
|
||||
.bind(
|
||||
() -> {
|
||||
return LxdContainerStore.builder()
|
||||
.containerName(st.getContainerName())
|
||||
.cmd(st.getCmd())
|
||||
.identity(identity.getValue())
|
||||
.build();
|
||||
},
|
||||
store)
|
||||
.buildDialog();
|
||||
return q;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String summaryString(StoreEntryWrapper wrapper) {
|
||||
LxdContainerStore s = wrapper.getEntry().getStore().asNeeded();
|
||||
return DataStoreFormatter.toApostropheName(
|
||||
s.getCmd().getStore().getHost().get()) + " container";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> informationString(StoreSection section) {
|
||||
return ShellStoreFormat.shellStore(
|
||||
section, (ContainerStoreState s) -> DataStoreFormatter.capitalize(s.getContainerState()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleNames() {
|
||||
return List.of("lxd", "lxd_container");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<?>> getStoreClasses() {
|
||||
return List.of(LxdContainerStore.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package io.xpipe.ext.system.lxd;
|
||||
|
||||
import io.xpipe.app.ext.ScanProvider;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
|
||||
public class LxdScanProvider extends ScanProvider {
|
||||
|
||||
@Override
|
||||
public ScanOpportunity create(DataStoreEntry entry, ShellControl sc) throws Exception {
|
||||
if (sc.getOsType() != OsType.LINUX) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ScanOpportunity("system.lxdContainers", !new LxdCommandView(sc).isSupported(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scan(DataStoreEntry entry, ShellControl sc) {
|
||||
var e = DataStorage.get()
|
||||
.addStoreIfNotPresent(
|
||||
entry,
|
||||
"LXD containers",
|
||||
LxdCmdStore.builder().host(entry.ref()).build());
|
||||
DataStorage.get().refreshChildren(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package io.xpipe.ext.system.podman;
|
||||
|
||||
import io.xpipe.app.ext.ContainerStoreState;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.CommandSupport;
|
||||
import io.xpipe.app.util.FixedHierarchyStore;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import io.xpipe.core.store.*;
|
||||
import io.xpipe.ext.base.SelfReferentialStore;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@JsonTypeName("podmanCmd")
|
||||
@SuperBuilder
|
||||
@Jacksonized
|
||||
@Value
|
||||
public class PodmanCmdStore
|
||||
implements FixedHierarchyStore, StatefulDataStore<PodmanCmdStore.State>, SelfReferentialStore {
|
||||
|
||||
DataStoreEntryRef<ShellStore> host;
|
||||
|
||||
public PodmanCmdStore(DataStoreEntryRef<ShellStore> host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws Throwable {
|
||||
Validators.nonNull(host);
|
||||
Validators.isType(host, ShellStore.class);
|
||||
host.checkComplete();
|
||||
}
|
||||
|
||||
private List<DataStoreEntryRef<PodmanContainerStore>> listContainers(ShellControl sc) throws Exception {
|
||||
var view = new PodmanCommandView(sc);
|
||||
var l = view.container().listContainersAndStates();
|
||||
return l.stream()
|
||||
.map(s -> {
|
||||
boolean running =
|
||||
s.getStatus().startsWith("running") || s.getStatus().startsWith("up");
|
||||
var c = PodmanContainerStore.builder()
|
||||
.cmd(getSelfEntry().ref())
|
||||
.containerName(s.getName())
|
||||
.build();
|
||||
var entry = DataStoreEntry.createNew(s.getName(), c);
|
||||
entry.setStorePersistentState(ContainerStoreState.builder()
|
||||
.containerState(s.getStatus())
|
||||
.imageName(s.getImage())
|
||||
.running(running)
|
||||
.build());
|
||||
return entry.<PodmanContainerStore>ref();
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void updateState(ShellControl host) throws Exception {
|
||||
var out = new PodmanCommandView(host).version();
|
||||
|
||||
var namePattern = Pattern.compile("Server:\\s+(.+)");
|
||||
var nameMatcher = namePattern.matcher(out);
|
||||
var name = nameMatcher.find() ? nameMatcher.group(1) : null;
|
||||
|
||||
var versionPattern = Pattern.compile("Version:\\s+(.+)");
|
||||
var versionMatcher = versionPattern.matcher(out);
|
||||
var version = versionMatcher.find() ? versionMatcher.group(1) : null;
|
||||
|
||||
setState(getState().toBuilder()
|
||||
.running(true)
|
||||
.serverName(name)
|
||||
.version(version)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends DataStoreEntryRef<? extends FixedChildStore>> listChildren() throws Exception {
|
||||
var sc = getHost().getStore().getOrStartSession();
|
||||
var view = new PodmanCommandView(sc);
|
||||
CommandSupport.isSupported(() -> view.isSupported(), "Podman CLI", host.get());
|
||||
var running = view.isDaemonRunning();
|
||||
if (!running) {
|
||||
setState(getState().toBuilder().running(false).build());
|
||||
throw ErrorEvent.expected(new IllegalStateException("Podman daemon is not running"));
|
||||
}
|
||||
|
||||
updateState(sc);
|
||||
return listContainers(sc);
|
||||
}
|
||||
|
||||
@Value
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@SuperBuilder(toBuilder = true)
|
||||
@Jacksonized
|
||||
public static class State extends DataStoreState {
|
||||
String serverName;
|
||||
String version;
|
||||
boolean running;
|
||||
boolean showNonRunning;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package io.xpipe.ext.system.podman;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.store.StoreEntryComp;
|
||||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.comp.store.StoreSection;
|
||||
import io.xpipe.app.comp.store.StoreToggleComp;
|
||||
import io.xpipe.app.comp.store.SystemStateComp;
|
||||
import io.xpipe.app.ext.DataStoreProvider;
|
||||
import io.xpipe.app.ext.DataStoreUsageCategory;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.DataStoreFormatter;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PodmanCmdStoreProvider implements DataStoreProvider {
|
||||
|
||||
@Override
|
||||
public DataStoreUsageCategory getUsageCategory() {
|
||||
return DataStoreUsageCategory.GROUP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
|
||||
var nonRunning = StoreToggleComp.<PodmanCmdStore>childrenToggle(
|
||||
null, true, sec, s -> s.getState().isShowNonRunning(), (s, aBoolean) -> {
|
||||
s.setState(s.getState().toBuilder().showNonRunning(aBoolean).build());
|
||||
});
|
||||
return StoreEntryComp.create(sec, nonRunning, preferLarge);
|
||||
}
|
||||
|
||||
public Comp<?> stateDisplay(StoreEntryWrapper w) {
|
||||
return new SystemStateComp(BindingsHelper.map(w.getPersistentState(), o -> {
|
||||
var state = (PodmanCmdStore.State) o;
|
||||
if (state.isRunning()) {
|
||||
return SystemStateComp.State.SUCCESS;
|
||||
}
|
||||
|
||||
return SystemStateComp.State.FAILURE;
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataStoreEntry getDisplayParent(DataStoreEntry store) {
|
||||
PodmanCmdStore s = store.getStore().asNeeded();
|
||||
return s.getHost().get();
|
||||
}
|
||||
|
||||
public String summaryString(StoreEntryWrapper wrapper) {
|
||||
PodmanCmdStore s = wrapper.getEntry().getStore().asNeeded();
|
||||
return DataStoreFormatter.toApostropheName(s.getHost().get()) + " containers";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> informationString(StoreSection section) {
|
||||
return BindingsHelper.map(section.getWrapper().getPersistentState(), o -> {
|
||||
var state = (PodmanCmdStore.State) o;
|
||||
if (!state.isRunning()) {
|
||||
return "Connection failed";
|
||||
}
|
||||
|
||||
return (state.getServerName() != null ? state.getServerName() : "Podman") + " v" + state.getVersion();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayIconFileName(DataStore store) {
|
||||
return "system:podman_icon.svg";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleNames() {
|
||||
return List.of("podmanCmd");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<?>> getStoreClasses() {
|
||||
return List.of(PodmanCmdStore.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package io.xpipe.ext.system.podman;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.CommandView;
|
||||
import io.xpipe.app.util.CommandViewBase;
|
||||
import io.xpipe.core.process.*;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class PodmanCommandView extends CommandViewBase {
|
||||
|
||||
public PodmanCommandView(ShellControl shellControl) {
|
||||
super(shellControl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PodmanCommandView start() throws Exception {
|
||||
shellControl.start();
|
||||
return this;
|
||||
}
|
||||
|
||||
private static String formatErrorMessage(String s) {
|
||||
return "Podman connection failed:\n" + s;
|
||||
}
|
||||
|
||||
private static <T extends Throwable> T convertException(T s) {
|
||||
return ErrorEvent.expectedIfContains(
|
||||
s,
|
||||
"Error: unable to connect to Podman.",
|
||||
"no connection could be made because the target machine actively refused it.",
|
||||
"unable to connect to Podman socket",
|
||||
"no such container",
|
||||
"OCI runtime attempted to invoke a command that was not found");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CommandControl build(Consumer<CommandBuilder> builder) {
|
||||
var cmd = CommandBuilder.of().add("podman");
|
||||
builder.accept(cmd);
|
||||
return shellControl
|
||||
.command(cmd)
|
||||
.withErrorFormatter(PodmanCommandView::formatErrorMessage)
|
||||
.withExceptionConverter(PodmanCommandView::convertException);
|
||||
}
|
||||
|
||||
public boolean isSupported() throws Exception {
|
||||
return shellControl
|
||||
.command("podman --help")
|
||||
.withErrorFormatter(PodmanCommandView::formatErrorMessage)
|
||||
.withExceptionConverter(PodmanCommandView::convertException)
|
||||
.executeAndCheck();
|
||||
}
|
||||
|
||||
public String version() throws Exception {
|
||||
return build(commandBuilder -> commandBuilder.add("version")).readStdoutOrThrow();
|
||||
}
|
||||
|
||||
public boolean isDaemonRunning() throws Exception {
|
||||
return build(commandBuilder -> commandBuilder.add("version")).executeAndCheck();
|
||||
}
|
||||
|
||||
public Container container() {
|
||||
return new Container();
|
||||
}
|
||||
|
||||
public class Container extends CommandView {
|
||||
|
||||
@Override
|
||||
public Container start() throws Exception {
|
||||
shellControl.start();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CommandControl build(Consumer<CommandBuilder> builder) {
|
||||
return PodmanCommandView.this.build((b) -> {
|
||||
b.add("container");
|
||||
builder.accept(b);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ShellControl getShellControl() {
|
||||
return PodmanCommandView.this.getShellControl();
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class ContainerEntry {
|
||||
String name;
|
||||
String image;
|
||||
String status;
|
||||
}
|
||||
|
||||
public List<ContainerEntry> listContainersAndStates() throws Exception {
|
||||
if (!PodmanCommandView.this.isDaemonRunning()) {
|
||||
throw new IllegalStateException("Podman daemon is not running");
|
||||
}
|
||||
|
||||
try (var c = build(commandBuilder ->
|
||||
commandBuilder.add("ls -a --format=\"{{.Names}};{{.Image}};{{.Status}}\""))
|
||||
.start()) {
|
||||
var output = c.readStdoutOrThrow();
|
||||
return output.lines()
|
||||
.filter(s -> s.split(";").length == 3)
|
||||
.map(s -> new ContainerEntry(s.split(";")[0], s.split(";")[1], s.split(";")[2]))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
public ShellControl exec(String container) {
|
||||
return shellControl
|
||||
.subShell(createOpenFunction(container, false), createOpenFunction(container, true))
|
||||
.withErrorFormatter(PodmanCommandView::formatErrorMessage)
|
||||
.withExceptionConverter(PodmanCommandView::convertException);
|
||||
}
|
||||
|
||||
private ShellOpenFunction createOpenFunction(String containerName, boolean terminal) {
|
||||
return new ShellOpenFunction() {
|
||||
@Override
|
||||
public CommandBuilder prepareWithoutInitCommand() {
|
||||
return execCommand(terminal)
|
||||
.addQuoted(containerName)
|
||||
.add(ShellDialects.SH.getLaunchCommand().loginCommand());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandBuilder prepareWithInitCommand(@NonNull String command) {
|
||||
return execCommand(terminal).addQuoted(containerName).add(command);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public CommandBuilder execCommand(boolean terminal) {
|
||||
return CommandBuilder.of().add("podman", "container", "exec", terminal ? "-it" : "-i");
|
||||
}
|
||||
|
||||
public void start(String container) throws Exception {
|
||||
build(commandBuilder -> commandBuilder.add("start").addQuoted(container))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public void stop(String container) throws Exception {
|
||||
build(commandBuilder -> commandBuilder.add("stop").addQuoted(container))
|
||||
.execute();
|
||||
}
|
||||
|
||||
public String port(String container) throws Exception {
|
||||
return build(commandBuilder -> commandBuilder.add("port").addQuoted(container))
|
||||
.readStdoutOrThrow();
|
||||
}
|
||||
|
||||
public String inspect(String container) throws Exception {
|
||||
return build(commandBuilder -> commandBuilder.add("inspect").addQuoted(container))
|
||||
.readStdoutOrThrow();
|
||||
}
|
||||
|
||||
public CommandControl attach(String container) {
|
||||
return build(commandBuilder -> commandBuilder.add("attach").addQuoted(container));
|
||||
}
|
||||
|
||||
public CommandControl logs(String container) {
|
||||
return build(commandBuilder -> commandBuilder.add("logs").add("-f").addQuoted(container));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package io.xpipe.ext.system.podman;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.ext.base.store.StoreRestartAction;
|
||||
import io.xpipe.ext.base.store.StoreStartAction;
|
||||
import io.xpipe.ext.base.store.StoreStopAction;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PodmanContainerActionMenu implements ActionProvider {
|
||||
|
||||
@Override
|
||||
public BranchDataStoreCallSite<?> getBranchDataStoreCallSite() {
|
||||
return new BranchDataStoreCallSite<PodmanContainerStore>() {
|
||||
|
||||
@Override
|
||||
public Class<PodmanContainerStore> getApplicableClass() {
|
||||
return PodmanContainerStore.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMajor(DataStoreEntryRef<PodmanContainerStore> o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(DataStoreEntryRef<PodmanContainerStore> store) {
|
||||
return AppI18n.observable("containerActions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(DataStoreEntryRef<PodmanContainerStore> store) {
|
||||
return "mdi2p-package-variant-closed";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ActionProvider> getChildren(DataStoreEntryRef<PodmanContainerStore> store) {
|
||||
return List.of(
|
||||
new StoreStartAction(),
|
||||
new StoreStopAction(),
|
||||
new StoreRestartAction(),
|
||||
new PodmanContainerInspectAction(),
|
||||
new PodmanContainerLogsAction(),
|
||||
new PodmanContainerAttachAction());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package io.xpipe.ext.system.podman;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.terminal.TerminalLauncher;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
public class PodmanContainerAttachAction implements ActionProvider {
|
||||
|
||||
@Override
|
||||
public LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
|
||||
return new LeafDataStoreCallSite<PodmanContainerStore>() {
|
||||
|
||||
@Override
|
||||
public Action createAction(DataStoreEntryRef<PodmanContainerStore> store) {
|
||||
return () -> {
|
||||
var d = store.getStore();
|
||||
var view = d.commandView(
|
||||
d.getCmd().getStore().getHost().getStore().getOrStartSession());
|
||||
TerminalLauncher.open(store.get().getName(), view.attach(d.getContainerName()));
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<PodmanContainerStore> getApplicableClass() {
|
||||
return PodmanContainerStore.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(DataStoreEntryRef<PodmanContainerStore> store) {
|
||||
return AppI18n.observable("attachContainer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(DataStoreEntryRef<PodmanContainerStore> store) {
|
||||
return "mdi2a-attachment";
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.xpipe.ext.system.podman;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.FileOpener;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
public class PodmanContainerInspectAction implements ActionProvider {
|
||||
|
||||
@Override
|
||||
public LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
|
||||
return new LeafDataStoreCallSite<PodmanContainerStore>() {
|
||||
|
||||
@Override
|
||||
public ActionProvider.Action createAction(DataStoreEntryRef<PodmanContainerStore> store) {
|
||||
return new Action(store.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<PodmanContainerStore> getApplicableClass() {
|
||||
return PodmanContainerStore.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(DataStoreEntryRef<PodmanContainerStore> store) {
|
||||
return AppI18n.observable("inspectContainer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(DataStoreEntryRef<PodmanContainerStore> store) {
|
||||
return "mdi2i-information-outline";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Value
|
||||
static class Action implements ActionProvider.Action {
|
||||
|
||||
DataStoreEntry store;
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
var d = (PodmanContainerStore) store.getStore();
|
||||
var view = d.commandView(d.getCmd().getStore().getHost().getStore().getOrStartSession());
|
||||
var output = view.inspect(d.getContainerName());
|
||||
FileOpener.openReadOnlyString(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package io.xpipe.ext.system.podman;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.terminal.TerminalLauncher;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
public class PodmanContainerLogsAction implements ActionProvider {
|
||||
|
||||
@Override
|
||||
public LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
|
||||
return new LeafDataStoreCallSite<PodmanContainerStore>() {
|
||||
|
||||
@Override
|
||||
public ActionProvider.Action createAction(DataStoreEntryRef<PodmanContainerStore> store) {
|
||||
return new Action(store.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<PodmanContainerStore> getApplicableClass() {
|
||||
return PodmanContainerStore.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(DataStoreEntryRef<PodmanContainerStore> store) {
|
||||
return AppI18n.observable("containerLogs");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon(DataStoreEntryRef<PodmanContainerStore> store) {
|
||||
return "mdi2v-view-list-outline";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Value
|
||||
static class Action implements ActionProvider.Action {
|
||||
|
||||
DataStoreEntry store;
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
var d = (PodmanContainerStore) store.getStore();
|
||||
var view = d.commandView(d.getCmd().getStore().getHost().getStore().getOrStartSession());
|
||||
TerminalLauncher.open(store.getName(), view.logs(d.getContainerName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package io.xpipe.ext.system.podman;
|
||||
|
||||
import io.xpipe.app.ext.ContainerImageStore;
|
||||
import io.xpipe.app.ext.ContainerStoreState;
|
||||
import io.xpipe.app.ext.ShellControlFunction;
|
||||
import io.xpipe.app.ext.ShellControlParentStoreFunction;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.LicenseRequiredException;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import io.xpipe.core.store.FixedChildStore;
|
||||
import io.xpipe.core.store.InternalCacheDataStore;
|
||||
import io.xpipe.core.store.StatefulDataStore;
|
||||
import io.xpipe.ext.base.SelfReferentialStore;
|
||||
import io.xpipe.ext.base.service.AbstractServiceStore;
|
||||
import io.xpipe.ext.base.service.FixedServiceCreatorStore;
|
||||
import io.xpipe.ext.base.service.MappedServiceStore;
|
||||
import io.xpipe.ext.base.store.StartableStore;
|
||||
import io.xpipe.ext.base.store.StoppableStore;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@JsonTypeName("podman")
|
||||
@SuperBuilder
|
||||
@Jacksonized
|
||||
@Value
|
||||
public class PodmanContainerStore
|
||||
implements StartableStore,
|
||||
StoppableStore,
|
||||
ShellStore,
|
||||
InternalCacheDataStore,
|
||||
FixedChildStore,
|
||||
StatefulDataStore<ContainerStoreState>,
|
||||
FixedServiceCreatorStore,
|
||||
SelfReferentialStore,
|
||||
ContainerImageStore {
|
||||
|
||||
DataStoreEntryRef<PodmanCmdStore> cmd;
|
||||
String containerName;
|
||||
|
||||
@Override
|
||||
public String getImageName() {
|
||||
return getState().getImageName();
|
||||
}
|
||||
|
||||
public PodmanCommandView.Container commandView(ShellControl parent) {
|
||||
return new PodmanCommandView(parent).container();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
var view = commandView(getCmd().getStore().getHost().getStore().getOrStartSession());
|
||||
view.start(containerName);
|
||||
var state = getState().toBuilder().running(true).containerState("Up").build();
|
||||
setState(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
var view = commandView(getCmd().getStore().getHost().getStore().getOrStartSession());
|
||||
view.stop(containerName);
|
||||
var state =
|
||||
getState().toBuilder().running(false).containerState("Exited").build();
|
||||
setState(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends DataStoreEntryRef<? extends AbstractServiceStore>> createFixedServices() throws Exception {
|
||||
return findServices().stream()
|
||||
.map(s -> DataStoreEntry.createNew("Service", s).<MappedServiceStore>ref())
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<MappedServiceStore> findServices() throws Exception {
|
||||
var entry = getSelfEntry();
|
||||
var view = commandView(getCmd().getStore().getHost().getStore().getOrStartSession());
|
||||
var out = view.port(containerName);
|
||||
return out.lines()
|
||||
.map(l -> {
|
||||
var matcher = Pattern.compile("(\\d+)/\\w+\\s*->\\s*[^:]+?:(\\d+)")
|
||||
.matcher(l);
|
||||
if (!matcher.matches()) {
|
||||
return (MappedServiceStore) null;
|
||||
}
|
||||
|
||||
var containerPort = Integer.parseInt(matcher.group(1));
|
||||
var remotePort = Integer.parseInt(matcher.group(2));
|
||||
return MappedServiceStore.builder()
|
||||
.host(getCmd().getStore().getHost().asNeeded())
|
||||
.displayParent(entry.ref())
|
||||
.containerPort(containerPort)
|
||||
.remotePort(remotePort)
|
||||
.build();
|
||||
})
|
||||
.filter(dockerServiceStore -> dockerServiceStore != null)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<ContainerStoreState> getStateClass() {
|
||||
return ContainerStoreState.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt getFixedId() {
|
||||
return OptionalInt.of(Objects.hash(containerName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkComplete() throws Throwable {
|
||||
Validators.nonNull(cmd);
|
||||
Validators.isType(cmd, PodmanCmdStore.class);
|
||||
cmd.checkComplete();
|
||||
Validators.nonNull(containerName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShellControlFunction shellFunction() {
|
||||
return new ShellControlParentStoreFunction() {
|
||||
|
||||
@Override
|
||||
public ShellStore getParentStore() {
|
||||
return getCmd().getStore().getHost().getStore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShellControl control(ShellControl parent) {
|
||||
var pc = new PodmanCommandView(parent).container().exec(containerName);
|
||||
pc.withSourceStore(PodmanContainerStore.this);
|
||||
pc.withShellStateInit(PodmanContainerStore.this);
|
||||
pc.onInit(shellControl -> {
|
||||
var s = getState().toBuilder()
|
||||
.osType(shellControl.getOsType())
|
||||
.shellDialect(shellControl.getShellDialect())
|
||||
.ttyState(shellControl.getTtyState())
|
||||
.running(true)
|
||||
.osName(shellControl.getOsName())
|
||||
.build();
|
||||
setState(s);
|
||||
});
|
||||
pc.onStartupFail(throwable -> {
|
||||
if (throwable instanceof LicenseRequiredException) {
|
||||
return;
|
||||
}
|
||||
|
||||
var stateBuilder = getState().toBuilder();
|
||||
stateBuilder.running(false);
|
||||
var hasShell = throwable.getMessage() == null
|
||||
|| !throwable.getMessage().contains("OCI runtime exec failed");
|
||||
if (!hasShell) {
|
||||
stateBuilder.containerState("No shell available");
|
||||
} else {
|
||||
stateBuilder.containerState("Connection failed");
|
||||
}
|
||||
setState(stateBuilder.build());
|
||||
});
|
||||
return pc;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package io.xpipe.ext.system.podman;
|
||||
|
||||
import io.xpipe.app.comp.store.StoreChoiceComp;
|
||||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.comp.store.StoreSection;
|
||||
import io.xpipe.app.comp.store.StoreViewState;
|
||||
import io.xpipe.app.ext.ContainerStoreState;
|
||||
import io.xpipe.app.ext.GuiDialog;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.DataStoreFormatter;
|
||||
import io.xpipe.app.util.OptionsBuilder;
|
||||
import io.xpipe.app.util.ShellStoreFormat;
|
||||
import io.xpipe.app.util.SimpleValidator;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
import io.xpipe.ext.base.service.FixedServiceGroupStore;
|
||||
import io.xpipe.ext.base.store.ShellStoreProvider;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PodmanContainerStoreProvider implements ShellStoreProvider {
|
||||
|
||||
public void onParentRefresh(DataStoreEntry entry) {
|
||||
var services = FixedServiceGroupStore.builder().parent(entry.ref()).build();
|
||||
var servicesEntry = DataStorage.get().getStoreEntryIfPresent(services, false);
|
||||
if (servicesEntry.isPresent()) {
|
||||
DataStorage.get().refreshChildren(servicesEntry.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShow(StoreEntryWrapper w) {
|
||||
PodmanContainerStore s = w.getEntry().getStore().asNeeded();
|
||||
var state = s.getState();
|
||||
return Boolean.TRUE.equals(state.getRunning())
|
||||
|| s.getCmd().getStore().getState().isShowNonRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShowScan() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String createInsightsMarkdown(DataStore store) {
|
||||
var podman = (PodmanContainerStore) store;
|
||||
return String.format(
|
||||
"""
|
||||
XPipe will execute:
|
||||
```
|
||||
%s
|
||||
```
|
||||
in a host shell of `%s` to open a shell into the container.
|
||||
""",
|
||||
podman.commandView(null).execCommand(true).buildSimple(),
|
||||
podman.getCmd().get().getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataStoreEntry getDisplayParent(DataStoreEntry store) {
|
||||
PodmanContainerStore s = store.getStore().asNeeded();
|
||||
return s.getCmd().get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {
|
||||
var val = new SimpleValidator();
|
||||
PodmanContainerStore st = (PodmanContainerStore) store.getValue();
|
||||
|
||||
var q = new OptionsBuilder()
|
||||
.name("host")
|
||||
.description("podmanHostDescription")
|
||||
.addComp(StoreChoiceComp.host(
|
||||
new SimpleObjectProperty<>(
|
||||
st.getCmd() != null ? st.getCmd().getStore().getHost() : null),
|
||||
StoreViewState.get().getAllConnectionsCategory()))
|
||||
.disable()
|
||||
.name("container")
|
||||
.description("podmanContainerDescription")
|
||||
.addString(new SimpleObjectProperty<>(st.getContainerName()), false)
|
||||
.disable()
|
||||
.buildComp();
|
||||
return new GuiDialog(q, val);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String summaryString(StoreEntryWrapper wrapper) {
|
||||
PodmanContainerStore s = wrapper.getEntry().getStore().asNeeded();
|
||||
return DataStoreFormatter.toApostropheName(
|
||||
s.getCmd().getStore().getHost().get()) + " container";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> informationString(StoreSection section) {
|
||||
return ShellStoreFormat.shellStore(section, (ContainerStoreState s) -> s.getContainerState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayIconFileName(DataStore store) {
|
||||
return "system:podman_icon.svg";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleNames() {
|
||||
return List.of("podman", "podman_container");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<?>> getStoreClasses() {
|
||||
return List.of(PodmanContainerStore.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package io.xpipe.ext.system.podman;
|
||||
|
||||
import io.xpipe.app.ext.ScanProvider;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
|
||||
public class PodmanScanProvider extends ScanProvider {
|
||||
|
||||
@Override
|
||||
public ScanOpportunity create(DataStoreEntry entry, ShellControl sc) throws Exception {
|
||||
var view = new PodmanCommandView(sc);
|
||||
return new ScanOpportunity("system.podmanContainers", !view.isSupported(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scan(DataStoreEntry entry, ShellControl sc) throws Throwable {
|
||||
var view = new PodmanCommandView(sc);
|
||||
var e = DataStorage.get()
|
||||
.addStoreIfNotPresent(
|
||||
entry,
|
||||
"Podman containers",
|
||||
PodmanCmdStore.builder().host(entry.ref()).build());
|
||||
if (view.isDaemonRunning()) {
|
||||
DataStorage.get().refreshChildren(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
ext/system/src/main/java/module-info.java
Normal file
42
ext/system/src/main/java/module-info.java
Normal file
@@ -0,0 +1,42 @@
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.ext.DataStoreProvider;
|
||||
import io.xpipe.app.ext.ScanProvider;
|
||||
import io.xpipe.ext.system.incus.IncusContainerActionMenu;
|
||||
import io.xpipe.ext.system.incus.IncusContainerStoreProvider;
|
||||
import io.xpipe.ext.system.incus.IncusInstallStoreProvider;
|
||||
import io.xpipe.ext.system.incus.IncusScanProvider;
|
||||
import io.xpipe.ext.system.lxd.LxdCmdStoreProvider;
|
||||
import io.xpipe.ext.system.lxd.LxdContainerActionMenu;
|
||||
import io.xpipe.ext.system.lxd.LxdContainerStoreProvider;
|
||||
import io.xpipe.ext.system.lxd.LxdScanProvider;
|
||||
import io.xpipe.ext.system.podman.PodmanCmdStoreProvider;
|
||||
import io.xpipe.ext.system.podman.PodmanContainerActionMenu;
|
||||
import io.xpipe.ext.system.podman.PodmanContainerStoreProvider;
|
||||
import io.xpipe.ext.system.podman.PodmanScanProvider;
|
||||
|
||||
open module io.xpipe.ext.system {
|
||||
requires com.fasterxml.jackson.databind;
|
||||
requires com.fasterxml.jackson.annotation;
|
||||
requires java.net.http;
|
||||
requires static lombok;
|
||||
requires static javafx.controls;
|
||||
requires static io.xpipe.app;
|
||||
requires io.xpipe.core;
|
||||
requires io.xpipe.ext.base;
|
||||
|
||||
provides ScanProvider with
|
||||
LxdScanProvider,
|
||||
IncusScanProvider,
|
||||
PodmanScanProvider;
|
||||
provides DataStoreProvider with
|
||||
LxdCmdStoreProvider,
|
||||
LxdContainerStoreProvider,
|
||||
IncusInstallStoreProvider,
|
||||
IncusContainerStoreProvider,
|
||||
PodmanContainerStoreProvider,
|
||||
PodmanCmdStoreProvider;
|
||||
provides ActionProvider with
|
||||
IncusContainerActionMenu,
|
||||
LxdContainerActionMenu,
|
||||
PodmanContainerActionMenu;
|
||||
}
|
||||
Reference in New Issue
Block a user