// The ipfs-cluster-follow application. package main import ( "fmt" "os" "os/signal" "os/user" "path/filepath" "syscall" "github.com/ipfs/ipfs-cluster/api/rest/client" "github.com/ipfs/ipfs-cluster/cmdutils" "github.com/ipfs/ipfs-cluster/version" "github.com/multiformats/go-multiaddr" "github.com/pkg/errors" semver "github.com/blang/semver" logging "github.com/ipfs/go-log" cli "github.com/urfave/cli/v2" ) const ( // ProgramName of this application programName = "ipfs-cluster-follow" clusterNameFlag = "clusterName" logLevel = "info" ) // Default location for the configurations and data var ( // DefaultFolder is the name of the cluster folder DefaultFolder = ".ipfs-cluster-follow" // DefaultPath is set on init() to $HOME/DefaultFolder // and holds all the ipfs-cluster data DefaultPath string // The name of the configuration file inside DefaultPath DefaultConfigFile = "service.json" // The name of the identity file inside DefaultPath DefaultIdentityFile = "identity.json" ) var ( commit string logger = logging.Logger("clusterfollow") configPath string identityPath string signalChan = make(chan os.Signal, 20) ) // Description provides a short summary of the functionality of this tool var Description = fmt.Sprintf(` %s helps running IPFS Cluster follower peers. Follower peers subscribe to a Cluster controlled by a set of "trusted peers". They collaborate in pinning items as dictated by the trusted peers and do not have the power to make Cluster-wide modifications to the pinset. Follower peers cannot access information nor trigger actions in other peers. %s can be used to follow different clusters by launching it with different options. Each Cluster has an identity, a configuration and a datastore associated to it, which are kept under "~/%s/". For feedback, bug reports or any additional information, visit https://github.com/ipfs/ipfs-cluster. EXAMPLES: List configured follower peers: $ %s Display information for a follower peer: $ %s info Initialize a follower peer: $ %s init Launch a follower peer (will stay running): $ %s run List items in the pinset for a given cluster: $ %s list Getting help and usage info: $ %s --help $ %s --help $ %s info --help $ %s init --help $ %s run --help $ %s list --help `, programName, programName, DefaultFolder, programName, programName, programName, programName, programName, programName, programName, programName, programName, programName, programName, ) func init() { // Set build information. if build, err := semver.NewBuildVersion(commit); err == nil { version.Version.Build = []string{"git" + build} } // We try guessing user's home from the HOME variable. This // allows HOME hacks for things like Snapcraft builds. HOME // should be set in all UNIX by the OS. Alternatively, we fall back to // usr.HomeDir (which should work on Windows etc.). home := os.Getenv("HOME") if home == "" { usr, err := user.Current() if err != nil { panic(fmt.Sprintf("cannot get current user: %s", err)) } home = usr.HomeDir } DefaultPath = filepath.Join(home, DefaultFolder) // This will abort the program on signal. We close the signal channel // when launching the peer so that we can do an orderly shutdown in // that case though. go func() { signal.Notify( signalChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, ) _, ok := <-signalChan // channel closed. if !ok { return } os.Exit(1) }() } func main() { app := cli.NewApp() app.Name = programName app.Usage = "IPFS Cluster Follower" app.UsageText = fmt.Sprintf("%s [global options] [subcommand]...", programName) app.Description = Description //app.Copyright = "© Protocol Labs, Inc." app.Version = version.Version.String() app.Flags = []cli.Flag{ &cli.StringFlag{ Name: "config, c", Value: DefaultPath, Usage: "path to the followers configuration and data `FOLDER`", EnvVars: []string{"IPFS_CLUSTER_PATH"}, }, } app.Action = func(c *cli.Context) error { if !c.Args().Present() { return listClustersCmd(c) } clusterName := c.Args().Get(0) clusterApp := cli.NewApp() clusterApp.Name = fmt.Sprintf("%s %s", programName, clusterName) clusterApp.HelpName = clusterApp.Name clusterApp.Usage = fmt.Sprintf("Follower peer management for \"%s\"", clusterName) clusterApp.UsageText = fmt.Sprintf("%s %s [subcommand]", programName, clusterName) clusterApp.Action = infoCmd clusterApp.HideVersion = true clusterApp.Flags = []cli.Flag{ &cli.StringFlag{ // pass clusterName to subcommands Name: clusterNameFlag, Value: clusterName, Hidden: true, }, } clusterApp.Commands = []*cli.Command{ { Name: "info", Usage: "displays information for this peer", ArgsUsage: "", Description: fmt.Sprintf(` This command display useful information for "%s"'s follower peer. `, clusterName), Action: infoCmd, }, { Name: "init", Usage: "initializes the follower peer", ArgsUsage: "", Description: fmt.Sprintf(` This command initializes a follower peer for the cluster named "%s". You will need to pass the peer configuration URL. The command will generate a new peer identity and leave things ready to run "%s %s run". An error will be returned if a configuration folder for a cluster peer with this name already exists. If you wish to re-initialize from scratch, delete this folder first. `, clusterName, programName, clusterName), Action: initCmd, }, { Name: "run", Usage: "runs the follower peer", ArgsUsage: "", Description: fmt.Sprintf(` This commands runs a "%s" cluster follower peer. The peer should have already been initialized with "init" alternatively the --init flag needs to be passed. Before running, ensure that you have connectivity and that the IPFS daemon is running. You can obtain more information about this follower peer by running "%s %s" (without any arguments). The peer will stay running in the foreground until manually stopped. `, clusterName, programName, clusterName), Action: runCmd, Flags: []cli.Flag{ &cli.StringFlag{ Name: "init", Usage: "initialize cluster peer with the given URL before running", }, }, }, { Name: "list", Usage: "list items in the peers' pinset", ArgsUsage: "", Description: ` This commands lists all the items pinned by this follower cluster peer on IPFS. If the peer is currently running, it will display status information for each pin (such as PINNING). If not, it will just display the current list of pins as obtained from the internal state on disk. `, Action: listCmd, }, } return clusterApp.RunAsSubcommand(c) } app.Run(os.Args) } // build paths returns the path to the configuration folder, // the identity.json and the service.json files. func buildPaths(c *cli.Context, clusterName string) (string, string, string) { absPath, err := filepath.Abs(c.String("config")) if err != nil { cmdutils.ErrorOut("error getting absolute path for %s: %s", err, clusterName) os.Exit(1) } // ~/.ipfs-cluster-follow/clusterName absPath = filepath.Join(absPath, clusterName) // ~/.ipfs-cluster-follow/clusterName/service.json configPath = filepath.Join(absPath, DefaultConfigFile) // ~/.ipfs-cluster-follow/clusterName/indentity.json identityPath = filepath.Join(absPath, DefaultIdentityFile) return absPath, configPath, identityPath } func socketAddress(absPath, clusterName string) (multiaddr.Multiaddr, error) { socket := fmt.Sprintf("/unix/%s", filepath.Join(absPath, "api-socket")) ma, err := multiaddr.NewMultiaddr(socket) if err != nil { return nil, errors.Wrapf(err, "error parsing socket: %s", socket) } return ma, nil } // returns an REST API client. Points to the socket address unless // CLUSTER_RESTAPI_HTTPLISTENMULTIADDRESS is set, in which case it uses it. func getClient(absPath, clusterName string) (client.Client, error) { var endp multiaddr.Multiaddr var err error if endpStr := os.Getenv("CLUSTER_RESTAPI_HTTPLISTENMULTIADDRESS"); endpStr != "" { endp, err = multiaddr.NewMultiaddr(endpStr) if err != nil { return nil, errors.Wrapf(err, "error parsing the value of CLUSTER_RESTAPI_HTTPLISTENMULTIADDRESS: %s", endpStr) } } else { endp, err = socketAddress(absPath, clusterName) } if err != nil { return nil, err } cfg := client.Config{ APIAddr: endp, } return client.NewDefaultClient(&cfg) }