125 lines
4.2 KiB
Go
125 lines
4.2 KiB
Go
package model
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var idPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`)
|
|
|
|
func ValidateConfig(c Config) error {
|
|
if c.SchemaVersion != SchemaVersion {
|
|
return fmt.Errorf("unsupported schemaVersion %d", c.SchemaVersion)
|
|
}
|
|
repos := make(map[string]Repository, len(c.Repositories))
|
|
for _, repo := range c.Repositories {
|
|
if err := ValidateRepository(repo); err != nil {
|
|
return fmt.Errorf("repository %q: %w", repo.ID, err)
|
|
}
|
|
if _, exists := repos[repo.ID]; exists {
|
|
return fmt.Errorf("duplicate repository id %q", repo.ID)
|
|
}
|
|
repos[repo.ID] = repo
|
|
}
|
|
jobs := make(map[string]struct{}, len(c.Jobs))
|
|
repositoryOwners := make(map[string]string, len(c.Jobs))
|
|
for _, job := range c.Jobs {
|
|
if err := ValidateJob(job); err != nil {
|
|
return fmt.Errorf("job %q: %w", job.ID, err)
|
|
}
|
|
if _, exists := jobs[job.ID]; exists {
|
|
return fmt.Errorf("duplicate job id %q", job.ID)
|
|
}
|
|
jobs[job.ID] = struct{}{}
|
|
if _, exists := repos[job.RepositoryID]; !exists {
|
|
return fmt.Errorf("job %q references unknown repository %q", job.ID, job.RepositoryID)
|
|
}
|
|
if owner, exists := repositoryOwners[job.RepositoryID]; exists {
|
|
return fmt.Errorf("repository %q is already assigned to job %q", job.RepositoryID, owner)
|
|
}
|
|
repositoryOwners[job.RepositoryID] = job.ID
|
|
}
|
|
if !filepath.IsAbs(c.Settings.RestoreRoot) {
|
|
return errors.New("restoreRoot must be absolute")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateJob(j Job) error {
|
|
if j.SchemaVersion != SchemaVersion || !idPattern.MatchString(j.ID) {
|
|
return errors.New("invalid schemaVersion or id")
|
|
}
|
|
if strings.TrimSpace(j.Name) == "" || strings.TrimSpace(j.RepositoryID) == "" {
|
|
return errors.New("name and repositoryId are required")
|
|
}
|
|
switch j.Type {
|
|
case JobShare, JobAppdata, JobDocker, JobVM, JobFlash:
|
|
default:
|
|
return fmt.Errorf("unsupported job type %q", j.Type)
|
|
}
|
|
if len(j.Sources) == 0 && j.Type != JobFlash {
|
|
return errors.New("at least one source is required")
|
|
}
|
|
if j.Type == JobFlash && len(j.Sources) == 0 {
|
|
j.Sources = []Source{{Path: "/boot"}}
|
|
}
|
|
for _, source := range j.Sources {
|
|
if source.Path != "" && !filepath.IsAbs(source.Path) {
|
|
return fmt.Errorf("source path %q must be absolute", source.Path)
|
|
}
|
|
if source.Path == "" && source.WorkloadID == "" {
|
|
return errors.New("source requires path or workloadId")
|
|
}
|
|
}
|
|
if j.Compression != "auto" && j.Compression != "off" && j.Compression != "max" {
|
|
return errors.New("compression must be auto, off, or max")
|
|
}
|
|
if j.Consistency.Mode != "live" && j.Consistency.Mode != "stop" {
|
|
return errors.New("consistency mode must be live or stop")
|
|
}
|
|
if j.Retention.Daily < 0 || j.Retention.Weekly < 0 || j.Retention.Monthly < 0 {
|
|
return errors.New("retention values cannot be negative")
|
|
}
|
|
for _, exclude := range j.Excludes {
|
|
if strings.ContainsAny(exclude, "\x00\r\n") {
|
|
return errors.New("exclude contains forbidden control characters")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateRepository(r Repository) error {
|
|
if r.SchemaVersion != SchemaVersion || !idPattern.MatchString(r.ID) {
|
|
return errors.New("invalid schemaVersion or id")
|
|
}
|
|
if r.Name == "" || r.Location == "" || r.PasswordRef == "" {
|
|
return errors.New("name, location, and passwordRef are required")
|
|
}
|
|
if !idPattern.MatchString(r.PasswordRef) || (r.CredentialRef != "" && !idPattern.MatchString(r.CredentialRef)) {
|
|
return errors.New("secret references must be valid IDs")
|
|
}
|
|
switch r.Type {
|
|
case RepositoryLocal, RepositorySMB, RepositoryNFS, RepositorySFTP:
|
|
default:
|
|
return fmt.Errorf("unsupported repository type %q", r.Type)
|
|
}
|
|
if r.Type == RepositoryLocal && !filepath.IsAbs(r.Location) {
|
|
return errors.New("local repository location must be absolute")
|
|
}
|
|
if (r.Type == RepositorySMB || r.Type == RepositoryNFS) && r.Mount != nil && r.Mount.Managed {
|
|
if r.Mount.Remote == "" || !filepath.IsAbs(r.Mount.MountPoint) {
|
|
return errors.New("managed mount requires remote and absolute mountPoint")
|
|
}
|
|
}
|
|
if r.Type == RepositorySFTP && r.CredentialRef != "" {
|
|
knownHosts := r.Options["knownHostsPath"]
|
|
if !filepath.IsAbs(knownHosts) || strings.ContainsAny(knownHosts, " \t\r\n") {
|
|
return errors.New("SFTP key authentication requires an absolute knownHostsPath without whitespace")
|
|
}
|
|
}
|
|
return nil
|
|
}
|