mirror of
https://github.com/ipfs/kubo.git
synced 2025-05-17 23:16:11 +08:00

* Create spellcheck.yml * Create .codespell-ignore --------- Co-authored-by: Marcin Rataj <lidel@lidel.org> Co-authored-by: Guillaume Michel <guillaumemichel@users.noreply.github.com>
300 lines
8.9 KiB
Go
300 lines
8.9 KiB
Go
package commands
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"runtime/debug"
|
|
"strings"
|
|
|
|
versioncmp "github.com/hashicorp/go-version"
|
|
cmds "github.com/ipfs/go-ipfs-cmds"
|
|
version "github.com/ipfs/kubo"
|
|
"github.com/ipfs/kubo/config"
|
|
"github.com/ipfs/kubo/core"
|
|
"github.com/ipfs/kubo/core/commands/cmdenv"
|
|
"github.com/libp2p/go-libp2p-kad-dht/fullrt"
|
|
peer "github.com/libp2p/go-libp2p/core/peer"
|
|
pstore "github.com/libp2p/go-libp2p/core/peerstore"
|
|
)
|
|
|
|
const (
|
|
versionNumberOptionName = "number"
|
|
versionCommitOptionName = "commit"
|
|
versionRepoOptionName = "repo"
|
|
versionAllOptionName = "all"
|
|
versionCheckThresholdOptionName = "min-percent"
|
|
)
|
|
|
|
var VersionCmd = &cmds.Command{
|
|
Helptext: cmds.HelpText{
|
|
Tagline: "Show IPFS version information.",
|
|
ShortDescription: "Returns the current version of IPFS and exits.",
|
|
},
|
|
Subcommands: map[string]*cmds.Command{
|
|
"deps": depsVersionCommand,
|
|
"check": checkVersionCommand,
|
|
},
|
|
|
|
Options: []cmds.Option{
|
|
cmds.BoolOption(versionNumberOptionName, "n", "Only show the version number."),
|
|
cmds.BoolOption(versionCommitOptionName, "Show the commit hash."),
|
|
cmds.BoolOption(versionRepoOptionName, "Show repo version."),
|
|
cmds.BoolOption(versionAllOptionName, "Show all version information"),
|
|
},
|
|
// must be permitted to run before init
|
|
Extra: CreateCmdExtras(SetDoesNotUseRepo(true), SetDoesNotUseConfigAsInput(true)),
|
|
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
|
return cmds.EmitOnce(res, version.GetVersionInfo())
|
|
},
|
|
Encoders: cmds.EncoderMap{
|
|
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, version *version.VersionInfo) error {
|
|
all, _ := req.Options[versionAllOptionName].(bool)
|
|
if all {
|
|
ver := version.Version
|
|
if version.Commit != "" {
|
|
ver += "-" + version.Commit
|
|
}
|
|
out := fmt.Sprintf("Kubo version: %s\n"+
|
|
"Repo version: %s\nSystem version: %s\nGolang version: %s\n",
|
|
ver, version.Repo, version.System, version.Golang)
|
|
fmt.Fprint(w, out)
|
|
return nil
|
|
}
|
|
|
|
commit, _ := req.Options[versionCommitOptionName].(bool)
|
|
commitTxt := ""
|
|
if commit && version.Commit != "" {
|
|
commitTxt = "-" + version.Commit
|
|
}
|
|
|
|
repo, _ := req.Options[versionRepoOptionName].(bool)
|
|
if repo {
|
|
fmt.Fprintln(w, version.Repo)
|
|
return nil
|
|
}
|
|
|
|
number, _ := req.Options[versionNumberOptionName].(bool)
|
|
if number {
|
|
fmt.Fprintln(w, version.Version+commitTxt)
|
|
return nil
|
|
}
|
|
|
|
fmt.Fprintf(w, "ipfs version %s%s\n", version.Version, commitTxt)
|
|
return nil
|
|
}),
|
|
},
|
|
Type: version.VersionInfo{},
|
|
}
|
|
|
|
type Dependency struct {
|
|
Path string
|
|
Version string
|
|
ReplacedBy string
|
|
Sum string
|
|
}
|
|
|
|
const pkgVersionFmt = "%s@%s"
|
|
|
|
var depsVersionCommand = &cmds.Command{
|
|
Helptext: cmds.HelpText{
|
|
Tagline: "Shows information about dependencies used for build.",
|
|
ShortDescription: `
|
|
Print out all dependencies and their versions.`,
|
|
},
|
|
Type: Dependency{},
|
|
|
|
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
|
info, ok := debug.ReadBuildInfo()
|
|
if !ok {
|
|
return errors.New("no embedded dependency information")
|
|
}
|
|
toDependency := func(mod *debug.Module) (dep Dependency) {
|
|
dep.Path = mod.Path
|
|
dep.Version = mod.Version
|
|
dep.Sum = mod.Sum
|
|
if repl := mod.Replace; repl != nil {
|
|
dep.ReplacedBy = fmt.Sprintf(pkgVersionFmt, repl.Path, repl.Version)
|
|
}
|
|
return
|
|
}
|
|
if err := res.Emit(toDependency(&info.Main)); err != nil {
|
|
return err
|
|
}
|
|
for _, dep := range info.Deps {
|
|
if err := res.Emit(toDependency(dep)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
Encoders: cmds.EncoderMap{
|
|
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, dep Dependency) error {
|
|
fmt.Fprintf(w, pkgVersionFmt, dep.Path, dep.Version)
|
|
if dep.ReplacedBy != "" {
|
|
fmt.Fprintf(w, " => %s", dep.ReplacedBy)
|
|
}
|
|
fmt.Fprintf(w, "\n")
|
|
return nil
|
|
}),
|
|
},
|
|
}
|
|
|
|
const DefaultMinimalVersionFraction = 0.05 // 5%
|
|
|
|
type VersionCheckOutput struct {
|
|
UpdateAvailable bool
|
|
RunningVersion string
|
|
GreatestVersion string
|
|
PeersSampled int
|
|
WithGreaterVersion int
|
|
}
|
|
|
|
var checkVersionCommand = &cmds.Command{
|
|
Helptext: cmds.HelpText{
|
|
Tagline: "Checks Kubo version against connected peers.",
|
|
ShortDescription: `
|
|
This command uses the libp2p identify protocol to check the 'AgentVersion'
|
|
of connected peers and see if the Kubo version we're running is outdated.
|
|
|
|
Peers with an AgentVersion that doesn't start with 'kubo/' are ignored.
|
|
'UpdateAvailable' is set to true only if the 'min-fraction' criteria are met.
|
|
|
|
The 'ipfs daemon' does the same check regularly and logs when a new version
|
|
is available. You can stop these regular checks by setting
|
|
Version.SwarmCheckEnabled:false in the config.
|
|
`,
|
|
},
|
|
Options: []cmds.Option{
|
|
cmds.IntOption(versionCheckThresholdOptionName, "t", "Percentage (1-100) of sampled peers with the new Kubo version needed to trigger an update warning.").WithDefault(config.DefaultSwarmCheckPercentThreshold),
|
|
},
|
|
Type: VersionCheckOutput{},
|
|
|
|
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
|
|
}
|
|
|
|
minPercent, _ := req.Options[versionCheckThresholdOptionName].(int64)
|
|
output, err := DetectNewKuboVersion(nd, minPercent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := cmds.EmitOnce(res, output); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// DetectNewKuboVersion observers kubo version reported by other peers via
|
|
// libp2p identify protocol and notifies when threshold fraction of seen swarm
|
|
// is running updated Kubo. It is used by RPC and CLI at 'ipfs version check'
|
|
// and also periodically when 'ipfs daemon' is running.
|
|
func DetectNewKuboVersion(nd *core.IpfsNode, minPercent int64) (VersionCheckOutput, error) {
|
|
ourVersion, err := versioncmp.NewVersion(version.CurrentVersionNumber)
|
|
if err != nil {
|
|
return VersionCheckOutput{}, fmt.Errorf("could not parse our own version %q: %w",
|
|
version.CurrentVersionNumber, err)
|
|
}
|
|
// MAJOR.MINOR.PATCH without any suffix
|
|
ourVersion = ourVersion.Core()
|
|
|
|
greatestVersionSeen := ourVersion
|
|
totalPeersSampled := 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] != "kubo" {
|
|
return
|
|
}
|
|
versionNumber := segments[1] // As in our CurrentVersionNumber
|
|
|
|
peerVersion, err := versioncmp.NewVersion(versionNumber)
|
|
if err != nil {
|
|
// Do not error on invalid remote versions, just ignore
|
|
return
|
|
}
|
|
|
|
// Ignore prereleases and development releases (-dev, -rcX)
|
|
if peerVersion.Metadata() != "" || peerVersion.Prerelease() != "" {
|
|
return
|
|
}
|
|
|
|
// MAJOR.MINOR.PATCH without any suffix
|
|
peerVersion = peerVersion.Core()
|
|
|
|
// Valid peer version number
|
|
totalPeersSampled += 1
|
|
if ourVersion.LessThan(peerVersion) {
|
|
withGreaterVersion += 1
|
|
}
|
|
if peerVersion.GreaterThan(greatestVersionSeen) {
|
|
greatestVersionSeen = peerVersion
|
|
}
|
|
}
|
|
|
|
processPeerstoreEntry := func(id peer.ID) {
|
|
if v, err := nd.Peerstore.Get(id, "AgentVersion"); err == nil {
|
|
recordPeerVersion(v.(string))
|
|
} else if errors.Is(err, pstore.ErrNotFound) { // ignore noop
|
|
} else { // a bug, usually.
|
|
log.Errorw("failed to get agent version from peerstore", "error", err)
|
|
}
|
|
}
|
|
|
|
// Amino DHT client keeps information about previously seen peers
|
|
if nd.DHTClient != nd.DHT && nd.DHTClient != nil {
|
|
client, ok := nd.DHTClient.(*fullrt.FullRT)
|
|
if !ok {
|
|
return VersionCheckOutput{}, errors.New("could not perform version check due to missing or incompatible DHT configuration")
|
|
}
|
|
for _, p := range client.Stat() {
|
|
processPeerstoreEntry(p)
|
|
}
|
|
} else if nd.DHT != nil && nd.DHT.WAN != nil {
|
|
for _, pi := range nd.DHT.WAN.RoutingTable().GetPeerInfos() {
|
|
processPeerstoreEntry(pi.Id)
|
|
}
|
|
} else if nd.DHT != nil && nd.DHT.LAN != nil {
|
|
for _, pi := range nd.DHT.LAN.RoutingTable().GetPeerInfos() {
|
|
processPeerstoreEntry(pi.Id)
|
|
}
|
|
} else {
|
|
return VersionCheckOutput{}, errors.New("could not perform version check due to missing or incompatible DHT configuration")
|
|
}
|
|
|
|
if minPercent < 1 || minPercent > 100 {
|
|
if minPercent == 0 {
|
|
minPercent = config.DefaultSwarmCheckPercentThreshold
|
|
} else {
|
|
return VersionCheckOutput{}, errors.New("Version.SwarmCheckPercentThreshold must be between 1 and 100")
|
|
}
|
|
}
|
|
|
|
minFraction := float64(minPercent) / 100.0
|
|
|
|
// UpdateAvailable flag is set only if minFraction was reached
|
|
greaterFraction := float64(withGreaterVersion) / float64(totalPeersSampled)
|
|
|
|
// Gathered metric are returned every time
|
|
return VersionCheckOutput{
|
|
UpdateAvailable: (greaterFraction >= minFraction),
|
|
RunningVersion: ourVersion.String(),
|
|
GreatestVersion: greatestVersionSeen.String(),
|
|
PeersSampled: totalPeersSampled,
|
|
WithGreaterVersion: withGreaterVersion,
|
|
}, nil
|
|
}
|