fixes openziti/ziti#4304 build flags on the version response (#4305)

- adds a package-level buildFlags string the linker sets at build time, parsed
  into a capped list of [A-Z0-9_] names with blanks, duplicates, and malformed
  tokens dropped
- returns those names in the new buildFlags field on /version, separate from
  capabilities, and prints them under ziti version -v
- bumps edge-api to v0.36.0 for the buildFlags field
This commit is contained in:
Andrew Martinez
2026-08-25 22:29:08 +01:00
committed by GitHub
parent 661016508a
commit d47ccaffee
7 changed files with 191 additions and 1 deletions
+31
View File
@@ -16,6 +16,7 @@
* [DNS Upstream Query Modes](#dns-upstream-query-modes) - choose how multiple DNS upstreams are queried: parallel fan-out (default) or serial fail-through
* [Controller Read Throughput Under Load](#controller-read-throughput-under-load) - a bbolt upgrade lifts a ceiling on concurrent read transactions that could stall a busy controller
* [Logging Now Uses slog with an Async Handler](#logging-now-uses-slog-with-an-async-handler) - Logging moves to Go's `log/slog` behind an asynchronous sink; output is unchanged by default, with new flags to tune buffering
* [Build Flags](#build-flags) - A build of the controller can name the build time choices it was made with, and clients can read them from `/version`
* [Security Advisories](#security-advisories) - Eight security advisories, plus the two control-plane certificate validation fixes first released in 2.0.2
## Security Advisories
@@ -496,6 +497,36 @@ most call sites are not yet migrated, per-channel overrides have limited reach
today and expand as packages are converted. The global `ziti agent set-log-level
<level>` still affects everything.
## Build Flags
A controller binary can now carry a list of build flags: short names for the
build time choices it was made with. The `/version` endpoint returns them in a
new `buildFlags` field, and `ziti version -v` prints them, so a client or an
operator can tell what a running controller was built to do without asking the
person who built it.
Flags are set with the Go linker at build time. There is one symbol, and one
build owner composes the whole list:
```
go build -ldflags "-X github.com/openziti/ziti/v2/common/build.buildFlags=ALPHA,BRAVO_MODE" ./ziti
```
Names are uppercase letters, digits, and underscores. Anything else in the list
is dropped rather than served. A second `-X` against the same symbol replaces
the first rather than adding to it, and the linker silently ignores an `-X`
whose symbol path it cannot resolve, so a typo in that path yields a binary with
no flags rather than a build error.
Releases from this repository set no flags, so `buildFlags` is empty and `ziti
version -v` prints no build flags line. The names themselves are defined by
whoever produced the build; a client should ignore any it does not recognize.
Build flags are not capabilities. `capabilities` on `/version` describes what
the controller offers over the API, is defined by this repository, and is
enumerated at `/enumerated-capabilities`. Build flags describe how the binary
was built, are open-ended, and are enumerated nowhere.
## Deprecated Features
Deprecated features still work, but are no longer recommended and will be removed
+63
View File
@@ -0,0 +1,63 @@
package build
import (
"regexp"
"strings"
)
const (
// maxBuildFlags is the greatest number of flag names accepted from buildFlags.
maxBuildFlags = 32
// maxBuildFlagNameLength is the greatest length accepted for a single flag name.
maxBuildFlagNameLength = 64
)
// buildFlagNamePattern matches a well formed build flag name.
var buildFlagNamePattern = regexp.MustCompile(`^[A-Z0-9_]+$`)
// buildFlags is a comma separated list of flag names supplied at build time by the linker, empty
// in a stock build:
//
// -X github.com/openziti/ziti/v2/common/build.buildFlags=ALPHA,BRAVO
//
// The symbol path is a contract with downstream builds. The linker silently ignores -X against a
// symbol it cannot resolve, so renaming this variable or moving it to another package produces a
// binary with no build flags rather than a build error. A second -X against this symbol replaces
// the first rather than adding to it: one build owner composes the whole list.
var buildFlags string
// GetBuildFlags returns the well formed flag names supplied at build time, in the order they were
// given. It returns an empty slice for a stock build.
func GetBuildFlags() []string {
return parseBuildFlags(buildFlags)
}
// parseBuildFlags splits raw on commas and returns the well formed names in first seen order,
// dropping blanks, duplicates, and anything outside [A-Z0-9_]. The result is never nil, so it
// serializes as an empty JSON array rather than null. The count of names and the length of each
// are capped, because the result is served over an unauthenticated API.
func parseBuildFlags(raw string) []string {
result := []string{}
seen := map[string]struct{}{}
for _, token := range strings.Split(raw, ",") {
if len(result) == maxBuildFlags {
break
}
name := strings.TrimSpace(token)
if len(name) > maxBuildFlagNameLength || !buildFlagNamePattern.MatchString(name) {
continue
}
if _, ok := seen[name]; ok {
continue
}
seen[name] = struct{}{}
result = append(result, name)
}
return result
}
+87
View File
@@ -0,0 +1,87 @@
package build
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestParseBuildFlags(t *testing.T) {
tests := []struct {
name string
raw string
expected []string
}{
{
name: "empty string yields an empty, non nil slice",
raw: "",
expected: []string{},
},
{
name: "single name",
raw: "ALPHA",
expected: []string{"ALPHA"},
},
{
name: "multiple names keep their order",
raw: "ALPHA,BRAVO,CHARLIE",
expected: []string{"ALPHA", "BRAVO", "CHARLIE"},
},
{
name: "surrounding whitespace is trimmed",
raw: " ALPHA , BRAVO\t,\nCHARLIE ",
expected: []string{"ALPHA", "BRAVO", "CHARLIE"},
},
{
name: "blank tokens are dropped",
raw: ",ALPHA,, ,BRAVO,",
expected: []string{"ALPHA", "BRAVO"},
},
{
name: "duplicates are dropped, first occurrence wins",
raw: "ALPHA,BRAVO,ALPHA",
expected: []string{"ALPHA", "BRAVO"},
},
{
name: "digits and underscores are accepted",
raw: "ALPHA_2,BRAVO_MODE,3",
expected: []string{"ALPHA_2", "BRAVO_MODE", "3"},
},
{
name: "malformed tokens are dropped, well formed ones survive",
raw: "alpha,BRAVO,Char-lie,DELTA ECHO,FOXTROT!,GOLF",
expected: []string{"BRAVO", "GOLF"},
},
{
name: "names longer than the cap are dropped",
raw: "ALPHA," + strings.Repeat("B", maxBuildFlagNameLength+1) + ",CHARLIE",
expected: []string{"ALPHA", "CHARLIE"},
},
{
name: "names exactly at the length cap are kept",
raw: strings.Repeat("B", maxBuildFlagNameLength),
expected: []string{strings.Repeat("B", maxBuildFlagNameLength)},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := parseBuildFlags(test.raw)
require.Equal(t, test.expected, result)
})
}
}
func TestParseBuildFlagsStopsAtTheCountCap(t *testing.T) {
names := make([]string, 0, maxBuildFlags+5)
for i := 0; i < maxBuildFlags+5; i++ {
names = append(names, "NAME_"+strings.Repeat("X", i))
}
result := parseBuildFlags(strings.Join(names, ","))
require.Len(t, result, maxBuildFlags, "accepted names must be capped")
require.Equal(t, names[:maxBuildFlags], result, "the first names up to the cap are the ones kept")
}
@@ -105,6 +105,7 @@ func (ir *VersionRouter) buildVersions(ae *env.AppEnv) *rest_model.Version {
Version: buildInfo.Version(),
APIVersions: map[string]map[string]rest_model.APIVersion{},
Capabilities: []string{},
BuildFlags: build.GetBuildFlags(),
}
for apiBinding, apiVersionToPathMap := range webapis.AllApiBindingVersions {
+1 -1
View File
@@ -65,7 +65,7 @@ require (
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/openziti/channel/v5 v5.0.27
github.com/openziti/cobra-to-md v1.0.1
github.com/openziti/edge-api v0.35.2
github.com/openziti/edge-api v0.36.0
github.com/openziti/foundation/v2 v2.0.100
github.com/openziti/identity v1.0.140
github.com/openziti/jwks v1.0.6
+2
View File
@@ -534,6 +534,8 @@ github.com/openziti/cobra-to-md v1.0.1 h1:WRinNoIRmwWUSJm+pSNXMjOrtU48oxXDZgeCYQ
github.com/openziti/cobra-to-md v1.0.1/go.mod h1:FjCpk/yzHF7/r28oSTNr5P57yN5VolpdAtS/g7KNi2c=
github.com/openziti/edge-api v0.35.2 h1:+5+i0BkhzsKZkURBAr6fgVp7BnHITjaHDiRxxdW3DuE=
github.com/openziti/edge-api v0.35.2/go.mod h1:m1oAQ6+fnkEO0NOAkDy6WpnXDxPhj2sko4eBUvIMxkE=
github.com/openziti/edge-api v0.36.0 h1:adp0gCDbxee3r4tPBCXm7IyLTcIGhDcWmQ9QJMO+AwY=
github.com/openziti/edge-api v0.36.0/go.mod h1:m1oAQ6+fnkEO0NOAkDy6WpnXDxPhj2sko4eBUvIMxkE=
github.com/openziti/foundation/v2 v2.0.100 h1:Q9DzyZYp7/QF0No2CDadcmhWpp1FqOvxwhNFENk6GrU=
github.com/openziti/foundation/v2 v2.0.100/go.mod h1:voePbn/hwFypw6Ogr2k5sl/g3ZiHWdHO202uHBNpsAQ=
github.com/openziti/go-term-markdown v1.0.1 h1:9uzMpK4tav6OtvRxRt99WwPTzAzCh+Pj9zWU2FBp3Qg=
+6
View File
@@ -18,7 +18,9 @@ package common
import (
"fmt"
"strings"
"github.com/openziti/ziti/v2/common/build"
"github.com/openziti/ziti/v2/common/version"
"github.com/spf13/cobra"
)
@@ -36,6 +38,10 @@ func NewVersionCmd() *cobra.Command {
fmt.Printf("Build Date: %s\n", version.GetBuildDate())
fmt.Printf("Go Version: %s\n", version.GetGoVersion())
fmt.Printf("OS/Arch: %s/%s\n", version.GetOS(), version.GetArchitecture())
if buildFlags := build.GetBuildFlags(); len(buildFlags) > 0 {
fmt.Printf("Build Flags: %s\n", strings.Join(buildFlags, ", "))
}
} else {
fmt.Println(version.GetVersion())
}