master
go 299 lines 8.94 KB
Raw
1 package commands
2
3 import (
4 "errors"
5 "fmt"
6 "io"
7 "runtime/debug"
8 "strings"
9
10 versioncmp "github.com/hashicorp/go-version"
11 cmds "github.com/ipfs/go-ipfs-cmds"
12 version "github.com/ipfs/kubo"
13 "github.com/ipfs/kubo/config"
14 "github.com/ipfs/kubo/core"
15 "github.com/ipfs/kubo/core/commands/cmdenv"
16 "github.com/libp2p/go-libp2p-kad-dht/fullrt"
17 peer "github.com/libp2p/go-libp2p/core/peer"
18 pstore "github.com/libp2p/go-libp2p/core/peerstore"
19 )
20
21 const (
22 versionNumberOptionName = "number"
23 versionCommitOptionName = "commit"
24 versionRepoOptionName = "repo"
25 versionAllOptionName = "all"
26 versionCheckThresholdOptionName = "min-percent"
27 )
28
29 var VersionCmd = &cmds.Command{
30 Helptext: cmds.HelpText{
31 Tagline: "Show IPFS version information.",
32 ShortDescription: "Returns the current version of IPFS and exits.",
33 },
34 Subcommands: map[string]*cmds.Command{
35 "deps": depsVersionCommand,
36 "check": checkVersionCommand,
37 },
38
39 Options: []cmds.Option{
40 cmds.BoolOption(versionNumberOptionName, "n", "Only show the version number."),
41 cmds.BoolOption(versionCommitOptionName, "Show the commit hash."),
42 cmds.BoolOption(versionRepoOptionName, "Show repo version."),
43 cmds.BoolOption(versionAllOptionName, "Show all version information"),
44 },
45 // must be permitted to run before init
46 Extra: CreateCmdExtras(SetDoesNotUseRepo(true), SetDoesNotUseConfigAsInput(true)),
47 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
48 return cmds.EmitOnce(res, version.GetVersionInfo())
49 },
50 Encoders: cmds.EncoderMap{
51 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, version *version.VersionInfo) error {
52 all, _ := req.Options[versionAllOptionName].(bool)
53 if all {
54 ver := version.Version
55 if version.Commit != "" {
56 ver += "-" + version.Commit
57 }
58 out := fmt.Sprintf("Kubo version: %s\n"+
59 "Repo version: %s\nSystem version: %s\nGolang version: %s\n",
60 ver, version.Repo, version.System, version.Golang)
61 fmt.Fprint(w, out)
62 return nil
63 }
64
65 commit, _ := req.Options[versionCommitOptionName].(bool)
66 commitTxt := ""
67 if commit && version.Commit != "" {
68 commitTxt = "-" + version.Commit
69 }
70
71 repo, _ := req.Options[versionRepoOptionName].(bool)
72 if repo {
73 fmt.Fprintln(w, version.Repo)
74 return nil
75 }
76
77 number, _ := req.Options[versionNumberOptionName].(bool)
78 if number {
79 fmt.Fprintln(w, version.Version+commitTxt)
80 return nil
81 }
82
83 fmt.Fprintf(w, "ipfs version %s%s\n", version.Version, commitTxt)
84 return nil
85 }),
86 },
87 Type: version.VersionInfo{},
88 }
89
90 type Dependency struct {
91 Path string
92 Version string
93 ReplacedBy string
94 Sum string
95 }
96
97 const pkgVersionFmt = "%s@%s"
98
99 var depsVersionCommand = &cmds.Command{
100 Helptext: cmds.HelpText{
101 Tagline: "Shows information about dependencies used for build.",
102 ShortDescription: `
103 Print out all dependencies and their versions.`,
104 },
105 Type: Dependency{},
106
107 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
108 info, ok := debug.ReadBuildInfo()
109 if !ok {
110 return errors.New("no embedded dependency information")
111 }
112 toDependency := func(mod *debug.Module) (dep Dependency) {
113 dep.Path = mod.Path
114 dep.Version = mod.Version
115 dep.Sum = mod.Sum
116 if repl := mod.Replace; repl != nil {
117 dep.ReplacedBy = fmt.Sprintf(pkgVersionFmt, repl.Path, repl.Version)
118 }
119 return
120 }
121 if err := res.Emit(toDependency(&info.Main)); err != nil {
122 return err
123 }
124 for _, dep := range info.Deps {
125 if err := res.Emit(toDependency(dep)); err != nil {
126 return err
127 }
128 }
129 return nil
130 },
131 Encoders: cmds.EncoderMap{
132 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, dep Dependency) error {
133 fmt.Fprintf(w, pkgVersionFmt, dep.Path, dep.Version)
134 if dep.ReplacedBy != "" {
135 fmt.Fprintf(w, " => %s", dep.ReplacedBy)
136 }
137 fmt.Fprintf(w, "\n")
138 return nil
139 }),
140 },
141 }
142
143 const DefaultMinimalVersionFraction = 0.05 // 5%
144
145 type VersionCheckOutput struct {
146 UpdateAvailable bool
147 RunningVersion string
148 GreatestVersion string
149 PeersSampled int
150 WithGreaterVersion int
151 }
152
153 var checkVersionCommand = &cmds.Command{
154 Helptext: cmds.HelpText{
155 Tagline: "Checks Kubo version against connected peers.",
156 ShortDescription: `
157 This command uses the libp2p identify protocol to check the 'AgentVersion'
158 of connected peers and see if the Kubo version we're running is outdated.
159
160 Peers with an AgentVersion that doesn't start with 'kubo/' are ignored.
161 'UpdateAvailable' is set to true only if the 'min-fraction' criteria are met.
162
163 The 'ipfs daemon' does the same check regularly and logs when a new version
164 is available. You can stop these regular checks by setting
165 Version.SwarmCheckEnabled:false in the config.
166 `,
167 },
168 Options: []cmds.Option{
169 cmds.IntOption(versionCheckThresholdOptionName, "t", "Percentage (1-100) of sampled peers with the new Kubo version needed to trigger an update warning.").WithDefault(config.DefaultSwarmCheckPercentThreshold),
170 },
171 Type: VersionCheckOutput{},
172
173 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
174 nd, err := cmdenv.GetNode(env)
175 if err != nil {
176 return err
177 }
178
179 if !nd.IsOnline {
180 return ErrNotOnline
181 }
182
183 minPercent, _ := req.Options[versionCheckThresholdOptionName].(int64)
184 output, err := DetectNewKuboVersion(nd, minPercent)
185 if err != nil {
186 return err
187 }
188
189 if err := cmds.EmitOnce(res, output); err != nil {
190 return err
191 }
192 return nil
193 },
194 }
195
196 // DetectNewKuboVersion observers kubo version reported by other peers via
197 // libp2p identify protocol and notifies when threshold fraction of seen swarm
198 // is running updated Kubo. It is used by RPC and CLI at 'ipfs version check'
199 // and also periodically when 'ipfs daemon' is running.
200 func DetectNewKuboVersion(nd *core.IpfsNode, minPercent int64) (VersionCheckOutput, error) {
201 ourVersion, err := versioncmp.NewVersion(version.CurrentVersionNumber)
202 if err != nil {
203 return VersionCheckOutput{}, fmt.Errorf("could not parse our own version %q: %w",
204 version.CurrentVersionNumber, err)
205 }
206 // MAJOR.MINOR.PATCH without any suffix
207 ourVersion = ourVersion.Core()
208
209 greatestVersionSeen := ourVersion
210 totalPeersSampled := 1 // Us (and to avoid division-by-zero edge case)
211 withGreaterVersion := 0
212
213 recordPeerVersion := func(agentVersion string) {
214 // We process the version as is it assembled in GetUserAgentVersion
215 segments := strings.Split(agentVersion, "/")
216 if len(segments) < 2 {
217 return
218 }
219 if segments[0] != "kubo" {
220 return
221 }
222 versionNumber := segments[1] // As in our CurrentVersionNumber
223
224 peerVersion, err := versioncmp.NewVersion(versionNumber)
225 if err != nil {
226 // Do not error on invalid remote versions, just ignore
227 return
228 }
229
230 // Ignore prereleases and development releases (-dev, -rcX)
231 if peerVersion.Metadata() != "" || peerVersion.Prerelease() != "" {
232 return
233 }
234
235 // MAJOR.MINOR.PATCH without any suffix
236 peerVersion = peerVersion.Core()
237
238 // Valid peer version number
239 totalPeersSampled += 1
240 if ourVersion.LessThan(peerVersion) {
241 withGreaterVersion += 1
242 }
243 if peerVersion.GreaterThan(greatestVersionSeen) {
244 greatestVersionSeen = peerVersion
245 }
246 }
247
248 processPeerstoreEntry := func(id peer.ID) {
249 if v, err := nd.Peerstore.Get(id, "AgentVersion"); err == nil {
250 recordPeerVersion(v.(string))
251 } else if errors.Is(err, pstore.ErrNotFound) { // ignore noop
252 } else { // a bug, usually.
253 log.Errorw("failed to get agent version from peerstore", "error", err)
254 }
255 }
256
257 // Amino DHT client keeps information about previously seen peers
258 if nd.HasActiveDHTClient() && nd.DHTClient != nd.DHT {
259 client, ok := nd.DHTClient.(*fullrt.FullRT)
260 if !ok {
261 return VersionCheckOutput{}, errors.New("could not perform version check due to missing or incompatible DHT configuration")
262 }
263 for _, p := range client.Stat() {
264 processPeerstoreEntry(p)
265 }
266 } else if nd.DHT != nil && nd.DHT.WAN != nil {
267 for _, pi := range nd.DHT.WAN.RoutingTable().GetPeerInfos() {
268 processPeerstoreEntry(pi.Id)
269 }
270 } else if nd.DHT != nil && nd.DHT.LAN != nil {
271 for _, pi := range nd.DHT.LAN.RoutingTable().GetPeerInfos() {
272 processPeerstoreEntry(pi.Id)
273 }
274 } else {
275 return VersionCheckOutput{}, errors.New("could not perform version check due to missing or incompatible DHT configuration")
276 }
277
278 if minPercent < 1 || minPercent > 100 {
279 if minPercent == 0 {
280 minPercent = config.DefaultSwarmCheckPercentThreshold
281 } else {
282 return VersionCheckOutput{}, errors.New("Version.SwarmCheckPercentThreshold must be between 1 and 100")
283 }
284 }
285
286 minFraction := float64(minPercent) / 100.0
287
288 // UpdateAvailable flag is set only if minFraction was reached
289 greaterFraction := float64(withGreaterVersion) / float64(totalPeersSampled)
290
291 // Gathered metric are returned every time
292 return VersionCheckOutput{
293 UpdateAvailable: (greaterFraction >= minFraction),
294 RunningVersion: ourVersion.String(),
295 GreatestVersion: greatestVersionSeen.String(),
296 PeersSampled: totalPeersSampled,
297 WithGreaterVersion: withGreaterVersion,
298 }, nil
299 }