510 lines
18 KiB
Go
510 lines
18 KiB
Go
package setup
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
|
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
|
"github.com/aritmolab/thothii/tools/tht/internal/testsupport"
|
|
)
|
|
|
|
func TestEnsureFilesCreatesDiscoverableConfigurationInProjectWithSpaces(t *testing.T) {
|
|
root := newProject(t, "checkout with spaces")
|
|
secrets := newExternalSecrets(t, root)
|
|
setNonInteractiveAnswers(t, secrets)
|
|
|
|
trackedExample := filepath.Join(root, "deploy", "env", "local.env.example")
|
|
before, err := os.ReadFile(trackedExample)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
result, err := EnsureFiles(Request{
|
|
ProjectRoot: root, InstallationID: "local-dev", Profile: "local", NonInteractive: true,
|
|
}, strings.NewReader(""), ioDiscard{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
wantDirectory := filepath.Join(root, "deploy", "local-dev")
|
|
if result.DescriptorPath != filepath.Join(wantDirectory, "thothii-installation.yaml") {
|
|
t.Fatalf("descriptor = %q", result.DescriptorPath)
|
|
}
|
|
if result.EnvironmentPath != filepath.Join(wantDirectory, "operator.env") {
|
|
t.Fatalf("environment = %q", result.EnvironmentPath)
|
|
}
|
|
for _, path := range []string{result.DescriptorPath, result.EnvironmentPath} {
|
|
info, statErr := os.Stat(path)
|
|
if statErr != nil {
|
|
t.Fatalf("generated file %s: %v", path, statErr)
|
|
}
|
|
if info.Mode().Perm()&0o077 != 0 {
|
|
t.Errorf("generated file %s has permissions %o, want owner-only", path, info.Mode().Perm())
|
|
}
|
|
}
|
|
installation, err := config.Load(result.DescriptorPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := safeio.ValidatePrivateDirectory(installation.AuthenticationDirectory()); err != nil {
|
|
t.Fatalf("authentication directory is not private: %v", err)
|
|
}
|
|
|
|
descriptor, err := os.ReadFile(result.DescriptorPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
environment, err := os.ReadFile(result.EnvironmentPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(environment), "THT_AUTH_CONFIG_ROOT=") {
|
|
t.Fatalf("generated environment does not declare the authentication config root: %s", environment)
|
|
}
|
|
for _, secretValue := range []string{"super-secret-value", "pi-secret-value", "private-key-value"} {
|
|
if bytes.Contains(descriptor, []byte(secretValue)) || bytes.Contains(environment, []byte(secretValue)) {
|
|
t.Fatalf("generated configuration contains a secret value %q", secretValue)
|
|
}
|
|
}
|
|
for _, path := range []string{secrets.secrets, secrets.piAuth, secrets.sshKey, secrets.knownHosts} {
|
|
contents, readErr := os.ReadFile(path)
|
|
if readErr != nil || len(contents) == 0 {
|
|
t.Fatalf("existing secret file %s was not preserved: %v", path, readErr)
|
|
}
|
|
}
|
|
if _, err := config.Resolve("", nil, root); err != nil {
|
|
t.Fatalf("generated descriptor was not discoverable: %v", err)
|
|
}
|
|
after, err := os.ReadFile(trackedExample)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(before, after) {
|
|
t.Fatal("tracked example was modified")
|
|
}
|
|
}
|
|
|
|
func TestEnsureFilesIsIdempotentForCompatibleFiles(t *testing.T) {
|
|
root := newProject(t, "linked worktree")
|
|
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
|
|
request := Request{ProjectRoot: root, InstallationID: "worktree", Profile: "local", NonInteractive: true}
|
|
|
|
first, err := EnsureFiles(request, strings.NewReader(""), ioDiscard{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, err := os.ReadFile(first.EnvironmentPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := EnsureFiles(request, strings.NewReader(""), ioDiscard{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(second.Created) != 0 {
|
|
t.Fatalf("compatible rerun created %v, want no files", second.Created)
|
|
}
|
|
after, err := os.ReadFile(first.EnvironmentPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(before, after) {
|
|
t.Fatal("compatible environment was rewritten")
|
|
}
|
|
}
|
|
|
|
func TestEnsureFilesRefusesConflictingConfiguration(t *testing.T) {
|
|
root := newProject(t, "conflict")
|
|
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
|
|
directory := filepath.Join(root, "deploy", "existing")
|
|
if err := os.Mkdir(directory, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
descriptor := filepath.Join(directory, "thothii-installation.yaml")
|
|
if err := os.WriteFile(descriptor, []byte("profile: server\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err := EnsureFiles(Request{ProjectRoot: root, InstallationID: "existing", Profile: "local", NonInteractive: true}, strings.NewReader(""), ioDiscard{})
|
|
if err == nil || !strings.Contains(err.Error(), descriptor) || !strings.Contains(err.Error(), "different installation ID") {
|
|
t.Fatalf("EnsureFiles() error = %v, want exact file and corrective action", err)
|
|
}
|
|
if _, statErr := os.Stat(filepath.Join(directory, "operator.env")); !errors.Is(statErr, os.ErrNotExist) {
|
|
t.Fatalf("operator.env was created after conflict: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestEnsureFilesRemovesOwnFilesWhenAtomicWriteIsInterrupted(t *testing.T) {
|
|
root := newProject(t, "interrupted")
|
|
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
|
|
previous := atomicWriteNewFile
|
|
t.Cleanup(func() { atomicWriteNewFile = previous })
|
|
calls := 0
|
|
atomicWriteNewFile = func(path string, contents []byte, mode os.FileMode) error {
|
|
calls++
|
|
if calls == 2 {
|
|
return errors.New("interrupted write")
|
|
}
|
|
return previous(path, contents, mode)
|
|
}
|
|
|
|
_, err := EnsureFiles(Request{ProjectRoot: root, InstallationID: "interrupted", Profile: "local", NonInteractive: true}, strings.NewReader(""), ioDiscard{})
|
|
if err == nil || !strings.Contains(err.Error(), "interrupted write") {
|
|
t.Fatalf("EnsureFiles() error = %v, want interrupted write", err)
|
|
}
|
|
directory := filepath.Join(root, "deploy", "interrupted")
|
|
for _, name := range []string{"thothii-installation.yaml", "operator.env"} {
|
|
if _, statErr := os.Stat(filepath.Join(directory, name)); !errors.Is(statErr, os.ErrNotExist) {
|
|
t.Fatalf("%s remains after interrupted write: %v", name, statErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnsureFilesCreatesSecretTemplatesOnlyAfterExplicitConfirmation(t *testing.T) {
|
|
root := newProject(t, "secret prompt")
|
|
var output bytes.Buffer
|
|
input := strings.Join([]string{
|
|
"demo", "local", "", "", "https://git.example.invalid/workspaces.git", "main", "https", "", "", "", "", "yes",
|
|
}, "\n") + "\n"
|
|
result, err := EnsureFiles(Request{ProjectRoot: root}, strings.NewReader(input), &output)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(output.String(), "Create blank secret-file templates") {
|
|
t.Fatalf("prompt = %q, want explicit secret-template confirmation", output.String())
|
|
}
|
|
for _, path := range []string{
|
|
filepath.Join(root, "deploy", "demo", "secrets", "thothii.secrets"),
|
|
filepath.Join(root, "deploy", "demo", "secrets", "pi-auth.json"),
|
|
filepath.Join(root, "deploy", "demo", "secrets", "workspace-git-credentials"),
|
|
filepath.Join(root, "deploy", "demo", "secrets", "workspace-git-ca.pem"),
|
|
} {
|
|
info, statErr := os.Stat(path)
|
|
if statErr != nil {
|
|
t.Fatalf("secret template %s: %v", path, statErr)
|
|
}
|
|
if info.Mode().Perm()&0o077 != 0 {
|
|
t.Errorf("secret template %s has permissions %o, want owner-only", path, info.Mode().Perm())
|
|
}
|
|
}
|
|
if _, err := os.Stat(result.DescriptorPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestEnsureFilesRequiresExplicitNonInteractiveAnswers(t *testing.T) {
|
|
root := newProject(t, "noninteractive")
|
|
_, err := EnsureFiles(Request{ProjectRoot: root, InstallationID: "ci", Profile: "local", NonInteractive: true}, strings.NewReader(""), ioDiscard{})
|
|
if err == nil || !strings.Contains(err.Error(), "THT_SETUP_WORKSPACE_REMOTE") {
|
|
t.Fatalf("EnsureFiles() error = %v, want non-interactive environment guidance", err)
|
|
}
|
|
}
|
|
|
|
func TestEnsureFilesIncludesServerStorageLocations(t *testing.T) {
|
|
root := newProject(t, "server profile")
|
|
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
|
|
result, err := EnsureFiles(Request{ProjectRoot: root, InstallationID: "server", Profile: "server", NonInteractive: true}, strings.NewReader(""), ioDiscard{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
contents, err := os.ReadFile(result.EnvironmentPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, name := range []string{"THT_DATA_ROOT", "THT_PI_STATE_ROOT", "THT_WORKSPACE_REGISTRY_ROOT", "THT_BACKUP_ROOT"} {
|
|
if !strings.Contains(string(contents), name+"=") {
|
|
t.Errorf("server configuration is missing %s: %s", name, contents)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnsureFilesProjectedServerRefusesBeforeAnyWriteWhenNotRoot(t *testing.T) {
|
|
root := newProject(t, "projected server non-root")
|
|
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
|
|
previous := effectiveUID
|
|
effectiveUID = func() int { return 1000 }
|
|
t.Cleanup(func() { effectiveUID = previous })
|
|
|
|
_, err := EnsureFiles(Request{
|
|
ProjectRoot: root, InstallationID: "server", Profile: "server", NonInteractive: true,
|
|
}, strings.NewReader(""), ioDiscard{})
|
|
if err == nil || !strings.Contains(err.Error(), "root") {
|
|
t.Fatalf("EnsureFiles() error = %v, want root refusal", err)
|
|
}
|
|
if _, statErr := os.Lstat(filepath.Join(root, "deploy", "server")); !errors.Is(statErr, os.ErrNotExist) {
|
|
t.Fatalf("projected server path was created before root refusal: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestEnsureFilesRejectsUnsafeServiceEndpointsBeforeWritingConfiguration(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name, environment, value string
|
|
}{
|
|
{"DWH user info", "THT_SETUP_DWH_REST_URL", "https://operator@dwh.example.invalid/api"},
|
|
{"DWH password", "THT_SETUP_DWH_REST_URL", "https://operator:password@dwh.example.invalid/api"},
|
|
{"DWH query", "THT_SETUP_DWH_REST_URL", "https://dwh.example.invalid/api?token=secret"},
|
|
{"LLM fragment", "THT_SETUP_LLM_URL", "https://llm.example.invalid/api#secret"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
root := newProject(t, "unsafe endpoint")
|
|
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
|
|
t.Setenv(test.environment, test.value)
|
|
id := "rejected"
|
|
_, err := EnsureFiles(Request{ProjectRoot: root, InstallationID: id, Profile: "local", NonInteractive: true}, strings.NewReader(""), ioDiscard{})
|
|
if err == nil || !strings.Contains(err.Error(), "endpoint") {
|
|
t.Fatalf("EnsureFiles() error = %v, want endpoint rejection", err)
|
|
}
|
|
assertNoConfigurationOutput(t, root, id)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnsureFilesRejectsUnsafeExistingSecretFiles(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
mutate func(t *testing.T, paths *secretPaths)
|
|
}{
|
|
{
|
|
name: "directory",
|
|
mutate: func(t *testing.T, paths *secretPaths) {
|
|
paths.piAuth = filepath.Dir(paths.piAuth)
|
|
},
|
|
},
|
|
{
|
|
name: "symlink",
|
|
mutate: func(t *testing.T, paths *secretPaths) {
|
|
link := paths.piAuth + ".link"
|
|
testsupport.SymlinkOrSkip(t, paths.piAuth, link)
|
|
paths.piAuth = link
|
|
},
|
|
},
|
|
{
|
|
name: "path beneath a symlinked directory",
|
|
mutate: func(t *testing.T, paths *secretPaths) {
|
|
link := filepath.Join(filepath.Dir(paths.piAuth), "parent-link")
|
|
testsupport.SymlinkOrSkip(t, filepath.Dir(paths.piAuth), link)
|
|
paths.piAuth = filepath.Join(link, filepath.Base(paths.piAuth))
|
|
},
|
|
},
|
|
{
|
|
name: "unreadable file",
|
|
mutate: func(t *testing.T, paths *secretPaths) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("POSIX read permissions are not portable to Windows")
|
|
}
|
|
if err := os.Chmod(paths.piAuth, 0o000); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.Chmod(paths.piAuth, 0o600) })
|
|
file, err := os.Open(paths.piAuth)
|
|
if err == nil {
|
|
_ = file.Close()
|
|
t.Skip("effective user can read mode 000 files")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "stat error",
|
|
mutate: func(t *testing.T, paths *secretPaths) {
|
|
paths.knownHosts = filepath.Join(paths.sshKey, "not-a-directory")
|
|
},
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
root := newProject(t, "unsafe secret")
|
|
paths := newExternalSecrets(t, root)
|
|
test.mutate(t, &paths)
|
|
setNonInteractiveAnswers(t, paths)
|
|
_, err := EnsureFiles(Request{ProjectRoot: root, InstallationID: "rejected", Profile: "local", NonInteractive: true}, strings.NewReader(""), ioDiscard{})
|
|
if err == nil || (!strings.Contains(err.Error(), "readable regular file") && !strings.Contains(err.Error(), "could not be inspected")) {
|
|
t.Fatalf("EnsureFiles() error = %v, want unsafe secret-file rejection", err)
|
|
}
|
|
assertNoConfigurationOutput(t, root, "rejected")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnsureFilesRejectsUnwritableDeploymentDirectory(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("POSIX directory write permissions are not portable to Windows")
|
|
}
|
|
root := newProject(t, "unwritable deployment")
|
|
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
|
|
deploy := filepath.Join(root, "deploy")
|
|
if err := os.Chmod(deploy, 0o500); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.Chmod(deploy, 0o700) })
|
|
_, err := EnsureFiles(Request{ProjectRoot: root, InstallationID: "rejected", Profile: "local", NonInteractive: true}, strings.NewReader(""), ioDiscard{})
|
|
if err == nil {
|
|
t.Skip("effective user can create files in a mode 0500 directory")
|
|
}
|
|
assertNoConfigurationOutput(t, root, "rejected")
|
|
}
|
|
|
|
func TestEnsureFilesIgnoresGeneratedFilesInActualLinkedGitWorktree(t *testing.T) {
|
|
if _, err := exec.LookPath("git"); err != nil {
|
|
t.Skip("git is unavailable")
|
|
}
|
|
worktree := newLinkedGitWorktree(t)
|
|
secrets := newExternalSecretsAt(t, filepath.Join(t.TempDir(), "external secrets"))
|
|
setNonInteractiveAnswers(t, secrets)
|
|
result, err := EnsureFiles(Request{ProjectRoot: worktree, InstallationID: "linked", Profile: "local", NonInteractive: true}, strings.NewReader(""), ioDiscard{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range []string{result.DescriptorPath, result.EnvironmentPath} {
|
|
gitRun(t, worktree, "check-ignore", "--quiet", path)
|
|
if err := exec.Command("git", "-C", worktree, "ls-files", "--error-unmatch", "--", path).Run(); err == nil {
|
|
t.Fatalf("generated path %s was added to the Git index", path)
|
|
}
|
|
}
|
|
status := gitOutput(t, worktree, "status", "--porcelain", "--untracked-files=all")
|
|
if status != "" {
|
|
t.Fatalf("generated configuration appears in linked-worktree Git status: %q", status)
|
|
}
|
|
}
|
|
|
|
func assertNoConfigurationOutput(t *testing.T, root, id string) {
|
|
t.Helper()
|
|
directory := filepath.Join(root, "deploy", id)
|
|
for _, name := range []string{"thothii-installation.yaml", "operator.env"} {
|
|
if _, err := os.Lstat(filepath.Join(directory, name)); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("unexpected configuration output %s: %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newLinkedGitWorktree(t *testing.T) string {
|
|
t.Helper()
|
|
repository := newProject(t, "source repository")
|
|
if err := os.WriteFile(filepath.Join(repository, ".gitignore"), []byte("deploy/*/thothii-installation.yaml\ndeploy/*/operator.env\ndeploy/*/secrets/*\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gitRun(t, repository, "init")
|
|
gitRun(t, repository, "config", "user.email", "tests@example.invalid")
|
|
gitRun(t, repository, "config", "user.name", "ThothII tests")
|
|
gitRun(t, repository, "add", ".")
|
|
gitRun(t, repository, "commit", "-m", "fixture")
|
|
worktree := filepath.Join(t.TempDir(), "linked worktree")
|
|
gitRun(t, repository, "worktree", "add", "-b", "linked-fixture", worktree)
|
|
canonical, err := filepath.EvalSymlinks(worktree)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return canonical
|
|
}
|
|
|
|
func gitRun(t *testing.T, directory string, args ...string) {
|
|
t.Helper()
|
|
command := exec.Command("git", append([]string{"-C", directory}, args...)...)
|
|
if output, err := command.CombinedOutput(); err != nil {
|
|
t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, output)
|
|
}
|
|
}
|
|
|
|
func gitOutput(t *testing.T, directory string, args ...string) string {
|
|
t.Helper()
|
|
command := exec.Command("git", append([]string{"-C", directory}, args...)...)
|
|
output, err := command.Output()
|
|
if err != nil {
|
|
t.Fatalf("git %s: %v", strings.Join(args, " "), err)
|
|
}
|
|
return string(output)
|
|
}
|
|
|
|
func newProject(t *testing.T, name string) string {
|
|
t.Helper()
|
|
root := filepath.Join(t.TempDir(), name)
|
|
for _, path := range []string{
|
|
filepath.Join(root, "deploy", "env"),
|
|
filepath.Join(root, "deploy"),
|
|
filepath.Join(root, ".git"),
|
|
filepath.Join(root, "backend"),
|
|
filepath.Join(root, "frontend"),
|
|
filepath.Join(root, "harness"),
|
|
filepath.Join(root, "tools"),
|
|
} {
|
|
if err := os.MkdirAll(path, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for path, contents := range map[string]string{
|
|
filepath.Join(root, "compose.yaml"): "services: {}\n",
|
|
filepath.Join(root, "deploy", "compose.local.yaml"): "services: {}\n",
|
|
filepath.Join(root, "deploy", "compose.git-ssh.yaml"): "services: {}\n",
|
|
filepath.Join(root, "deploy", "compose.git-https.yaml"): "services: {}\n",
|
|
filepath.Join(root, "deploy", "env", "local.env.example"): "tracked example\n",
|
|
} {
|
|
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
canonical, err := filepath.EvalSymlinks(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return canonical
|
|
}
|
|
|
|
type secretPaths struct {
|
|
secrets, piAuth, sshKey, knownHosts string
|
|
}
|
|
|
|
func newExternalSecrets(t *testing.T, root string) secretPaths {
|
|
t.Helper()
|
|
return newExternalSecretsAt(t, filepath.Join(root, "external secrets"))
|
|
}
|
|
|
|
func newExternalSecretsAt(t *testing.T, directory string) secretPaths {
|
|
t.Helper()
|
|
if err := os.Mkdir(directory, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
canonical, err := filepath.EvalSymlinks(directory)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
directory = canonical
|
|
paths := secretPaths{
|
|
secrets: filepath.Join(directory, "thothii.secrets"), piAuth: filepath.Join(directory, "pi-auth.json"),
|
|
sshKey: filepath.Join(directory, "git-key"), knownHosts: filepath.Join(directory, "known-hosts"),
|
|
}
|
|
for path, contents := range map[string]string{
|
|
paths.secrets: "super-secret-value\n", paths.piAuth: "pi-secret-value\n", paths.sshKey: "private-key-value\n", paths.knownHosts: "git.example.invalid ssh-ed25519 AAAA\n",
|
|
} {
|
|
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func setNonInteractiveAnswers(t *testing.T, paths secretPaths) {
|
|
t.Helper()
|
|
for name, value := range map[string]string{
|
|
"THT_SETUP_WORKSPACE_REMOTE": "git@git.example.invalid:team/workspaces.git",
|
|
"THT_SETUP_WORKSPACE_BRANCH": "main",
|
|
"THT_SETUP_WORKSPACE_ACCESS": "ssh",
|
|
"THT_SETUP_SECRETS_FILE": paths.secrets,
|
|
"THT_SETUP_PI_AUTH_FILE": paths.piAuth,
|
|
"THT_SETUP_GIT_SSH_KEY_FILE": paths.sshKey,
|
|
"THT_SETUP_GIT_KNOWN_HOSTS_FILE": paths.knownHosts,
|
|
"THT_SETUP_DWH_REST_URL": "https://dwh.example.invalid",
|
|
"THT_SETUP_LLM_URL": "https://llm.example.invalid",
|
|
} {
|
|
t.Setenv(name, value)
|
|
}
|
|
}
|
|
|
|
type ioDiscard struct{}
|
|
|
|
func (ioDiscard) Write(value []byte) (int, error) { return len(value), nil }
|