Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ Top-level non-resource commands:
| 6 | server error (5xx) |
| 7 | network / transport failure |

## Version information

`twoctl version` and `twoctl --version` print the same version, Go runtime,
and platform information. `twoctl version --json` emits a JSON object with
`version`, `go_version`, `os`, and `arch` fields for scripts. Version queries
do not read credentials, check for upgrades, or make network requests.

## Self-upgrade

`twoctl` checks for new releases at most once every 24 hours and prompts when one is available. The prompt offers:
Expand Down
14 changes: 1 addition & 13 deletions cmd/twoctl/cli/explain.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
package cli

// kubectl-flavored top-level helpers: `version`, `explain`, `api-resources`.
// kubectl-flavored top-level helpers: `explain`, `api-resources`.

import (
"encoding/json"
"fmt"
"runtime"
"sort"

"github.com/getkin/kin-openapi/openapi3"
"github.com/spf13/cobra"

"github.com/two-inc/twoctl-cli/internal/httpx"
)

func init() {
register(&cobra.Command{
Use: "version",
Short: "Print twoctl version, commit, and Go runtime",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Fprintf(cmd.OutOrStdout(), "twoctl version %s\ngo %s\nplatform %s/%s\n",
httpx.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
return nil
},
})

register(&cobra.Command{
Use: "explain <resource> <action>",
Expand Down
1 change: 1 addition & 0 deletions cmd/twoctl/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func init() {
rootCmd.PersistentFlags().StringVar(&flagURL, "url", "", "raw API base URL (overrides the context's URL)")
rootCmd.PersistentFlags().StringVarP(&flagOutput, "output", "o", "auto", "output format: table, json, yaml (auto = table on TTY, json when piped)")
rootCmd.Version = httpx.Version
rootCmd.SetVersionTemplate(versionText("{{.Version}}"))
}

// Root returns the configured root cobra.Command.
Expand Down
46 changes: 46 additions & 0 deletions cmd/twoctl/cli/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cli

import (
"encoding/json"
"fmt"
"runtime"

"github.com/spf13/cobra"
)

func init() {
version := &cobra.Command{
Use: "version",
Short: "Print twoctl version, Go runtime, and platform",
Args: cobra.NoArgs,
// Local version information must not invoke the inherited upgrade
// check, especially when a script requests machine-readable output.
PersistentPreRunE: func(*cobra.Command, []string) error { return nil },
RunE: runVersion,
}
version.Flags().Bool("json", false, "print version information as JSON")
register(version)
}

type versionInfo struct {
Version string `json:"version"`
GoVersion string `json:"go_version"`
OS string `json:"os"`
Arch string `json:"arch"`
}

func versionText(version string) string {
return fmt.Sprintf("twoctl version %s\ngo %s\nplatform %s/%s\n",
version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
}

func runVersion(cmd *cobra.Command, _ []string) error {
asJSON, _ := cmd.Flags().GetBool("json")
if asJSON {
return json.NewEncoder(cmd.OutOrStdout()).Encode(versionInfo{
Version: rootCmd.Version, GoVersion: runtime.Version(), OS: runtime.GOOS, Arch: runtime.GOARCH,
})
}
_, err := fmt.Fprint(cmd.OutOrStdout(), versionText(rootCmd.Version))
return err
}
67 changes: 67 additions & 0 deletions cmd/twoctl/cli/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package cli

import (
"encoding/json"
"errors"
"runtime"
"strings"
"testing"

"github.com/spf13/cobra"
)

func TestVersionFormsMatch(t *testing.T) {
resetGlobals(t)
original := Root().Version
Root().Version = "v1.2.3-test"
t.Cleanup(func() { Root().Version = original })
text, stderr, err := runRoot(t, "version")
if err != nil || stderr != "" {
t.Fatalf("version failed: %v, stderr=%q", err, stderr)
}
resetCobraFlags(Root())
flag, stderr, err := runRoot(t, "--version")
if err != nil || stderr != "" {
t.Fatalf("--version failed: %v, stderr=%q", err, stderr)
}
if flag != text {
t.Fatalf("version forms disagree:\ncommand: %q\nflag: %q", text, flag)
}
for _, want := range []string{"v1.2.3-test", runtime.Version(), runtime.GOOS + "/" + runtime.GOARCH} {
if !strings.Contains(text, want) {
t.Errorf("version output omitted %q: %s", want, text)
}
}
}

func TestVersionJSONIsOfflineAndMachineReadable(t *testing.T) {
resetGlobals(t)
original := Root().PersistentPreRunE
Root().PersistentPreRunE = func(*cobra.Command, []string) error {
return errors.New("version must not invoke the inherited upgrade hook")
}
t.Cleanup(func() { Root().PersistentPreRunE = original })
stdout, stderr, err := runRoot(t, "version", "--json")
if err != nil || stderr != "" {
t.Fatalf("version --json failed: %v, stderr=%q", err, stderr)
}
var info map[string]string
if err := json.Unmarshal([]byte(stdout), &info); err != nil {
t.Fatalf("output is not a single JSON object: %v\n%s", err, stdout)
}
want := map[string]string{
"version": Root().Version, "go_version": runtime.Version(), "os": runtime.GOOS, "arch": runtime.GOARCH,
}
for key, value := range want {
if info[key] != value {
t.Errorf("%s = %q, want %q", key, info[key], value)
}
}
}

func TestVersionRejectsUnexpectedArguments(t *testing.T) {
resetGlobals(t)
if _, _, err := runRoot(t, "version", "unexpected"); err == nil {
t.Fatal("version accepted an unexpected positional argument")
}
}
Loading