cli/pkg/deploy/python/python.go
Snider a794f6b55f feat(deploy): add pure-Go Ansible executor and Coolify API integration
Implement infrastructure deployment system with:

- pkg/ansible: Pure Go Ansible executor
  - Playbook/inventory parsing (types.go, parser.go)
  - Full execution engine with variable templating, loops, blocks,
    conditionals, handlers, and fact gathering (executor.go)
  - SSH client with key/password auth and privilege escalation (ssh.go)
  - 35+ module implementations: shell, command, copy, template, file,
    apt, service, systemd, user, group, git, docker_compose, etc. (modules.go)

- pkg/deploy/coolify: Coolify API client wrapping Python swagger client
  - List/get servers, projects, applications, databases, services
  - Generic Call() for any OpenAPI operation

- pkg/deploy/python: Embedded Python runtime for swagger client integration

- internal/cmd/deploy: CLI commands
  - core deploy servers/projects/apps/databases/services/team
  - core deploy call <operation> [params-json]

This enables Docker-free infrastructure deployment with Ansible-compatible
playbooks executed natively in Go.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 18:01:10 +00:00

130 lines
3 KiB
Go

package python
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/kluctl/go-embed-python/python"
)
var (
once sync.Once
ep *python.EmbeddedPython
initErr error
)
// Init initializes the embedded Python runtime.
func Init() error {
once.Do(func() {
ep, initErr = python.NewEmbeddedPython("core-deploy")
})
return initErr
}
// GetPython returns the embedded Python instance.
func GetPython() *python.EmbeddedPython {
return ep
}
// RunScript runs a Python script with the given code and returns stdout.
func RunScript(ctx context.Context, code string, args ...string) (string, error) {
if err := Init(); err != nil {
return "", err
}
// Write code to temp file
tmpFile, err := os.CreateTemp("", "core-*.py")
if err != nil {
return "", fmt.Errorf("failed to create temp file: %w", err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.WriteString(code); err != nil {
tmpFile.Close()
return "", fmt.Errorf("failed to write script: %w", err)
}
tmpFile.Close()
// Build args: script path + any additional args
cmdArgs := append([]string{tmpFile.Name()}, args...)
// Get the command
cmd, err := ep.PythonCmd(cmdArgs...)
if err != nil {
return "", fmt.Errorf("failed to create Python command: %w", err)
}
// Run with context
output, err := cmd.Output()
if err != nil {
// Try to get stderr for better error message
if exitErr, ok := err.(*os.PathError); ok {
return "", fmt.Errorf("script failed: %v", exitErr)
}
return "", fmt.Errorf("script failed: %w", err)
}
return string(output), nil
}
// RunModule runs a Python module (python -m module_name).
func RunModule(ctx context.Context, module string, args ...string) (string, error) {
if err := Init(); err != nil {
return "", err
}
cmdArgs := append([]string{"-m", module}, args...)
cmd, err := ep.PythonCmd(cmdArgs...)
if err != nil {
return "", fmt.Errorf("failed to create Python command: %w", err)
}
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("module %s failed: %w", module, err)
}
return string(output), nil
}
// DevOpsPath returns the path to the DevOps repo.
func DevOpsPath() string {
if path := os.Getenv("DEVOPS_PATH"); path != "" {
return path
}
home, _ := os.UserHomeDir()
return filepath.Join(home, "Code", "DevOps")
}
// CoolifyModulePath returns the path to the Coolify module_utils.
func CoolifyModulePath() string {
return filepath.Join(DevOpsPath(), "playbooks", "roles", "coolify", "module_utils")
}
// CoolifyScript generates Python code to call the Coolify API.
func CoolifyScript(baseURL, apiToken, operation string, params map[string]any) string {
paramsJSON, _ := json.Marshal(params)
return fmt.Sprintf(`
import sys
import json
sys.path.insert(0, %q)
from swagger.coolify_api import CoolifyClient
client = CoolifyClient(
base_url=%q,
api_token=%q,
timeout=30,
verify_ssl=True,
)
params = json.loads(%q)
result = client._call(%q, params, check_response=False)
print(json.dumps(result))
`, CoolifyModulePath(), baseURL, apiToken, string(paramsJSON), operation)
}