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
21 changes: 21 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ tasks:
cmds:
- task --list

build:
desc: Build datumctl locally with go, embedding a real <last-tag>+<sha> version
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 .

nix-update-hash:
desc: Automatically update the vendorHash in flake.nix after Go dependency changes
cmds:
Expand All @@ -14,6 +28,13 @@ tasks:
nix-build:
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.
- defer: git reset -- VERSION && rm -f VERSION
- git describe --tags --abbrev=0 > VERSION
- git add -N VERSION
- nix build

nix-dev:
Expand Down
38 changes: 26 additions & 12 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,36 @@
inherit system;
};

# Get version from git, fallback to "dev" if not in a git repo
version =
if (builtins.pathExists ./.git)
then builtins.replaceStrings ["\n"] [""] (builtins.readFile (
pkgs.runCommand "get-version" {} ''
cd ${./.}
${pkgs.git}/bin/git describe --tags --always --dirty 2>/dev/null > $out || echo "dev" > $out
''
))
else "dev";
# 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}";

in
{
packages = {
default = (pkgs.buildGoModule.override { go = pkgs.go_1_26; }) {
pname = "datumctl";
inherit version;
version = gitVersion;

src = ./.;

Expand All @@ -50,7 +63,8 @@
ldflags = [
"-s"
"-w"
"-X main.version=${version}"
"-X k8s.io/component-base/version.gitVersion=${gitVersion}"
"-X k8s.io/component-base/version.gitCommit=${gitCommit}"
"-extldflags=-static"
];

Expand Down
66 changes: 66 additions & 0 deletions internal/version/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Package version derives a fallback for k8s.io/component-base/version's
// GitVersion when datumctl is built without the ldflags that release, nix,
// and `task build` all inject (e.g. a bare `go run .` or `go test`). Go's
// toolchain stamps VCS info into the binary automatically from the module's
// git checkout, so this uses that instead of leaving component-base's
// compiled-in placeholder, which fails to parse in places that expect a real
// semantic version (plugin compatibility checks, the update checker).
//
// The fallback can only ever be "v0.0.0+<sha>[-dev]", never a real tag:
// component-base's SetDynamicVersion rejects any version whose major/minor/
// patch don't match the compiled-in placeholder's (0.0.0), and Go's VCS
// stamping has no notion of the nearest tag anyway. Getting the real last
// tag requires setting GitVersion via ldflags at compile time instead, which
// is what `task build` (and nix/goreleaser) do.
package version

import (
"runtime/debug"

componentversion "k8s.io/component-base/version"
)

// placeholder is k8s.io/component-base/version's compiled-in default,
// present whenever no ldflags have set a real GitVersion.
const placeholder = "v0.0.0-master+$Format:%H$"

// ApplyFallback installs a fallback GitVersion derived from the binary's
// embedded VCS info, unless ldflags have already set a real one or no VCS
// info was stamped into the binary (e.g. building outside a git checkout).
func ApplyFallback() {
if componentversion.Get().GitVersion != placeholder {
return
}
info, ok := debug.ReadBuildInfo()
if !ok {
return
}
if fallback, ok := fallbackFromBuildInfo(info); ok {
_ = componentversion.SetDynamicVersion(fallback)
}
}

func fallbackFromBuildInfo(info *debug.BuildInfo) (string, bool) {
var revision string
var dirty bool
for _, setting := range info.Settings {
switch setting.Key {
case "vcs.revision":
revision = setting.Value
case "vcs.modified":
dirty = setting.Value == "true"
}
}
if revision == "" {
return "", false
}
if len(revision) > 8 {
revision = revision[:8]
}

fallback := "v0.0.0+" + revision
if dirty {
fallback += "-dev"
}
return fallback, true
}
58 changes: 58 additions & 0 deletions internal/version/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package version

import (
"runtime/debug"
"testing"
)

func TestFallbackFromBuildInfo(t *testing.T) {
cases := []struct {
name string
settings []debug.BuildSetting
want string
wantOK bool
}{
{
name: "clean checkout",
settings: []debug.BuildSetting{
{Key: "vcs.revision", Value: "931f022eb016a57fcc8ee63e328e9ba12ded6aba"},
{Key: "vcs.modified", Value: "false"},
},
want: "v0.0.0+931f022e",
wantOK: true,
},
{
name: "dirty checkout",
settings: []debug.BuildSetting{
{Key: "vcs.revision", Value: "931f022eb016a57fcc8ee63e328e9ba12ded6aba"},
{Key: "vcs.modified", Value: "true"},
},
want: "v0.0.0+931f022e-dev",
wantOK: true,
},
{
name: "short revision left untruncated",
settings: []debug.BuildSetting{
{Key: "vcs.revision", Value: "abc123"},
},
want: "v0.0.0+abc123",
wantOK: true,
},
{
name: "no vcs info",
settings: nil,
wantOK: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, ok := fallbackFromBuildInfo(&debug.BuildInfo{Settings: tc.settings})
if ok != tc.wantOK {
t.Fatalf("ok = %v, want %v", ok, tc.wantOK)
}
if got != tc.want {
t.Fatalf("fallback = %q, want %q", got, tc.want)
}
})
}
}
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/spf13/cobra"
"go.datum.net/datumctl/internal/cmd"
customerrors "go.datum.net/datumctl/internal/errors"
datumversion "go.datum.net/datumctl/internal/version"
"go.miloapis.com/service-catalog/pkg/activation"
"k8s.io/component-base/cli"
"k8s.io/component-base/logs"
Expand All @@ -21,6 +22,8 @@ import (
)

func main() {
datumversion.ApplyFallback()

logs.GlogSetter(kubectlcmd.GetLogVerbosity(os.Args))
rootCmd := cmd.RootCmd()

Expand Down