diff --git a/core/commands/version.go b/core/commands/version.go index e404074fe753..c52dfa20ce2a 100644 --- a/core/commands/version.go +++ b/core/commands/version.go @@ -5,17 +5,24 @@ import ( "fmt" "io" "runtime/debug" + "strings" version "github.com/ipfs/kubo" + "github.com/ipfs/kubo/core/commands/cmdenv" cmds "github.com/ipfs/go-ipfs-cmds" + + versioncmp "github.com/hashicorp/go-version" + "github.com/libp2p/go-libp2p-kad-dht/fullrt" + pstore "github.com/libp2p/go-libp2p/core/peerstore" ) const ( - versionNumberOptionName = "number" - versionCommitOptionName = "commit" - versionRepoOptionName = "repo" - versionAllOptionName = "all" + versionNumberOptionName = "number" + versionCommitOptionName = "commit" + versionRepoOptionName = "repo" + versionAllOptionName = "all" + versionCompareNewFractionOptionName = "--newer-fraction" ) var VersionCmd = &cmds.Command{ @@ -24,7 +31,8 @@ var VersionCmd = &cmds.Command{ ShortDescription: "Returns the current version of IPFS and exits.", }, Subcommands: map[string]*cmds.Command{ - "deps": depsVersionCommand, + "deps": depsVersionCommand, + "check": checkVersionCommand, }, Options: []cmds.Option{ @@ -130,3 +138,135 @@ Print out all dependencies and their versions.`, }), }, } + +type CheckOutput struct { + PeersCounted int + GreatestVersion string + OldVersion bool +} + +var checkVersionCommand = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "Checks IPFS version against network (online only).", + ShortDescription: ` +Checks node versions in our DHT to compare if we're running an older version.`, + }, + Options: []cmds.Option{ + cmds.FloatOption(versionCompareNewFractionOptionName, "f", "Fraction of peers with new version to generate update warning.").WithDefault(0.5), + }, + Type: CheckOutput{}, + + Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { + nd, err := cmdenv.GetNode(env) + if err != nil { + return err + } + + if !nd.IsOnline { + return ErrNotOnline + } + + if nd.DHT == nil { + return ErrNotDHT + } + + ourVersion, err := versioncmp.NewVersion(strings.Replace(version.CurrentVersionNumber, "-dev", "", -1)) + if err != nil { + return fmt.Errorf("could not parse our own version %s: %w", + version.CurrentVersionNumber, err) + } + + greatestVersionSeen := ourVersion + totalPeersCounted := 1 // Us (and to avoid division-by-zero edge case). + withGreaterVersion := 0 + + recordPeerVersion := func(agentVersion string) { + // We process the version as is it assembled in GetUserAgentVersion. + segments := strings.Split(agentVersion, "/") + if len(segments) < 2 { + return + } + if segments[0] != "go-ipfs" { + return + } + versionNumber := segments[1] // As in our CurrentVersionNumber. + + // Ignore development releases. + if strings.Contains(versionNumber, "-dev") { + return + } + if strings.Contains(versionNumber, "-rc") { + return + } + + peerVersion, err := versioncmp.NewVersion(versionNumber) + if err != nil { + // Do not error on invalid remote versions, just ignore. + return + } + + // Valid peer version number. + totalPeersCounted += 1 + if ourVersion.LessThan(peerVersion) { + withGreaterVersion += 1 + } + if peerVersion.GreaterThan(greatestVersionSeen) { + greatestVersionSeen = peerVersion + } + } + + // Logic taken from `ipfs stats dht` command. + if nd.DHTClient != nd.DHT { + client, ok := nd.DHTClient.(*fullrt.FullRT) + if !ok { + return cmds.Errorf(cmds.ErrClient, "could not generate stats for the WAN DHT client type") + } + for _, p := range client.Stat() { + if ver, err := nd.Peerstore.Get(p, "AgentVersion"); err == nil { + recordPeerVersion(ver.(string)) + } else if err == pstore.ErrNotFound { + // ignore + } else { + // this is a bug, usually. + log.Errorw( + "failed to get agent version from peerstore", + "error", err, + ) + } + } + } else { + for _, pi := range nd.DHT.WAN.RoutingTable().GetPeerInfos() { + if ver, err := nd.Peerstore.Get(pi.Id, "AgentVersion"); err == nil { + recordPeerVersion(ver.(string)) + } else if err == pstore.ErrNotFound { + // ignore + } else { + // this is a bug, usually. + log.Errorw( + "failed to get agent version from peerstore", + "error", err, + ) + } + } + } + + newerFraction, _ := req.Options[versionCompareNewFractionOptionName].(float64) + if err := cmds.EmitOnce(res, CheckOutput{ + PeersCounted: totalPeersCounted, + GreatestVersion: greatestVersionSeen.String(), + OldVersion: (float64(withGreaterVersion) / float64(totalPeersCounted)) > newerFraction, + }); err != nil { + return err + } + return nil + }, + Encoders: cmds.EncoderMap{ + cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, checkOutput CheckOutput) error { + if checkOutput.OldVersion { + fmt.Fprintf(w, "WARNING: we're running an outdated version compared to our peers (%d discovered), update to %s\n", + checkOutput.PeersCounted, checkOutput.GreatestVersion) + } + return nil + }), + }, +} diff --git a/go.mod b/go.mod index 2b66a8a35fa0..9993c1b190f2 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 github.com/google/uuid v1.3.0 github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-version v1.6.0 github.com/ipfs/boxo v0.10.2-0.20230629140307-fdad9f921191 github.com/ipfs/go-block-format v0.1.2 github.com/ipfs/go-cid v0.4.1 diff --git a/go.sum b/go.sum index 7b07ea02bd7d..640c658331d2 100644 --- a/go.sum +++ b/go.sum @@ -339,6 +339,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=