Squash merge branch 22-release into master

This commit is contained in:
crschnick committed 2026-03-26 11:35:04 +00:00
1 parent 0194ed313d
commit 609bdc478d
353 files changed
+9170 -2290

No files matched your search

@@ -1,18 +1,20 @@
package io.xpipe.ext.system.incus;
import io.xpipe.app.ext.ContainerStoreState;
import io.xpipe.app.ext.NetworkContainerStoreState;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.*;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.JacksonMapper;
import io.xpipe.ext.base.identity.IdentityValue;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.NonNull;
import lombok.Value;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class IncusCommandView extends CommandViewBase {
@@ -84,18 +86,30 @@ public class IncusCommandView extends CommandViewBase {
return build(commandBuilder -> commandBuilder.add("version")).readStdoutOrThrow();
}
public void start(String containerName) throws Exception {
build(commandBuilder -> commandBuilder.add("start").addQuoted(containerName))
public void start(String projectName, String containerName) throws Exception {
build(commandBuilder -> commandBuilder
.add("start")
.addQuoted(containerName)
.add("--project")
.addQuoted(projectName))
.execute();
}
public void stop(String containerName) throws Exception {
build(commandBuilder -> commandBuilder.add("stop").addQuoted(containerName))
public void stop(String projectName, String containerName) throws Exception {
build(commandBuilder -> commandBuilder
.add("stop")
.addQuoted(containerName)
.add("--project")
.addQuoted(projectName))
.execute();
}
public void pause(String containerName) throws Exception {
build(commandBuilder -> commandBuilder.add("pause").addQuoted(containerName))
public void pause(String projectName, String containerName) throws Exception {
build(commandBuilder -> commandBuilder
.add("pause")
.addQuoted(containerName)
.add("--project")
.addQuoted(projectName))
.execute();
}
@@ -109,14 +123,17 @@ public class IncusCommandView extends CommandViewBase {
public List<DataStoreEntryRef<IncusContainerStore>> listContainers(DataStoreEntryRef<IncusInstallStore> store)
throws Exception {
return listContainersAndStates().entrySet().stream()
return listContainers().stream()
.map(s -> {
boolean running = s.getValue().toLowerCase(Locale.ROOT).equals("running");
var c = new IncusContainerStore(store, s.getKey(), IdentityValue.ofBreakout(store.get()));
boolean running = s.getState().toLowerCase(Locale.ROOT).equals("running");
var c = new IncusContainerStore(
store, s.getProject(), s.getName(), IdentityValue.ofBreakout(store.get()));
var entry = DataStoreEntry.createNew(c.getContainerName(), c);
entry.setStorePersistentState(ContainerStoreState.builder()
.containerState(s.getValue())
entry.setStorePersistentState(NetworkContainerStoreState.builder()
.containerState(s.getState())
.running(running)
.ipv4(s.getIpv4Address())
.ipv6(s.getIpv6Address())
.build());
return Optional.of(entry.<IncusContainerStore>ref());
})
@@ -124,37 +141,87 @@ public class IncusCommandView extends CommandViewBase {
.toList();
}
public String queryContainerState(String containerName) throws Exception {
var states = listContainersAndStates();
return states.getOrDefault(containerName, "?");
public Optional<ContainerEntry> queryContainerState(String projectName, String containerName) throws Exception {
var l = listContainers();
var found = l.stream()
.filter(containerEntry -> (containerEntry.getProject().equals(projectName)
|| projectName == null
&& containerEntry.getProject().equals("default"))
&& containerEntry.getName().equals(containerName))
.findFirst();
return found;
}
private Map<String, String> listContainersAndStates() throws Exception {
try (var c = build(commandBuilder -> commandBuilder.add("list", "-f", "csv", "-c", "ns"))
@Value
public static class ContainerEntry {
String project;
String name;
String state;
String ipv4Address;
String ipv6Address;
}
private List<ContainerEntry> listContainers() throws Exception {
try (var c = build(commandBuilder -> commandBuilder.add("list", "--all-projects", "-f", "json"))
.start()) {
var output = c.readStdoutOrThrow();
return output.lines()
.collect(Collectors.toMap(
s -> s.strip().split(",")[0],
s -> s.strip().split(",")[1],
(x, y) -> y,
LinkedHashMap::new));
var json = JacksonMapper.getDefault().readTree(output);
var l = new ArrayList<ContainerEntry>();
for (JsonNode jsonNode : json) {
var status = jsonNode.required("status").textValue();
var project = jsonNode.required("project").textValue();
var name = jsonNode.required("name").textValue();
var state = jsonNode.required("state");
var network = state.get("network");
String ipv4 = null;
String ipv6 = null;
if (network != null) {
var eth0 = network.get("eth0");
if (eth0 == null && network.size() > 0) {
for (var it = network.fieldNames(); it.hasNext(); ) {
var field = it.next();
if (!field.equals("lo")) {
eth0 = network.required(field);
break;
}
}
}
if (eth0 != null) {
var addresses = eth0.required("addresses");
for (JsonNode address : addresses) {
if (!address.required("scope").textValue().equals("global")) {
continue;
}
var family = address.required("family").textValue();
if (family.equals("inet")) {
ipv4 = address.required("address").textValue();
} else if (family.equals("inet6")) {
ipv6 = address.required("address").textValue();
}
}
}
}
l.add(new ContainerEntry(project, name, status, ipv4, ipv6));
}
return l;
}
}
public ShellControl exec(String container, String user, Supplier<Boolean> busybox) {
public ShellControl exec(String projectName, String container, String user, Supplier<Boolean> busybox) {
var sub = shellControl.subShell();
sub.setDumbOpen(createOpenFunction(container, user, false, busybox));
sub.setTerminalOpen(createOpenFunction(container, user, true, busybox));
sub.setDumbOpen(createOpenFunction(projectName, container, user, false, busybox));
sub.setTerminalOpen(createOpenFunction(projectName, container, user, true, busybox));
return sub.withExceptionConverter(IncusCommandView::convertException).elevated(requiresElevation());
}
private ShellOpenFunction createOpenFunction(
String containerName, String user, boolean terminal, Supplier<Boolean> busybox) {
String projectName, String containerName, String user, boolean terminal, Supplier<Boolean> busybox) {
return new ShellOpenFunction() {
@Override
public CommandBuilder prepareWithoutInitCommand() {
var b = execCommand(containerName, terminal).add("su", "-l");
var b = execCommand(projectName, containerName, terminal).add("su", "-l");
if (user != null) {
b.addQuoted(user);
}
@@ -163,7 +230,7 @@ public class IncusCommandView extends CommandViewBase {
@Override
public CommandBuilder prepareWithInitCommand(@NonNull String command) {
var b = execCommand(containerName, terminal).add("su", "-l");
var b = execCommand(projectName, containerName, terminal).add("su", "-l");
if (user != null) {
b.addQuoted(user);
}
@@ -180,8 +247,11 @@ public class IncusCommandView extends CommandViewBase {
};
}
public CommandBuilder execCommand(String containerName, boolean terminal) {
public CommandBuilder execCommand(String projectName, String containerName, boolean terminal) {
var c = CommandBuilder.of().add("incus", "exec", terminal ? "-t" : "-T");
return c.addQuoted(containerName).add("--");
return c.addQuoted(containerName)
.add("--project")
.addQuoted(projectName)
.add("--");
}
}
@@ -5,6 +5,7 @@ import io.xpipe.app.hub.action.HubBranchProvider;
import io.xpipe.app.hub.action.HubMenuItemProvider;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.ext.base.host.CopyIpActionProvider;
import io.xpipe.ext.base.store.StorePauseActionProvider;
import io.xpipe.ext.base.store.StoreRestartActionProvider;
import io.xpipe.ext.base.store.StoreStartActionProvider;
@@ -43,6 +44,7 @@ public class IncusContainerActionProviderMenu implements HubBranchProvider<Incus
new StoreStopActionProvider(),
new StorePauseActionProvider(),
new StoreRestartActionProvider(),
new CopyIpActionProvider(),
new IncusContainerConsoleActionProvider(),
new IncusContainerEditConfigActionProvider(),
new IncusContainerEditRunConfigActionProvider());
@@ -5,6 +5,7 @@ import io.xpipe.app.process.BaseElevationHandler;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.*;
import io.xpipe.ext.base.host.HostAddressGatewayStore;
import io.xpipe.ext.base.identity.IdentityValue;
import io.xpipe.ext.base.store.PauseableStore;
import io.xpipe.ext.base.store.StartableStore;
@@ -17,6 +18,8 @@ import lombok.Value;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
@@ -29,19 +32,26 @@ import java.util.OptionalInt;
public class IncusContainerStore
implements ShellStore,
FixedChildStore,
StatefulDataStore<ContainerStoreState>,
StatefulDataStore<NetworkContainerStoreState>,
StartableStore,
StoppableStore,
PauseableStore,
NameableStore {
NameableStore,
HostAddressGatewayStore {
DataStoreEntryRef<IncusInstallStore> install;
String projectName;
String containerName;
IdentityValue identity;
@Override
public Class<ContainerStoreState> getStateClass() {
return ContainerStoreState.class;
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(install, identity != null ? identity.getDependencies() : null);
}
@Override
public Class<NetworkContainerStoreState> getStateClass() {
return NetworkContainerStoreState.class;
}
@Override
@@ -57,13 +67,20 @@ public class IncusContainerStore
@Override
public OptionalInt getFixedId() {
return OptionalInt.of(Objects.hash(containerName));
if (projectName != null) {
return OptionalInt.of(Objects.hash(projectName, containerName));
} else {
return OptionalInt.of(Objects.hash(containerName));
}
}
@Override
public FixedChildStore merge(FixedChildStore other) {
var o = (IncusContainerStore) other;
return toBuilder().identity(identity != null ? identity : o.identity).build();
return toBuilder()
.identity(identity != null ? identity : o.identity)
.projectName(o.projectName)
.build();
}
@Override
@@ -76,7 +93,7 @@ public class IncusContainerStore
getInstall().getStore().getHost().getStore().getOrStartSession());
var user = identity != null ? identity.unwrap().getUsername().retrieveUsername() : null;
var sc = new IncusCommandView(parent).exec(containerName, user, () -> {
var sc = new IncusCommandView(parent).exec(projectName, containerName, user, () -> {
var state = getState();
var alpine = state.getOsName() != null
&& state.getOsName().toLowerCase().contains("alpine");
@@ -114,10 +131,18 @@ public class IncusContainerStore
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();
var displayState = view.queryContainerState(projectName, containerName);
if (displayState.isEmpty()) {
return;
}
var running = "Running".equals(displayState.get().getState());
var newState = state.toBuilder()
.containerState(displayState.get().getState())
.running(running)
.ipv4(displayState.get().getIpv4Address())
.ipv6(displayState.get().getIpv6Address())
.build();
setState(newState);
}
@@ -125,15 +150,16 @@ public class IncusContainerStore
public void start() throws Exception {
var sc = getInstall().getStore().getHost().getStore().getOrStartSession();
var view = new IncusCommandView(sc);
view.start(containerName);
view.start(projectName, containerName);
refreshContainerState(sc);
}
@Override
public void stop() throws Exception {
stopSessionIfNeeded();
var sc = getInstall().getStore().getHost().getStore().getOrStartSession();
var view = new IncusCommandView(sc);
view.stop(containerName);
view.stop(projectName, containerName);
refreshContainerState(sc);
}
@@ -141,7 +167,7 @@ public class IncusContainerStore
public void pause() throws Exception {
var sc = getInstall().getStore().getHost().getStore().getOrStartSession();
var view = new IncusCommandView(sc);
view.pause(containerName);
view.pause(projectName, containerName);
refreshContainerState(sc);
}
@@ -149,4 +175,28 @@ public class IncusContainerStore
public String getName() {
return containerName;
}
@Override
public DataStoreEntryRef<NetworkTunnelStore> getTunnelGateway() {
var host = getInstall().getStore().getHost();
return host.getStore() instanceof NetworkTunnelStore ? host.asNeeded() : null;
}
@Override
public void refreshHostAddressOrThrow() throws Exception {
refreshContainerState(getInstall().getStore().getHost().getStore().getOrStartSession());
}
@Override
public HostAddress getHostAddress() {
var l = new ArrayList<String>();
var state = getState();
if (state.getIpv4() != null) {
l.add(state.getIpv4());
}
if (state.getIpv6() != null) {
l.add(state.getIpv6());
}
return !l.isEmpty() ? HostAddress.of(l) : HostAddress.empty();
}
}
@@ -4,7 +4,6 @@ import io.xpipe.app.comp.BaseRegionBuilder;
import io.xpipe.app.ext.ContainerStoreState;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.ext.GuiDialog;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.hub.comp.*;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.OptionsBuilder;
@@ -14,7 +13,6 @@ import io.xpipe.ext.base.identity.IdentityChoiceBuilder;
import io.xpipe.ext.base.store.ShellStoreProvider;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
@@ -76,18 +74,9 @@ public class IncusContainerStoreProvider implements ShellStoreProvider {
var identity = new SimpleObjectProperty<>(st.getIdentity());
var q = new OptionsBuilder()
.name("host")
.description("lxdHostDescription")
.addComp(new StoreChoiceComp<>(
entry,
new ReadOnlyObjectWrapper<>(st.getInstall().getStore().getHost()),
ShellStore.class,
null,
StoreViewState.get().getAllConnectionsCategory()))
.disable()
.name("container")
.description("lxdContainerDescription")
.addStaticString(st.getContainerName())
.description("containerDescription")
.addStaticString((st.getProjectName() != null ? st.getProjectName() + "/" : "") + st.getContainerName())
.sub(IdentityChoiceBuilder.container(identity), identity)
.bind(
() -> {
@@ -1,11 +1,6 @@
package io.xpipe.ext.system.incus;
import io.xpipe.app.ext.DataStoreState;
import io.xpipe.app.ext.FixedChildStore;
import io.xpipe.app.ext.FixedHierarchyStore;
import io.xpipe.app.ext.SelfReferentialStore;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.ext.StatefulDataStore;
import io.xpipe.app.ext.*;
import io.xpipe.app.process.CommandSupport;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.Validators;
@@ -34,6 +29,11 @@ public class IncusInstallStore
this.host = host;
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(host);
}
@Override
public void checkComplete() throws Throwable {
Validators.nonNull(host);
@@ -52,7 +52,7 @@ public class IncusInstallStoreProvider implements DataStoreProvider {
@Override
public DataStoreEntry getDisplayParent(DataStoreEntry store) {
IncusInstallStore s = store.getStore().asNeeded();
return s.getHost().get();
return s.getHost() != null ? s.getHost().get() : null;
}
@Override
@@ -1,11 +1,6 @@
package io.xpipe.ext.system.lxd;
import io.xpipe.app.ext.DataStoreState;
import io.xpipe.app.ext.FixedChildStore;
import io.xpipe.app.ext.FixedHierarchyStore;
import io.xpipe.app.ext.SelfReferentialStore;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.ext.StatefulDataStore;
import io.xpipe.app.ext.*;
import io.xpipe.app.process.CommandSupport;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.Validators;
@@ -31,6 +26,11 @@ public class LxdCmdStore implements FixedHierarchyStore, StatefulDataStore<LxdCm
this.host = host;
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(host);
}
@Override
public void checkComplete() throws Throwable {
Validators.nonNull(host);
@@ -52,7 +52,7 @@ public class LxdCmdStoreProvider implements DataStoreProvider {
@Override
public DataStoreEntry getDisplayParent(DataStoreEntry store) {
LxdCmdStore s = store.getStore().asNeeded();
return s.getHost().get();
return s.getHost() != null ? s.getHost().get() : null;
}
@Override
@@ -1,19 +1,21 @@
package io.xpipe.ext.system.lxd;
import io.xpipe.app.ext.ContainerStoreState;
import io.xpipe.app.ext.NetworkContainerStoreState;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.*;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.FilePath;
import io.xpipe.core.JacksonMapper;
import io.xpipe.ext.base.identity.IdentityValue;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.NonNull;
import lombok.Value;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class LxdCommandView extends CommandViewBase {
@@ -75,7 +77,7 @@ public class LxdCommandView extends CommandViewBase {
public boolean isSupported() throws Exception {
// Ubuntu always has the lxc command installed as a stub to install LXD
// We don't want to call it as this would automatically install LXD and take a while
if (shellControl.getOsName().toLowerCase().contains("ubuntu")) {
if (shellControl.getOsName() != null && shellControl.getOsName().toLowerCase().contains("ubuntu")) {
return shellControl.view().fileExists(FilePath.of("/snap/bin/lxc"));
}
@@ -95,23 +97,30 @@ public class LxdCommandView extends CommandViewBase {
.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))
public void start(String projectName, String containerName) throws Exception {
build(commandBuilder -> commandBuilder
.add("start")
.addQuoted(containerName)
.add("--project")
.addQuoted(projectName))
.execute();
}
public void stop(String containerName) throws Exception {
build(commandBuilder -> commandBuilder.add("stop").addQuoted(containerName))
public void stop(String projectName, String containerName) throws Exception {
build(commandBuilder -> commandBuilder
.add("stop")
.addQuoted(containerName)
.add("--project")
.addQuoted(projectName))
.execute();
}
public void pause(String containerName) throws Exception {
build(commandBuilder -> commandBuilder.add("pause").addQuoted(containerName))
public void pause(String projectName, String containerName) throws Exception {
build(commandBuilder -> commandBuilder
.add("pause")
.addQuoted(containerName)
.add("--project")
.addQuoted(projectName))
.execute();
}
@@ -125,18 +134,17 @@ public class LxdCommandView extends CommandViewBase {
public List<DataStoreEntryRef<LxdContainerStore>> listContainers(DataStoreEntryRef<LxdCmdStore> store)
throws Exception {
return listContainersAndStates().entrySet().stream()
return listContainers().stream()
.map(s -> {
boolean running = s.getValue().toLowerCase(Locale.ROOT).equals("running");
var c = LxdContainerStore.builder()
.cmd(store)
.containerName(s.getKey())
.identity(IdentityValue.ofBreakout(store.get()))
.build();
boolean running = s.getState().toLowerCase(Locale.ROOT).equals("running");
var c = new LxdContainerStore(
store, s.getProject(), s.getName(), IdentityValue.ofBreakout(store.get()));
var entry = DataStoreEntry.createNew(c.getContainerName(), c);
entry.setStorePersistentState(ContainerStoreState.builder()
.containerState(s.getValue())
entry.setStorePersistentState(NetworkContainerStoreState.builder()
.containerState(s.getState())
.running(running)
.ipv4(s.getIpv4Address())
.ipv6(s.getIpv6Address())
.build());
return Optional.of(entry.<LxdContainerStore>ref());
})
@@ -144,39 +152,87 @@ public class LxdCommandView extends CommandViewBase {
.toList();
}
private Map<String, String> listContainersAndStates() throws Exception {
try (var c = build(commandBuilder -> commandBuilder.add("list", "-f", "csv", "-c", "ns"))
public Optional<ContainerEntry> queryContainerState(String projectName, String containerName) throws Exception {
var l = listContainers();
var found = l.stream()
.filter(containerEntry -> (containerEntry.getProject().equals(projectName)
|| projectName == null
&& containerEntry.getProject().equals("default"))
&& containerEntry.getName().equals(containerName))
.findFirst();
return found;
}
@Value
public static class ContainerEntry {
String project;
String name;
String state;
String ipv4Address;
String ipv6Address;
}
private List<ContainerEntry> listContainers() throws Exception {
try (var c = build(commandBuilder -> commandBuilder.add("list", "--all-projects", "-f", "json"))
.start()) {
var output = c.readStdoutOrThrow();
return output.lines()
.collect(Collectors.toMap(
s -> s.strip().split(",")[0],
s -> s.strip().split(",")[1],
(x, y) -> y,
LinkedHashMap::new));
} catch (ProcessOutputException ex) {
if (ex.getOutput().contains("Error: unknown shorthand flag: 'f' in -f")) {
throw ErrorEventFactory.expected(
ProcessOutputException.withPrefix("Unsupported legacy LXD version", ex));
} else {
throw ex;
var json = JacksonMapper.getDefault().readTree(output);
var l = new ArrayList<ContainerEntry>();
for (JsonNode jsonNode : json) {
var status = jsonNode.required("status").textValue();
var project = jsonNode.required("project").textValue();
var name = jsonNode.required("name").textValue();
var state = jsonNode.required("state");
var network = state.get("network");
String ipv4 = null;
String ipv6 = null;
if (network != null) {
var eth0 = network.get("eth0");
if (eth0 == null && network.size() > 0) {
for (var it = network.fieldNames(); it.hasNext(); ) {
var field = it.next();
if (!field.equals("lo")) {
eth0 = network.required(field);
break;
}
}
}
if (eth0 != null) {
var addresses = eth0.required("addresses");
for (JsonNode address : addresses) {
if (!address.required("scope").textValue().equals("global")) {
continue;
}
var family = address.required("family").textValue();
if (family.equals("inet")) {
ipv4 = address.required("address").textValue();
} else if (family.equals("inet6")) {
ipv6 = address.required("address").textValue();
}
}
}
}
l.add(new ContainerEntry(project, name, status, ipv4, ipv6));
}
return l;
}
}
public ShellControl exec(String container, String user, Supplier<Boolean> busybox) {
public ShellControl exec(String projectName, String container, String user, Supplier<Boolean> busybox) {
var sub = shellControl.subShell();
sub.setDumbOpen(createOpenFunction(container, user, false, busybox));
sub.setTerminalOpen(createOpenFunction(container, user, true, busybox));
sub.setDumbOpen(createOpenFunction(projectName, container, user, false, busybox));
sub.setTerminalOpen(createOpenFunction(projectName, container, user, true, busybox));
return sub.withExceptionConverter(LxdCommandView::convertException).elevated(requiresElevation());
}
private ShellOpenFunction createOpenFunction(
String containerName, String user, boolean terminal, Supplier<Boolean> busybox) {
String projectName, String containerName, String user, boolean terminal, Supplier<Boolean> busybox) {
return new ShellOpenFunction() {
@Override
public CommandBuilder prepareWithoutInitCommand() {
var b = execCommand(containerName, terminal).add("su", "-l");
var b = execCommand(projectName, containerName, terminal).add("su", "-l");
if (user != null) {
b.addQuoted(user);
}
@@ -185,7 +241,7 @@ public class LxdCommandView extends CommandViewBase {
@Override
public CommandBuilder prepareWithInitCommand(@NonNull String command) {
var b = execCommand(containerName, terminal).add("su", "-l");
var b = execCommand(projectName, containerName, terminal).add("su", "-l");
if (user != null) {
b.addQuoted(user);
}
@@ -202,8 +258,11 @@ public class LxdCommandView extends CommandViewBase {
};
}
public CommandBuilder execCommand(String containerName, boolean terminal) {
public CommandBuilder execCommand(String projectName, String containerName, boolean terminal) {
var c = CommandBuilder.of().add("lxc", "exec", terminal ? "-t" : "-T");
return c.addQuoted(containerName).add("--");
return c.addQuoted(containerName)
.add("--project")
.addQuoted(projectName)
.add("--");
}
}
@@ -5,6 +5,7 @@ import io.xpipe.app.hub.action.HubBranchProvider;
import io.xpipe.app.hub.action.HubMenuItemProvider;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.ext.base.host.CopyIpActionProvider;
import io.xpipe.ext.base.store.StorePauseActionProvider;
import io.xpipe.ext.base.store.StoreRestartActionProvider;
import io.xpipe.ext.base.store.StoreStartActionProvider;
@@ -43,6 +44,7 @@ public class LxdContainerActionProviderMenu implements HubBranchProvider<LxdCont
new StoreStopActionProvider(),
new StorePauseActionProvider(),
new StoreRestartActionProvider(),
new CopyIpActionProvider(),
new LxdContainerConsoleActionProvider(),
new LxdContainerEditConfigActionProvider(),
new LxdContainerEditRunConfigActionProvider());
@@ -5,6 +5,7 @@ import io.xpipe.app.process.BaseElevationHandler;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.*;
import io.xpipe.ext.base.host.HostAddressGatewayStore;
import io.xpipe.ext.base.identity.IdentityValue;
import io.xpipe.ext.base.store.PauseableStore;
import io.xpipe.ext.base.store.StartableStore;
@@ -12,39 +13,45 @@ 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.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
@JsonTypeName("lxd")
@SuperBuilder(toBuilder = true)
@Jacksonized
@Value
@Getter
@AllArgsConstructor
@Value
public class LxdContainerStore
implements ShellStore,
FixedChildStore,
StatefulDataStore<ContainerStoreState>,
StatefulDataStore<NetworkContainerStoreState>,
StartableStore,
StoppableStore,
PauseableStore,
NameableStore {
NameableStore,
HostAddressGatewayStore {
DataStoreEntryRef<LxdCmdStore> cmd;
String projectName;
String containerName;
IdentityValue identity;
@Override
public String getName() {
return containerName;
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(cmd, identity != null ? identity.getDependencies() : null);
}
@Override
public Class<ContainerStoreState> getStateClass() {
return ContainerStoreState.class;
public Class<NetworkContainerStoreState> getStateClass() {
return NetworkContainerStoreState.class;
}
@Override
@@ -60,13 +67,20 @@ public class LxdContainerStore
@Override
public OptionalInt getFixedId() {
return OptionalInt.of(Objects.hash(containerName));
if (projectName != null) {
return OptionalInt.of(Objects.hash(projectName, containerName));
} else {
return OptionalInt.of(Objects.hash(containerName));
}
}
@Override
public FixedChildStore merge(FixedChildStore other) {
var o = (LxdContainerStore) other;
return toBuilder().identity(identity != null ? identity : o.identity).build();
return toBuilder()
.identity(identity != null ? identity : o.identity)
.projectName(o.projectName)
.build();
}
@Override
@@ -78,18 +92,18 @@ public class LxdContainerStore
refreshContainerState(getCmd().getStore().getHost().getStore().getOrStartSession());
var user = identity != null ? identity.unwrap().getUsername().retrieveUsername() : null;
var sc = new LxdCommandView(parent).exec(containerName, user, () -> {
var sc = new LxdCommandView(parent).exec(projectName, containerName, user, () -> {
var state = getState();
var alpine = state.getOsName() != null
&& state.getOsName().toLowerCase().contains("alpine");
return alpine;
});
sc.withSourceStore(LxdContainerStore.this);
if (identity != null && identity.unwrap().getPassword() != null) {
sc.setElevationHandler(new BaseElevationHandler(
LxdContainerStore.this, identity.unwrap().getPassword())
.orElse(sc.getElevationHandler()));
}
sc.withSourceStore(LxdContainerStore.this);
sc.withShellStateInit(LxdContainerStore.this);
sc.onStartupFail(throwable -> {
if (throwable instanceof LicenseRequiredException) {
@@ -102,6 +116,7 @@ public class LxdContainerStore
.build();
setState(s);
});
return sc;
}
@@ -115,10 +130,18 @@ public class LxdContainerStore
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();
var displayState = view.queryContainerState(projectName, containerName);
if (displayState.isEmpty()) {
return;
}
var running = "Running".equals(displayState.get().getState());
var newState = state.toBuilder()
.containerState(displayState.get().getState())
.running(running)
.ipv4(displayState.get().getIpv4Address())
.ipv6(displayState.get().getIpv6Address())
.build();
setState(newState);
}
@@ -126,15 +149,16 @@ public class LxdContainerStore
public void start() throws Exception {
var sc = getCmd().getStore().getHost().getStore().getOrStartSession();
var view = new LxdCommandView(sc);
view.start(containerName);
view.start(projectName, containerName);
refreshContainerState(sc);
}
@Override
public void stop() throws Exception {
stopSessionIfNeeded();
var sc = getCmd().getStore().getHost().getStore().getOrStartSession();
var view = new LxdCommandView(sc);
view.stop(containerName);
view.stop(projectName, containerName);
refreshContainerState(sc);
}
@@ -142,7 +166,36 @@ public class LxdContainerStore
public void pause() throws Exception {
var sc = getCmd().getStore().getHost().getStore().getOrStartSession();
var view = new LxdCommandView(sc);
view.pause(containerName);
view.pause(projectName, containerName);
refreshContainerState(sc);
}
@Override
public String getName() {
return containerName;
}
@Override
public DataStoreEntryRef<NetworkTunnelStore> getTunnelGateway() {
var host = getCmd().getStore().getHost();
return host.getStore() instanceof NetworkTunnelStore ? host.asNeeded() : null;
}
@Override
public void refreshHostAddressOrThrow() throws Exception {
refreshContainerState(getCmd().getStore().getHost().getStore().getOrStartSession());
}
@Override
public HostAddress getHostAddress() {
var l = new ArrayList<String>();
var state = getState();
if (state.getIpv4() != null) {
l.add(state.getIpv4());
}
if (state.getIpv6() != null) {
l.add(state.getIpv6());
}
return !l.isEmpty() ? HostAddress.of(l) : HostAddress.empty();
}
}
@@ -4,7 +4,6 @@ import io.xpipe.app.comp.BaseRegionBuilder;
import io.xpipe.app.ext.ContainerStoreState;
import io.xpipe.app.ext.DataStore;
import io.xpipe.app.ext.GuiDialog;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.hub.comp.*;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.OptionsBuilder;
@@ -14,7 +13,6 @@ import io.xpipe.ext.base.identity.IdentityChoiceBuilder;
import io.xpipe.ext.base.store.ShellStoreProvider;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
@@ -67,7 +65,7 @@ public class LxdContainerStoreProvider implements ShellStoreProvider {
@Override
public DataStoreEntry getDisplayParent(DataStoreEntry store) {
LxdContainerStore s = store.getStore().asNeeded();
return s.getCmd().get();
return s.getCmd() != null ? s.getCmd().get() : null;
}
@Override
@@ -76,18 +74,9 @@ public class LxdContainerStoreProvider implements ShellStoreProvider {
var identity = new SimpleObjectProperty<>(st.getIdentity());
var q = new OptionsBuilder()
.name("host")
.description("lxdHostDescription")
.addComp(new StoreChoiceComp<>(
entry,
new ReadOnlyObjectWrapper<>(st.getCmd().getStore().getHost()),
ShellStore.class,
null,
StoreViewState.get().getAllConnectionsCategory()))
.disable()
.name("container")
.description("lxdContainerDescription")
.addStaticString(st.getContainerName())
.description("containerDescription")
.addStaticString((st.getProjectName() != null ? st.getProjectName() + "/" : "") + st.getContainerName())
.sub(IdentityChoiceBuilder.container(identity), identity)
.bind(
() -> {
@@ -30,6 +30,16 @@ public class PodmanCmdStore
this.host = host;
}
@Override
public boolean removeLeftovers() {
return false;
}
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(host);
}
@Override
public void checkComplete() throws Throwable {
Validators.nonNull(host);
@@ -50,10 +60,11 @@ public class PodmanCmdStore
.containerName(s.getName())
.build();
var entry = DataStoreEntry.createNew(s.getName(), c);
entry.setStorePersistentState(ContainerStoreState.builder()
entry.setStorePersistentState(PodmanContainerStoreState.builder()
.containerState(s.getStatus())
.imageName(s.getImage())
.running(running)
.systemdUnit(s.getSystemdUnit())
.build());
return entry.<PodmanContainerStore>ref();
})
@@ -52,7 +52,7 @@ public class PodmanCmdStoreProvider implements DataStoreProvider {
@Override
public DataStoreEntry getDisplayParent(DataStoreEntry store) {
PodmanCmdStore s = store.getStore().asNeeded();
return s.getHost().get();
return s.getHost() != null ? s.getHost().get() : null;
}
@Override
@@ -2,11 +2,14 @@ package io.xpipe.ext.system.podman;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.*;
import io.xpipe.core.OsType;
import lombok.NonNull;
import lombok.Value;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
public class PodmanCommandView extends CommandViewBase {
@@ -67,6 +70,16 @@ public class PodmanCommandView extends CommandViewBase {
public class Container extends CommandView {
public Optional<String> queryLabel(String container, String label) throws Exception {
var command = build(b -> b.add("inspect")
.add(sc -> {
var quote = ShellDialects.isPowershell(sc) ? "'" : "\"";
return "--format=" + quote + "{{index (index .Config.Labels \\\"" + label + "\\\")}}" + quote;
})
.addQuoted(container));
return command.readStdoutIfPossible().filter(s -> !s.isBlank());
}
public String queryState(String container) throws Exception {
return build(commandBuilder -> commandBuilder.add(
"ls", "-a", "-f", "name=\"^" + container + "$\"", "--format=\"{{.Status}}\""))
@@ -101,10 +114,13 @@ public class PodmanCommandView extends CommandViewBase {
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();
var l = new ArrayList<ContainerEntry>();
for (String s : output.lines().toList()) {
var systemd =
queryLabel(s.split(";")[0], "PODMAN_SYSTEMD_UNIT").orElse(null);
l.add(new ContainerEntry(s.split(";")[0], s.split(";")[1], s.split(";")[2], systemd));
}
return l;
}
}
@@ -145,6 +161,20 @@ public class PodmanCommandView extends CommandViewBase {
.execute();
}
public void restart(String container, String service) throws Exception {
if (shellControl.getOsType() == OsType.LINUX && service != null) {
shellControl
.command(CommandBuilder.of()
.add("systemctl", "restart", "--user")
.addQuoted(service))
.execute();
return;
}
build(commandBuilder -> commandBuilder.add("restart").addQuoted(container))
.execute();
}
public String port(String container) throws Exception {
return build(commandBuilder -> commandBuilder.add("port").addQuoted(container))
.readStdoutOrThrow();
@@ -168,6 +198,7 @@ public class PodmanCommandView extends CommandViewBase {
String name;
String image;
String status;
String systemdUnit;
}
}
}
@@ -41,6 +41,7 @@ public class PodmanContainerActionProviderMenu implements HubBranchProvider<Podm
new StoreStartActionProvider(),
new StoreStopActionProvider(),
new StoreRestartActionProvider(),
new PodmanContainerUnitFileEditActionProvider(),
new PodmanContainerInspectActionProvider(),
new PodmanContainerLogsActionProvider(),
new PodmanContainerAttachActionProvider());
@@ -9,6 +9,7 @@ import io.xpipe.app.util.Validators;
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.RestartableStore;
import io.xpipe.ext.base.store.StartableStore;
import io.xpipe.ext.base.store.StoppableStore;
@@ -29,10 +30,11 @@ import java.util.regex.Pattern;
public class PodmanContainerStore
implements StartableStore,
StoppableStore,
RestartableStore,
ShellStore,
InternalCacheDataStore,
FixedChildStore,
StatefulDataStore<ContainerStoreState>,
StatefulDataStore<PodmanContainerStoreState>,
FixedServiceCreatorStore,
SelfReferentialStore,
ContainerImageStore,
@@ -41,6 +43,11 @@ public class PodmanContainerStore
DataStoreEntryRef<PodmanCmdStore> cmd;
String containerName;
@Override
public List<DataStoreEntryRef<?>> getDependencies() {
return DataStoreDependencies.of(cmd);
}
@Override
public String getName() {
return containerName;
@@ -65,6 +72,7 @@ public class PodmanContainerStore
@Override
public void stop() throws Exception {
stopSessionIfNeeded();
var sc = getCmd().getStore().getHost().getStore().getOrStartSession();
var view = commandView(sc);
view.stop(containerName);
@@ -104,8 +112,8 @@ public class PodmanContainerStore
}
@Override
public Class<ContainerStoreState> getStateClass() {
return ContainerStoreState.class;
public Class<PodmanContainerStoreState> getStateClass() {
return PodmanContainerStoreState.class;
}
@Override
@@ -154,13 +162,30 @@ public class PodmanContainerStore
};
}
private void refreshContainerState(ShellControl sc) throws Exception {
private boolean refreshContainerState(ShellControl sc) throws Exception {
var state = getState();
var view = new PodmanCommandView(sc).container();
var displayState = view.queryState(containerName);
if (displayState.isEmpty()) {
var newState =
state.toBuilder().containerState("Removed").running(false).build();
setState(newState);
return false;
}
var running = displayState.startsWith("Up");
var newState =
state.toBuilder().containerState(displayState).running(running).build();
setState(newState);
return true;
}
@Override
public void restart() throws Exception {
stopSessionIfNeeded();
var sc = getCmd().getStore().getHost().getStore().getOrStartSession();
var view = commandView(sc);
view.restart(containerName, getState().getSystemdUnit());
refreshContainerState(sc);
}
}
@@ -106,9 +106,12 @@ public class PodmanContainerStoreProvider implements ShellStoreProvider {
@Override
public ObservableValue<String> informationString(StoreSection section) {
var c = (ContainerStoreState) section.getWrapper().getPersistentState().getValue();
var c = (PodmanContainerStoreState)
section.getWrapper().getPersistentState().getValue();
var missing = c.getShellMissing() != null && c.getShellMissing() ? "No shell available" : null;
return StoreStateFormat.shellStore(
section, (ContainerStoreState s) -> new String[] {missing, s.getContainerState()}, null);
section,
(ContainerStoreState s) -> new String[] {missing, s.getContainerState(), c.getSystemdUnit()},
null);
}
}
@@ -0,0 +1,35 @@
package io.xpipe.ext.system.podman;
import io.xpipe.app.ext.ContainerStoreState;
import io.xpipe.app.ext.DataStoreState;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@Getter
@EqualsAndHashCode(callSuper = true)
@SuperBuilder(toBuilder = true)
@Jacksonized
public class PodmanContainerStoreState extends ContainerStoreState {
String systemdUnit;
@Override
public DataStoreState mergeCopy(DataStoreState newer) {
var n = (PodmanContainerStoreState) newer;
var b = toBuilder();
mergeBuilder(n, b);
return b.build();
}
protected void mergeBuilder(
PodmanContainerStoreState css, PodmanContainerStoreState.PodmanContainerStoreStateBuilder<?, ?> b) {
super.mergeBuilder(css, b);
b.systemdUnit = css.systemdUnit;
}
}
@@ -0,0 +1,94 @@
package io.xpipe.ext.system.podman;
import io.xpipe.app.action.AbstractAction;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.hub.action.HubLeafProvider;
import io.xpipe.app.hub.action.StoreAction;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.FilePath;
import javafx.beans.value.ObservableValue;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.regex.Pattern;
public class PodmanContainerUnitFileEditActionProvider implements HubLeafProvider<PodmanContainerStore> {
@Override
public AbstractAction createAction(DataStoreEntryRef<PodmanContainerStore> ref) {
return Action.builder().ref(ref).build();
}
@Override
public ObservableValue<String> getName(DataStoreEntryRef<PodmanContainerStore> store) {
return AppI18n.observable("editContainerUnitFile");
}
@Override
public LabelGraphic getIcon(DataStoreEntryRef<PodmanContainerStore> store) {
return new LabelGraphic.IconGraphic("mdi2f-file-document-edit");
}
@Override
public Class<PodmanContainerStore> getApplicableClass() {
return PodmanContainerStore.class;
}
@Override
public boolean isApplicable(DataStoreEntryRef<PodmanContainerStore> o) {
return o.getStore().getState().getSystemdUnit() != null;
}
@Override
public String getId() {
return "editPodmanContainerUnitFile";
}
@Jacksonized
@SuperBuilder
public static class Action extends StoreAction<PodmanContainerStore> {
@Override
public boolean isMutation() {
return true;
}
@Override
public void executeImpl() throws Exception {
var host = ref.getStore().getCmd().getStore().getHost();
var unitFileName = ref.getStore().getState().getSystemdUnit();
var sc = host.getStore().getOrStartSession();
var runtimeDir = FilePath.of(sc.view().getEnvironmentVariableOrThrow("XDG_RUNTIME_DIR"));
var unitFile = runtimeDir.join("systemd", "generator", unitFileName);
if (!sc.view().fileExists(unitFile)) {
throw ErrorEventFactory.expected(new IllegalArgumentException(
"Container unit file " + unitFileName + " not found at " + unitFile));
}
var content = sc.view().readTextFile(unitFile);
var matcher = Pattern.compile("SourcePath=(.+)").matcher(content);
if (!matcher.find()) {
throw ErrorEventFactory.expected(new IllegalArgumentException(
"Source of container unit file " + unitFileName + " not locatable from file " + unitFile));
}
var sourceFile = FilePath.of(matcher.group(1));
FileOpener.openString(
sourceFile.getFileName(),
sourceFile.getFileName(),
sc.view().readTextFile(sourceFile),
s -> {
try {
sc.view().writeTextFile(sourceFile, s);
} catch (Exception e) {
ErrorEventFactory.fromThrowable(e).handle();
}
});
}
}
}
+2 -1
View File
@@ -38,5 +38,6 @@ open module io.xpipe.ext.system {
PodmanContainerActionProviderMenu,
PodmanContainerInspectActionProvider,
PodmanContainerAttachActionProvider,
PodmanContainerLogsActionProvider;
PodmanContainerLogsActionProvider,
PodmanContainerUnitFileEditActionProvider;
}