network/pkg/ipfs/cluster_peer.go
2026-01-26 14:41:26 +02:00

394 lines
11 KiB
Go

package ipfs
import (
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/libp2p/go-libp2p/core/host"
"github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
"go.uber.org/zap"
)
// UpdatePeerAddresses updates the peer_addresses in service.json with given multiaddresses
func (cm *ClusterConfigManager) UpdatePeerAddresses(addrs []string) error {
serviceJSONPath := filepath.Join(cm.clusterPath, "service.json")
cfg, err := cm.loadOrCreateConfig(serviceJSONPath)
if err != nil {
return err
}
seen := make(map[string]bool)
uniqueAddrs := []string{}
for _, addr := range addrs {
if !seen[addr] {
uniqueAddrs = append(uniqueAddrs, addr)
seen[addr] = true
}
}
cfg.Cluster.PeerAddresses = uniqueAddrs
return cm.saveConfig(serviceJSONPath, cfg)
}
// UpdateAllClusterPeers discovers all cluster peers from the gateway and updates local config
func (cm *ClusterConfigManager) UpdateAllClusterPeers() error {
peers, err := cm.DiscoverClusterPeersFromGateway()
if err != nil {
return fmt.Errorf("failed to discover cluster peers: %w", err)
}
if len(peers) == 0 {
return nil
}
peerAddrs := []string{}
for _, p := range peers {
peerAddrs = append(peerAddrs, p.Multiaddress)
}
return cm.UpdatePeerAddresses(peerAddrs)
}
// RepairPeerConfiguration attempts to fix configuration issues and re-synchronize peers
func (cm *ClusterConfigManager) RepairPeerConfiguration() error {
cm.logger.Info("Attempting to repair IPFS Cluster peer configuration")
_ = cm.FixIPFSConfigAddresses()
peers, err := cm.DiscoverClusterPeersFromGateway()
if err != nil {
cm.logger.Warn("Could not discover peers from gateway during repair", zap.Error(err))
} else {
peerAddrs := []string{}
for _, p := range peers {
peerAddrs = append(peerAddrs, p.Multiaddress)
}
if len(peerAddrs) > 0 {
_ = cm.UpdatePeerAddresses(peerAddrs)
}
}
return nil
}
// DiscoverClusterPeersFromGateway queries the central gateway for registered IPFS Cluster peers
func (cm *ClusterConfigManager) DiscoverClusterPeersFromGateway() ([]ClusterPeerInfo, error) {
// Not implemented - would require a central gateway URL in config
return nil, nil
}
// DiscoverClusterPeersFromLibP2P discovers IPFS and IPFS Cluster peers by querying
// the /v1/network/status endpoint of connected libp2p peers.
// This is the correct approach since IPFS/Cluster peer IDs are different from libp2p peer IDs.
func (cm *ClusterConfigManager) DiscoverClusterPeersFromLibP2P(h host.Host) error {
if h == nil {
return nil
}
var clusterPeers []string
var ipfsPeers []IPFSPeerEntry
// Get unique IPs from connected libp2p peers
peerIPs := make(map[string]bool)
for _, p := range h.Peerstore().Peers() {
if p == h.ID() {
continue
}
info := h.Peerstore().PeerInfo(p)
for _, addr := range info.Addrs {
// Extract IP from multiaddr
ip := extractIPFromMultiaddr(addr)
if ip != "" && !strings.HasPrefix(ip, "127.") && !strings.HasPrefix(ip, "::1") {
peerIPs[ip] = true
}
}
}
if len(peerIPs) == 0 {
return nil
}
// Query each peer's /v1/network/status endpoint to get IPFS and Cluster info
client := &http.Client{Timeout: 5 * time.Second}
for ip := range peerIPs {
statusURL := fmt.Sprintf("http://%s:6001/v1/network/status", ip)
resp, err := client.Get(statusURL)
if err != nil {
cm.logger.Debug("Failed to query peer status", zap.String("ip", ip), zap.Error(err))
continue
}
var status NetworkStatusResponse
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
resp.Body.Close()
cm.logger.Debug("Failed to decode peer status", zap.String("ip", ip), zap.Error(err))
continue
}
resp.Body.Close()
// Add IPFS Cluster peer if available
if status.IPFSCluster != nil && status.IPFSCluster.PeerID != "" {
for _, addr := range status.IPFSCluster.Addresses {
if strings.Contains(addr, "/tcp/9100") {
clusterPeers = append(clusterPeers, addr)
cm.logger.Info("Discovered IPFS Cluster peer", zap.String("peer", addr))
}
}
}
// Add IPFS peer if available
if status.IPFS != nil && status.IPFS.PeerID != "" {
for _, addr := range status.IPFS.SwarmAddresses {
if strings.Contains(addr, "/tcp/4101") && !strings.Contains(addr, "127.0.0.1") {
ipfsPeers = append(ipfsPeers, IPFSPeerEntry{
ID: status.IPFS.PeerID,
Addrs: []string{addr},
})
cm.logger.Info("Discovered IPFS peer", zap.String("peer_id", status.IPFS.PeerID))
break // One address per peer is enough
}
}
}
}
// Update IPFS Cluster peer addresses
if len(clusterPeers) > 0 {
if err := cm.UpdatePeerAddresses(clusterPeers); err != nil {
cm.logger.Warn("Failed to update cluster peer addresses", zap.Error(err))
} else {
cm.logger.Info("Updated IPFS Cluster peer addresses", zap.Int("count", len(clusterPeers)))
}
}
// Update IPFS Peering.Peers
if len(ipfsPeers) > 0 {
if err := cm.UpdateIPFSPeeringConfig(ipfsPeers); err != nil {
cm.logger.Warn("Failed to update IPFS peering config", zap.Error(err))
} else {
cm.logger.Info("Updated IPFS Peering.Peers", zap.Int("count", len(ipfsPeers)))
}
}
return nil
}
// NetworkStatusResponse represents the response from /v1/network/status
type NetworkStatusResponse struct {
PeerID string `json:"peer_id"`
PeerCount int `json:"peer_count"`
IPFS *NetworkStatusIPFS `json:"ipfs,omitempty"`
IPFSCluster *NetworkStatusIPFSCluster `json:"ipfs_cluster,omitempty"`
}
type NetworkStatusIPFS struct {
PeerID string `json:"peer_id"`
SwarmAddresses []string `json:"swarm_addresses"`
}
type NetworkStatusIPFSCluster struct {
PeerID string `json:"peer_id"`
Addresses []string `json:"addresses"`
}
// IPFSPeerEntry represents an IPFS peer for Peering.Peers config
type IPFSPeerEntry struct {
ID string `json:"ID"`
Addrs []string `json:"Addrs"`
}
// extractIPFromMultiaddr extracts the IP address from a multiaddr
func extractIPFromMultiaddr(ma multiaddr.Multiaddr) string {
if ma == nil {
return ""
}
// Try to convert to net.Addr and extract IP
if addr, err := manet.ToNetAddr(ma); err == nil {
addrStr := addr.String()
// Handle "ip:port" format
if idx := strings.LastIndex(addrStr, ":"); idx > 0 {
return addrStr[:idx]
}
return addrStr
}
// Fallback: parse manually
parts := strings.Split(ma.String(), "/")
for i, part := range parts {
if (part == "ip4" || part == "ip6") && i+1 < len(parts) {
return parts[i+1]
}
}
return ""
}
// UpdateIPFSPeeringConfig updates the Peering.Peers section in IPFS config
func (cm *ClusterConfigManager) UpdateIPFSPeeringConfig(peers []IPFSPeerEntry) error {
// Find IPFS config path
ipfsRepoPath := cm.findIPFSRepoPath()
if ipfsRepoPath == "" {
return fmt.Errorf("could not find IPFS repo path")
}
configPath := filepath.Join(ipfsRepoPath, "config")
// Read existing config
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read IPFS config: %w", err)
}
var config map[string]interface{}
if err := json.Unmarshal(data, &config); err != nil {
return fmt.Errorf("failed to parse IPFS config: %w", err)
}
// Get or create Peering section
peering, ok := config["Peering"].(map[string]interface{})
if !ok {
peering = make(map[string]interface{})
}
// Get existing peers
existingPeers := []IPFSPeerEntry{}
if existingPeersList, ok := peering["Peers"].([]interface{}); ok {
for _, p := range existingPeersList {
if peerMap, ok := p.(map[string]interface{}); ok {
entry := IPFSPeerEntry{}
if id, ok := peerMap["ID"].(string); ok {
entry.ID = id
}
if addrs, ok := peerMap["Addrs"].([]interface{}); ok {
for _, a := range addrs {
if addr, ok := a.(string); ok {
entry.Addrs = append(entry.Addrs, addr)
}
}
}
if entry.ID != "" {
existingPeers = append(existingPeers, entry)
}
}
}
}
// Merge new peers with existing (avoid duplicates by ID)
seenIDs := make(map[string]bool)
mergedPeers := []interface{}{}
// Add existing peers first
for _, p := range existingPeers {
seenIDs[p.ID] = true
mergedPeers = append(mergedPeers, map[string]interface{}{
"ID": p.ID,
"Addrs": p.Addrs,
})
}
// Add new peers
for _, p := range peers {
if !seenIDs[p.ID] {
seenIDs[p.ID] = true
mergedPeers = append(mergedPeers, map[string]interface{}{
"ID": p.ID,
"Addrs": p.Addrs,
})
}
}
// Update config
peering["Peers"] = mergedPeers
config["Peering"] = peering
// Write back
updatedData, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal IPFS config: %w", err)
}
if err := os.WriteFile(configPath, updatedData, 0600); err != nil {
return fmt.Errorf("failed to write IPFS config: %w", err)
}
return nil
}
// findIPFSRepoPath finds the IPFS repository path
func (cm *ClusterConfigManager) findIPFSRepoPath() string {
dataDir := cm.cfg.Node.DataDir
if strings.HasPrefix(dataDir, "~") {
home, _ := os.UserHomeDir()
dataDir = filepath.Join(home, dataDir[1:])
}
possiblePaths := []string{
filepath.Join(dataDir, "ipfs", "repo"),
filepath.Join(dataDir, "node-1", "ipfs", "repo"),
filepath.Join(dataDir, "node-2", "ipfs", "repo"),
filepath.Join(filepath.Dir(dataDir), "ipfs", "repo"),
}
for _, path := range possiblePaths {
if _, err := os.Stat(filepath.Join(path, "config")); err == nil {
return path
}
}
return ""
}
func (cm *ClusterConfigManager) getPeerID() (string, error) {
dataDir := cm.cfg.Node.DataDir
if strings.HasPrefix(dataDir, "~") {
home, _ := os.UserHomeDir()
dataDir = filepath.Join(home, dataDir[1:])
}
possiblePaths := []string{
filepath.Join(dataDir, "ipfs", "repo"),
filepath.Join(dataDir, "node-1", "ipfs", "repo"),
filepath.Join(dataDir, "node-2", "ipfs", "repo"),
filepath.Join(filepath.Dir(dataDir), "node-1", "ipfs", "repo"),
filepath.Join(filepath.Dir(dataDir), "node-2", "ipfs", "repo"),
}
var ipfsRepoPath string
for _, path := range possiblePaths {
if _, err := os.Stat(filepath.Join(path, "config")); err == nil {
ipfsRepoPath = path
break
}
}
if ipfsRepoPath == "" {
return "", fmt.Errorf("could not find IPFS repo path")
}
idCmd := exec.Command("ipfs", "id", "-f", "<id>")
idCmd.Env = append(os.Environ(), "IPFS_PATH="+ipfsRepoPath)
out, err := idCmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
// ClusterPeerInfo represents information about an IPFS Cluster peer
type ClusterPeerInfo struct {
ID string `json:"id"`
Multiaddress string `json:"multiaddress"`
NodeName string `json:"node_name"`
LastSeen time.Time `json:"last_seen"`
}