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  }