mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-10-25 08:10:15 +00:00
Squash merge branch 22-release into master
This commit is contained in:
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("--");
|
||||
}
|
||||
}
|
||||
+2
@@ -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();
|
||||
}
|
||||
}
|
||||
+2
-13
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -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);
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
+94
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,5 +38,6 @@ open module io.xpipe.ext.system {
|
||||
PodmanContainerActionProviderMenu,
|
||||
PodmanContainerInspectActionProvider,
|
||||
PodmanContainerAttachActionProvider,
|
||||
PodmanContainerLogsActionProvider;
|
||||
PodmanContainerLogsActionProvider,
|
||||
PodmanContainerUnitFileEditActionProvider;
|
||||
}
|
||||
Reference in new issue
Block a user