Squash merge branch 14-release into master

This commit is contained in:
crschnick
2025-01-16 07:29:55 +00:00
parent 48c9f96c03
commit 45f6545fc8
2688 changed files with 41312 additions and 19924 deletions

View File

@@ -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("--");
}
}

View File

@@ -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());
}
};
}
}

View File

@@ -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()));
}
}
}

View File

@@ -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()));
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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("--");
}
}

View File

@@ -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());
}
};
}
}

View File

@@ -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()));
}
}
}

View File

@@ -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()));
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}
}

View File

@@ -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());
}
};
}
}

View File

@@ -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";
}
};
}
}

View File

@@ -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);
}
}
}

View File

@@ -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()));
}
}
}

View File

@@ -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;
}
};
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View 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;
}