package ansible import ( "bytes" "context" "fmt" "os" "os/exec" "path/filepath" "strings" "sync" ) // ErrUnknownDeployType is returned when an unsupported deployment type is specified var ErrUnknownDeployType = fmt.Errorf("unknown deploy type, expected 'docker' or 'binary'") // Executor handles running Ansible playbooks type Executor struct { workDir string grpcServerHost string grpcServerPort string backendURL string giteaReleasesURL string } // ExecutorConfig holds configuration for the Executor type ExecutorConfig struct { WorkDir string GRPCServerHost string GRPCServerPort string BackendURL string GiteaReleasesURL string } // NewExecutor creates a new Ansible executor func NewExecutor(cfg ExecutorConfig) *Executor { return &Executor{ workDir: cfg.WorkDir, grpcServerHost: cfg.GRPCServerHost, grpcServerPort: cfg.GRPCServerPort, backendURL: cfg.BackendURL, giteaReleasesURL: cfg.GiteaReleasesURL, } } // DeployResult holds the result of a deployment type DeployResult struct { Host string Success bool Stdout string Stderr string Err error } // WorkDir returns the work directory path func (e *Executor) WorkDir() string { return e.workDir } // GRPCURL returns the gRPC server URL (host:port) func (e *Executor) GRPCURL() string { return e.grpcServerHost + ":" + e.grpcServerPort } // CheckDockerCollection verifies that the community.docker Ansible collection is installed. // Returns an error if the collection is not found. func (e *Executor) CheckDockerCollection() error { cmd := exec.Command("ansible-galaxy", "collection", "list", "community.docker") var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return fmt.Errorf("community.docker collection not found: %s", stderr.String()) } // ansible-galaxy collection list returns output like: // # /usr/share/ansible/collections/ansible_collections // Collection Version // ---------------- ------- // community.docker 3.10.0 // // If the collection is not installed, it won't appear in the output. if !strings.Contains(stdout.String(), "community.docker") { return fmt.Errorf("community.docker collection is not installed. Run: ansible-galaxy collection install community.docker") } return nil } // Deploy runs Ansible playbook for the given inventory func (e *Executor) Deploy( ctx context.Context, inventoryPath string, deployType string, ) ([]DeployResult, error) { if deployType != "docker" && deployType != "binary" { return nil, fmt.Errorf("invalid deploy type %q: %w", deployType, ErrUnknownDeployType) } playbookName := "binary_deploy.yml" if deployType == "docker" { playbookName = "docker_deploy.yml" } playbookPath := filepath.Join(e.workDir, playbookName) cmd := exec.CommandContext(ctx, "ansible-playbook", "-i", inventoryPath, "-e", fmt.Sprintf("backend_url=%s", e.backendURL), "-e", fmt.Sprintf("grpc_url=%s", e.grpcServerHost+":"+e.grpcServerPort), "-e", fmt.Sprintf("gitea_releases_url=%s", e.giteaReleasesURL), playbookPath, ) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr runErr := cmd.Run() // Parse results per host (simplified - returns single result for all) return []DeployResult{ { Host: "all", Success: runErr == nil, Stdout: stdout.String(), Stderr: stderr.String(), Err: runErr, }, }, nil } // DeployParallel runs Ansible playbook for multiple inventories in parallel func (e *Executor) DeployParallel( ctx context.Context, inventoryPaths []string, deployType string, ) (map[string][]DeployResult, error) { var wg sync.WaitGroup var mu sync.Mutex results := make(map[string][]DeployResult) errCh := make(chan error, len(inventoryPaths)) for _, path := range inventoryPaths { wg.Add(1) go func(p string) { defer wg.Done() res, err := e.Deploy(ctx, p, deployType) if err != nil { errCh <- err } mu.Lock() results[p] = res mu.Unlock() }(path) } wg.Wait() close(errCh) // Collect errors var errs []error for err := range errCh { errs = append(errs, err) } if len(errs) > 0 { return results, fmt.Errorf("some deployments failed: %v", errs) } return results, nil } // WritePlaybook writes a playbook to the work directory func (e *Executor) WritePlaybook(name string, content string) error { path := filepath.Join(e.workDir, name) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } return os.WriteFile(path, []byte(content), 0644) } // WriteAllPlaybooks writes all playbooks to the work directory func (e *Executor) WriteAllPlaybooks() error { if err := e.WritePlaybook("binary_deploy.yml", BinaryDeployPlaybook); err != nil { return err } return e.WritePlaybook("docker_deploy.yml", DockerDeployPlaybook) }