feat: declare workspace repository in installation config
This commit is contained in:
@@ -9,6 +9,7 @@ const execFileAsync = promisify(execFile);
|
|||||||
|
|
||||||
export interface GitStatus {
|
export interface GitStatus {
|
||||||
branch: string;
|
branch: string;
|
||||||
|
repository?: WorkspaceRepositoryIdentity;
|
||||||
head?: string;
|
head?: string;
|
||||||
ahead: number;
|
ahead: number;
|
||||||
behind: number;
|
behind: number;
|
||||||
@@ -16,6 +17,12 @@ export interface GitStatus {
|
|||||||
lastError?: WorkspaceErrorCode;
|
lastError?: WorkspaceErrorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkspaceRepositoryIdentity {
|
||||||
|
host: string;
|
||||||
|
repository: string;
|
||||||
|
transport: "https" | "ssh" | "local";
|
||||||
|
}
|
||||||
|
|
||||||
export interface EvidenceTreeObject {
|
export interface EvidenceTreeObject {
|
||||||
mode: "100644" | "100755";
|
mode: "100644" | "100755";
|
||||||
oid: string;
|
oid: string;
|
||||||
@@ -29,6 +36,62 @@ export class WorkspaceRegistryError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function invalidRemote(): never {
|
||||||
|
throw new WorkspaceRegistryError("workspace_invalid", "Workspace Git remote is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeRepositoryPath(raw: string): string {
|
||||||
|
let decoded: string;
|
||||||
|
try {
|
||||||
|
decoded = decodeURIComponent(raw).replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/, "");
|
||||||
|
} catch {
|
||||||
|
return invalidRemote();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
decoded.length === 0
|
||||||
|
|| decoded.includes("\\")
|
||||||
|
|| decoded.split("/").some((part) => part === "" || part === "." || part === "..")
|
||||||
|
|| /[\p{Cc}\s?#]/u.test(decoded)
|
||||||
|
) return invalidRemote();
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert a configured remote to the only repository identity safe for API/UI responses. */
|
||||||
|
export function normalizeRepositoryIdentity(remote: string): WorkspaceRepositoryIdentity {
|
||||||
|
if (remote.length === 0 || remote.trim() !== remote || remote.includes("\0")) return invalidRemote();
|
||||||
|
if (isAbsolute(remote) || remote.startsWith("file://")) {
|
||||||
|
return { host: "local", repository: "configured-repository", transport: "local" };
|
||||||
|
}
|
||||||
|
const scp = /^git@([^:/\s]+):(.+)$/.exec(remote);
|
||||||
|
if (scp) {
|
||||||
|
return { host: scp[1].toLowerCase(), repository: safeRepositoryPath(scp[2]), transport: "ssh" };
|
||||||
|
}
|
||||||
|
let parsed: URL;
|
||||||
|
try {
|
||||||
|
parsed = new URL(remote);
|
||||||
|
} catch {
|
||||||
|
return invalidRemote();
|
||||||
|
}
|
||||||
|
if (parsed.search || parsed.hash || !parsed.hostname || parsed.port) return invalidRemote();
|
||||||
|
if (parsed.protocol === "https:") {
|
||||||
|
if (parsed.username || parsed.password) return invalidRemote();
|
||||||
|
return {
|
||||||
|
host: parsed.hostname.toLowerCase(),
|
||||||
|
repository: safeRepositoryPath(parsed.pathname),
|
||||||
|
transport: "https",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (parsed.protocol === "ssh:") {
|
||||||
|
if (parsed.password || (parsed.username !== "" && parsed.username !== "git")) return invalidRemote();
|
||||||
|
return {
|
||||||
|
host: parsed.hostname.toLowerCase(),
|
||||||
|
repository: safeRepositoryPath(parsed.pathname),
|
||||||
|
transport: "ssh",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return invalidRemote();
|
||||||
|
}
|
||||||
|
|
||||||
function isMissing(path: string): boolean {
|
function isMissing(path: string): boolean {
|
||||||
try {
|
try {
|
||||||
lstatSync(path);
|
lstatSync(path);
|
||||||
@@ -81,6 +144,7 @@ export class GitWorkspaceRepository {
|
|||||||
readonly statePath: string;
|
readonly statePath: string;
|
||||||
readonly locksPath: string;
|
readonly locksPath: string;
|
||||||
private readonly hooksPath: string;
|
private readonly hooksPath: string;
|
||||||
|
private readonly identity?: WorkspaceRepositoryIdentity;
|
||||||
|
|
||||||
constructor(private readonly config: WorkspaceRegistryConfig) {
|
constructor(private readonly config: WorkspaceRegistryConfig) {
|
||||||
if (!isAbsolute(config.root)) {
|
if (!isAbsolute(config.root)) {
|
||||||
@@ -92,6 +156,9 @@ export class GitWorkspaceRepository {
|
|||||||
this.statePath = join(this.root, "state");
|
this.statePath = join(this.root, "state");
|
||||||
this.locksPath = join(this.root, "locks");
|
this.locksPath = join(this.root, "locks");
|
||||||
this.hooksPath = join(this.locksPath, "empty-hooks");
|
this.hooksPath = join(this.locksPath, "empty-hooks");
|
||||||
|
this.identity = config.remoteUrl === undefined
|
||||||
|
? undefined
|
||||||
|
: normalizeRepositoryIdentity(config.remoteUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureLayout(): Promise<void> {
|
async ensureLayout(): Promise<void> {
|
||||||
@@ -134,6 +201,7 @@ export class GitWorkspaceRepository {
|
|||||||
const [ahead = "0", behind = "0"] = tracking ? tracking.trim().split(/\s+/) : [];
|
const [ahead = "0", behind = "0"] = tracking ? tracking.trim().split(/\s+/) : [];
|
||||||
return {
|
return {
|
||||||
branch: this.config.branch,
|
branch: this.config.branch,
|
||||||
|
...(this.identity ? { repository: this.identity } : {}),
|
||||||
head,
|
head,
|
||||||
ahead: Number(ahead),
|
ahead: Number(ahead),
|
||||||
behind: Number(behind),
|
behind: Number(behind),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
GitWorkspaceRepository,
|
GitWorkspaceRepository,
|
||||||
WorkspaceRegistryError,
|
WorkspaceRegistryError,
|
||||||
WorkspaceRepositoryLock,
|
WorkspaceRepositoryLock,
|
||||||
|
normalizeRepositoryIdentity,
|
||||||
type GitStatus,
|
type GitStatus,
|
||||||
} from "./git-repository.js";
|
} from "./git-repository.js";
|
||||||
import {
|
import {
|
||||||
@@ -492,6 +493,9 @@ export class WorkspaceRegistry {
|
|||||||
if (!active) throw safeError;
|
if (!active) throw safeError;
|
||||||
return {
|
return {
|
||||||
branch: this.config.branch,
|
branch: this.config.branch,
|
||||||
|
...(this.config.remoteUrl
|
||||||
|
? { repository: normalizeRepositoryIdentity(this.config.remoteUrl) }
|
||||||
|
: {}),
|
||||||
head: active.head,
|
head: active.head,
|
||||||
ahead: 0,
|
ahead: 0,
|
||||||
behind: 0,
|
behind: 0,
|
||||||
|
|||||||
@@ -632,7 +632,11 @@ test("normalizes historical operational state during offline fallback after rest
|
|||||||
rmSync(remote.remote, { recursive: true, force: true });
|
rmSync(remote.remote, { recursive: true, force: true });
|
||||||
|
|
||||||
const restored = new WorkspaceRegistry(config(root, remote.remote));
|
const restored = new WorkspaceRegistry(config(root, remote.remote));
|
||||||
await expect(restored.pull()).resolves.toMatchObject({ degraded: true, head: remote.initialCommit });
|
await expect(restored.pull()).resolves.toMatchObject({
|
||||||
|
degraded: true,
|
||||||
|
head: remote.initialCommit,
|
||||||
|
repository: { host: "local", repository: "configured-repository", transport: "local" },
|
||||||
|
});
|
||||||
const listed = await restored.list();
|
const listed = await restored.list();
|
||||||
const read = await restored.read("psd-clinical");
|
const read = await restored.read("psd-clinical");
|
||||||
expect(listed[0]).not.toHaveProperty("state");
|
expect(listed[0]).not.toHaveProperty("state");
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import { afterEach, expect, test } from "vitest";
|
import { afterEach, expect, test } from "vitest";
|
||||||
import { GitWorkspaceRepository, WorkspaceRepositoryLock } from "../src/workspaces/git-repository.js";
|
import {
|
||||||
|
GitWorkspaceRepository,
|
||||||
|
WorkspaceRepositoryLock,
|
||||||
|
normalizeRepositoryIdentity,
|
||||||
|
} from "../src/workspaces/git-repository.js";
|
||||||
import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js";
|
import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js";
|
||||||
|
|
||||||
const validYaml = `workspace:
|
const validYaml = `workspace:
|
||||||
@@ -102,6 +106,28 @@ test("does not expose repository mutation or publication operations", async () =
|
|||||||
expect(repository).not.toHaveProperty("commitAndPush");
|
expect(repository).not.toHaveProperty("commitAndPush");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
["https://github.com/aritmolab/workspaces.git", {
|
||||||
|
host: "github.com", repository: "aritmolab/workspaces", transport: "https",
|
||||||
|
}],
|
||||||
|
["ssh://git@gitlab.example.org/clinical/workspaces.git", {
|
||||||
|
host: "gitlab.example.org", repository: "clinical/workspaces", transport: "ssh",
|
||||||
|
}],
|
||||||
|
["git@gitea.example.org:clinical/workspaces.git", {
|
||||||
|
host: "gitea.example.org", repository: "clinical/workspaces", transport: "ssh",
|
||||||
|
}],
|
||||||
|
] as const)("normalizes the safe repository identity for %s", (remote, expected) => {
|
||||||
|
expect(normalizeRepositoryIdentity(remote)).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
"https://user:secret@github.com/aritmolab/workspaces.git",
|
||||||
|
"https://github.com/aritmolab/workspaces.git?token=secret",
|
||||||
|
"ssh://git:secret@gitlab.example.org/clinical/workspaces.git",
|
||||||
|
])("rejects a remote that could expose embedded credentials: %s", (remote) => {
|
||||||
|
expect(() => normalizeRepositoryIdentity(remote)).toThrow("Workspace Git remote is invalid");
|
||||||
|
});
|
||||||
|
|
||||||
test("bootstraps a persistent checkout from a local bare repository", async () => {
|
test("bootstraps a persistent checkout from a local bare repository", async () => {
|
||||||
const fixture = await temporaryRemote();
|
const fixture = await temporaryRemote();
|
||||||
const repository = new GitWorkspaceRepository(config(join(fixture.root, "registry"), fixture.remote));
|
const repository = new GitWorkspaceRepository(config(join(fixture.root, "registry"), fixture.remote));
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
profile: local
|
profile: local
|
||||||
projectDirectory: "<abs>/projects/ThothII"
|
projectDirectory: "<abs>/projects/ThothII"
|
||||||
envFile: "<abs>/projects/ThothII/deploy/psd/operator.env"
|
envFile: "<abs>/projects/ThothII/deploy/psd/operator.env"
|
||||||
|
workspaceRepository:
|
||||||
|
remote: git@github.com:mptyl/tht-workspace-psd.git
|
||||||
|
branch: main
|
||||||
|
access: ssh
|
||||||
overrides:
|
overrides:
|
||||||
- "<abs>/projects/ThothII/deploy/compose.git-https.yaml"
|
- "<abs>/projects/ThothII/deploy/compose.git-ssh.yaml"
|
||||||
- "<abs>/projects/ThothII/deploy/psd/connector-secrets.yaml"
|
- "<abs>/projects/ThothII/deploy/psd/connector-secrets.yaml"
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
profile: local
|
profile: local
|
||||||
projectDirectory: "/absolute/path/to/ThothII"
|
projectDirectory: "/absolute/path/to/ThothII"
|
||||||
envFile: "/absolute/path/to/ThothII/deploy/env/local.env"
|
envFile: "/absolute/path/to/ThothII/deploy/env/local.env"
|
||||||
|
workspaceRepository:
|
||||||
|
remote: git@git.example.com:organization/workspaces.git
|
||||||
|
branch: main
|
||||||
|
access: ssh
|
||||||
overrides:
|
overrides:
|
||||||
- "/absolute/path/to/ThothII/deploy/compose.git-ssh.yaml"
|
- "/absolute/path/to/ThothII/deploy/compose.git-ssh.yaml"
|
||||||
- "/absolute/path/to/thothii-operator/connector-secrets.local.yaml"
|
- "/absolute/path/to/thothii-operator/connector-secrets.local.yaml"
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
profile: server
|
profile: server
|
||||||
projectDirectory: "/absolute/path/to/ThothII"
|
projectDirectory: "/absolute/path/to/ThothII"
|
||||||
envFile: "/absolute/path/to/thothii-server-operator/server.env"
|
envFile: "/absolute/path/to/thothii-server-operator/server.env"
|
||||||
|
workspaceRepository:
|
||||||
|
remote: git@git.example.com:organization/workspaces.git
|
||||||
|
branch: main
|
||||||
|
access: ssh
|
||||||
overrides:
|
overrides:
|
||||||
- "/absolute/path/to/ThothII/deploy/compose.session-server.yaml.example"
|
- "/absolute/path/to/ThothII/deploy/compose.session-server.yaml.example"
|
||||||
- "/absolute/path/to/ThothII/deploy/compose.git-ssh.yaml"
|
- "/absolute/path/to/ThothII/deploy/compose.git-ssh.yaml"
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -31,9 +33,23 @@ type descriptor struct {
|
|||||||
Profile string `yaml:"profile"`
|
Profile string `yaml:"profile"`
|
||||||
ProjectDirectory string `yaml:"projectDirectory"`
|
ProjectDirectory string `yaml:"projectDirectory"`
|
||||||
EnvFile string `yaml:"envFile"`
|
EnvFile string `yaml:"envFile"`
|
||||||
|
WorkspaceRepository workspaceRepositoryDescriptor `yaml:"workspaceRepository"`
|
||||||
Overrides []string `yaml:"overrides"`
|
Overrides []string `yaml:"overrides"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type workspaceRepositoryDescriptor struct {
|
||||||
|
Remote string `yaml:"remote"`
|
||||||
|
Branch string `yaml:"branch"`
|
||||||
|
Access string `yaml:"access"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkspaceRepository is the non-secret Git source identity declared by one installation.
|
||||||
|
type WorkspaceRepository struct {
|
||||||
|
Remote string
|
||||||
|
Branch string
|
||||||
|
Access string
|
||||||
|
}
|
||||||
|
|
||||||
// Installation is a validated local Compose installation. It intentionally contains paths, not
|
// Installation is a validated local Compose installation. It intentionally contains paths, not
|
||||||
// environment values or secret content.
|
// environment values or secret content.
|
||||||
type Installation struct {
|
type Installation struct {
|
||||||
@@ -41,6 +57,7 @@ type Installation struct {
|
|||||||
Profile string
|
Profile string
|
||||||
ProjectDirectory string
|
ProjectDirectory string
|
||||||
EnvFile string
|
EnvFile string
|
||||||
|
WorkspaceRepository WorkspaceRepository
|
||||||
Overrides []string
|
Overrides []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +105,11 @@ func Load(path string) (Installation, error) {
|
|||||||
Profile: raw.Profile,
|
Profile: raw.Profile,
|
||||||
ProjectDirectory: filepath.Clean(raw.ProjectDirectory),
|
ProjectDirectory: filepath.Clean(raw.ProjectDirectory),
|
||||||
EnvFile: filepath.Clean(raw.EnvFile),
|
EnvFile: filepath.Clean(raw.EnvFile),
|
||||||
|
WorkspaceRepository: WorkspaceRepository{
|
||||||
|
Remote: raw.WorkspaceRepository.Remote,
|
||||||
|
Branch: raw.WorkspaceRepository.Branch,
|
||||||
|
Access: raw.WorkspaceRepository.Access,
|
||||||
|
},
|
||||||
Overrides: make([]string, 0, len(raw.Overrides)),
|
Overrides: make([]string, 0, len(raw.Overrides)),
|
||||||
}
|
}
|
||||||
for _, override := range raw.Overrides {
|
for _, override := range raw.Overrides {
|
||||||
@@ -101,6 +123,9 @@ func Load(path string) (Installation, error) {
|
|||||||
return Installation{}, err
|
return Installation{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := installation.validateWorkspaceRepository(); err != nil {
|
||||||
|
return Installation{}, err
|
||||||
|
}
|
||||||
if info, err := os.Lstat(installation.CurrentImageOverridePath()); err == nil {
|
if info, err := os.Lstat(installation.CurrentImageOverridePath()); err == nil {
|
||||||
if !info.Mode().IsRegular() {
|
if !info.Mode().IsRegular() {
|
||||||
return Installation{}, errors.New("installation current-image override must be a regular file")
|
return Installation{}, errors.New("installation current-image override must be a regular file")
|
||||||
@@ -111,6 +136,87 @@ func Load(path string) (Installation, error) {
|
|||||||
return installation, nil
|
return installation, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var safeGitBranch = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]*$`)
|
||||||
|
var scpSSHRemote = regexp.MustCompile(`^git@[^:/\s]+:[^\s]+$`)
|
||||||
|
|
||||||
|
func (i Installation) validateWorkspaceRepository() error {
|
||||||
|
gitAccess := ""
|
||||||
|
for _, override := range i.Overrides {
|
||||||
|
switch filepath.Base(override) {
|
||||||
|
case "compose.git-ssh.yaml":
|
||||||
|
if gitAccess != "" {
|
||||||
|
return errors.New("installation must select exactly one Git transport override")
|
||||||
|
}
|
||||||
|
gitAccess = "ssh"
|
||||||
|
case "compose.git-https.yaml":
|
||||||
|
if gitAccess != "" {
|
||||||
|
return errors.New("installation must select exactly one Git transport override")
|
||||||
|
}
|
||||||
|
gitAccess = "https"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
declared := i.WorkspaceRepository
|
||||||
|
if gitAccess == "" {
|
||||||
|
if declared.Remote != "" || declared.Branch != "" || declared.Access != "" {
|
||||||
|
return errors.New("workspaceRepository requires one Git transport override")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if declared.Remote == "" || declared.Branch == "" || declared.Access == "" {
|
||||||
|
return errors.New("workspaceRepository is required for a Git installation")
|
||||||
|
}
|
||||||
|
if declared.Access != gitAccess {
|
||||||
|
return errors.New("workspaceRepository access does not match the Git transport override")
|
||||||
|
}
|
||||||
|
if !safeGitBranch.MatchString(declared.Branch) || strings.Contains(declared.Branch, "..") ||
|
||||||
|
strings.Contains(declared.Branch, "@{") || strings.HasPrefix(declared.Branch, "-") ||
|
||||||
|
strings.HasSuffix(declared.Branch, ".lock") {
|
||||||
|
return errors.New("workspaceRepository branch is invalid")
|
||||||
|
}
|
||||||
|
if err := validateRepositoryRemote(declared.Remote, declared.Access); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values, err := i.environmentValues()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if values["THT_WORKSPACE_GIT_REMOTE"] != declared.Remote ||
|
||||||
|
values["THT_WORKSPACE_GIT_BRANCH"] != declared.Branch {
|
||||||
|
return errors.New("workspaceRepository does not match the installation environment")
|
||||||
|
}
|
||||||
|
required := []string{"THT_WORKSPACE_GIT_CREDENTIALS_FILE", "THT_WORKSPACE_GIT_CA_FILE"}
|
||||||
|
if gitAccess == "ssh" {
|
||||||
|
required = []string{"THT_WORKSPACE_GIT_SSH_KEY_FILE", "THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE"}
|
||||||
|
}
|
||||||
|
for _, name := range required {
|
||||||
|
if err := requireRegularFile(values[name], "workspaceRepository credential"); err != nil {
|
||||||
|
return errors.New("workspaceRepository credentials are unavailable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRepositoryRemote(remote, access string) error {
|
||||||
|
if remote == "" || strings.TrimSpace(remote) != remote || strings.ContainsRune(remote, '\x00') {
|
||||||
|
return errors.New("workspaceRepository remote is invalid")
|
||||||
|
}
|
||||||
|
if access == "ssh" && scpSSHRemote.MatchString(remote) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parsed, err := url.Parse(remote)
|
||||||
|
if err != nil || parsed.Hostname() == "" || parsed.RawQuery != "" || parsed.Fragment != "" ||
|
||||||
|
parsed.User != nil && access == "https" || parsed.User != nil && strings.Contains(parsed.User.String(), ":") {
|
||||||
|
return errors.New("workspaceRepository remote is invalid")
|
||||||
|
}
|
||||||
|
if access == "https" && parsed.Scheme != "https" {
|
||||||
|
return errors.New("workspaceRepository remote does not match HTTPS access")
|
||||||
|
}
|
||||||
|
if access == "ssh" && parsed.Scheme != "ssh" {
|
||||||
|
return errors.New("workspaceRepository remote does not match SSH access")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ComposeFiles returns the base file, selected profile file, and declared optional overrides in
|
// ComposeFiles returns the base file, selected profile file, and declared optional overrides in
|
||||||
// the exact order Compose applies them.
|
// the exact order Compose applies them.
|
||||||
func (i Installation) ComposeFiles() []string {
|
func (i Installation) ComposeFiles() []string {
|
||||||
|
|||||||
@@ -51,6 +51,67 @@ func TestLoadSelectsServerComposeFiles(t *testing.T) {
|
|||||||
assertStringsEqual(t, installation.ComposeFiles(), want)
|
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) {
|
func TestComposeArgsAutomaticallyIncludeTheInstallationCurrentImageOverride(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user