diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 3c592ab..6228a5d 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "time" "github.com/spf13/cobra" @@ -64,6 +65,20 @@ func hidePersistentFlags(cmd *cobra.Command, flags ...string) { } } +// isVersionSkewParseError reports whether err came from +// "k8s.io/kubectl/pkg/cmd/version".getVersionSkewWarning failing to parse +// the client or server GitVersion as semver, rather than from a real +// connectivity/discovery failure. That check runs after the client/server +// versions have already been printed, so a malformed version string +// shouldn't be treated as fatal. +func isVersionSkewParseError(err error) bool { + if err == nil { + return false + } + msg := err.Error() + return strings.HasPrefix(msg, "client version error:") || strings.HasPrefix(msg, "server version error:") +} + func RootCmd() *cobra.Command { // Resolve plugins directory early so ForwardCompletion can use it. // Failures are non-fatal here — if we can't find the dir, completion @@ -662,6 +677,31 @@ the server.` return nil } versionCmd.GroupID = "other" + versionCmd.Run = nil + versionCmd.RunE = func(cmd *cobra.Command, args []string) error { + o := version.NewOptions(ioStreams) + o.ClientOnly, _ = cmd.Flags().GetBool("client") + o.Output, _ = cmd.Flags().GetString("output") + + if err := o.Complete(factory, cmd, args); err != nil { + return err + } + if err := o.Validate(); err != nil { + return err + } + + err := o.Run() + if isVersionSkewParseError(err) { + // Client/server versions were already printed above; the server + // (or, less commonly, the client binary itself) reported a + // version string that isn't valid semver, e.g. an unresolved + // `git archive` export-subst placeholder like "$Format:%H$". + // That shouldn't prevent `datumctl version` from succeeding. + fmt.Fprintf(ioStreams.ErrOut, "Warning: could not compare client/server versions: %s\n", err) + return nil + } + return err + } rootCmd.AddCommand(versionCmd) activityCmd := activity.NewActivityCommand(activity.ActivityCommandOptions{ diff --git a/internal/cmd/version_test.go b/internal/cmd/version_test.go new file mode 100644 index 0000000..3d77550 --- /dev/null +++ b/internal/cmd/version_test.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "errors" + "fmt" + "testing" +) + +func TestIsVersionSkewParseError(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + {"nil error", nil, false}, + { + "unresolved git archive placeholder in server version", + fmt.Errorf("server version error: %w", errors.New(`could not parse pre-release/metadata (-master+$Format:%H$) in version "v0.0.0-master+$Format:%H$"`)), + true, + }, + { + "unparseable client version", + fmt.Errorf("client version error: %w", errors.New(`could not parse "not-a-version" as version`)), + true, + }, + {"unrelated error", errors.New("connection refused"), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isVersionSkewParseError(tt.err); got != tt.want { + t.Fatalf("isVersionSkewParseError(%v) = %v, want %v", tt.err, got, tt.want) + } + }) + } +}