Pull-based GitOps-style Docker Compose deployer: polls a (private) Git repo, detects changed stacks and reconciles only the affected

feat: add concurrency to deployments

Signed-off-by: A. Ottr <alex@otter.foo>

+38 -14
+1
config.yml.example
··· 1 1 # Configuration file for compose-sync 2 2 repo_url: "git@github.com:user/compose-repo.git" # Optional: URL for cloning (not used if repo_path already exists) 3 3 repo_path: "/path/to/local/repo" # Required: Local path to the git repository 4 + concurrency: 3 # Optional: Number of concurrent deployments (default: 3) 4 5
+30 -12
main.go
··· 7 7 "os" 8 8 "path/filepath" 9 9 "slices" 10 + "sync" 10 11 ) 11 12 12 13 func main() { ··· 79 80 return 80 81 } 81 82 82 - // Deploy each stack 83 - for _, stack := range stacksToDeploy { 84 - composePath := filepath.Join(cfg.RepoPath, "stacks", stack, "compose.yml") 85 - if _, err := os.Stat(composePath); os.IsNotExist(err) { 86 - composePath = filepath.Join(cfg.RepoPath, "stacks", stack, "compose.yaml") 87 - } 88 - fmt.Printf("Deploying stack: %s\n", stack) 89 - if err := deployStack(composePath); err != nil { 90 - log.Printf("Failed to deploy stack %s: %v", stack, err) 91 - continue 92 - } 93 - fmt.Printf("Successfully deployed stack: %s\n", stack) 83 + deployStacks(cfg.RepoPath, stacksToDeploy, cfg.Concurrency) 84 + } 85 + 86 + func deployStacks(repoPath string, stacks []string, concurrency int) { 87 + // Create a semaphore channel to limit concurrency 88 + semaphore := make(chan struct{}, concurrency) 89 + var wg sync.WaitGroup 90 + 91 + for _, stack := range stacks { 92 + wg.Add(1) 93 + go func(stack string) { 94 + defer wg.Done() 95 + 96 + semaphore <- struct{}{} 97 + defer func() { <-semaphore }() 98 + 99 + composePath := filepath.Join(repoPath, "stacks", stack, "compose.yml") 100 + if _, err := os.Stat(composePath); os.IsNotExist(err) { 101 + composePath = filepath.Join(repoPath, "stacks", stack, "compose.yaml") 102 + } 103 + 104 + fmt.Printf("Deploying stack: %s\n", stack) 105 + if err := deployStack(composePath); err != nil { 106 + log.Printf("Failed to deploy stack %s: %v", stack, err) 107 + return 108 + } 109 + fmt.Printf("Successfully deployed stack: %s\n", stack) 110 + }(stack) 94 111 } 112 + wg.Wait() 95 113 }
+7 -2
sync.go
··· 13 13 ) 14 14 15 15 type config struct { 16 - RepoURL string `yaml:"repo_url"` 17 - RepoPath string `yaml:"repo_path"` 16 + RepoURL string `yaml:"repo_url"` 17 + RepoPath string `yaml:"repo_path"` 18 + Concurrency int `yaml:"concurrency"` 18 19 } 19 20 20 21 func loadConfig(path string) (*config, error) { ··· 30 31 31 32 if cfg.RepoPath == "" { 32 33 return nil, fmt.Errorf("repo_path is required in config") 34 + } 35 + 36 + if cfg.Concurrency <= 0 { 37 + cfg.Concurrency = 3 // Safe default for Docker 33 38 } 34 39 35 40 return &cfg, nil