Skip to content
Merged
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
20 changes: 8 additions & 12 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,12 @@ tasks:
- task --list

build:
desc: Build datumctl locally with go, embedding a real <last-tag>+<sha> version
desc: Build datumctl locally with go (Go stamps a pseudo-version such as v0.23.4-0.<date>-<sha> after the last tag)
vars:
LAST_TAG:
sh: git describe --tags --abbrev=0 2>/dev/null || echo v0.0.0
GIT_SHA:
sh: git rev-parse --short=8 HEAD
GIT_COMMIT:
sh: git rev-parse HEAD
DIRTY_SUFFIX:
sh: (git diff --quiet && git diff --cached --quiet) || echo -dev
cmds:
- go build -ldflags "-X k8s.io/component-base/version.gitVersion={{.LAST_TAG}}+{{.GIT_SHA}}{{.DIRTY_SUFFIX}} -X k8s.io/component-base/version.gitCommit={{.GIT_COMMIT}}" -o datumctl .
- go build -ldflags "-X k8s.io/component-base/version.gitCommit={{.GIT_COMMIT}}" -o datumctl .

nix-update-hash:
desc: Automatically update the vendorHash in flake.nix after Go dependency changes
Expand All @@ -29,11 +23,13 @@ tasks:
desc: Build the project with Nix
cmds:
# `nix build` only sees files git knows about (even untracked ones must be
# staged), so drop the last tag in a VERSION file and stage it with
# intent-to-add; flake.nix reads it to embed <last-tag>+<sha> in the
# binary. Cleaned up afterwards so it's never actually committed.
# staged), so write `git describe` to a VERSION file and stage it with
# intent-to-add; flake.nix reads it to embed the same pseudo-version
# `go build` would. --dirty is captured before VERSION exists, since
# staging it makes nix see the tree as dirty. Cleaned up afterwards so
# it's never actually committed.
- defer: git reset -- VERSION && rm -f VERSION
- git describe --tags --abbrev=0 > VERSION
- git describe --tags --long --dirty > VERSION || true
- git add -N VERSION
- nix build

Expand Down
72 changes: 49 additions & 23 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,53 @@
inherit system;
};

# Nix flakes always strip .git from the copied source (self/./. here has
# no .git even though the real checkout does), so `self.rev`/`dirtyRev`
# -- populated by the flake's git-fetcher from outside that copy -- are
# the only reliable way to get the commit sha. There's no equivalent
# flake attribute for "nearest git tag", so that comes from an optional
# untracked VERSION file (`git describe --tags --abbrev=0 > VERSION`,
# not committed) which -- being an ordinary file -- does survive the
# source copy; absent that file this falls back to a valid placeholder.
# Either way the result must be semver-shaped ("vX.Y.Z[+meta]"), since
# k8s.io/component-base/version (used by `datumctl version`) parses it
# strictly and a bare "dev"/"unknown" would fail that parse.
# self.dirtyRev/dirtyShortRev append a literal "-dirty" suffix when the
# working tree has uncommitted changes; swap it for "-dev" everywhere.
markDev = builtins.replaceStrings [ "-dirty" ] [ "-dev" ];
gitCommit = markDev (self.dirtyRev or self.rev or "unknown");
gitSha = markDev (self.dirtyShortRev or self.shortRev or "unknown");

lastTag =
if (builtins.pathExists ./VERSION)
then builtins.replaceStrings ["\n"] [""] (builtins.readFile ./VERSION)
else "v0.0.0";

