310 lines
10 KiB
Go
310 lines
10 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadSelectsLocalComposeFilesForAnInstallationInPathsWithSpaces(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
installationPath, projectDirectory, envFile, override := writeInstallation(t, "local")
|
|
installation, err := Load(installationPath)
|
|
if err != nil {
|
|
t.Fatalf("Load() error = %v", err)
|
|
}
|
|
|
|
if installation.ProjectDirectory != projectDirectory {
|
|
t.Errorf("ProjectDirectory = %q, want %q", installation.ProjectDirectory, projectDirectory)
|
|
}
|
|
if installation.EnvFile != envFile {
|
|
t.Errorf("EnvFile = %q, want %q", installation.EnvFile, envFile)
|
|
}
|
|
if !strings.Contains(installationPath, "installation folder with spaces") {
|
|
t.Fatalf("test setup must exercise a path with spaces: %q", installationPath)
|
|
}
|
|
|
|
want := []string{
|
|
filepath.Join(projectDirectory, "compose.yaml"),
|
|
filepath.Join(projectDirectory, "deploy", "compose.local.yaml"),
|
|
override,
|
|
}
|
|
assertStringsEqual(t, installation.ComposeFiles(), want)
|
|
}
|
|
|
|
func TestLoadSelectsServerComposeFiles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
installationPath, projectDirectory, _, override := writeInstallation(t, "server")
|
|
installation, err := Load(installationPath)
|
|
if err != nil {
|
|
t.Fatalf("Load() error = %v", err)
|
|
}
|
|
|
|
want := []string{
|
|
filepath.Join(projectDirectory, "compose.yaml"),
|
|
filepath.Join(projectDirectory, "deploy", "compose.server.yaml"),
|
|
override,
|
|
}
|
|
assertStringsEqual(t, installation.ComposeFiles(), want)
|
|
}
|
|
|
|
func TestLoadRequiresAndReturnsTypedWorkspaceRepositoryForGitInstallations(t *testing.T) {
|
|
installationPath, projectDirectory, envFile, _ := writeInstallation(t, "local")
|
|
gitOverride := filepath.Join(projectDirectory, "deploy", "compose.git-ssh.yaml")
|
|
if err := os.WriteFile(gitOverride, []byte("services: {}\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secretRoot := filepath.Dir(envFile)
|
|
privateKey := filepath.Join(secretRoot, "git-key")
|
|
knownHosts := filepath.Join(secretRoot, "known-hosts")
|
|
for _, file := range []string{privateKey, knownHosts} {
|
|
if err := os.WriteFile(file, []byte("fixture\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
remote := "git@gitea.example.org:clinical/workspaces.git"
|
|
environment := strings.Join([]string{
|
|
"THT_WORKSPACE_GIT_REMOTE=" + remote,
|
|
"THT_WORKSPACE_GIT_BRANCH=main",
|
|
"THT_WORKSPACE_GIT_SSH_KEY_FILE=" + privateKey,
|
|
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=" + knownHosts,
|
|
}, "\n") + "\n"
|
|
if err := os.WriteFile(envFile, []byte(environment), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
contents := "profile: local\nprojectDirectory: " + projectDirectory +
|
|
"\nenvFile: " + envFile +
|
|
"\nworkspaceRepository:\n remote: " + remote +
|
|
"\n branch: main\n access: ssh\noverrides:\n - " + gitOverride + "\n"
|
|
if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
installation, err := Load(installationPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if installation.WorkspaceRepository.Remote != remote ||
|
|
installation.WorkspaceRepository.Branch != "main" ||
|
|
installation.WorkspaceRepository.Access != "ssh" {
|
|
t.Fatalf("WorkspaceRepository = %#v", installation.WorkspaceRepository)
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsGitOverrideWithoutTypedWorkspaceRepository(t *testing.T) {
|
|
installationPath, projectDirectory, envFile, _ := writeInstallation(t, "local")
|
|
gitOverride := filepath.Join(projectDirectory, "deploy", "compose.git-https.yaml")
|
|
if err := os.WriteFile(gitOverride, []byte("services: {}\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
contents := "profile: local\nprojectDirectory: " + projectDirectory +
|
|
"\nenvFile: " + envFile + "\noverrides:\n - " + gitOverride + "\n"
|
|
if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err := Load(installationPath)
|
|
if err == nil || !strings.Contains(err.Error(), "workspaceRepository") {
|
|
t.Fatalf("Load() error = %v, want workspaceRepository error", err)
|
|
}
|
|
}
|
|
|
|
func TestComposeArgsAutomaticallyIncludeTheInstallationCurrentImageOverride(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
installationPath, _, _, _ := writeInstallation(t, "local")
|
|
seed, err := Load(installationPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
currentImage := seed.CurrentImageOverridePath()
|
|
if err := os.MkdirAll(filepath.Dir(currentImage), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(currentImage, []byte("services:\n core:\n image: candidate\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
installation, err := Load(installationPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
args := installation.ComposeArgs("up", "--detach")
|
|
want := []string{"-f", currentImage, "up", "--detach"}
|
|
if !containsSequence(args, want) {
|
|
t.Fatalf("ComposeArgs() = %#v, want durable override immediately before command", args)
|
|
}
|
|
}
|
|
|
|
func TestComposeArgsWithFinalOverridePreservesCurrentImagePrecedence(t *testing.T) {
|
|
installationPath, _, _, _ := writeInstallation(t, "server")
|
|
seed, err := Load(installationPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(seed.ControlDirectory(), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(seed.CurrentImageOverridePath(), []byte("services: {}\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
final := filepath.Join(seed.ControlDirectory(), "migration.yaml")
|
|
if err := os.WriteFile(final, []byte("services: {}\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
installation, err := Load(installationPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
args, err := installation.ComposeArgsWithFinalOverride(final, "--profile", "session-migrate", "config")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []string{"-f", installation.CurrentImageOverridePath(), "-f", final, "--profile", "session-migrate", "config"}
|
|
if !containsSequence(args, want) {
|
|
t.Fatalf("ComposeArgsWithFinalOverride() = %#v, want %#v", args, want)
|
|
}
|
|
}
|
|
|
|
func TestPreservationPathsReturnsCanonicalBindRootsBackupsAndSecretFiles(t *testing.T) {
|
|
installationPath, _, envFile, _ := writeInstallation(t, "server")
|
|
root := filepath.Dir(envFile)
|
|
var wanted []string
|
|
var lines []string
|
|
for _, item := range []struct{ key, name string }{
|
|
{"THT_DATA_ROOT", "data"},
|
|
{"THT_PI_STATE_ROOT", "pi-state"},
|
|
{"THT_WORKSPACE_REGISTRY_ROOT", "workspace-registry"},
|
|
{"THT_BACKUP_ROOT", "backups"},
|
|
} {
|
|
path := filepath.Join(root, item.name)
|
|
if err := os.Mkdir(path, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wanted = append(wanted, path)
|
|
lines = append(lines, item.key+"="+path)
|
|
}
|
|
secret := filepath.Join(root, "secret")
|
|
if err := os.WriteFile(secret, []byte("secret"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wanted = append(wanted, secret)
|
|
lines = append(lines, "APP_TOKEN_FILE="+secret)
|
|
if err := os.WriteFile(envFile, []byte(strings.Join(lines, "\n")+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
installation, err := Load(installationPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := installation.PreservationPaths()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertStringsEqual(t, got, wanted)
|
|
}
|
|
|
|
func TestInstallationControlPathsAreIsolatedForDescriptorsSharingOneCheckout(t *testing.T) {
|
|
projectDirectory := t.TempDir()
|
|
first := Installation{Path: filepath.Join(t.TempDir(), installationFileName), ProjectDirectory: projectDirectory}
|
|
second := Installation{Path: filepath.Join(t.TempDir(), installationFileName), ProjectDirectory: projectDirectory}
|
|
|
|
if first.CurrentImageOverridePath() == second.CurrentImageOverridePath() {
|
|
t.Fatalf("shared-checkout installations reused %q", first.CurrentImageOverridePath())
|
|
}
|
|
for _, installation := range []Installation{first, second} {
|
|
if filepath.Dir(filepath.Dir(installation.CurrentImageOverridePath())) != filepath.Join(projectDirectory, ".thothctl") {
|
|
t.Fatalf("current-image path %q is not installation-specific under .thothctl", installation.CurrentImageOverridePath())
|
|
}
|
|
if filepath.Dir(installation.UpdateStatePath()) != filepath.Dir(installation.CurrentImageOverridePath()) {
|
|
t.Fatalf("state %q and selector %q do not share one installation control directory", installation.UpdateStatePath(), installation.CurrentImageOverridePath())
|
|
}
|
|
if got, want := installation.RestartStatePath(), filepath.Join(installation.ControlDirectory(), "restart-state.json"); got != want {
|
|
t.Fatalf("RestartStatePath() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsRelativeInstallationPaths(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := Load("thothii-installation.yaml")
|
|
if err == nil || !strings.Contains(err.Error(), "absolute") {
|
|
t.Fatalf("Load() error = %v, want an absolute-path error", err)
|
|
}
|
|
}
|
|
|
|
func writeInstallation(t *testing.T, profile string) (string, string, string, string) {
|
|
t.Helper()
|
|
|
|
temporaryRoot, err := filepath.EvalSymlinks(os.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
physicalRoot, err := os.MkdirTemp(temporaryRoot, "thothctl-config-")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.RemoveAll(physicalRoot) })
|
|
root := filepath.Join(physicalRoot, "installation folder with spaces")
|
|
projectDirectory := filepath.Join(root, "project directory with spaces")
|
|
if err := os.MkdirAll(filepath.Join(projectDirectory, "deploy"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, name := range []string{"compose.yaml", filepath.Join("deploy", "compose.local.yaml"), filepath.Join("deploy", "compose.server.yaml")} {
|
|
if err := os.WriteFile(filepath.Join(projectDirectory, name), []byte("services: {}\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
envFile := filepath.Join(root, "environment file.env")
|
|
if err := os.WriteFile(envFile, []byte("SAFE_VALUE=1\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
override := filepath.Join(root, "extra override.yaml")
|
|
if err := os.WriteFile(override, []byte("services: {}\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
installationPath := filepath.Join(root, "thothii-installation.yaml")
|
|
contents := "profile: " + profile + "\nprojectDirectory: " + projectDirectory + "\nenvFile: " + envFile + "\noverrides:\n - " + override + "\n"
|
|
if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return installationPath, projectDirectory, envFile, override
|
|
}
|
|
|
|
func assertStringsEqual(t *testing.T, got, want []string) {
|
|
t.Helper()
|
|
if len(got) != len(want) {
|
|
t.Fatalf("length = %d, want %d: got %#v", len(got), len(want), got)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("value[%d] = %q, want %q", i, got[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func containsSequence(values, wanted []string) bool {
|
|
for start := range values {
|
|
if len(values)-start < len(wanted) {
|
|
continue
|
|
}
|
|
matched := true
|
|
for offset := range wanted {
|
|
if values[start+offset] != wanted[offset] {
|
|
matched = false
|
|
break
|
|
}
|
|
}
|
|
if matched {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|