projection: begin the exact-path public epoch
The public tree and its history contain only the listed paths. Earlier projection history remains preserved internally. Source-Sha: 913fde029b935671833254797f0f20f1eb9fabba Policy-Sha: 913fde029b935671833254797f0f20f1eb9fabba Tree-Digest: 180ae530c1058a2a5c89837bdce2d323ae83e669e38590ca72e75b8d92b7262f
This commit is contained in:
commit
67e26be2d9
838 changed files with 157302 additions and 0 deletions
68
server/internal/version/version.go
Normal file
68
server/internal/version/version.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Version represents a semantic version string
|
||||
type Version string
|
||||
|
||||
// Platform represents combined platform-architecture format (e.g., "linux-amd64")
|
||||
type Platform string
|
||||
|
||||
// ParsePlatform converts "linux-amd64" → platform="linux", arch="amd64"
|
||||
func ParsePlatform(p Platform) (platform, architecture string) {
|
||||
parts := strings.SplitN(string(p), "-", 2)
|
||||
if len(parts) == 2 {
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
return string(p), ""
|
||||
}
|
||||
|
||||
// String returns the full platform string
|
||||
func (p Platform) String() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
// Compare returns -1, 0, or 1 for v < other, v == other, v > other
|
||||
func (v Version) Compare(other Version) int {
|
||||
v1Parts := strings.Split(string(v), ".")
|
||||
v2Parts := strings.Split(string(other), ".")
|
||||
|
||||
maxLen := len(v1Parts)
|
||||
if len(v2Parts) > maxLen {
|
||||
maxLen = len(v2Parts)
|
||||
}
|
||||
|
||||
for i := 0; i < maxLen; i++ {
|
||||
v1Num := 0
|
||||
v2Num := 0
|
||||
|
||||
if i < len(v1Parts) {
|
||||
v1Num, _ = strconv.Atoi(v1Parts[i])
|
||||
}
|
||||
if i < len(v2Parts) {
|
||||
v2Num, _ = strconv.Atoi(v2Parts[i])
|
||||
}
|
||||
|
||||
if v1Num < v2Num {
|
||||
return -1
|
||||
}
|
||||
if v1Num > v2Num {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// IsUpgrade returns true if other is newer than v
|
||||
func (v Version) IsUpgrade(other Version) bool {
|
||||
return v.Compare(other) < 0
|
||||
}
|
||||
|
||||
// IsValid returns true if version string is non-empty
|
||||
func (v Version) IsValid() bool {
|
||||
return string(v) != ""
|
||||
}
|
||||
118
server/internal/version/versions.go
Normal file
118
server/internal/version/versions.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Version coordination for Server Authority model
|
||||
// The server is the single source of truth for all version information
|
||||
|
||||
// Version information (SERVER AUTHORITY).
|
||||
// Values are maintained by scripts/bump-version.sh and must match the release
|
||||
// tag — the release gate enforces this. ldflags may override at build time;
|
||||
// the release pipeline injects the tag so binaries and source agree.
|
||||
var (
|
||||
AgentVersion = "0.2.9.6"
|
||||
ConfigVersion = "0.2.9.6"
|
||||
MinAgentVersion = "0.1.22"
|
||||
)
|
||||
|
||||
// CurrentVersions holds the authoritative version information
|
||||
type CurrentVersions struct {
|
||||
AgentVersion string `json:"agent_version"`
|
||||
ConfigVersion string `json:"config_version"`
|
||||
MinAgentVersion string `json:"min_agent_version"`
|
||||
BuildTime time.Time `json:"build_time"`
|
||||
}
|
||||
|
||||
// GetCurrentVersions returns the current version information
|
||||
func GetCurrentVersions() CurrentVersions {
|
||||
return CurrentVersions{
|
||||
AgentVersion: AgentVersion,
|
||||
ConfigVersion: ConfigVersion,
|
||||
MinAgentVersion: MinAgentVersion,
|
||||
BuildTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// CompareVersions compares two version strings using octet-based comparison.
|
||||
// Returns -1 if a < b, 0 if a == b, 1 if a > b.
|
||||
// Handles "dev" as always older than any release version.
|
||||
// Version format: "0.1.26.0" (up to 4 octets, padded with zeros).
|
||||
func CompareVersions(a, b string) int {
|
||||
a = strings.TrimPrefix(a, "v")
|
||||
b = strings.TrimPrefix(b, "v")
|
||||
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
if a == "dev" || a == "" {
|
||||
return -1
|
||||
}
|
||||
if b == "dev" || b == "" {
|
||||
return 1
|
||||
}
|
||||
|
||||
aParts := strings.Split(a, ".")
|
||||
bParts := strings.Split(b, ".")
|
||||
|
||||
maxLen := len(aParts)
|
||||
if len(bParts) > maxLen {
|
||||
maxLen = len(bParts)
|
||||
}
|
||||
|
||||
for i := 0; i < maxLen; i++ {
|
||||
aVal := 0
|
||||
bVal := 0
|
||||
if i < len(aParts) {
|
||||
if n, err := strconv.Atoi(aParts[i]); err == nil {
|
||||
aVal = n
|
||||
}
|
||||
}
|
||||
if i < len(bParts) {
|
||||
if n, err := strconv.Atoi(bParts[i]); err == nil {
|
||||
bVal = n
|
||||
}
|
||||
}
|
||||
if aVal < bVal {
|
||||
return -1
|
||||
}
|
||||
if aVal > bVal {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ExtractConfigVersionFromAgent extracts config version from agent version.
|
||||
// Agent version format: "0.1.23.6" where the last octet is the config version.
|
||||
func ExtractConfigVersionFromAgent(agentVersion string) string {
|
||||
cleanVersion := strings.TrimPrefix(agentVersion, "v")
|
||||
parts := strings.Split(cleanVersion, ".")
|
||||
if len(parts) >= 1 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return "3"
|
||||
}
|
||||
|
||||
// ValidateAgentVersion checks if an agent version is compatible
|
||||
func ValidateAgentVersion(agentVersion string) error {
|
||||
current := GetCurrentVersions()
|
||||
if CompareVersions(agentVersion, current.MinAgentVersion) < 0 {
|
||||
return fmt.Errorf("agent version %s is below minimum %s", agentVersion, current.MinAgentVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBuildFlags returns the ldflags to inject versions into agent builds
|
||||
func GetBuildFlags() []string {
|
||||
versions := GetCurrentVersions()
|
||||
return []string{
|
||||
fmt.Sprintf("-X github.com/Fimeg/RedFlag/agent/internal/version.Version=%s", versions.AgentVersion),
|
||||
fmt.Sprintf("-X github.com/Fimeg/RedFlag/agent/internal/version.ConfigVersion=%s", versions.ConfigVersion),
|
||||
fmt.Sprintf("-X github.com/Fimeg/RedFlag/agent/internal/version.BuildTime=%s", versions.BuildTime.Format(time.RFC3339)),
|
||||
}
|
||||
}
|
||||
86
server/internal/version/versions_test.go
Normal file
86
server/internal/version/versions_test.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompareVersionsCorrect(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b string
|
||||
expected int
|
||||
desc string
|
||||
}{
|
||||
{"0.1.22", "0.1.9", 1, "multi-digit minor beats single-digit"},
|
||||
{"0.2.0", "0.1.99", 1, "major bump beats high patch"},
|
||||
{"dev", "0.1.0", -1, "dev is always older"},
|
||||
{"0.1.0", "dev", 1, "any release beats dev"},
|
||||
{"0.1.26.0", "0.1.26.0", 0, "equal versions"},
|
||||
{"0.1.26.1", "0.1.26.0", 1, "config version differs"},
|
||||
{"1.0.0", "0.99.99", 1, "major version wins"},
|
||||
{"dev", "dev", 0, "dev equals dev"},
|
||||
{"", "0.1.0", -1, "empty is older"},
|
||||
{"v0.1.22", "0.1.22", 0, "v prefix stripped"},
|
||||
{"0.1.22", "v0.1.22", 0, "v prefix on second arg"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := CompareVersions(tt.a, tt.b)
|
||||
if result != tt.expected {
|
||||
t.Errorf("CompareVersions(%q, %q): got %d, want %d (%s)", tt.a, tt.b, result, tt.expected, tt.desc)
|
||||
}
|
||||
}
|
||||
t.Logf("[INFO] [server] [version] U-2 VERIFIED: all %d semver comparison cases pass", len(tests))
|
||||
}
|
||||
|
||||
func TestExtractConfigVersionFromAgent(t *testing.T) {
|
||||
tests := []struct {
|
||||
version string
|
||||
expected string
|
||||
}{
|
||||
{"0.1.23.6", "6"},
|
||||
{"0.1.23", "23"},
|
||||
{"v0.1.30", "30"},
|
||||
{"0.1.26.10", "10"},
|
||||
{"dev", "dev"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := ExtractConfigVersionFromAgent(tt.version)
|
||||
if result != tt.expected {
|
||||
t.Errorf("ExtractConfigVersionFromAgent(%q): got %q, want %q", tt.version, result, tt.expected)
|
||||
}
|
||||
}
|
||||
t.Logf("[INFO] [server] [version] U-10 VERIFIED: config version extraction works for multi-digit versions")
|
||||
}
|
||||
|
||||
func TestValidateAgentVersionSemverAware(t *testing.T) {
|
||||
// Save and restore original MinAgentVersion
|
||||
orig := MinAgentVersion
|
||||
defer func() { MinAgentVersion = orig }()
|
||||
|
||||
MinAgentVersion = "0.1.10"
|
||||
|
||||
// 0.1.9 < 0.1.10 — should fail
|
||||
if err := ValidateAgentVersion("0.1.9"); err == nil {
|
||||
t.Error("expected 0.1.9 to fail validation against min 0.1.10, but it passed")
|
||||
}
|
||||
|
||||
// 0.1.22 > 0.1.10 — should pass
|
||||
if err := ValidateAgentVersion("0.1.22"); err != nil {
|
||||
t.Errorf("expected 0.1.22 to pass validation against min 0.1.10, got: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("[INFO] [server] [version] U-2 VERIFIED: ValidateAgentVersion uses octet comparison, not lexicographic")
|
||||
}
|
||||
|
||||
func TestInfoEndpointReturnsCurrentVersion(t *testing.T) {
|
||||
// Test that GetCurrentVersions returns values (not the old hardcoded v0.1.21)
|
||||
versions := GetCurrentVersions()
|
||||
if versions.AgentVersion == "v0.1.21" {
|
||||
t.Error("AgentVersion is still the old hardcoded v0.1.21 — should be dynamic")
|
||||
}
|
||||
if versions.AgentVersion == "" {
|
||||
t.Error("AgentVersion is empty")
|
||||
}
|
||||
t.Logf("[INFO] [server] [version] U-1 VERIFIED: GetCurrentVersions returns %q (not hardcoded v0.1.21)", versions.AgentVersion)
|
||||
}
|
||||
Loading…
Reference in a new issue