gitVersion = "${lastTag}+${gitSha}";
# Nix flakes always strip .git from the copied source, so the commit
# comes from self.rev/self.dirtyRev and its timestamp from
# self.lastModifiedDate (UTC, YYYYMMDDHHMMSS). There's no flake
# attribute for the nearest tag, so `task nix-build` writes
# `git describe --tags --long --dirty` (e.g. "v0.23.3-38-g818025e")
# to an untracked VERSION file, which survives the source copy.
# Staging that file makes nix see every tree as dirty, so when it
# exists its "-dirty" suffix decides dirtiness instead.
#
# From those this builds the same version a plain `go build` stamps:
# at a tag: v0.23.3
# after a tag: v0.23.4-0.20261103201002-818025e9fe34
# no VERSION file: v0.0.0-20261103201002-818025e9fe34
# i.e. a Go pseudo-version, which sorts after the last release and
# before the next, so it can't be mistaken for a release. A dirty
# tree appends "+dev".
lib = pkgs.lib;
hasVersionFile = builtins.pathExists ./VERSION;
describeRaw = if hasVersionFile then lib.trim (builtins.readFile ./VERSION) else "";
describe = lib.removeSuffix "-dirty" describeRaw;
dirty = if hasVersionFile then lib.hasSuffix "-dirty" describeRaw else self ? dirtyRev;
rev = self.rev or (lib.removeSuffix "-dirty" (self.dirtyRev or ""));
gitCommit = if rev == "" then "unknown" else rev;
gitTreeState = if dirty then "dirty" else "clean";
# Commit time as RFC 3339, matching what a plain `go build` reports.
d = self.lastModifiedDate;
buildDate = "${builtins.substring 0 4 d}-${builtins.substring 4 2 d}-${builtins.substring 6 2 d}T${builtins.substring 8 2 d}:${builtins.substring 10 2 d}:${builtins.substring 12 2 d}Z";
pseudoSuffix = "${self.lastModifiedDate}-${builtins.substring 0 12 rev}";

# [ tag commitsSinceTag ], or null for an empty or unrecognized VERSION.
described = builtins.match "(v[0-9]+\\.[0-9]+\\.[0-9]+.*)-([0-9]+)-g[0-9a-f]+" describe;
# [ major minor patch prerelease-or-null ]
tagParts = builtins.match "v([0-9]+)\\.([0-9]+)\\.([0-9]+)(-.*)?" (builtins.elemAt described 0);

pseudoVersion =
if described == null || tagParts == null then "v0.0.0-${pseudoSuffix}"
else if builtins.elemAt described 1 == "0" then builtins.elemAt described 0
else if builtins.elemAt tagParts 3 != null then "${builtins.elemAt described 0}.0.${pseudoSuffix}"
else
let
patch = lib.toInt (builtins.elemAt tagParts 2) + 1;
in
"v${builtins.elemAt tagParts 0}.${builtins.elemAt tagParts 1}.${toString patch}-0.${pseudoSuffix}";

gitVersion =
if rev == "" then "v0.0.0"
else pseudoVersion + lib.optionalString dirty "+dev";

in
{
Expand All @@ -65,6 +89,8 @@
"-w"
"-X k8s.io/component-base/version.gitVersion=${gitVersion}"
"-X k8s.io/component-base/version.gitCommit=${gitCommit}"
"-X k8s.io/component-base/version.gitTreeState=${gitTreeState}"
"-X k8s.io/component-base/version.buildDate=${buildDate}"
"-extldflags=-static"
];

Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/autoupdate/autoupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (
"time"

"github.com/spf13/cobra"
componentversion "k8s.io/component-base/version"

"go.datum.net/datumctl/internal/datumconfig"
customerrors "go.datum.net/datumctl/internal/errors"
"go.datum.net/datumctl/internal/updatecheck"
datumversion "go.datum.net/datumctl/internal/version"
)

