mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
62bfdbca7e
Open the v6.4.3 candidate line from main. The v6.4.2 tag was staged on 2026-08-31 but never activated: its release run was cancelled after the private Pro build failed the compiler memory gate, so the latest published stable is still v6.4.1. This candidate carries the complete v6.4.2 change set plus the corrections landed since that tag, including the stale PBS Backup Running state (#1815), the Windows Unified Agent auto-update 404 (#1820), and shared-token same-hostname agent identity collapse (#1753). Packet: VERSION, compose and install-docker defaults, Helm chart metadata, release notes with a declined visual plan, changelog, pointer docs and the shipped docs mirror, and the deployment-installability cutoff note. Rollback target is v6.4.1 and the mobile decision is no-mobile-impact. Tests: the packet tests now describe the 6.4.3 train, v6.4.2 is recorded as an unpublished stable so it is never derived as the previous stable or rollback target, and the Python v6.4.2 notes expectation matches the phrase the notes actually use.
491 lines
19 KiB
Go
491 lines
19 KiB
Go
package installtests
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func currentReleaseVersion(t *testing.T) string {
|
|
t.Helper()
|
|
content, err := os.ReadFile(repoFile("VERSION"))
|
|
if err != nil {
|
|
t.Fatalf("read VERSION: %v", err)
|
|
}
|
|
version := strings.TrimSpace(string(content))
|
|
if version == "" {
|
|
t.Fatal("VERSION is empty")
|
|
}
|
|
return version
|
|
}
|
|
|
|
func requiredReleaseBranchForVersion(t *testing.T, version string) string {
|
|
t.Helper()
|
|
cmd := exec.Command("python3", repoFile("scripts", "release_control", "control_plane.py"), "--branch-for-version", version)
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
t.Fatalf("resolve release branch for %s: %v", version, err)
|
|
}
|
|
branch := strings.TrimSpace(string(output))
|
|
if branch == "" {
|
|
t.Fatalf("release branch for %s is empty", version)
|
|
}
|
|
return branch
|
|
}
|
|
|
|
func isPrereleaseVersion(version string) bool {
|
|
return strings.Contains(version, "-")
|
|
}
|
|
|
|
// unpublishedStableVersions lists stable versions whose tag and packet exist in
|
|
// the repository but which never activated as a public GitHub release. They are
|
|
// not valid rollback targets and must not be treated as the previous stable.
|
|
var unpublishedStableVersions = map[string]bool{
|
|
// Tagged 2026-08-31; the private Pro build failed its memory gate and the
|
|
// release commit verdict failed, so the packet shipped through v6.4.3 instead.
|
|
"6.4.2": true,
|
|
}
|
|
|
|
func previousStablePatchVersion(version string) (string, bool) {
|
|
if isPrereleaseVersion(version) {
|
|
return "", false
|
|
}
|
|
parts := strings.Split(version, ".")
|
|
if len(parts) != 3 {
|
|
return "", false
|
|
}
|
|
patch, err := strconv.Atoi(parts[2])
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
for patch > 0 {
|
|
patch--
|
|
candidate := fmt.Sprintf("%s.%s.%d", parts[0], parts[1], patch)
|
|
if !unpublishedStableVersions[candidate] {
|
|
return candidate, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func previousStableForPrereleaseVersion(version string) (string, bool) {
|
|
if !isPrereleaseVersion(version) {
|
|
return "", false
|
|
}
|
|
base, _, ok := strings.Cut(version, "-")
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
baseParts, ok := parseStableVersion(base)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
|
|
releaseNotes, err := filepath.Glob(repoFile("docs", "releases", "RELEASE_NOTES_v*.md"))
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
|
|
best := [3]int{-1, -1, -1}
|
|
found := false
|
|
for _, releaseNote := range releaseNotes {
|
|
filename := filepath.Base(releaseNote)
|
|
candidate := strings.TrimSuffix(strings.TrimPrefix(filename, "RELEASE_NOTES_v"), ".md")
|
|
parts, valid := parseStableVersion(candidate)
|
|
if !valid || compareStableVersions(parts, baseParts) >= 0 || unpublishedStableVersions[candidate] {
|
|
continue
|
|
}
|
|
if !found || compareStableVersions(parts, best) > 0 {
|
|
best = parts
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return "", false
|
|
}
|
|
return fmt.Sprintf("%d.%d.%d", best[0], best[1], best[2]), true
|
|
}
|
|
|
|
func parseStableVersion(version string) ([3]int, bool) {
|
|
parts := strings.Split(version, ".")
|
|
if len(parts) != 3 {
|
|
return [3]int{}, false
|
|
}
|
|
parsed := [3]int{}
|
|
for index, part := range parts {
|
|
value, err := strconv.Atoi(part)
|
|
if err != nil || value < 0 {
|
|
return [3]int{}, false
|
|
}
|
|
parsed[index] = value
|
|
}
|
|
return parsed, true
|
|
}
|
|
|
|
func compareStableVersions(left, right [3]int) int {
|
|
for index := range left {
|
|
if left[index] < right[index] {
|
|
return -1
|
|
}
|
|
if left[index] > right[index] {
|
|
return 1
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func TestPreviousStableForPrereleaseVersionCrossesMinorBoundaries(t *testing.T) {
|
|
tests := []struct {
|
|
version string
|
|
want string
|
|
}{
|
|
{version: "6.0.5-rc.4", want: "6.0.4"},
|
|
{version: "6.2.0-rc.4", want: "6.1.2"},
|
|
{version: "6.2.0-rc.5", want: "6.1.2"},
|
|
{version: "6.2.0-rc.6", want: "6.1.2"},
|
|
{version: "6.2.0-rc.7", want: "6.1.2"},
|
|
{version: "6.2.0-rc.8", want: "6.1.2"},
|
|
{version: "6.2.0-rc.9", want: "6.1.2"},
|
|
{version: "6.2.0-rc.10", want: "6.1.2"},
|
|
{version: "6.2.0-rc.11", want: "6.1.2"},
|
|
{version: "6.2.2-rc.1", want: "6.2.1"},
|
|
{version: "6.2.2-rc.2", want: "6.2.1"},
|
|
{version: "6.2.2-rc.3", want: "6.2.1"},
|
|
{version: "6.3.0-rc.1", want: "6.2.1"},
|
|
{version: "6.3.0-rc.2", want: "6.2.1"},
|
|
{version: "6.4.0-rc.1", want: "6.3.2"},
|
|
{version: "6.4.0-rc.2", want: "6.3.2"},
|
|
{version: "6.4.0-rc.3", want: "6.3.2"},
|
|
{version: "6.4.0-rc.4", want: "6.3.2"},
|
|
{version: "6.4.0-rc.5", want: "6.3.2"},
|
|
{version: "6.4.0-rc.6", want: "6.3.2"},
|
|
{version: "6.4.0-rc.7", want: "6.3.2"},
|
|
{version: "6.4.0-rc.8", want: "6.3.2"},
|
|
{version: "6.4.0-rc.9", want: "6.3.2"},
|
|
{version: "6.4.0-rc.10", want: "6.3.2"},
|
|
{version: "6.4.0-rc.11", want: "6.3.2"},
|
|
{version: "6.4.0-rc.12", want: "6.3.2"},
|
|
{version: "6.4.0-rc.13", want: "6.3.2"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.version, func(t *testing.T) {
|
|
got, ok := previousStableForPrereleaseVersion(test.version)
|
|
if !ok {
|
|
t.Fatalf("previousStableForPrereleaseVersion(%q) did not find a stable release", test.version)
|
|
}
|
|
if got != test.want {
|
|
t.Fatalf("previousStableForPrereleaseVersion(%q) = %q, want %q", test.version, got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func previousPrereleaseVersion(version string) (string, bool) {
|
|
base, suffix, ok := strings.Cut(version, "-rc.")
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
rc, err := strconv.Atoi(suffix)
|
|
if err != nil || rc <= 1 {
|
|
return "", false
|
|
}
|
|
return fmt.Sprintf("%s-rc.%d", base, rc-1), true
|
|
}
|
|
|
|
func TestInstallDockerScriptUsesConfiguredImageRepoDefault(t *testing.T) {
|
|
workDir := t.TempDir()
|
|
version := currentReleaseVersion(t)
|
|
runInstallDockerScript(t, workDir, "DOCKER_IMAGE_REPO=example/pulse-enterprise")
|
|
|
|
composePath := filepath.Join(workDir, "docker-compose.yml")
|
|
composeContent, err := os.ReadFile(composePath)
|
|
if err != nil {
|
|
t.Fatalf("read docker-compose.yml: %v", err)
|
|
}
|
|
composeText := string(composeContent)
|
|
if !strings.Contains(composeText, "image: ${PULSE_IMAGE:-example/pulse-enterprise:"+version+"}") {
|
|
t.Fatalf("docker-compose.yml missing configured image default:\n%s", composeText)
|
|
}
|
|
if strings.Contains(composeText, ":latest") {
|
|
t.Fatalf("docker-compose.yml must not default to a floating latest tag:\n%s", composeText)
|
|
}
|
|
|
|
envPath := filepath.Join(workDir, ".env")
|
|
envContent, err := os.ReadFile(envPath)
|
|
if err != nil {
|
|
t.Fatalf("read .env: %v", err)
|
|
}
|
|
envText := string(envContent)
|
|
if !strings.Contains(envText, "PULSE_IMAGE=example/pulse-enterprise:"+version) {
|
|
t.Fatalf(".env missing configured image default:\n%s", envText)
|
|
}
|
|
}
|
|
|
|
func TestInstallDockerScriptPrefersExplicitPulseImage(t *testing.T) {
|
|
workDir := t.TempDir()
|
|
version := currentReleaseVersion(t)
|
|
runInstallDockerScript(
|
|
t,
|
|
workDir,
|
|
"DOCKER_IMAGE_REPO=example/pulse-enterprise",
|
|
"PULSE_IMAGE=ghcr.io/example/pulse-enterprise:v9.9.9",
|
|
)
|
|
|
|
composePath := filepath.Join(workDir, "docker-compose.yml")
|
|
composeContent, err := os.ReadFile(composePath)
|
|
if err != nil {
|
|
t.Fatalf("read docker-compose.yml: %v", err)
|
|
}
|
|
composeText := string(composeContent)
|
|
if !strings.Contains(composeText, "image: ${PULSE_IMAGE:-example/pulse-enterprise:"+version+"}") {
|
|
t.Fatalf("docker-compose.yml lost configured default image:\n%s", composeText)
|
|
}
|
|
|
|
envPath := filepath.Join(workDir, ".env")
|
|
envContent, err := os.ReadFile(envPath)
|
|
if err != nil {
|
|
t.Fatalf("read .env: %v", err)
|
|
}
|
|
envText := string(envContent)
|
|
if !strings.Contains(envText, "PULSE_IMAGE=ghcr.io/example/pulse-enterprise:v9.9.9") {
|
|
t.Fatalf(".env did not preserve explicit image override:\n%s", envText)
|
|
}
|
|
}
|
|
|
|
func TestRepoDockerComposeDefaultPinsCurrentVersion(t *testing.T) {
|
|
version := currentReleaseVersion(t)
|
|
content, err := os.ReadFile(repoFile("docker-compose.yml"))
|
|
if err != nil {
|
|
t.Fatalf("read docker-compose.yml: %v", err)
|
|
}
|
|
|
|
text := string(content)
|
|
if !strings.Contains(text, "image: ${PULSE_IMAGE:-rcourtman/pulse:"+version+"}") {
|
|
t.Fatalf("repo docker-compose.yml must pin the current release version:\n%s", text)
|
|
}
|
|
if !isPrereleaseVersion(version) && strings.Contains(text, "-rc.") {
|
|
t.Fatalf("stable repo docker-compose.yml must not keep a prerelease image default:\n%s", text)
|
|
}
|
|
if !isPrereleaseVersion(version) && version == "6.0.0" && !strings.Contains(text, "rcourtman/pulse:6.0.0") {
|
|
t.Fatalf("v6 GA repo docker-compose.yml must default to the stable v6 image:\n%s", text)
|
|
}
|
|
if !isPrereleaseVersion(version) && version != "6.0.0" && strings.Contains(text, "rcourtman/pulse:6.0.0") {
|
|
t.Fatalf("stable patch repo docker-compose.yml must move off the initial GA image tag:\n%s", text)
|
|
}
|
|
if previous, ok := previousStablePatchVersion(version); ok && strings.Contains(text, "rcourtman/pulse:"+previous) {
|
|
t.Fatalf("repo docker-compose.yml must not retain the previous stable patch image tag %s:\n%s", previous, text)
|
|
}
|
|
if previous, ok := previousPrereleaseVersion(version); ok && strings.Contains(text, "rcourtman/pulse:"+previous) {
|
|
t.Fatalf("repo docker-compose.yml must not retain the previous prerelease image tag %s:\n%s", previous, text)
|
|
}
|
|
if strings.Contains(text, ":latest") {
|
|
t.Fatalf("repo docker-compose.yml must not default to a floating latest tag:\n%s", text)
|
|
}
|
|
}
|
|
|
|
func TestInstallDockerScriptFallbackPinsCurrentVersion(t *testing.T) {
|
|
version := currentReleaseVersion(t)
|
|
content, err := os.ReadFile(repoFile("scripts", "install-docker.sh"))
|
|
if err != nil {
|
|
t.Fatalf("read install-docker.sh: %v", err)
|
|
}
|
|
|
|
text := string(content)
|
|
if !strings.Contains(text, `CANONICAL_DEFAULT_PULSE_VERSION="`+version+`"`) {
|
|
t.Fatalf("install-docker.sh fallback must pin the current release version:\n%s", text)
|
|
}
|
|
if !isPrereleaseVersion(version) && strings.Contains(text, `CANONICAL_DEFAULT_PULSE_VERSION="`) && strings.Contains(text, "-rc.") {
|
|
t.Fatalf("stable install-docker.sh fallback must not keep a prerelease default:\n%s", text)
|
|
}
|
|
if !isPrereleaseVersion(version) && version == "6.0.0" && !strings.Contains(text, `CANONICAL_DEFAULT_PULSE_VERSION="6.0.0"`) {
|
|
t.Fatalf("v6 GA install-docker.sh fallback must default to the stable v6 image tag:\n%s", text)
|
|
}
|
|
if !isPrereleaseVersion(version) && version != "6.0.0" && strings.Contains(text, `CANONICAL_DEFAULT_PULSE_VERSION="6.0.0"`) {
|
|
t.Fatalf("stable patch install-docker.sh fallback must move off the initial GA image tag:\n%s", text)
|
|
}
|
|
if previous, ok := previousStablePatchVersion(version); ok && strings.Contains(text, `CANONICAL_DEFAULT_PULSE_VERSION="`+previous+`"`) {
|
|
t.Fatalf("install-docker.sh fallback must not retain the previous stable patch version %s:\n%s", previous, text)
|
|
}
|
|
if previous, ok := previousPrereleaseVersion(version); ok && strings.Contains(text, `CANONICAL_DEFAULT_PULSE_VERSION="`+previous+`"`) {
|
|
t.Fatalf("install-docker.sh fallback must not retain the previous prerelease version %s:\n%s", previous, text)
|
|
}
|
|
}
|
|
|
|
func TestInstallDockerProofTracksStablePatchReleaseContract(t *testing.T) {
|
|
version := currentReleaseVersion(t)
|
|
if isPrereleaseVersion(version) {
|
|
t.Skip("current release is a prerelease")
|
|
}
|
|
previous, ok := previousStablePatchVersion(version)
|
|
if !ok {
|
|
t.Skip("current release is not a stable patch release")
|
|
}
|
|
|
|
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
|
|
"The active stable `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.",
|
|
"This patch release uses the stable hotfix path with `rollback_version=v"+previous+"`, `hotfix_exception=true`, a release-owner reason, and no fabricated same-version RC tag.",
|
|
"active customer harm",
|
|
"`no-mobile-impact`",
|
|
"For the active stable `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"`",
|
|
)
|
|
if version == "6.3.1" {
|
|
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
|
|
"the prior `v"+previous+"` decision could not be reused for this patch",
|
|
"the release owner recorded that separate `v6.3.1` exception",
|
|
"public Unknown Publisher disclosure",
|
|
)
|
|
} else {
|
|
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
|
|
"standing SignPath-unavailable policy from `v6.3.2` onward",
|
|
"Public Unknown Publisher disclosure",
|
|
)
|
|
}
|
|
if version == "6.4.2" {
|
|
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
|
|
"The governed branch is `main`",
|
|
"authenticated non-administrator organization members can reach infrastructure action control",
|
|
"SSO-only deployments can treat every authenticated IdP user as an instance administrator without an explicit grant",
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestInstallDockerProofTracksStableMinorContract(t *testing.T) {
|
|
version := currentReleaseVersion(t)
|
|
if isPrereleaseVersion(version) {
|
|
t.Skip("current release is a prerelease")
|
|
}
|
|
parts, valid := parseStableVersion(version)
|
|
if !valid || parts[1] == 0 || parts[2] != 0 {
|
|
t.Skip("current release is not a stable minor release")
|
|
}
|
|
previous, ok := previousStableForPrereleaseVersion(version + "-rc.1")
|
|
if !ok {
|
|
t.Fatal("stable minor release has no earlier stable rollback packet")
|
|
}
|
|
|
|
assertFileContainsAll(t, repoFile("docker-compose.yml"),
|
|
"image: ${PULSE_IMAGE:-rcourtman/pulse:"+version+"}",
|
|
)
|
|
assertFileContainsAll(t, repoFile("scripts", "install-docker.sh"),
|
|
`CANONICAL_DEFAULT_PULSE_VERSION="`+version+`"`,
|
|
)
|
|
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
|
|
"The active stable `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.",
|
|
"`rollback_version=v"+previous+"`",
|
|
"`hotfix_exception=true` transports that approved waiver through the shared promotion resolver. It does not reclassify v"+version+" as a patch hotfix.",
|
|
"The integrated single-build workflow must pass its exact-SHA preflight and immutable readiness gates before publication.",
|
|
"The stable server cut is classified `existing-mobile-build-compatible`.",
|
|
"Stable `v"+version+"` skips SignPath under the standing unavailable policy and retains exact-SHA, checksum, detached-signature, immutable-manifest, published-digest, and Unknown Publisher disclosure controls.",
|
|
"For the active stable `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"`",
|
|
)
|
|
}
|
|
|
|
func TestInstallDockerProofTracksPrereleaseContract(t *testing.T) {
|
|
version := currentReleaseVersion(t)
|
|
if !isPrereleaseVersion(version) {
|
|
t.Skip("current release is stable")
|
|
}
|
|
previous, ok := previousStableForPrereleaseVersion(version)
|
|
if !ok {
|
|
t.Skip("current prerelease does not have a previous stable patch")
|
|
}
|
|
stableTarget, _, ok := strings.Cut(version, "-")
|
|
if !ok {
|
|
t.Fatalf("current prerelease %q has no stable target", version)
|
|
}
|
|
comparisonVersion, ok := previousPrereleaseVersion(version)
|
|
comparisonLine := "It follows stable `v" + previous + "` and opens the published `v" + stableTarget + "` candidate line."
|
|
if !ok {
|
|
comparisonVersion = previous
|
|
} else {
|
|
comparisonLine = "It follows `v" + comparisonVersion + "` on the published `v" + stableTarget + "` candidate line."
|
|
}
|
|
if version == "6.4.0-rc.10" {
|
|
comparisonLine = "The `v6.4.0-rc.9` release staged an immutable draft, tag, and exact-version artifacts but did not activate publicly"
|
|
}
|
|
|
|
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
|
|
"The active prerelease `v"+version+"` cut sets the repo-root `VERSION`, repo-root `docker-compose.yml` image default, `scripts/install-docker.sh` fallback, and Helm chart release metadata to the same `"+version+"` release version.",
|
|
comparisonLine,
|
|
"This prerelease keeps `rollback_version=v"+previous+"`, publishes a versioned public GitHub prerelease plus versioned Docker and Helm artifacts, and does not move stable/latest install pointers or stable semver aliases.",
|
|
"No governed mobile-facing path changed from `v"+previous+"`, so the release decision is `no-mobile-impact`",
|
|
"no companion upload or public mobile-store rollout is part of this candidate.",
|
|
"The prerelease Windows path retains exact-SHA, checksum, and detached-signature verification without Authenticode. Stable `v"+stableTarget+"` also skips SignPath under the standing unavailable policy",
|
|
"For the active prerelease `v"+version+"` cut, the repo-root compose default and `scripts/install-docker.sh` fallback must both pin `"+version+"` until the next governed stable cut moves them forward.",
|
|
)
|
|
if version == "6.3.0-rc.6" {
|
|
assertFileContainsAllNormalized(t, repoFile("docs", "release-control", "v6", "internal", "subsystems", "deployment-installability.md"),
|
|
"For `v6.3.0-rc.6`, the release path retains credential-free PVE compilation while requiring the measured memory floor for API race shards after a bounded admission wait.",
|
|
"Public server and provider control-plane image publication uses independent matrix jobs that each revalidate the exact checkout and candidate manifest.",
|
|
"Private packaging transfers only the products consumed by Pro assembly, and paid-runtime Docker and direct-binary mismatch proofs execute concurrently without weakening either proof.",
|
|
)
|
|
}
|
|
}
|
|
|
|
func runInstallDockerScript(t *testing.T, workDir string, envVars ...string) {
|
|
t.Helper()
|
|
|
|
scriptPath := repoFile("scripts", "install-docker.sh")
|
|
content, err := os.ReadFile(scriptPath)
|
|
if err != nil {
|
|
t.Fatalf("read install-docker.sh: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
script = strings.Replace(script, rootCheckBlock, ":", 1)
|
|
script = strings.Replace(script, containerCheckBlock, ":", 1)
|
|
|
|
tmpScript := filepath.Join(workDir, "install-docker.sh")
|
|
if err := os.WriteFile(tmpScript, []byte(script), 0o755); err != nil {
|
|
t.Fatalf("write temp install-docker.sh: %v", err)
|
|
}
|
|
|
|
binDir := filepath.Join(workDir, "bin")
|
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir bin: %v", err)
|
|
}
|
|
writeTestStub(t, filepath.Join(binDir, "docker"), "#!/bin/sh\nif [ \"$1\" = \"compose\" ] && [ \"$2\" = \"version\" ]; then exit 0; fi\nexit 0\n")
|
|
writeTestStub(t, filepath.Join(binDir, "timedatectl"), "#!/bin/sh\necho Europe/London\n")
|
|
writeTestStub(t, filepath.Join(binDir, "hostname"), "#!/bin/sh\nif [ \"$1\" = \"-I\" ]; then echo 192.0.2.10; else echo pulse-host; fi\n")
|
|
|
|
cmd := exec.Command("bash", tmpScript)
|
|
cmd.Dir = workDir
|
|
cmd.Env = append(os.Environ(), append([]string{
|
|
"PATH=" + binDir + string(os.PathListSeparator) + os.Getenv("PATH"),
|
|
}, envVars...)...)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("run install-docker.sh: %v\n%s", err, out)
|
|
}
|
|
}
|
|
|
|
func writeTestStub(t *testing.T, path, content string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
|
|
t.Fatalf("write stub %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
const rootCheckBlock = `# Check if running as root (early check for better error messages)
|
|
if [ "$EUID" -ne 0 ]; then
|
|
echo "❌ ERROR: This script must be run as root"
|
|
echo ""
|
|
echo "Please run: sudo $0"
|
|
exit 1
|
|
fi
|
|
`
|
|
|
|
const containerCheckBlock = `# Detect if running in a container
|
|
if [ -f /.dockerenv ] || [ -f /run/.containerenv ]; then
|
|
echo "❌ ERROR: This script must run on the Docker host, not inside a container"
|
|
echo ""
|
|
echo "Please run this script on your Docker host machine."
|
|
exit 1
|
|
fi
|
|
`
|