Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions backup/cmd/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ var runCmd = &cobra.Command{
Use: "run",
Short: "Execute the sync jobs",
Run: func(cmd *cobra.Command, args []string) {
cfg := loadResolvedConfig(configPath)
cfg := internal.LoadResolvedConfig(configPath)
executeSyncJobs(cfg, false)
},
}
Expand All @@ -61,7 +61,7 @@ var simulateCmd = &cobra.Command{
Use: "simulate",
Short: "Simulate the sync jobs",
Run: func(cmd *cobra.Command, args []string) {
cfg := loadResolvedConfig(configPath)
cfg := internal.LoadResolvedConfig(configPath)
executeSyncJobs(cfg, true)
},
}
Expand All @@ -70,7 +70,7 @@ var listCmd = &cobra.Command{
Use: "list",
Short: "List the commands that will be executed",
Run: func(cmd *cobra.Command, args []string) {
cfg := loadResolvedConfig(configPath)
cfg := internal.LoadResolvedConfig(configPath)
listCommands(cfg)
},
}
Expand Down
2 changes: 1 addition & 1 deletion backup/cmd/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var checkCmd = &cobra.Command{
Use: "check-coverage",
Short: "Check path coverage",
Run: func(cmd *cobra.Command, args []string) {
cfg := loadResolvedConfig(configPath)
cfg := internal.LoadResolvedConfig(configPath)
uncoveredPaths := internal.ListUncoveredPaths(AppFs, cfg)
fmt.Println("Uncovered paths:")
for _, path := range uncoveredPaths {
Expand Down
150 changes: 3 additions & 147 deletions backup/cmd/config.go
Original file line number Diff line number Diff line change
@@ -1,158 +1,14 @@
package cmd

import (
"backup-rsync/backup/internal"
"fmt"
"io"
"log"
"os"
"strings"

"path/filepath"

"backup-rsync/backup/internal"

"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

func loadConfig(reader io.Reader) (internal.Config, error) {
var cfg internal.Config
if err := yaml.NewDecoder(reader).Decode(&cfg); err != nil {
return internal.Config{}, err
}
return cfg, nil
}

func substituteVariables(input string, variables map[string]string) string {
for key, value := range variables {
placeholder := fmt.Sprintf("${%s}", key)
input = strings.ReplaceAll(input, placeholder, value)
}
return input
}

func resolveConfig(cfg internal.Config) internal.Config {
resolvedCfg := cfg
for i, job := range resolvedCfg.Jobs {
resolvedCfg.Jobs[i].Source = substituteVariables(job.Source, cfg.Variables)
resolvedCfg.Jobs[i].Target = substituteVariables(job.Target, cfg.Variables)
}
return resolvedCfg
}

func validateJobNames(jobs []internal.Job) error {
invalidNames := []string{}
nameSet := make(map[string]bool)

for _, job := range jobs {
if nameSet[job.Name] {
invalidNames = append(invalidNames, fmt.Sprintf("duplicate job name: %s", job.Name))
} else {
nameSet[job.Name] = true
}

for _, r := range job.Name {
if r > 127 || r == ' ' {
invalidNames = append(invalidNames, fmt.Sprintf("invalid characters in job name: %s", job.Name))
break
}
}
}

if len(invalidNames) > 0 {
return fmt.Errorf("job validation errors: %v", invalidNames)
}
return nil
}

func validatePath(jobPath string, paths []internal.Path, pathType string, jobName string) error {
for _, path := range paths {
if strings.HasPrefix(jobPath, path.Path) {
return nil
}
}
return fmt.Errorf("invalid %s path for job '%s': %s", pathType, jobName, jobPath)
}

func validatePaths(cfg internal.Config) error {
invalidPaths := []string{}

for _, job := range cfg.Jobs {
if err := validatePath(job.Source, cfg.Sources, "source", job.Name); err != nil {
invalidPaths = append(invalidPaths, err.Error())
}
if err := validatePath(job.Target, cfg.Targets, "target", job.Name); err != nil {
invalidPaths = append(invalidPaths, err.Error())
}
}

if len(invalidPaths) > 0 {
return fmt.Errorf("path validation errors: %v", invalidPaths)
}
return nil
}

func validateJobPaths(jobs []internal.Job, pathType string, getPath func(job internal.Job) string) error {
for i, job1 := range jobs {
for j, job2 := range jobs {
if i != j {
path1, path2 := internal.NormalizePath(getPath(job1)), internal.NormalizePath(getPath(job2))

// Check if path2 is part of job1's exclusions
excluded := false
if pathType == "source" {
for _, exclusion := range job2.Exclusions {
exclusionPath := internal.NormalizePath(filepath.Join(job2.Source, exclusion))
// log.Printf("job2: %s %s\n", job2.Name, exclusionPath)
if strings.HasPrefix(path1, exclusionPath) {
excluded = true
break
}
}
}

if !excluded && strings.HasPrefix(path1, path2) {
return fmt.Errorf("Job '%s' has a %s path overlapping with job '%s'", job1.Name, pathType, job2.Name)
}
}
}
}
return nil
}

func loadResolvedConfig(configPath string) internal.Config {
f, err := os.Open(configPath)
if err != nil {
log.Fatalf("Failed to open config: %v", err)
}
defer f.Close()

cfg, err := loadConfig(f)
if err != nil {
log.Fatalf("Failed to parse YAML: %v", err)
}

if err := validateJobNames(cfg.Jobs); err != nil {
log.Fatalf("Job validation failed: %v", err)
}

resolvedCfg := resolveConfig(cfg)

if err := validatePaths(resolvedCfg); err != nil {
log.Fatalf("Path validation failed: %v", err)
}

if err := validateJobPaths(resolvedCfg.Jobs, "source", func(job internal.Job) string { return job.Source }); err != nil {
log.Fatalf("Job source path validation failed: %v", err)
}

if err := validateJobPaths(resolvedCfg.Jobs, "target", func(job internal.Job) string { return job.Target }); err != nil {
log.Fatalf("Job target path validation failed: %v", err)
}

return resolvedCfg
}

// configCmd represents the config command
var configCmd = &cobra.Command{
Use: "config",
Expand All @@ -168,7 +24,7 @@ var showCmd = &cobra.Command{
Use: "show",
Short: "Show resolved configuration",
Run: func(cmd *cobra.Command, args []string) {
cfg := loadResolvedConfig(configPath)
cfg := internal.LoadResolvedConfig(configPath)
out, err := yaml.Marshal(cfg)
if err != nil {
log.Fatalf("Failed to marshal resolved configuration: %v", err)
Expand All @@ -182,7 +38,7 @@ var validateCmd = &cobra.Command{
Use: "validate",
Short: "Validate configuration",
Run: func(cmd *cobra.Command, args []string) {
loadResolvedConfig(configPath)
internal.LoadResolvedConfig(configPath)
fmt.Println("Configuration is valid.")
},
}
Expand Down
9 changes: 0 additions & 9 deletions backup/cmd/root_test.go

This file was deleted.

153 changes: 153 additions & 0 deletions backup/internal/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package internal

import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"

"gopkg.in/yaml.v3"
)

func LoadConfig(reader io.Reader) (Config, error) {
var cfg Config
if err := yaml.NewDecoder(reader).Decode(&cfg); err != nil {
return Config{}, err
}

// Defaults are handled in Job.UnmarshalYAML

return cfg, nil
}

func substituteVariables(input string, variables map[string]string) string {
for key, value := range variables {
placeholder := fmt.Sprintf("${%s}", key)
input = strings.ReplaceAll(input, placeholder, value)
}
return input
}

func resolveConfig(cfg Config) Config {
resolvedCfg := cfg
for i, job := range resolvedCfg.Jobs {
resolvedCfg.Jobs[i].Source = substituteVariables(job.Source, cfg.Variables)
resolvedCfg.Jobs[i].Target = substituteVariables(job.Target, cfg.Variables)
}
return resolvedCfg
}

func validateJobNames(jobs []Job) error {
invalidNames := []string{}
nameSet := make(map[string]bool)

for _, job := range jobs {
if nameSet[job.Name] {
invalidNames = append(invalidNames, fmt.Sprintf("duplicate job name: %s", job.Name))
} else {
nameSet[job.Name] = true
}

for _, r := range job.Name {
if r > 127 || r == ' ' {
invalidNames = append(invalidNames, fmt.Sprintf("invalid characters in job name: %s", job.Name))
break
}
}
}

if len(invalidNames) > 0 {
return fmt.Errorf("job validation errors: %v", invalidNames)
}
return nil
}

func validatePath(jobPath string, paths []Path, pathType string, jobName string) error {
for _, path := range paths {
if strings.HasPrefix(jobPath, path.Path) {
return nil
}
}
return fmt.Errorf("invalid %s path for job '%s': %s", pathType, jobName, jobPath)
}

func validatePaths(cfg Config) error {
invalidPaths := []string{}

for _, job := range cfg.Jobs {
if err := validatePath(job.Source, cfg.Sources, "source", job.Name); err != nil {
invalidPaths = append(invalidPaths, err.Error())
}
if err := validatePath(job.Target, cfg.Targets, "target", job.Name); err != nil {
invalidPaths = append(invalidPaths, err.Error())
}
}

if len(invalidPaths) > 0 {
return fmt.Errorf("path validation errors: %v", invalidPaths)
}
return nil
}

func validateJobPaths(jobs []Job, pathType string, getPath func(job Job) string) error {
for i, job1 := range jobs {
for j, job2 := range jobs {
if i != j {
path1, path2 := NormalizePath(getPath(job1)), NormalizePath(getPath(job2))

// Check if path2 is part of job1's exclusions
excluded := false
if pathType == "source" {
for _, exclusion := range job2.Exclusions {
exclusionPath := NormalizePath(filepath.Join(job2.Source, exclusion))
// log.Printf("job2: %s %s\n", job2.Name, exclusionPath)
if strings.HasPrefix(path1, exclusionPath) {
excluded = true
break
}
}
}

if !excluded && strings.HasPrefix(path1, path2) {
return fmt.Errorf("Job '%s' has a %s path overlapping with job '%s'", job1.Name, pathType, job2.Name)
}
}
}
}
return nil
}

func LoadResolvedConfig(configPath string) Config {
f, err := os.Open(configPath)
if err != nil {
log.Fatalf("Failed to open config: %v", err)
}
defer f.Close()

cfg, err := LoadConfig(f)
if err != nil {
log.Fatalf("Failed to parse YAML: %v", err)
}

if err := validateJobNames(cfg.Jobs); err != nil {
log.Fatalf("Job validation failed: %v", err)
}

resolvedCfg := resolveConfig(cfg)

if err := validatePaths(resolvedCfg); err != nil {
log.Fatalf("Path validation failed: %v", err)
}

if err := validateJobPaths(resolvedCfg.Jobs, "source", func(job Job) string { return job.Source }); err != nil {
log.Fatalf("Job source path validation failed: %v", err)
}

if err := validateJobPaths(resolvedCfg.Jobs, "target", func(job Job) string { return job.Target }); err != nil {
log.Fatalf("Job target path validation failed: %v", err)
}

return resolvedCfg
}
Loading