2026-04-07 12:30:27 +08:00

200 lines
4.8 KiB
Go

package orchestrator
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os/exec"
"strings"
"app-deploy-platform/backend/internal/model"
)
type Target struct {
Host model.Host
Instance model.ServiceInstance
}
type Context struct {
Deployment model.Deployment
Release *model.Release
}
type BuildContext struct {
ServiceName string
ReleaseID string
Branch string
OnOutput func(string)
}
type BuildResult struct {
ServiceName string `json:"service_name"`
ReleaseID string `json:"release_id"`
Branch string `json:"branch"`
RepoURL string `json:"repo_url"`
RepoPath string `json:"repo_path"`
CommitSHA string `json:"commit_sha"`
BuildHost string `json:"build_host"`
COSKey string `json:"cos_key"`
ArtifactURL string `json:"artifact_url"`
SHA256 string `json:"sha256"`
}
type Executor interface {
Deploy(context.Context, Target, Context) (string, error)
Restart(context.Context, Target, Context) (string, error)
Rollback(context.Context, Target, Context) (string, error)
}
type BuildExecutor interface {
Build(context.Context, BuildContext) (BuildResult, string, error)
}
type DeployCommandExecutor struct {
operatorScript string
configPath string
workDir string
}
type ScriptBuildExecutor struct {
operatorScript string
configPath string
workDir string
}
func NewDeployCommandExecutor(operatorScript, configPath, workDir string) *DeployCommandExecutor {
return &DeployCommandExecutor{
operatorScript: operatorScript,
configPath: configPath,
workDir: workDir,
}
}
func NewBuildCommandExecutor(operatorScript, configPath, workDir string) *ScriptBuildExecutor {
return &ScriptBuildExecutor{
operatorScript: operatorScript,
configPath: configPath,
workDir: workDir,
}
}
func (e *DeployCommandExecutor) Deploy(ctx context.Context, target Target, deployment Context) (string, error) {
return e.run(ctx, model.OperationDeploy, target, deployment)
}
func (e *DeployCommandExecutor) Restart(ctx context.Context, target Target, deployment Context) (string, error) {
return e.run(ctx, model.OperationRestart, target, deployment)
}
func (e *DeployCommandExecutor) Rollback(ctx context.Context, target Target, deployment Context) (string, error) {
return e.run(ctx, model.OperationRollback, target, deployment)
}
func (e *DeployCommandExecutor) run(ctx context.Context, action string, target Target, deployment Context) (string, error) {
if e.operatorScript == "" {
return fmt.Sprintf("no %s script configured, simulated success for %s@%s", action, target.Instance.ServiceName, target.Host.PrivateIP), nil
}
args := []string{
e.operatorScript,
"--config", e.configPath,
"--action", action,
"--service", target.Instance.ServiceName,
"--instance-id", target.Host.InstanceID,
}
if deployment.Deployment.ReleaseID != "" {
args = append(args, "--release-id", deployment.Deployment.ReleaseID)
}
command := exec.CommandContext(ctx, "python3", args...)
if e.workDir != "" {
command.Dir = e.workDir
}
var output bytes.Buffer
command.Stdout = &output
command.Stderr = &output
if err := command.Run(); err != nil {
return output.String(), err
}
return output.String(), nil
}
func (e *ScriptBuildExecutor) Build(ctx context.Context, buildCtx BuildContext) (BuildResult, string, error) {
var result BuildResult
if e.operatorScript == "" {
return result, "no build script configured", fmt.Errorf("no build script configured")
}
args := []string{
e.operatorScript,
"--config", e.configPath,
"--service", buildCtx.ServiceName,
"--release-id", buildCtx.ReleaseID,
}
if buildCtx.Branch != "" {
args = append(args, "--branch", buildCtx.Branch)
}
command := exec.CommandContext(ctx, "python3", args...)
if e.workDir != "" {
command.Dir = e.workDir
}
stdout, err := command.StdoutPipe()
if err != nil {
return result, "", err
}
command.Stderr = command.Stdout
var output bytes.Buffer
if err := command.Start(); err != nil {
return result, "", err
}
reader := bufio.NewReader(stdout)
for {
chunk, readErr := reader.ReadString('\n')
if chunk != "" {
output.WriteString(chunk)
if buildCtx.OnOutput != nil {
buildCtx.OnOutput(chunk)
}
}
if readErr == nil {
continue
}
if readErr == io.EOF {
break
}
return result, output.String(), readErr
}
err = command.Wait()
raw := output.String()
for _, line := range strings.Split(raw, "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "BUILD_RESULT_JSON=") {
continue
}
payload := strings.TrimPrefix(line, "BUILD_RESULT_JSON=")
if unmarshalErr := json.Unmarshal([]byte(payload), &result); unmarshalErr != nil {
return result, raw, unmarshalErr
}
break
}
if err != nil {
return result, raw, err
}
if result.ReleaseID == "" {
return result, raw, fmt.Errorf("build script did not return build result")
}
return result, raw, nil
}