Files
HellreigN/backend/internal/handlers/agent_deploy.go
T
2026-04-05 01:43:38 +03:00

235 lines
6.7 KiB
Go

package handlers
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"time"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/ansible"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"github.com/gin-gonic/gin"
)
type AgentDeployGroup struct {
*Handlers
executor *ansible.Executor
}
func NewAgentDeployGroup(h *Handlers) *AgentDeployGroup {
workDir := os.Getenv("ANSIBLE_WORK_DIR")
if workDir == "" {
workDir = "/tmp/hellreign/ansible"
}
grpcPort := os.Getenv("GRPC_PORT")
if grpcPort == "" {
grpcPort = "9001"
}
grpcHost := os.Getenv("GRPC_SERVER_HOST")
if grpcHost == "" {
grpcHost = "0.0.0.0"
}
backendURL := os.Getenv("BACKEND_URL")
if backendURL == "" {
backendURL = "http://localhost:8080"
}
giteaURL := os.Getenv("GITEA_RELEASES_URL")
if giteaURL == "" {
giteaURL = "https://gitea.d3m0k1d.ru/d3m0k1d/HellreigN/releases/latest/download"
}
exec := ansible.NewExecutor(ansible.ExecutorConfig{
WorkDir: workDir,
GRPCServerHost: grpcHost,
GRPCServerPort: grpcPort,
BackendURL: backendURL,
GiteaReleasesURL: giteaURL,
})
// Write playbooks on init
if err := exec.WriteAllPlaybooks(); err != nil {
// Log the error - deployment will fail later if playbooks can't be written
fmt.Fprintf(os.Stderr, "WARNING: failed to write Ansible playbooks: %v\n", err)
}
return &AgentDeployGroup{
Handlers: h,
executor: exec,
}
}
// DeployAgents deploys agents to multiple servers
// @Summary Deploy agents to multiple servers via Ansible
// @Description Deploy HellreigN agents to multiple servers using Ansible playbooks. Supports Docker and Binary deployment types.
// @Tags agents
// @Accept json
// @Produce json
// @Param request body repository.DeployAgentsRequest true "Deployment configuration for servers"
// @Success 200 {object} repository.DeployResponse "Deployment results with tokens for each server"
// @Failure 400 {object} map[string]string "Invalid request"
// @Failure 500 {object} map[string]string "Internal server error"
// @Security Bearer
// @Router /agents/deploy [post]
func (adg *AgentDeployGroup) DeployAgents(c *gin.Context) {
var req repository.DeployAgentsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Validate auth credentials for each server
for i, server := range req.Servers {
switch server.AuthMethod {
case repository.AuthMethodKey:
if server.SSHKey == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("server %d (%s): sshKey is required when authMethod is 'key'", i, server.IP),
})
return
}
case repository.AuthMethodPassword:
if server.Password == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("server %d (%s): password is required when authMethod is 'password'", i, server.IP),
})
return
}
default:
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("server %d (%s): invalid authMethod %q, expected 'key' or 'password'", i, server.IP, server.AuthMethod),
})
return
}
}
// Pre-flight check: verify community.docker collection is available for docker deployments
needsDockerCollection := false
for _, server := range req.Servers {
if server.DeployType == repository.DeployTypeDocker {
needsDockerCollection = true
break
}
}
if needsDockerCollection {
if err := adg.executor.CheckDockerCollection(); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Docker deployment requires 'community.docker' Ansible collection: %v", err),
})
return
}
}
// Create work directory
workDir := adg.executor.WorkDir()
if err := os.MkdirAll(workDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create work directory"})
return
}
// Generate registration tokens for each server
results := make([]repository.DeployResult, 0, len(req.Servers))
timestamp := time.Now().UnixMilli()
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Minute)
defer cancel()
for i, server := range req.Servers {
// Create registration token
token, err := adg.Repo.CreateRegistrationToken(server.AgentLabel)
if err != nil {
results = append(results, repository.DeployResult{
IP: server.IP,
AgentLabel: server.AgentLabel,
Success: false,
Error: fmt.Sprintf("failed to create token: %v", err),
})
continue
}
// Set default port
port := server.Port
if port == 0 {
port = 22
}
// Generate inventory for this single server
inventoryHosts := []ansible.InventoryHost{
{
Name: server.AgentLabel,
IP: server.IP,
Port: port,
User: server.User,
AuthMethod: string(server.AuthMethod),
SSHKey: server.SSHKey,
Password: server.Password,
DeployType: string(server.DeployType),
Token: token,
GRPCURL: adg.executor.GRPCURL(),
},
}
inventoryPath := filepath.Join(workDir, fmt.Sprintf("inventory_%d_%d", timestamp, i))
if err := ansible.GenerateInventory(inventoryHosts, inventoryPath); err != nil {
// Rollback: delete the token we just created
_ = adg.Repo.DeleteRegistrationToken(token)
results = append(results, repository.DeployResult{
IP: server.IP,
AgentLabel: server.AgentLabel,
Token: token,
Success: false,
Error: fmt.Sprintf("failed to generate inventory: %v", err),
})
continue
}
// Run Ansible playbook for this server
deployResults, err := adg.executor.Deploy(ctx, inventoryPath, string(server.DeployType))
// Clean up inventory file (log error but don't fail deployment)
if cleanupErr := os.Remove(inventoryPath); cleanupErr != nil {
fmt.Fprintf(os.Stderr, "WARNING: failed to remove inventory file %s: %v\n", inventoryPath, cleanupErr)
}
if err != nil {
// Rollback: delete the token since deployment failed
_ = adg.Repo.DeleteRegistrationToken(token)
results = append(results, repository.DeployResult{
IP: server.IP,
AgentLabel: server.AgentLabel,
Token: token,
Success: false,
Error: fmt.Sprintf("deployment failed: %v", err),
})
continue
}
success := true
errMsg := ""
if len(deployResults) > 0 && !deployResults[0].Success {
success = false
errMsg = deployResults[0].Stderr
// Rollback: delete the token since ansible playbook reported failure
_ = adg.Repo.DeleteRegistrationToken(token)
}
results = append(results, repository.DeployResult{
IP: server.IP,
AgentLabel: server.AgentLabel,
Token: token,
Success: success,
Error: errMsg,
})
}
c.JSON(http.StatusOK, repository.DeployResponse{
Message: "Deployment completed",
Results: results,
})
}