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 }