// Command returns the "auto-update" command tree.
Expand Down Expand Up @@ -46,7 +46,7 @@ arguments performs an upgrade immediately, regardless of that setting.`,
}

func runUpdate(cmd *cobra.Command, _ []string) error {
current := componentversion.Get().GitVersion
current := datumversion.Get()

ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second)
defer cancel()
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/plugin/browse.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import (
"charm.land/huh/v2"
"github.com/spf13/cobra"
"golang.org/x/term"
componentversion "k8s.io/component-base/version"
"k8s.io/kubectl/pkg/util/templates"

customerrors "go.datum.net/datumctl/internal/errors"
"go.datum.net/datumctl/internal/pluginstore"
datumversion "go.datum.net/datumctl/internal/version"
)

// browseEntry is one selectable plugin in the interactive browser, paired with
Expand Down Expand Up @@ -98,7 +98,7 @@ func browseCmd() *cobra.Command {
return nil
}

currentVersion := componentversion.Get().GitVersion
currentVersion := datumversion.Get()
// Empty version installs the catalog's recommended release.
return installFromCatalog(cmd, pluginsDir, reg, selected.catalog.Name, selected.plugin.Name, "", currentVersion)
},
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/plugin/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import (
"time"

"github.com/spf13/cobra"
componentversion "k8s.io/component-base/version"

"go.datum.net/datumctl/internal/client"
customerrors "go.datum.net/datumctl/internal/errors"
"go.datum.net/datumctl/internal/plugindispatch"
"go.datum.net/datumctl/internal/pluginstore"
datumversion "go.datum.net/datumctl/internal/version"
)

func installCmd(_ *client.DatumCloudFactory) *cobra.Command {
Expand Down Expand Up @@ -86,7 +86,7 @@ The plugin binary is written to the managed plugins directory
return err
}

currentVersion := componentversion.Get().GitVersion
currentVersion := datumversion.Get()

if allAvailable {
if len(args) > 0 {
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/plugin/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import (
"strings"

"github.com/spf13/cobra"
componentversion "k8s.io/component-base/version"
"k8s.io/kubectl/pkg/util/templates"

"go.datum.net/datumctl/internal/client"
customerrors "go.datum.net/datumctl/internal/errors"
"go.datum.net/datumctl/internal/pluginstore"
datumversion "go.datum.net/datumctl/internal/version"
)

func upgradeCmd(_ *client.DatumCloudFactory) *cobra.Command {
Expand Down Expand Up @@ -58,7 +58,7 @@ func upgradeCmd(_ *client.DatumCloudFactory) *cobra.Command {
return fmt.Errorf("load plugins manifest: %w", err)
}

currentVersion := componentversion.Get().GitVersion
currentVersion := datumversion.Get()

if all {
return upgradeAll(cmd, pluginsDir, manifest, currentVersion)
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/plugin_suggest.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import (

"github.com/spf13/cobra"
"golang.org/x/term"
componentversion "k8s.io/component-base/version"

plugincmd "go.datum.net/datumctl/internal/cmd/plugin"
"go.datum.net/datumctl/internal/client"
"go.datum.net/datumctl/internal/plugindispatch"
"go.datum.net/datumctl/internal/pluginstore"
datumversion "go.datum.net/datumctl/internal/version"
)

// suggestAndInstallPlugin checks the local plugin index for an exact match on
Expand Down Expand Up @@ -60,7 +60,7 @@ func suggestAndInstallPlugin(cmd *cobra.Command, name, pluginsDir string, origin
}

fmt.Fprintf(cmd.ErrOrStderr(), "\nInstalling %s...\n", entry.Name)
currentVersion := componentversion.Get().GitVersion
currentVersion := datumversion.Get()
installed, _, installErr := plugincmd.InstallPlugin(cmd.Context(), pluginsDir, entry.Name, "", currentVersion, idx)
if installErr != nil {
return fmt.Errorf("install plugin: %w", installErr)
Expand Down
10 changes: 7 additions & 3 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest"
"k8s.io/client-go/transport"
componentversion "k8s.io/component-base/version"
"k8s.io/kubectl/pkg/cmd/annotate"
"k8s.io/kubectl/pkg/cmd/apiresources"
"k8s.io/kubectl/pkg/cmd/apply"
Expand Down Expand Up @@ -48,6 +47,7 @@ import (
"go.datum.net/datumctl/internal/plugindispatch"
"go.datum.net/datumctl/internal/pluginstore"
"go.datum.net/datumctl/internal/updatecheck"
datumversion "go.datum.net/datumctl/internal/version"
)

// hideFlags hides the named flags from a command's flag set. Flags that do not
Expand Down Expand Up @@ -200,7 +200,7 @@ are unaffected.`,
manifest, loadErr := pluginstore.Load(pluginsDir)
if loadErr == nil && manifest.Plugins != nil {
if entry, ok := manifest.Plugins[name]; ok && entry.Manifest != nil {
currentVersion := componentversion.Get().GitVersion
currentVersion := datumversion.Get()
if warn, compatErr := plugindispatch.CheckCompatibilityAtInvocation(entry.Manifest, currentVersion, plugindispatch.PluginAPIVersion); compatErr != nil {
return customerrors.NewUserError(compatErr.Error())
} else if warn != "" {
Expand Down Expand Up @@ -830,6 +830,10 @@ the server.`
}
return nil
}
versionCmd.Run = nil
versionCmd.RunE = func(cmd *cobra.Command, args []string) error {
return runVersion(factory, ioStreams.Out, cmd)
}
versionCmd.GroupID = "other"
rootCmd.AddCommand(versionCmd)

Expand Down Expand Up @@ -989,7 +993,7 @@ func startUpdateCheck(cmd *cobra.Command) {
if updatecheck.SkipFromEnvironment() {
return
}
current := componentversion.Get().GitVersion
current := datumversion.Get()
checker := updatecheck.New(current)
checker.Start(cmd.Context())
activeUpdateChecker[cmd.Root()] = checker
Expand Down
73 changes: 73 additions & 0 deletions internal/cmd/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package cmd

import (
"encoding/json"
"fmt"
"io"

"github.com/spf13/cobra"
"k8s.io/client-go/tools/clientcmd"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/cmd/version"
"sigs.k8s.io/yaml"

datumversion "go.datum.net/datumctl/internal/version"
)

// runVersion replaces kubectl's version Run. kubectl reads the client
// version straight from k8s.io/component-base/version, which can't hold
// the pseudo-version datumversion.Get resolves for unreleased builds. It
// also skips kubectl's client/server skew check: datumctl's version and
// the Datum Cloud API server's version are independent, so comparing them
// is meaningless, and the check fails outright on non-semver server
// versions.
func runVersion(f cmdutil.Factory, out io.Writer, cmd *cobra.Command) error {
clientOnly, _ := cmd.Flags().GetBool("client")
output, _ := cmd.Flags().GetString("output")
if output != "" && output != "yaml" && output != "json" {
return fmt.Errorf("--output must be 'yaml' or 'json'")
}

clientInfo := datumversion.Info()
info := version.Version{ClientVersion: &clientInfo}
if kv, ok := version.GetKustomizeModVersion(); ok {
info.KustomizeVersion = kv
}

var serverErr error
if !clientOnly {
dc, err := f.ToDiscoveryClient()
// An empty config means no context is set up; print client info only.
if err != nil && !clientcmd.IsEmptyConfig(err) {
return err
}
if dc != nil {
dc.Invalidate()
info.ServerVersion, serverErr = dc.ServerVersion()
}
}

switch output {
case "":
fmt.Fprintf(out, "Client Version: %s\n", info.ClientVersion.GitVersion)
if info.KustomizeVersion != "" {
fmt.Fprintf(out, "Kustomize Version: %s\n", info.KustomizeVersion)
}
if info.ServerVersion != nil {
fmt.Fprintf(out, "Server Version: %s\n", info.ServerVersion.GitVersion)
}
case "yaml":
b, err := yaml.Marshal(&info)
if err != nil {
return err
}
fmt.Fprintln(out, string(b))
case "json":
b, err := json.MarshalIndent(&info, "", " ")
if err != nil {
return err
}
fmt.Fprintln(out, string(b))
}
return serverErr
}
Loading
Loading