-
Notifications
You must be signed in to change notification settings - Fork 3
fix: Run exec agent #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 10 commits
8223bd0
01bc449
e1a6568
2d98dd4
2d395fb
f01a216
eae19f1
df80fe2
a29b566
ab92d45
6d1fda9
6e6e77c
840e166
3660019
f51d8d4
5160555
c6fdca4
09ccdc6
33ea1ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,9 +8,8 @@ import ( | |
"os" | ||
"os/exec" | ||
"runtime" | ||
"strconv" | ||
"syscall" | ||
|
||
"github.com/charmbracelet/log" | ||
"github.com/ctrlplanedev/cli/internal/api" | ||
"github.com/ctrlplanedev/cli/pkg/jobagent" | ||
) | ||
|
@@ -24,72 +23,49 @@ type ExecConfig struct { | |
Script string `json:"script"` | ||
} | ||
|
||
func (r *ExecRunner) Status(job api.Job) (api.JobStatus, string) { | ||
externalId, err := strconv.Atoi(*job.ExternalId) | ||
if err != nil { | ||
return api.JobStatusExternalRunNotFound, fmt.Sprintf("invalid process id: %v", err) | ||
} | ||
|
||
process, err := os.FindProcess(externalId) | ||
if err != nil { | ||
return api.JobStatusExternalRunNotFound, fmt.Sprintf("failed to find process: %v", err) | ||
} | ||
|
||
// On Unix systems, FindProcess always succeeds, so we need to send signal 0 | ||
// to check if process exists | ||
err = process.Signal(syscall.Signal(0)) | ||
if err != nil { | ||
return api.JobStatusSuccessful, fmt.Sprintf("process not running: %v", err) | ||
} | ||
|
||
return api.JobStatusInProgress, fmt.Sprintf("process running with pid %d", externalId) | ||
} | ||
|
||
func (r *ExecRunner) Start(job api.Job) (string, error) { | ||
// Create temp script file | ||
// Start creates a temporary script file, starts the process, and updates job status when the process completes. | ||
func (r *ExecRunner) Start(job api.Job, jobDetails map[string]interface{}, statusUpdateFunc func(jobID string, status api.JobStatus, message string)) (string, api.JobStatus, error) { | ||
// Determine file extension based on OS. | ||
ext := ".sh" | ||
if runtime.GOOS == "windows" { | ||
ext = ".ps1" | ||
} | ||
|
||
tmpFile, err := os.CreateTemp("", "script*"+ext) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to create temp script file: %w", err) | ||
return "", api.JobStatusFailure, fmt.Errorf("failed to create temp script file: %w", err) | ||
} | ||
defer os.Remove(tmpFile.Name()) | ||
|
||
config := ExecConfig{} | ||
jsonBytes, err := json.Marshal(job.JobAgentConfig) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to marshal job agent config: %w", err) | ||
return "", api.JobStatusFailure, fmt.Errorf("failed to marshal job agent config: %w", err) | ||
} | ||
if err := json.Unmarshal(jsonBytes, &config); err != nil { | ||
return "", fmt.Errorf("failed to unmarshal job agent config: %w", err) | ||
return "", api.JobStatusFailure, fmt.Errorf("failed to unmarshal job agent config: %w", err) | ||
} | ||
|
||
templatedScript, err := template.New("script").Parse(config.Script) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to parse script template: %w", err) | ||
return "", api.JobStatusFailure, fmt.Errorf("failed to parse script template: %w", err) | ||
} | ||
|
||
buf := new(bytes.Buffer) | ||
if err := templatedScript.Execute(buf, job); err != nil { | ||
return "", fmt.Errorf("failed to execute script template: %w", err) | ||
if err := templatedScript.Execute(buf, jobDetails); err != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid potential code injection in the script. |
||
return "", api.JobStatusFailure, fmt.Errorf("failed to execute script template: %w", err) | ||
} | ||
script := buf.String() | ||
|
||
// Write script contents | ||
if _, err := tmpFile.WriteString(script); err != nil { | ||
return "", fmt.Errorf("failed to write script file: %w", err) | ||
return "", api.JobStatusFailure, fmt.Errorf("failed to write script file: %w", err) | ||
} | ||
if err := tmpFile.Close(); err != nil { | ||
return "", fmt.Errorf("failed to close script file: %w", err) | ||
return "", api.JobStatusFailure, fmt.Errorf("failed to close script file: %w", err) | ||
} | ||
|
||
// Make executable on Unix systems | ||
if runtime.GOOS != "windows" { | ||
if err := os.Chmod(tmpFile.Name(), 0700); err != nil { | ||
return "", fmt.Errorf("failed to make script executable: %w", err) | ||
return "", api.JobStatusFailure, fmt.Errorf("failed to make script executable: %w", err) | ||
} | ||
} | ||
|
||
|
@@ -104,9 +80,28 @@ func (r *ExecRunner) Start(job api.Job) (string, error) { | |
cmd.Stdout = os.Stdout | ||
cmd.Stderr = os.Stderr | ||
|
||
if err := cmd.Run(); err != nil { | ||
return "", fmt.Errorf("failed to execute script: %w", err) | ||
if err := cmd.Start(); err != nil { | ||
os.Remove(tmpFile.Name()) | ||
return "", api.JobStatusFailure, fmt.Errorf("failed to start process: %w", err) | ||
} | ||
|
||
return strconv.Itoa(cmd.Process.Pid), nil | ||
// Use the pointer address as the handle | ||
handle := fmt.Sprintf("%p", cmd) | ||
|
||
// Spawn a goroutine to wait for the process to finish and update the job status | ||
go func(handle, scriptPath string) { | ||
defer os.Remove(scriptPath) | ||
|
||
err := cmd.Wait() | ||
|
||
if err != nil { | ||
log.Error("Process execution failed", "handle", handle, "error", err) | ||
statusUpdateFunc(job.Id.String(), api.JobStatusFailure, err.Error()) | ||
} else { | ||
log.Info("Process execution succeeded", "handle", handle) | ||
statusUpdateFunc(job.Id.String(), api.JobStatusSuccessful, "") | ||
} | ||
}(handle, tmpFile.Name()) | ||
|
||
return handle, api.JobStatusInProgress, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package jobagent | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
|
||
"github.com/ctrlplanedev/cli/internal/api" | ||
"github.com/spf13/viper" | ||
) | ||
|
||
func fetchJobDetails(ctx context.Context, jobID string) (map[string]interface{}, error) { | ||
client, err := api.NewAPIKeyClientWithResponses(viper.GetString("url"), viper.GetString("api-key")) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to create API client for job details: %w", err) | ||
} | ||
|
||
resp, err := client.GetJobWithResponse(ctx, jobID) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get job details: %w", err) | ||
} | ||
if resp.JSON200 == nil { | ||
return nil, fmt.Errorf("received empty response from job details API") | ||
} | ||
|
||
var details map[string]interface{} | ||
detailsBytes, err := json.Marshal(resp.JSON200) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to marshal job response: %w", err) | ||
} | ||
if err := json.Unmarshal(detailsBytes, &details); err != nil { | ||
return nil, fmt.Errorf("failed to unmarshal job details: %w", err) | ||
} | ||
return details, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could move input validation above the client creation?