github.com/filecoin-project/bacalhau@v0.3.23-0.20230228154132-45c989550ace/pkg/ipfs/node.go (about) 1 package ipfs 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net" 8 "os" 9 "strconv" 10 "sync" 11 12 "github.com/filecoin-project/bacalhau/pkg/system" 13 "github.com/hashicorp/go-multierror" 14 icore "github.com/ipfs/interface-go-ipfs-core" 15 "github.com/libp2p/go-libp2p/core/peer" 16 ma "github.com/multiformats/go-multiaddr" 17 manet "github.com/multiformats/go-multiaddr/net" 18 "github.com/phayes/freeport" 19 "github.com/pkg/errors" 20 21 "github.com/rs/zerolog/log" 22 23 "github.com/ipfs/kubo/commands" 24 "github.com/ipfs/kubo/config" 25 "github.com/ipfs/kubo/core" 26 "github.com/ipfs/kubo/core/coreapi" 27 "github.com/ipfs/kubo/core/corehttp" 28 "github.com/ipfs/kubo/core/node/libp2p" 29 "github.com/ipfs/kubo/plugin/loader" 30 kuboRepo "github.com/ipfs/kubo/repo" 31 "github.com/ipfs/kubo/repo/fsrepo" 32 ) 33 34 var ( 35 // For loading ipfs plugins once per process: 36 pluginOnce sync.Once 37 38 // Global cache of the plugin loader: 39 pluginLoader *loader.PluginLoader 40 ) 41 42 const ( 43 // The default size of a node's repo keypair. 44 defaultKeypairSize = 2048 45 ) 46 47 // Node is a wrapper around an in-process IPFS node that can be used to 48 // interact with the IPFS network without requiring an `ipfs` binary. 49 type Node struct { 50 api icore.CoreAPI 51 ipfsNode *core.IpfsNode 52 53 // Mode is the mode the ipfs node was created in. 54 Mode NodeMode 55 56 // RepoPath is the path to the ipfs node's data repository. 57 RepoPath string 58 59 // APIPort is the port that the node's ipfs API is listening on. 60 APIPort int 61 62 // SwarmPort is the port that the node's ipfs swarm is listening on. 63 SwarmPort int 64 } 65 66 // NodeMode configures how the node treats the public IPFS network. 67 type NodeMode int 68 69 const ( 70 // ModeDefault is the default node mode, which uses an IPFS repo backed 71 // by the `flatfs` datastore, and connects to the public IPFS network. 72 ModeDefault NodeMode = iota 73 74 // ModeLocal is a node mode that uses an IPFS repo backed by the `flatfs` 75 // datastore and ignores the public IPFS network completely, for setting 76 // up test environments without polluting the public IPFS nodes. 77 ModeLocal 78 ) 79 80 // Config contains configuration for the IPFS node. 81 type Config struct { 82 // PeerAddrs is a list of additional IPFS node multiaddrs to use as 83 // peers. By default, the IPFS node will connect to whatever nodes are 84 // specified by its mode. 85 PeerAddrs []string 86 87 // Mode configures the node's default settings. 88 Mode NodeMode 89 90 // KeypairSize is the number of bits to use for the node's repo keypair. If 91 // nil, then a default value of 2048 is used. 92 KeypairSize int 93 } 94 95 func (cfg *Config) getKeypairSize() int { 96 if cfg.KeypairSize == 0 { 97 return defaultKeypairSize 98 } 99 100 return cfg.KeypairSize 101 } 102 103 func (cfg *Config) getMode() NodeMode { 104 return cfg.Mode 105 } 106 107 func (cfg *Config) getPeerAddrs() []string { 108 return cfg.PeerAddrs 109 } 110 111 // NewNode creates a new IPFS node in default mode, which creates an IPFS 112 // repo in a temporary directory, uses the public libp2p nodes as peers and 113 // generates a repo keypair with 2048 bits. 114 func NewNode(ctx context.Context, cm *system.CleanupManager, peerAddrs []string) (*Node, error) { 115 return newNode(ctx, cm, peerAddrs, ModeDefault) 116 } 117 118 // NewLocalNode creates a new local IPFS node in local mode, which can be used 119 // to create test environments without polluting the public IPFS nodes. 120 func NewLocalNode(ctx context.Context, cm *system.CleanupManager, peerAddrs []string) (*Node, error) { 121 return newNode(ctx, cm, peerAddrs, ModeLocal) 122 } 123 124 func newNode(ctx context.Context, cm *system.CleanupManager, peerAddrs []string, mode NodeMode) (*Node, error) { 125 // filter out any empty peer addresses 126 filteredPeerAddrs := make([]string, 0, len(peerAddrs)) 127 for _, addr := range peerAddrs { 128 if addr != "" { 129 filteredPeerAddrs = append(filteredPeerAddrs, addr) 130 } 131 } 132 return tryCreateNode(ctx, cm, Config{ 133 Mode: mode, 134 PeerAddrs: filteredPeerAddrs, 135 }) 136 } 137 138 func tryCreateNode(ctx context.Context, cm *system.CleanupManager, cfg Config) (*Node, error) { 139 // Starting up an IPFS node can have issues as there's a race between finding a free port and getting the listener 140 // running on that port (e.g. find the port, write the config file, save the file, start up IPFS, then start the listener) 141 attempts := 3 142 var err error 143 for i := 0; i < attempts; i++ { 144 var ipfsNode *Node 145 ipfsNode, err = newNodeWithConfig(ctx, cm, cfg) 146 if err != nil { 147 if errors.Is(err, addressInUseError) { 148 log.Ctx(ctx).Debug().Err(err).Msg("Failed to start up node as port was already in use") 149 continue 150 } 151 return nil, err 152 } 153 154 return ipfsNode, nil 155 } 156 return nil, err 157 } 158 159 // newNodeWithConfig creates a new IPFS node with the given configuration. 160 // NOTE: use NewNode() or NewLocalNode() unless you know what you're doing. 161 func newNodeWithConfig(ctx context.Context, cm *system.CleanupManager, cfg Config) (*Node, error) { 162 var err error 163 pluginOnce.Do(func() { 164 err = loadPlugins(cm) 165 }) 166 if err != nil { 167 return nil, err 168 } 169 170 api, ipfsNode, repoPath, err := createNode(ctx, cm, cfg) 171 if err != nil { 172 return nil, fmt.Errorf("failed to create ipfs node: %w", err) 173 } 174 defer func() { 175 if err != nil { 176 _ = ipfsNode.Close() 177 } 178 }() 179 180 if err = connectToPeers(ctx, api, ipfsNode, cfg.getPeerAddrs()); err != nil { 181 log.Ctx(ctx).Error().Msgf("ipfs node failed to connect to peers: %s", err) 182 } 183 184 if err = serveAPI(cm, ipfsNode, repoPath); err != nil { 185 return nil, fmt.Errorf("failed to serve API: %w", err) 186 } 187 188 // Fetch useful info from the newly initialized node: 189 nodeCfg, err := ipfsNode.Repo.Config() 190 if err != nil { 191 return nil, fmt.Errorf("failed to get repo config: %w", err) 192 } 193 194 var apiPort int 195 if len(nodeCfg.Addresses.API) > 0 { 196 apiPort, err = getTCPPort(nodeCfg.Addresses.API[0]) 197 if err != nil { 198 return nil, fmt.Errorf("failed to parse api port: %w", err) 199 } 200 } 201 202 var swarmPort int 203 if len(nodeCfg.Addresses.Swarm) > 0 { 204 swarmPort, err = getTCPPort(nodeCfg.Addresses.Swarm[0]) 205 if err != nil { 206 return nil, fmt.Errorf("failed to parse swarm port: %w", err) 207 } 208 } 209 210 n := Node{ 211 api: api, 212 ipfsNode: ipfsNode, 213 Mode: cfg.getMode(), 214 RepoPath: repoPath, 215 APIPort: apiPort, 216 SwarmPort: swarmPort, 217 } 218 219 cm.RegisterCallbackWithContext(n.Close) 220 221 // Log details so that user can connect to the new node: 222 log.Ctx(ctx).Trace().Msgf("IPFS node created with ID: %s", ipfsNode.Identity) 223 n.LogDetails() 224 225 return &n, nil 226 } 227 228 // ID returns the node's ipfs ID. 229 func (n *Node) ID() string { 230 return n.ipfsNode.Identity.String() 231 } 232 233 // APIAddresses returns the node's api addresses. 234 func (n *Node) APIAddresses() ([]string, error) { 235 cfg, err := n.ipfsNode.Repo.Config() 236 if err != nil { 237 return nil, fmt.Errorf("failed to get repo config: %w", err) 238 } 239 240 var res []string 241 for _, addr := range cfg.Addresses.API { 242 res = append(res, fmt.Sprintf("%s/p2p/%s", addr, n.ID())) 243 } 244 245 return res, nil 246 } 247 248 // SwarmAddresses returns the node's swarm addresses. 249 func (n *Node) SwarmAddresses() ([]string, error) { 250 cfg, err := n.ipfsNode.Repo.Config() 251 if err != nil { 252 return nil, fmt.Errorf("failed to get repo config: %w", err) 253 } 254 255 var res []string 256 for _, addr := range cfg.Addresses.Swarm { 257 res = append(res, fmt.Sprintf("%s/p2p/%s", addr, n.ID())) 258 } 259 260 return res, nil 261 } 262 263 // LogDetails logs connection details for the node's swarm and API servers. 264 func (n *Node) LogDetails() { 265 apiAddrs, err := n.APIAddresses() 266 if err != nil { 267 log.Debug().Msgf("error fetching api addresses: %s", err) 268 return 269 } 270 271 var swarmAddrs []string 272 swarmAddrs, err = n.SwarmAddresses() 273 if err != nil { 274 log.Debug().Msgf("error fetching swarm addresses: %s", err) 275 } 276 277 id := n.ID() 278 for _, apiAddr := range apiAddrs { 279 log.Trace().Msgf("IPFS node %s listening for API on: %s", id, apiAddr) 280 } 281 for _, swarmAddr := range swarmAddrs { 282 log.Trace().Msgf("IPFS node %s listening for swarm on: %s", id, swarmAddr) 283 } 284 } 285 286 // Client returns an API client for interacting with the node. 287 func (n *Node) Client() Client { 288 return NewClient(n.api) 289 } 290 291 func (n *Node) Close(ctx context.Context) error { 292 log.Ctx(ctx).Debug().Msgf("Closing IPFS node %s", n.ID()) 293 var errs *multierror.Error 294 if n.ipfsNode != nil { 295 errs = multierror.Append(errs, n.ipfsNode.Close()) 296 297 // We need to make sure we close the repo before we delete the disk contents as this will cause IPFS to print out messages about how 298 // 'flatfs could not store final value of disk usage to file', which is both annoying and can cause test flakes 299 // as the message can be written just after the test has finished but before the repo has been told by node 300 // that it's supposed to shut down. 301 if n.ipfsNode.Repo != nil { 302 if err := n.ipfsNode.Repo.Close(); err != nil { //nolint:govet 303 errs = multierror.Append(errs, fmt.Errorf("failed to close repo: %w", err)) 304 } 305 } 306 } 307 308 if n.RepoPath != "" { 309 if err := os.RemoveAll(n.RepoPath); err != nil { //nolint:govet 310 errs = multierror.Append(errs, fmt.Errorf("failed to clean up repo directory: %w", err)) 311 } 312 } 313 return errs.ErrorOrNil() 314 } 315 316 // createNode spawns a new IPFS node using a temporary repo path. 317 func createNode(ctx context.Context, _ *system.CleanupManager, cfg Config) (icore.CoreAPI, *core.IpfsNode, string, error) { 318 repoPath, err := os.MkdirTemp("", "ipfs-tmp") 319 if err != nil { 320 return nil, nil, "", fmt.Errorf("failed to create repo dir: %w", err) 321 } 322 323 var repo kuboRepo.Repo 324 if err = createRepo(repoPath, cfg); err != nil { 325 return nil, nil, "", fmt.Errorf("failed to create repo: %w", err) 326 } 327 328 repo, err = fsrepo.Open(repoPath) 329 if err != nil { 330 return nil, nil, "", fmt.Errorf("failed to open temp repo: %w", err) 331 } 332 333 nodeOptions := &core.BuildCfg{ 334 Repo: repo, 335 Online: true, 336 Routing: libp2p.DHTClientOption, 337 } 338 339 node, err := core.NewNode(ctx, nodeOptions) 340 if err != nil { 341 return nil, nil, "", fmt.Errorf("failed to create node: %w", err) 342 } 343 344 api, err := coreapi.NewCoreAPI(node) 345 return api, node, repoPath, err 346 } 347 348 // serveAPI starts a new API server for the node on the given address. 349 func serveAPI(cm *system.CleanupManager, node *core.IpfsNode, repoPath string) error { 350 cfg, err := node.Repo.Config() 351 if err != nil { 352 return fmt.Errorf("failed to get repo config: %w", err) 353 } 354 355 var listeners []manet.Listener 356 for _, addr := range cfg.Addresses.API { 357 maddr, err := ma.NewMultiaddr(addr) 358 if err != nil { 359 return fmt.Errorf("failed to parse multiaddr: %w", err) 360 } 361 362 listener, err := manet.Listen(maddr) 363 if err != nil { 364 return fmt.Errorf("failed to listen on api multiaddr: %w", err) 365 } 366 367 cm.RegisterCallback(func() error { 368 if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { 369 return errors.Wrap(err, "error shutting down IPFS listener") 370 } 371 return nil 372 }) 373 374 listeners = append(listeners, listener) 375 } 376 377 // We need to construct a commands.Context in order to use the node APIs: 378 cmdContext := commands.Context{ 379 ReqLog: &commands.ReqLog{}, 380 Plugins: pluginLoader, 381 ConfigRoot: repoPath, 382 ConstructNode: func() (n *core.IpfsNode, err error) { 383 return node, nil 384 }, 385 } 386 387 // Options determine which functionality the API should include: 388 var opts = []corehttp.ServeOption{ 389 corehttp.VersionOption(), 390 corehttp.GatewayOption(false), 391 corehttp.WebUIOption, 392 corehttp.CommandsOption(cmdContext), 393 } 394 395 for _, listener := range listeners { 396 // NOTE: this is not critical, but we should log for debugging 397 go func(listener manet.Listener) { 398 if err := corehttp.Serve(node, manet.NetListener(listener), opts...); err != nil { 399 log.Debug().Msgf("node '%s' failed to serve ipfs api: %s", node.Identity, err) 400 } 401 }(listener) 402 } 403 404 return nil 405 } 406 407 // connectToPeers connects the node to a list of IPFS bootstrap peers. 408 // event though we have Peering enabled, some test scenarios relies on the node being eagerly connected to the peers 409 func connectToPeers(ctx context.Context, api icore.CoreAPI, node *core.IpfsNode, peerAddrs []string) error { 410 log.Ctx(ctx).Debug().Msgf("IPFS node %s has current peers: %v", node.Identity, node.Peerstore.Peers()) 411 log.Ctx(ctx).Debug().Msgf("IPFS node %s is connecting to new peers: %v", node.Identity, peerAddrs) 412 413 // Parse the bootstrap node multiaddrs and fetch their IPFS peer info: 414 peerInfos, err := ParsePeersString(peerAddrs) 415 if err != nil { 416 return err 417 } 418 419 // Bootstrap the node's list of peers: 420 var anyErr error 421 var wg sync.WaitGroup 422 wg.Add(len(peerInfos)) 423 for _, peerInfo := range peerInfos { 424 go func(peerInfo peer.AddrInfo) { 425 defer wg.Done() 426 if err := api.Swarm().Connect(ctx, peerInfo); err != nil { 427 anyErr = err 428 log.Ctx(ctx).Debug().Msgf( 429 "failed to connect to ipfs peer %s, skipping: %s", 430 peerInfo.ID, err) 431 } 432 }(peerInfo) 433 } 434 435 wg.Wait() 436 return anyErr 437 } 438 439 // createRepo creates an IPFS repository in a given directory. 440 func createRepo(path string, nodeConfig Config) error { 441 cfg, err := config.Init(io.Discard, nodeConfig.getKeypairSize()) 442 if err != nil { 443 return fmt.Errorf("failed to initialize config: %w", err) 444 } 445 446 profile := "flatfs" 447 if nodeConfig.getMode() == ModeLocal { 448 profile = "test" 449 } 450 451 transformer, ok := config.Profiles[profile] 452 if !ok { 453 return fmt.Errorf("invalid configuration profile: %s", profile) 454 } 455 if err := transformer.Transform(cfg); err != nil { //nolint: govet 456 return err 457 } 458 459 var apiPort int 460 apiPort, err = freeport.GetFreePort() 461 if err != nil { 462 return fmt.Errorf("could not create port for api: %w", err) 463 } 464 465 // If we're in local mode, then we need to manually change the config to 466 // serve an IPFS swarm client on some local port: 467 if nodeConfig.getMode() == ModeLocal { 468 var gatewayPort int 469 gatewayPort, err = freeport.GetFreePort() 470 if err != nil { 471 return fmt.Errorf("could not create port for gateway: %w", err) 472 } 473 474 var swarmPort int 475 swarmPort, err = freeport.GetFreePort() 476 if err != nil { 477 return fmt.Errorf("could not create port for swarm: %w", err) 478 } 479 480 cfg.AutoNAT.ServiceMode = config.AutoNATServiceDisabled 481 cfg.Swarm.EnableHolePunching = config.False 482 cfg.Swarm.DisableNatPortMap = true 483 cfg.Swarm.RelayClient.Enabled = config.False 484 cfg.Swarm.RelayService.Enabled = config.False 485 cfg.Swarm.Transports.Network.Relay = config.False 486 cfg.Discovery.MDNS.Enabled = false 487 cfg.Addresses.Gateway = []string{ 488 fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", gatewayPort), 489 } 490 cfg.Addresses.API = []string{ 491 fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", apiPort), 492 } 493 cfg.Addresses.Swarm = []string{ 494 fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", swarmPort), 495 } 496 } else { 497 cfg.Addresses.API = []string{ 498 fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", apiPort), 499 } 500 } 501 502 // establish peering with the passed nodes. This is different than bootstrapping or manually connecting to peers, 503 //and kubo will create sticky connections with these nodes and reconnect if the connection is lost 504 // https://github.com/ipfs/kubo/blob/master/docs/config.md#peering 505 swarmPeers, err := ParsePeersString(nodeConfig.getPeerAddrs()) 506 if err != nil { 507 return fmt.Errorf("failed to parse peer addresses: %w", err) 508 } 509 cfg.Peering = config.Peering{ 510 Peers: swarmPeers, 511 } 512 513 err = fsrepo.Init(path, cfg) 514 if err != nil { 515 return fmt.Errorf("failed to init ipfs repo: %w", err) 516 } 517 518 return nil 519 } 520 521 // loadPlugins initializes and injects the standard set of ipfs plugins. 522 func loadPlugins(cm *system.CleanupManager) error { 523 plugins, err := loader.NewPluginLoader("") 524 if err != nil { 525 return fmt.Errorf("error loading plugins: %s", err) 526 } 527 528 if err := plugins.Initialize(); err != nil { 529 return fmt.Errorf("error initializing plugins: %s", err) 530 } 531 532 if err := plugins.Inject(); err != nil { 533 return fmt.Errorf("error initializing plugins: %s", err) 534 } 535 536 // Set the global cache so we can use it in the ipfs daemon: 537 pluginLoader = plugins 538 cm.RegisterCallback(plugins.Close) 539 return nil 540 } 541 542 // getTCPPort returns the tcp port in a multiaddress. 543 func getTCPPort(addr string) (int, error) { 544 maddr, err := ma.NewMultiaddr(addr) 545 if err != nil { 546 return 0, err 547 } 548 549 p, err := maddr.ValueForProtocol(ma.P_TCP) 550 if err != nil { 551 return 0, err 552 } 553 554 return strconv.Atoi(p) 555 }