diff --git a/cmd/tailscale/cli/version.go b/cmd/tailscale/cli/version.go index eca3f5148c694..fdbe661283ece 100644 --- a/cmd/tailscale/cli/version.go +++ b/cmd/tailscale/cli/version.go @@ -6,10 +6,13 @@ package cli import ( "context" + "encoding/json" "flag" "fmt" + "os" "github.com/peterbourgon/ff/v3/ffcli" + "tailscale.com/ipn/ipnstate" "tailscale.com/version" ) @@ -20,6 +23,7 @@ var versionCmd = &ffcli.Command{ FlagSet: (func() *flag.FlagSet { fs := newFlagSet("version") fs.BoolVar(&versionArgs.daemon, "daemon", false, "also print local node's daemon version") + fs.BoolVar(&versionArgs.json, "json", false, "output in JSON format") return fs })(), Exec: runVersion, @@ -27,23 +31,38 @@ var versionCmd = &ffcli.Command{ var versionArgs struct { daemon bool // also check local node's daemon version + json bool } func runVersion(ctx context.Context, args []string) error { if len(args) > 0 { return fmt.Errorf("too many non-flag arguments: %q", args) } - if !versionArgs.daemon { - outln(version.String()) - return nil + var err error + var st *ipnstate.Status + + if versionArgs.daemon { + st, err = localClient.StatusWithoutPeers(ctx) + if err != nil { + return err + } } - printf("Client: %s\n", version.String()) + if versionArgs.json { + m := version.GetMeta() + if st != nil { + m.DaemonLong = st.Version + } + e := json.NewEncoder(os.Stdout) + e.SetIndent("", "\t") + return e.Encode(m) + } - st, err := localClient.StatusWithoutPeers(ctx) - if err != nil { - return err + if st == nil { + outln(version.String()) + return nil } + printf("Client: %s\n", version.String()) printf("Daemon: %s\n", st.Version) return nil } diff --git a/tailcfg/tailcfg_export_test.go b/tailcfg/tailcfg_export_test.go new file mode 100644 index 0000000000000..9cb18425e6202 --- /dev/null +++ b/tailcfg/tailcfg_export_test.go @@ -0,0 +1,7 @@ +// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tailcfg + +var ExportKeyMarshalText = keyMarshalText diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index 0a3904ad6d348..438c8d1cb7be9 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package tailcfg +package tailcfg_test import ( "encoding" @@ -16,6 +16,7 @@ import ( "testing" "time" + . "tailscale.com/tailcfg" "tailscale.com/tstest" "tailscale.com/types/key" "tailscale.com/util/must" @@ -660,7 +661,7 @@ func BenchmarkKeyMarshalText(b *testing.B) { b.ReportAllocs() var k [32]byte for i := 0; i < b.N; i++ { - sinkBytes = keyMarshalText("prefix", k) + sinkBytes = ExportKeyMarshalText("prefix", k) } } @@ -670,7 +671,7 @@ func TestAppendKeyAllocs(t *testing.T) { } var k [32]byte err := tstest.MinAllocsPerRun(t, 1, func() { - sinkBytes = keyMarshalText("prefix", k) + sinkBytes = ExportKeyMarshalText("prefix", k) }) if err != nil { t.Fatal(err) diff --git a/version/prop.go b/version/prop.go index 946059fedfcbb..fe2b1fe2957c5 100644 --- a/version/prop.go +++ b/version/prop.go @@ -12,7 +12,9 @@ import ( "strings" "sync" + tailscaleroot "tailscale.com" "tailscale.com/syncs" + "tailscale.com/tailcfg" ) // IsMobile reports whether this is a mobile client build. @@ -104,3 +106,76 @@ func initUnstable() { } isUnstableBuild = minor%2 == 1 } + +// Meta is a JSON-serializable type that contains all the version +// information. +type Meta struct { + // MajorMinorPatch is the "major.minor.patch" version string, without + // any hyphenated suffix. + MajorMinorPatch string `json:"majorMinorPatch"` + + // IsDev is whether Short contains a -dev suffix. This is whether the build + // is a development build (as opposed to an official stable or unstable + // build stamped in the usual ways). If you just run "go install" or "go + // build" on a dev branch, this will be true. + IsDev bool `json:"isDev,omitempty"` + + // Short is MajorMinorPatch but optionally adding "-dev" or "-devYYYYMMDD" + // for dev builds, depending on how it was build. + Short string `json:"short"` + + // Long is the full version string, including git commit hash(es) as the + // suffix. + Long string `json:"long"` + + // UnstableBranch is whether the build is from an unstable (development) + // branch. That is, it reports whether the minor version is odd. + UnstableBranch bool `json:"unstableBranch,omitempty"` + + // GitCommit, if non-empty, is the git commit of the + // github.com/tailscale/tailscale repository at which Tailscale was + // built. Its format is the one returned by `git describe --always + // --exclude "*" --dirty --abbrev=200`. + GitCommit string `json:"gitCommit,omitempty"` + + // GitDirty is whether Go stamped the binary as having dirty version + // control changes in the working directory (debug.ReadBuildInfo + // setting "vcs.modified" was true). + GitDirty bool `json:"gitDirty,omitempty"` + + // ExtraGitCommit, if non-empty, is the git commit of a "supplemental" + // repository at which Tailscale was built. Its format is the same as + // gitCommit. + // + // ExtraGitCommit is used to track the source revision when the main + // Tailscale repository is integrated into and built from another + // repository (for example, Tailscale's proprietary code, or the + // Android OSS repository). Together, GitCommit and ExtraGitCommit + // exactly describe what repositories and commits were used in a + // build. + ExtraGitCommit string `json:"extraGitCommit,omitempty"` + + // DaemonLong is the version number from the tailscaled + // daemon, if requested. + DaemonLong string `json:"daemonLong,omitempty"` + + // Cap is the current Tailscale capability version. It's a monotonically + // incrementing integer that's incremented whenever a new capability is + // added. + Cap int `json:"cap"` +} + +// GetMeta returns version metadata about the current build. +func GetMeta() Meta { + return Meta{ + MajorMinorPatch: strings.TrimSpace(tailscaleroot.Version), + Short: Short, + Long: Long, + GitCommit: GitCommit, + GitDirty: GitDirty, + ExtraGitCommit: ExtraGitCommit, + IsDev: strings.Contains(Short, "-dev"), // TODO(bradfitz): could make a bool for this in init + UnstableBranch: IsUnstableBuild(), + Cap: int(tailcfg.CurrentCapabilityVersion), + } +} diff --git a/version/version.go b/version/version.go index 82f694cf6cf16..0557eccfd527c 100644 --- a/version/version.go +++ b/version/version.go @@ -74,7 +74,7 @@ func init() { // --exclude "*" --dirty --abbrev=200`. var GitCommit = "" -// GitDirty is whether Go stamped the binary has having dirty version +// GitDirty is whether Go stamped the binary as having dirty version // control changes in the working directory (debug.ReadBuildInfo // setting "vcs.modified" was true). var GitDirty bool