feat: declare workspace repository in installation config
This commit is contained in:
@@ -7,8 +7,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -28,20 +30,35 @@ const maxSecretSources = 32
|
||||
var dotenvParseMu sync.Mutex
|
||||
|
||||
type descriptor struct {
|
||||
Profile string `yaml:"profile"`
|
||||
ProjectDirectory string `yaml:"projectDirectory"`
|
||||
EnvFile string `yaml:"envFile"`
|
||||
Overrides []string `yaml:"overrides"`
|
||||
Profile string `yaml:"profile"`
|
||||
ProjectDirectory string `yaml:"projectDirectory"`
|
||||
EnvFile string `yaml:"envFile"`
|
||||
WorkspaceRepository workspaceRepositoryDescriptor `yaml:"workspaceRepository"`
|
||||
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
|
||||
// environment values or secret content.
|
||||
type Installation struct {
|
||||
Path string
|
||||
Profile string
|
||||
ProjectDirectory string
|
||||
EnvFile string
|
||||
Overrides []string
|
||||
Path string
|
||||
Profile string
|
||||
ProjectDirectory string
|
||||
EnvFile string
|
||||
WorkspaceRepository WorkspaceRepository
|
||||
Overrides []string
|
||||
}
|
||||
|
||||
// Load reads and validates an installation descriptor at an absolute path.
|
||||
@@ -88,7 +105,12 @@ func Load(path string) (Installation, error) {
|
||||
Profile: raw.Profile,
|
||||
ProjectDirectory: filepath.Clean(raw.ProjectDirectory),
|
||||
EnvFile: filepath.Clean(raw.EnvFile),
|
||||
Overrides: make([]string, 0, len(raw.Overrides)),
|
||||
WorkspaceRepository: WorkspaceRepository{
|
||||
Remote: raw.WorkspaceRepository.Remote,
|
||||
Branch: raw.WorkspaceRepository.Branch,
|
||||
Access: raw.WorkspaceRepository.Access,
|
||||
},
|
||||
Overrides: make([]string, 0, len(raw.Overrides)),
|
||||
}
|
||||
for _, override := range raw.Overrides {
|
||||
if err := requireRegularFile(override, "override"); err != nil {
|
||||
@@ -101,6 +123,9 @@ func Load(path string) (Installation, error) {
|
||||
return Installation{}, err
|
||||
}
|
||||
}
|
||||
if err := installation.validateWorkspaceRepository(); err != nil {
|
||||
return Installation{}, err
|
||||
}
|
||||
if info, err := os.Lstat(installation.CurrentImageOverridePath()); err == nil {
|
||||
if !info.Mode().IsRegular() {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
// the exact order Compose applies them.
|
||||
func (i Installation) ComposeFiles() []string {
|
||||
|
||||
Reference in New Issue
Block a user