191 lines
4.8 KiB
Go
191 lines
4.8 KiB
Go
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)
|
|
}
|