API draft to manually set peer addresses

This commit is contained in:
2025-03-30 16:11:09 +02:00
parent 8b3c0a6f2c
commit 69eb96b10c
25 changed files with 2915 additions and 2399 deletions

View File

@@ -3,11 +3,9 @@ package com.usatiuk.dhfs.repository;
import com.usatiuk.dhfs.PeerId; import com.usatiuk.dhfs.PeerId;
import com.usatiuk.dhfs.repository.peerdiscovery.PeerAddress; import com.usatiuk.dhfs.repository.peerdiscovery.PeerAddress;
import com.usatiuk.dhfs.repository.peerdiscovery.PeerDiscoveryDirectory; import com.usatiuk.dhfs.repository.peerdiscovery.PeerDiscoveryDirectory;
import com.usatiuk.dhfs.repository.peersync.PeerInfo;
import com.usatiuk.dhfs.repository.peersync.PeerInfoService; import com.usatiuk.dhfs.repository.peersync.PeerInfoService;
import com.usatiuk.dhfs.repository.peersync.api.PeerSyncApiClientDynamic; import com.usatiuk.dhfs.repository.peersync.api.PeerSyncApiClientDynamic;
import com.usatiuk.dhfs.repository.peertrust.PeerTrustManager; import com.usatiuk.dhfs.repository.peertrust.PeerTrustManager;
import com.usatiuk.dhfs.repository.webapi.AvailablePeerInfo;
import com.usatiuk.objects.transaction.Transaction; import com.usatiuk.objects.transaction.Transaction;
import com.usatiuk.objects.transaction.TransactionManager; import com.usatiuk.objects.transaction.TransactionManager;
import io.quarkus.logging.Log; import io.quarkus.logging.Log;
@@ -70,7 +68,7 @@ public class PeerManager {
if (_heartbeatExecutor == null) return; if (_heartbeatExecutor == null) return;
try { try {
var peers = peerInfoService.getPeersNoSelf(); var peers = peerInfoService.getPeersNoSelf();
var pids = peers.stream().map(PeerInfo::id).toList(); var pids = peers.stream().map(com.usatiuk.dhfs.repository.peersync.PeerInfo::id).toList();
List<PeerId> stale = _states.keySet().stream().filter(p -> !pids.contains(p)).toList(); List<PeerId> stale = _states.keySet().stream().filter(p -> !pids.contains(p)).toList();
stale.forEach(_states.keySet()::remove); stale.forEach(_states.keySet()::remove);
@@ -98,7 +96,7 @@ public class PeerManager {
} }
} }
private void handleConnectionSuccess(PeerInfo host, PeerAddress address) { private void handleConnectionSuccess(com.usatiuk.dhfs.repository.peersync.PeerInfo host, PeerAddress address) {
boolean wasReachable = isReachable(host); boolean wasReachable = isReachable(host);
boolean shouldSync = !persistentPeerDataService.isInitialSyncDone(host.id()); boolean shouldSync = !persistentPeerDataService.isInitialSyncDone(host.id());
@@ -120,7 +118,7 @@ public class PeerManager {
} }
} }
public void handleConnectionError(PeerInfo host) { public void handleConnectionError(com.usatiuk.dhfs.repository.peersync.PeerInfo host) {
boolean wasReachable = isReachable(host); boolean wasReachable = isReachable(host);
if (wasReachable) if (wasReachable)
@@ -134,7 +132,7 @@ public class PeerManager {
} }
// FIXME: // FIXME:
private boolean pingCheck(PeerInfo host, PeerAddress address) { private boolean pingCheck(com.usatiuk.dhfs.repository.peersync.PeerInfo host, PeerAddress address) {
try { try {
return rpcClientFactory.withObjSyncClient(host.id(), address, pingTimeout, (peer, c) -> { return rpcClientFactory.withObjSyncClient(host.id(), address, pingTimeout, (peer, c) -> {
c.ping(PingRequest.getDefaultInstance()); c.ping(PingRequest.getDefaultInstance());
@@ -150,7 +148,7 @@ public class PeerManager {
return _states.containsKey(host); return _states.containsKey(host);
} }
public boolean isReachable(PeerInfo host) { public boolean isReachable(com.usatiuk.dhfs.repository.peersync.PeerInfo host) {
return isReachable(host.id()); return isReachable(host.id());
} }
@@ -170,7 +168,7 @@ public class PeerManager {
public HostStateSnapshot getHostStateSnapshot() { public HostStateSnapshot getHostStateSnapshot() {
return transactionManager.run(() -> { return transactionManager.run(() -> {
var partition = peerInfoService.getPeersNoSelf().stream().map(PeerInfo::id) var partition = peerInfoService.getPeersNoSelf().stream().map(com.usatiuk.dhfs.repository.peersync.PeerInfo::id)
.collect(Collectors.partitioningBy(this::isReachable)); .collect(Collectors.partitioningBy(this::isReachable));
return new HostStateSnapshot(partition.get(true), partition.get(false)); return new HostStateSnapshot(partition.get(true), partition.get(false));
}); });
@@ -201,10 +199,9 @@ public class PeerManager {
peerTrustManager.reloadTrustManagerHosts(transactionManager.run(() -> peerInfoService.getPeers().stream().toList())); //FIXME: peerTrustManager.reloadTrustManagerHosts(transactionManager.run(() -> peerInfoService.getPeers().stream().toList())); //FIXME:
} }
public Collection<AvailablePeerInfo> getSeenButNotAddedHosts() { public Collection<PeerId> getSeenButNotAddedHosts() {
return transactionManager.run(() -> { return transactionManager.run(() -> {
return peerDiscoveryDirectory.getReachablePeers().stream().filter(p -> !peerInfoService.getPeerInfo(p).isPresent()) return peerDiscoveryDirectory.getReachablePeers().stream().filter(p -> !peerInfoService.getPeerInfo(p).isPresent()).toList();
.map(p -> new AvailablePeerInfo(p.toString())).toList();
}); });
} }

View File

@@ -1,7 +1,9 @@
package com.usatiuk.dhfs.repository; package com.usatiuk.dhfs.repository;
import com.usatiuk.dhfs.ShutdownChecker;
import com.usatiuk.dhfs.PeerId; import com.usatiuk.dhfs.PeerId;
import com.usatiuk.dhfs.ShutdownChecker;
import com.usatiuk.dhfs.repository.peerdiscovery.IpPeerAddress;
import com.usatiuk.dhfs.repository.peerdiscovery.PeerAddressType;
import com.usatiuk.dhfs.repository.peersync.PeerInfoService; import com.usatiuk.dhfs.repository.peersync.PeerInfoService;
import com.usatiuk.dhfs.repository.peertrust.PeerTrustManager; import com.usatiuk.dhfs.repository.peertrust.PeerTrustManager;
import com.usatiuk.objects.transaction.Transaction; import com.usatiuk.objects.transaction.Transaction;
@@ -13,6 +15,7 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes; import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.pcollections.HashTreePMap;
import org.pcollections.HashTreePSet; import org.pcollections.HashTreePSet;
import java.io.File; import java.io.File;
@@ -23,6 +26,7 @@ import java.nio.file.StandardOpenOption;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@@ -70,7 +74,7 @@ public class PersistentPeerDataService {
_selfKeyPair = CertificateTools.generateKeyPair(); _selfKeyPair = CertificateTools.generateKeyPair();
_selfCertificate = CertificateTools.generateCertificate(_selfKeyPair, _selfUuid.toString()); _selfCertificate = CertificateTools.generateCertificate(_selfKeyPair, _selfUuid.toString());
curTx.put(new PersistentRemoteHostsData(_selfUuid, _selfCertificate, _selfKeyPair, HashTreePSet.empty())); curTx.put(new PersistentRemoteHostsData(_selfUuid, _selfCertificate, _selfKeyPair, HashTreePSet.empty(), HashTreePMap.empty()));
peerInfoService.putPeer(_selfUuid, _selfCertificate.getEncoded()); peerInfoService.putPeer(_selfUuid, _selfCertificate.getEncoded());
} catch (CertificateEncodingException e) { } catch (CertificateEncodingException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@@ -153,4 +157,39 @@ public class PersistentPeerDataService {
return data.initialSyncDone().contains(peerId); return data.initialSyncDone().contains(peerId);
}); });
} }
public List<IpPeerAddress> getPersistentPeerAddresses() {
return txm.run(() -> {
var data = curTx.get(PersistentRemoteHostsData.class, PersistentRemoteHostsData.KEY).orElse(null);
if (data == null) throw new IllegalStateException("Self data not found");
return data.persistentPeerAddress().values().stream().toList();
});
}
public void addPersistentPeerAddress(PeerId peerId, IpPeerAddress address) {
txm.run(() -> {
var data = curTx.get(PersistentRemoteHostsData.class, PersistentRemoteHostsData.KEY).orElse(null);
if (data == null) throw new IllegalStateException("Self data not found");
var newData = data.persistentPeerAddress().plus(peerId, address.withType(PeerAddressType.WAN)); //TODO:
curTx.put(data.withPersistentPeerAddress(newData));
});
}
public void removePersistentPeerAddress(PeerId peerId) {
txm.run(() -> {
var data = curTx.get(PersistentRemoteHostsData.class, PersistentRemoteHostsData.KEY).orElse(null);
if (data == null) throw new IllegalStateException("Self data not found");
var newData = data.persistentPeerAddress().minus(peerId);
curTx.put(data.withPersistentPeerAddress(newData));
});
}
public IpPeerAddress getPersistentPeerAddress(PeerId peerId) {
return txm.run(() -> {
var data = curTx.get(PersistentRemoteHostsData.class, PersistentRemoteHostsData.KEY).orElse(null);
if (data == null) throw new IllegalStateException("Self data not found");
return data.persistentPeerAddress().get(peerId);
});
}
} }

View File

@@ -1,8 +1,10 @@
package com.usatiuk.dhfs.repository; package com.usatiuk.dhfs.repository;
import com.usatiuk.dhfs.PeerId;
import com.usatiuk.dhfs.repository.peerdiscovery.IpPeerAddress;
import com.usatiuk.objects.JData; import com.usatiuk.objects.JData;
import com.usatiuk.objects.JObjectKey; import com.usatiuk.objects.JObjectKey;
import com.usatiuk.dhfs.PeerId; import org.pcollections.PMap;
import org.pcollections.PSet; import org.pcollections.PSet;
import java.io.Serializable; import java.io.Serializable;
@@ -12,7 +14,8 @@ import java.security.cert.X509Certificate;
public record PersistentRemoteHostsData(PeerId selfUuid, public record PersistentRemoteHostsData(PeerId selfUuid,
X509Certificate selfCertificate, X509Certificate selfCertificate,
KeyPair selfKeyPair, KeyPair selfKeyPair,
PSet<PeerId> initialSyncDone) implements JData, Serializable { PSet<PeerId> initialSyncDone,
PMap<PeerId, IpPeerAddress> persistentPeerAddress) implements JData, Serializable {
public static final JObjectKey KEY = JObjectKey.of("self_peer_data"); public static final JObjectKey KEY = JObjectKey.of("self_peer_data");
@Override @Override
@@ -20,9 +23,12 @@ public record PersistentRemoteHostsData(PeerId selfUuid,
return KEY; return KEY;
} }
public PersistentRemoteHostsData withInitialSyncDone(PSet<PeerId> initialSyncDone) { public PersistentRemoteHostsData withInitialSyncDone(PSet<PeerId> initialSyncDone) {
return new PersistentRemoteHostsData(selfUuid, selfCertificate, selfKeyPair, initialSyncDone); return new PersistentRemoteHostsData(selfUuid, selfCertificate, selfKeyPair, initialSyncDone, persistentPeerAddress);
}
public PersistentRemoteHostsData withPersistentPeerAddress(PMap<PeerId, IpPeerAddress> persistentPeerAddress) {
return new PersistentRemoteHostsData(selfUuid, selfCertificate, selfKeyPair, initialSyncDone, persistentPeerAddress);
} }
@Override @Override

View File

@@ -6,4 +6,7 @@ import java.net.InetAddress;
public record IpPeerAddress(PeerId peer, PeerAddressType type, public record IpPeerAddress(PeerId peer, PeerAddressType type,
InetAddress address, int port, int securePort) implements PeerAddress { InetAddress address, int port, int securePort) implements PeerAddress {
public IpPeerAddress withType(PeerAddressType type) {
return new IpPeerAddress(peer, type, address, port, securePort);
}
} }

View File

@@ -0,0 +1,36 @@
package com.usatiuk.dhfs.repository.peerdiscovery;
import com.usatiuk.dhfs.PeerId;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Optional;
public class PeerAddrStringHelper {
public static Optional<IpPeerAddress> parse(String addr) {
if (addr.isEmpty()) {
return Optional.empty();
}
var split = addr.split(":");
try {
return Optional.of(new IpPeerAddress(PeerId.of(split[0]), PeerAddressType.LAN, InetAddress.getByName(split[1]),
Integer.parseInt(split[2]), Integer.parseInt(split[3])));
} catch (UnknownHostException ex) {
throw new RuntimeException(ex);
}
}
public static Optional<IpPeerAddress> parseNoPeer(PeerId peerId, String addr) {
if (addr.isEmpty()) {
return Optional.empty();
}
var split = addr.split(":");
try {
return Optional.of(new IpPeerAddress(peerId, PeerAddressType.LAN, InetAddress.getByName(split[0]),
Integer.parseInt(split[1]), Integer.parseInt(split[2])));
} catch (UnknownHostException ex) {
throw new RuntimeException(ex);
}
}
}

View File

@@ -2,7 +2,9 @@ package com.usatiuk.dhfs.repository.peerdiscovery;
import com.usatiuk.dhfs.PeerId; import com.usatiuk.dhfs.PeerId;
public interface PeerAddress { import java.io.Serializable;
public interface PeerAddress extends Serializable {
PeerId peer(); PeerId peer();
PeerAddressType type(); PeerAddressType type();

View File

@@ -0,0 +1,22 @@
package com.usatiuk.dhfs.repository.peerdiscovery;
import com.usatiuk.dhfs.repository.PersistentPeerDataService;
import io.quarkus.scheduler.Scheduled;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class PersistentStaticPeerDiscovery {
@Inject
PeerDiscoveryDirectory peerDiscoveryDirectory;
@Inject
PersistentPeerDataService persistentPeerDataService;
@Scheduled(every = "1s", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)
public void discoverPeers() {
var addrs = persistentPeerDataService.getPersistentPeerAddresses();
for (var addr : addrs) {
peerDiscoveryDirectory.notifyAddr(addr);
}
}
}

View File

@@ -1,17 +1,13 @@
package com.usatiuk.dhfs.repository.peerdiscovery; package com.usatiuk.dhfs.repository.peerdiscovery;
import com.usatiuk.dhfs.PeerId;
import io.quarkus.scheduler.Scheduled; import io.quarkus.scheduler.Scheduled;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream;
@ApplicationScoped @ApplicationScoped
public class StaticPeerDiscovery { public class StaticPeerDiscovery {
@@ -22,18 +18,8 @@ public class StaticPeerDiscovery {
public StaticPeerDiscovery(@ConfigProperty(name = "dhfs.peerdiscovery.static-peers") Optional<String> staticPeers) { public StaticPeerDiscovery(@ConfigProperty(name = "dhfs.peerdiscovery.static-peers") Optional<String> staticPeers) {
var peers = staticPeers.orElse(""); var peers = staticPeers.orElse("");
_peers = Arrays.stream(peers.split(",")).flatMap(e -> _peers = Arrays.stream(peers.split(",")).flatMap(e ->
{ PeerAddrStringHelper.parse(e).stream()
if (e.isEmpty()) { ).toList();
return Stream.of();
}
var split = e.split(":");
try {
return Stream.of(new IpPeerAddress(PeerId.of(split[0]), PeerAddressType.LAN, InetAddress.getByName(split[1]),
Integer.parseInt(split[2]), Integer.parseInt(split[3])));
} catch (UnknownHostException ex) {
throw new RuntimeException(ex);
}
}).toList();
} }
@Scheduled(every = "1s", concurrentExecution = Scheduled.ConcurrentExecution.SKIP) @Scheduled(every = "1s", concurrentExecution = Scheduled.ConcurrentExecution.SKIP)

View File

@@ -1,4 +0,0 @@
package com.usatiuk.dhfs.repository.webapi;
public record AvailablePeerInfo(String uuid) {
}

View File

@@ -0,0 +1,4 @@
package com.usatiuk.dhfs.repository.webapi;
public record PeerAddressInfo(String uuid, String address) {
}

View File

@@ -0,0 +1,4 @@
package com.usatiuk.dhfs.repository.webapi;
public record PeerInfo(String uuid, String address) {
}

View File

@@ -12,8 +12,8 @@ import jakarta.ws.rs.Path;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@Path("/objects-manage") @Path("/peers-manage")
public class ManagementApi { public class PeerManagementApi {
@Inject @Inject
PeerInfoService peerInfoService; PeerInfoService peerInfoService;
@Inject @Inject
@@ -39,7 +39,20 @@ public class ManagementApi {
@Path("available-peers") @Path("available-peers")
@GET @GET
public Collection<AvailablePeerInfo> availablePeers() { public Collection<KnownPeerInfo> availablePeers() {
return peerManager.getSeenButNotAddedHosts(); return peerManager.getSeenButNotAddedHosts().stream().map(p -> new KnownPeerInfo(p.toString())).toList();
}
@Path("peer-state")
@GET
public Collection<PeerInfo> peerInfos(Collection<String> peerIdStrings) {
return peerIdStrings.stream().map(PeerId::of).map(
peerId -> {
return new PeerInfo(
peerId.toString(),
peerManager.getAddress(peerId).toString()
);
}
).toList();
} }
} }

View File

@@ -0,0 +1,52 @@
package com.usatiuk.dhfs.repository.webapi;
import com.usatiuk.dhfs.PeerId;
import com.usatiuk.dhfs.repository.PeerManager;
import com.usatiuk.dhfs.repository.PersistentPeerDataService;
import com.usatiuk.dhfs.repository.peerdiscovery.PeerAddrStringHelper;
import com.usatiuk.dhfs.repository.peersync.PeerInfoService;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import java.util.Collection;
@Path("/peers-addr-manage")
public class PersistentPeerAddressApi {
@Inject
PeerInfoService peerInfoService;
@Inject
PeerManager peerManager;
@Inject
PersistentPeerDataService persistentPeerDataService;
@Path("{peerId}")
@PUT
public void addPeerAddress(String peerAddr, @PathParam("peerId") String peerId) {
if (peerAddr.isEmpty()) {
deletePeerAddress(peerId);
return;
}
persistentPeerDataService.addPersistentPeerAddress(PeerId.of(peerId), PeerAddrStringHelper.parseNoPeer(PeerId.of(peerId), peerAddr).orElseThrow(IllegalArgumentException::new));
}
@Path("{peerId}")
@DELETE
public void deletePeerAddress(@PathParam("peerId") String peerId) {
persistentPeerDataService.removePersistentPeerAddress(PeerId.of(peerId));
}
@Path("{peerId}")
@GET
public String getPeerAddress(@PathParam("peerId") String peerId) {
return persistentPeerDataService.getPersistentPeerAddress(PeerId.of(peerId)).toString();
}
@Path("")
@GET
public Collection<PeerAddressInfo> getPeerAddresses() {
return persistentPeerDataService.getPersistentPeerAddresses()
.stream()
.map(p -> new PeerAddressInfo(p.peer().toString(), p.address().getHostAddress() + ":" + p.port() + ":" + p.securePort()))
.toList();
}
}

View File

@@ -66,13 +66,13 @@ public class DhfsFuseIT {
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request PUT " + " --request PUT " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " + " --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
var c2curl = container2.execInContainer("/bin/sh", "-c", var c2curl = container2.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request PUT " + " --request PUT " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " + " --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS); waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS); waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
@@ -246,7 +246,7 @@ public class DhfsFuseIT {
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request DELETE " + " --request DELETE " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " + " --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
await().atMost(45, TimeUnit.SECONDS).until(() -> 0 == container2.execInContainer("/bin/sh", "-c", "echo rewritten > /root/dhfs_default/fuse/testf1").getExitCode()); await().atMost(45, TimeUnit.SECONDS).until(() -> 0 == container2.execInContainer("/bin/sh", "-c", "echo rewritten > /root/dhfs_default/fuse/testf1").getExitCode());
await().atMost(45, TimeUnit.SECONDS).until(() -> 0 == container2.execInContainer("/bin/sh", "-c", "echo jioadsd > /root/dhfs_default/fuse/newfile1").getExitCode()); await().atMost(45, TimeUnit.SECONDS).until(() -> 0 == container2.execInContainer("/bin/sh", "-c", "echo jioadsd > /root/dhfs_default/fuse/newfile1").getExitCode());
@@ -262,7 +262,7 @@ public class DhfsFuseIT {
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request PUT " + " --request PUT " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " + " --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS); waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS); waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);

View File

@@ -92,25 +92,25 @@ public class DhfsFusex3IT {
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request PUT " + " --request PUT " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " + " --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
var c2curl1 = container2.execInContainer("/bin/sh", "-c", var c2curl1 = container2.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request PUT " + " --request PUT " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " + " --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
var c2curl3 = container2.execInContainer("/bin/sh", "-c", var c2curl3 = container2.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request PUT " + " --request PUT " +
" --data '{\"uuid\":\"" + c3uuid + "\"}' " + " --data '{\"uuid\":\"" + c3uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
var c3curl = container3.execInContainer("/bin/sh", "-c", var c3curl = container3.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request PUT " + " --request PUT " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " + " --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
waitingConsumer3.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS, 2); waitingConsumer3.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS, 2);
waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS, 2); waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS, 2);
@@ -191,7 +191,7 @@ public class DhfsFusex3IT {
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request DELETE " + " --request DELETE " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " + " --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
Thread.sleep(10000); Thread.sleep(10000);

View File

@@ -73,13 +73,13 @@ public class ResyncIT {
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request PUT " + " --request PUT " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " + " --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
var c2curl = container2.execInContainer("/bin/sh", "-c", var c2curl = container2.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request PUT " + " --request PUT " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " + " --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS); waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS); waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
@@ -113,13 +113,13 @@ public class ResyncIT {
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request PUT " + " --request PUT " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " + " --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
var c2curl = container2.execInContainer("/bin/sh", "-c", var c2curl = container2.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request PUT " + " --request PUT " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " + " --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS); waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS); waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
@@ -153,13 +153,13 @@ public class ResyncIT {
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request PUT " + " --request PUT " +
" --data '{\"uuid\":\"" + c2uuid + "\"}' " + " --data '{\"uuid\":\"" + c2uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
var c2curl = container2.execInContainer("/bin/sh", "-c", var c2curl = container2.execInContainer("/bin/sh", "-c",
"curl --header \"Content-Type: application/json\" " + "curl --header \"Content-Type: application/json\" " +
" --request PUT " + " --request PUT " +
" --data '{\"uuid\":\"" + c1uuid + "\"}' " + " --data '{\"uuid\":\"" + c1uuid + "\"}' " +
" http://localhost:8080/objects-manage/known-peers"); " http://localhost:8080/peers-manage/known-peers");
waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS); waitingConsumer2.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);
waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS); waitingConsumer1.waitUntil(frame -> frame.getUtf8String().contains("Connected"), 60, TimeUnit.SECONDS);

4868
webui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,27 +10,31 @@
"browserslist": "> 0.5%, last 2 versions, not dead", "browserslist": "> 0.5%, last 2 versions, not dead",
"dependencies": { "dependencies": {
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"react": "^18.3.1", "react": "^19.1.0",
"react-dom": "^18.3.1", "react-dom": "^19.1.0",
"react-router-dom": "^6.24.0", "react-router": "^7.4.1",
"zod": "^3.23.8" "react-router-dom": "^7.4.1",
"zod": "^3.24.2"
},
"@parcel/resolver-default": {
"packageExports": true
}, },
"devDependencies": { "devDependencies": {
"@parcel/transformer-sass": "^2.12.0", "@parcel/transformer-sass": "^2.14.4",
"@parcel/transformer-typescript-tsc": "^2.12.0", "@parcel/transformer-typescript-tsc": "^2.14.4",
"@parcel/validator-typescript": "^2.12.0", "@parcel/validator-typescript": "^2.14.4",
"@types/eslint": "^8.56.10", "@types/eslint": "^9.6.1",
"@types/eslint-config-prettier": "^6.11.3", "@types/eslint-config-prettier": "^6.11.3",
"@types/react": "^18.3.3", "@types/react": "^19.0.12",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^19.0.4",
"@typescript-eslint/eslint-plugin": "^7.14.1", "@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^7.14.1", "@typescript-eslint/parser": "^8.28.0",
"eslint": "^8", "eslint": "^9",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^10.1.1",
"eslint-plugin-react": "^7.34.3", "eslint-plugin-react": "^7.37.4",
"parcel": "^2.12.0", "parcel": "^2.14.4",
"prettier": "^3.3.2", "prettier": "^3.5.3",
"process": "^0.11.10", "process": "^0.11.10",
"typescript": "^5.5.2" "typescript": "^5.8.2"
} }
} }

View File

@@ -9,7 +9,6 @@ export interface TPeerAvailableCardProps {
export function PeerAvailableCard({ peerInfo }: TPeerAvailableCardProps) { export function PeerAvailableCard({ peerInfo }: TPeerAvailableCardProps) {
const fetcher = useFetcher(); const fetcher = useFetcher();
return ( return (
<div className="peerAvailableCard"> <div className="peerAvailableCard">
<div className={"peerInfo"}> <div className={"peerInfo"}>
@@ -22,8 +21,8 @@ export function PeerAvailableCard({ peerInfo }: TPeerAvailableCardProps) {
action={"/home/peers"} action={"/home/peers"}
> >
<button type="submit">connect</button> <button type="submit">connect</button>
<input name="intent" hidden={true} value={"add_peer"} /> <input name="intent" hidden={true} defaultValue={"add_peer"} />
<input name="uuid" hidden={true} value={peerInfo.uuid} /> <input name="uuid" hidden={true} defaultValue={peerInfo.uuid} />
</fetcher.Form> </fetcher.Form>
</div> </div>
); );

View File

@@ -1,7 +1,9 @@
import { TKnownPeerInfoTo } from "./api/dto"; import { TKnownPeerInfoTo } from "./api/dto";
import "./PeerKnownCard.scss"; import "./PeerKnownCard.scss";
import { useFetcher } from "react-router-dom"; import { useFetcher, useLoaderData } from "react-router-dom";
import { LoaderToType } from "./commonPlumbing";
import { peerStateLoader } from "./PeerStatePlumbing";
export interface TPeerKnownCardProps { export interface TPeerKnownCardProps {
peerInfo: TKnownPeerInfoTo; peerInfo: TKnownPeerInfoTo;
@@ -9,12 +11,42 @@ export interface TPeerKnownCardProps {
export function PeerKnownCard({ peerInfo }: TPeerKnownCardProps) { export function PeerKnownCard({ peerInfo }: TPeerKnownCardProps) {
const fetcher = useFetcher(); const fetcher = useFetcher();
const loaderData = useLoaderData() as LoaderToType<typeof peerStateLoader>;
const addr = loaderData.peerAddresses.find(
(item) => item.uuid === peerInfo.uuid,
);
return ( return (
<div className="peerKnownCard"> <div className="peerKnownCard">
<div className={"peerInfo"}> <div className={"peerInfo"}>
<span>UUID: </span> <div>
<span>{peerInfo.uuid}</span> <span>UUID: </span>
<span>{peerInfo.uuid}</span>
</div>
<div>
<fetcher.Form
className="actions"
method="put"
action={"/home/peers"}
>
<input
name="intent"
hidden={true}
defaultValue={"save_addr"}
/>
<input
name="uuid"
hidden={true}
defaultValue={peerInfo.uuid}
/>
<input
name="address"
defaultValue={addr?.address || ""}
/>
<button type="submit">save</button>
</fetcher.Form>
</div>
</div> </div>
<fetcher.Form <fetcher.Form
className="actions" className="actions"
@@ -22,8 +54,12 @@ export function PeerKnownCard({ peerInfo }: TPeerKnownCardProps) {
action={"/home/peers"} action={"/home/peers"}
> >
<button type="submit">remove</button> <button type="submit">remove</button>
<input name="intent" hidden={true} value={"remove_peer"} /> <input
<input name="uuid" hidden={true} value={peerInfo.uuid} /> name="intent"
hidden={true}
defaultValue={"remove_peer"}
/>
<input name="uuid" hidden={true} defaultValue={peerInfo.uuid} />
</fetcher.Form> </fetcher.Form>
</div> </div>
); );

View File

@@ -1,7 +1,9 @@
import { import {
getAvailablePeers, getAvailablePeers,
getKnownPeers, getKnownPeers,
getPeerAddresses,
putKnownPeer, putKnownPeer,
putPeerAddress,
removeKnownPeer, removeKnownPeer,
} from "./api/PeerState"; } from "./api/PeerState";
import { ActionFunctionArgs } from "react-router-dom"; import { ActionFunctionArgs } from "react-router-dom";
@@ -10,10 +12,15 @@ export async function peerStateLoader() {
return { return {
availablePeers: await getAvailablePeers(), availablePeers: await getAvailablePeers(),
knownPeers: await getKnownPeers(), knownPeers: await getKnownPeers(),
peerAddresses: await getPeerAddresses(),
}; };
} }
export type PeerStateActionType = "add_peer" | "remove_peer" | unknown; export type PeerStateActionType =
| "add_peer"
| "remove_peer"
| "save_addr"
| unknown;
export async function peerStateAction({ request }: ActionFunctionArgs) { export async function peerStateAction({ request }: ActionFunctionArgs) {
const formData = await request.formData(); const formData = await request.formData();
@@ -22,6 +29,11 @@ export async function peerStateAction({ request }: ActionFunctionArgs) {
return await putKnownPeer(formData.get("uuid") as string); return await putKnownPeer(formData.get("uuid") as string);
} else if (intent === "remove_peer") { } else if (intent === "remove_peer") {
return await removeKnownPeer(formData.get("uuid") as string); return await removeKnownPeer(formData.get("uuid") as string);
} else if (intent === "save_addr") {
return await putPeerAddress(
formData.get("uuid") as string,
formData.get("address") as string,
);
} else { } else {
throw new Error("Malformed action: " + JSON.stringify(request)); throw new Error("Malformed action: " + JSON.stringify(request));
} }

View File

@@ -3,36 +3,73 @@ import {
AvailablePeerInfoToResp, AvailablePeerInfoToResp,
KnownPeerInfoToResp, KnownPeerInfoToResp,
NoContentToResp, NoContentToResp,
PeerAddressInfoToResp,
TAvailablePeerInfoArrTo, TAvailablePeerInfoArrTo,
TAvailablePeerInfoToResp, TAvailablePeerInfoToResp,
TKnownPeerInfoArrTo, TKnownPeerInfoArrTo,
TKnownPeerInfoToResp, TKnownPeerInfoToResp,
TNoContentToResp, TNoContentToResp,
TPeerAddressInfoArrTo,
TPeerAddressInfoToResp,
} from "./dto"; } from "./dto";
export async function getAvailablePeers(): Promise<TAvailablePeerInfoArrTo> { export async function getAvailablePeers(): Promise<TAvailablePeerInfoArrTo> {
return fetchJSON_throws< return fetchJSON_throws<
TAvailablePeerInfoToResp, TAvailablePeerInfoToResp,
typeof AvailablePeerInfoToResp typeof AvailablePeerInfoToResp
>("/objects-manage/available-peers", "GET", AvailablePeerInfoToResp); >("/peers-manage/available-peers", "GET", AvailablePeerInfoToResp);
} }
export async function getKnownPeers(): Promise<TKnownPeerInfoArrTo> { export async function getKnownPeers(): Promise<TKnownPeerInfoArrTo> {
return fetchJSON_throws<TKnownPeerInfoToResp, typeof KnownPeerInfoToResp>( return fetchJSON_throws<TKnownPeerInfoToResp, typeof KnownPeerInfoToResp>(
"/objects-manage/known-peers", "/peers-manage/known-peers",
"GET", "GET",
KnownPeerInfoToResp, KnownPeerInfoToResp,
); );
} }
export async function putKnownPeer(uuid: string): Promise<TNoContentToResp> { export async function putKnownPeer(uuid: string): Promise<TNoContentToResp> {
return fetchJSON("/objects-manage/known-peers", "PUT", NoContentToResp, { return fetchJSON("/peers-manage/known-peers", "PUT", NoContentToResp, {
uuid, uuid,
}); });
} }
export async function removeKnownPeer(uuid: string): Promise<TNoContentToResp> { export async function removeKnownPeer(uuid: string): Promise<TNoContentToResp> {
return fetchJSON("/objects-manage/known-peers", "DELETE", NoContentToResp, { return fetchJSON("/peers-manage/known-peers", "DELETE", NoContentToResp, {
uuid, uuid,
}); });
} }
export async function getPeerAddresses(): Promise<TPeerAddressInfoArrTo> {
return fetchJSON_throws<
TPeerAddressInfoToResp,
typeof PeerAddressInfoToResp
>("/peers-addr-manage", "GET", PeerAddressInfoToResp);
}
export async function putPeerAddress(
uuid: string,
address: string,
): Promise<TNoContentToResp> {
return fetchJSON(
`/peers-addr-manage/${uuid}`,
"PUT",
NoContentToResp,
address,
);
}
export async function removePeerAddress(
uuid: string,
): Promise<TNoContentToResp> {
return fetchJSON(`/peers-addr-manage/${uuid}`, "DELETE", NoContentToResp);
}
export async function getPeerAddress(
uuid: string,
): Promise<TPeerAddressInfoToResp> {
return fetchJSON_throws<
TPeerAddressInfoToResp,
typeof PeerAddressInfoToResp
>(`/peers-addr-manage/${uuid}`, "GET", PeerAddressInfoToResp);
}

View File

@@ -64,6 +64,19 @@ export type TKnownPeerInfoArrTo = z.infer<typeof KnownPeerInfoArrTo>;
export const KnownPeerInfoToResp = CreateAPIResponse(KnownPeerInfoArrTo); export const KnownPeerInfoToResp = CreateAPIResponse(KnownPeerInfoArrTo);
export type TKnownPeerInfoToResp = z.infer<typeof KnownPeerInfoToResp>; export type TKnownPeerInfoToResp = z.infer<typeof KnownPeerInfoToResp>;
// PeerAddressInfo
export const PeerAddressInfoTo = z.object({
uuid: z.string(),
address: z.string(),
});
export type TPeerAddressInfoTo = z.infer<typeof PeerAddressInfoTo>;
export const PeerAddressInfoArrTo = z.array(PeerAddressInfoTo);
export type TPeerAddressInfoArrTo = z.infer<typeof PeerAddressInfoArrTo>;
export const PeerAddressInfoToResp = CreateAPIResponse(PeerAddressInfoArrTo);
export type TPeerAddressInfoToResp = z.infer<typeof PeerAddressInfoToResp>;
// KnownPeerPut // KnownPeerPut
export const KnownPeerPutTo = z.object({ uuid: z.string() }); export const KnownPeerPutTo = z.object({ uuid: z.string() });
export type TKnownPeerPutTo = z.infer<typeof KnownPeerPutTo>; export type TKnownPeerPutTo = z.infer<typeof KnownPeerPutTo>;

View File

@@ -44,14 +44,17 @@ export async function fetchJSON<T, P extends { parse: (arg: string) => T }>(
body?: string | Record<string, unknown> | File, body?: string | Record<string, unknown> | File,
headers?: Record<string, string>, headers?: Record<string, string>,
): Promise<T> { ): Promise<T> {
const reqBody = () => const reqBody = () => {
body instanceof File if (typeof body === "string" || body instanceof String)
return body.toString();
return body instanceof File
? (() => { ? (() => {
const fd = new FormData(); const fd = new FormData();
fd.append("file", body); fd.append("file", body);
return fd; return fd;
})() })()
: JSON.stringify(body); : JSON.stringify(body);
};
const reqHeaders = () => const reqHeaders = () =>
body instanceof File body instanceof File

View File

@@ -6,7 +6,7 @@
], ],
"jsx": "react-jsx", "jsx": "react-jsx",
"target": "es2015", "target": "es2015",
"moduleResolution": "Node", "moduleResolution": "bundler",
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"sourceMap": true, "sourceMap": true,