-
Notifications
You must be signed in to change notification settings - Fork 41
Sync upstream - v23/v27 (2026-01-27) #82
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
Changes from 12 commits
5e8c82f
cd243de
4ffd35b
2acc2b4
7cb361f
bb770b7
b850e0f
f062258
41833cd
e0a36b1
2a6faf7
662247d
6f9e959
42b1499
ecd8f14
e809a85
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 |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| package api | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "time" | ||
|
|
||
| "github.com/onkernel/kernel-images/server/lib/logger" | ||
| "github.com/onkernel/kernel-images/server/lib/oapi" | ||
| ) | ||
|
|
||
| // ExecutePlaywrightCode implements the Playwright code execution endpoint | ||
| func (s *ApiService) ExecutePlaywrightCode(ctx context.Context, request oapi.ExecutePlaywrightCodeRequestObject) (oapi.ExecutePlaywrightCodeResponseObject, error) { | ||
| log := logger.FromContext(ctx) | ||
|
|
||
| // Validate request | ||
| if request.Body == nil || request.Body.Code == "" { | ||
| return oapi.ExecutePlaywrightCode400JSONResponse{ | ||
| BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ | ||
| Message: "code is required", | ||
| }, | ||
| }, nil | ||
| } | ||
|
|
||
| // Determine timeout (default to 60 seconds) | ||
| timeout := 60 * time.Second | ||
| if request.Body.TimeoutSec != nil && *request.Body.TimeoutSec > 0 { | ||
| timeout = time.Duration(*request.Body.TimeoutSec) * time.Second | ||
| } | ||
|
|
||
| // Create a temporary file for the user code | ||
| tmpFile, err := os.CreateTemp("", "playwright-code-*.ts") | ||
| if err != nil { | ||
| log.Error("failed to create temp file", "error", err) | ||
| return oapi.ExecutePlaywrightCode500JSONResponse{ | ||
| InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ | ||
| Message: fmt.Sprintf("failed to create temp file: %v", err), | ||
| }, | ||
| }, nil | ||
| } | ||
| tmpFilePath := tmpFile.Name() | ||
| defer os.Remove(tmpFilePath) // Clean up the temp file | ||
|
|
||
| // Write the user code to the temp file | ||
| if _, err := tmpFile.WriteString(request.Body.Code); err != nil { | ||
| tmpFile.Close() | ||
| log.Error("failed to write code to temp file", "error", err) | ||
| return oapi.ExecutePlaywrightCode500JSONResponse{ | ||
| InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ | ||
| Message: fmt.Sprintf("failed to write code to temp file: %v", err), | ||
| }, | ||
| }, nil | ||
| } | ||
| tmpFile.Close() | ||
|
|
||
| // Create context with timeout | ||
| execCtx, cancel := context.WithTimeout(ctx, timeout) | ||
| defer cancel() | ||
|
|
||
| // Execute the Playwright code via the executor script | ||
| cmd := exec.CommandContext(execCtx, "tsx", "/usr/local/lib/playwright-executor.ts", tmpFilePath) | ||
|
|
||
| output, err := cmd.CombinedOutput() | ||
|
|
||
| if err != nil { | ||
| if execCtx.Err() == context.DeadlineExceeded { | ||
| log.Error("playwright execution timed out", "timeout", timeout) | ||
| success := false | ||
| errorMsg := fmt.Sprintf("execution timed out after %v", timeout) | ||
| return oapi.ExecutePlaywrightCode200JSONResponse{ | ||
| Success: success, | ||
| Error: &errorMsg, | ||
| }, nil | ||
| } | ||
|
|
||
| log.Error("playwright execution failed", "error", err, "output", string(output)) | ||
|
|
||
| // Try to parse the error output as JSON | ||
| var result struct { | ||
| Success bool `json:"success"` | ||
| Result interface{} `json:"result,omitempty"` | ||
| Error string `json:"error,omitempty"` | ||
| Stack string `json:"stack,omitempty"` | ||
| } | ||
| if jsonErr := json.Unmarshal(output, &result); jsonErr == nil { | ||
| success := result.Success | ||
| errorMsg := result.Error | ||
| stderr := string(output) | ||
| return oapi.ExecutePlaywrightCode200JSONResponse{ | ||
| Success: success, | ||
| Error: &errorMsg, | ||
| Stderr: &stderr, | ||
| }, nil | ||
| } | ||
|
|
||
| // If we can't parse the output, return a generic error | ||
| success := false | ||
| errorMsg := fmt.Sprintf("execution failed: %v", err) | ||
| stderr := string(output) | ||
| return oapi.ExecutePlaywrightCode200JSONResponse{ | ||
| Success: success, | ||
| Error: &errorMsg, | ||
| Stderr: &stderr, | ||
| }, nil | ||
| } | ||
|
|
||
| // Parse successful output | ||
| var result struct { | ||
| Success bool `json:"success"` | ||
| Result interface{} `json:"result,omitempty"` | ||
| } | ||
| if err := json.Unmarshal(output, &result); err != nil { | ||
| log.Error("failed to parse playwright output", "error", err, "output", string(output)) | ||
| success := false | ||
| errorMsg := fmt.Sprintf("failed to parse output: %v", err) | ||
| stdout := string(output) | ||
| return oapi.ExecutePlaywrightCode200JSONResponse{ | ||
| Success: success, | ||
| Error: &errorMsg, | ||
| Stdout: &stdout, | ||
| }, nil | ||
| } | ||
|
|
||
| return oapi.ExecutePlaywrightCode200JSONResponse{ | ||
| Success: result.Success, | ||
| Result: &result.Result, | ||
| }, nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import ( | |
| "bytes" | ||
| "context" | ||
| "encoding/base64" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "mime/multipart" | ||
|
|
@@ -729,3 +730,83 @@ func getXvfbResolution(ctx context.Context) (width, height int, err error) { | |
|
|
||
| return 0, 0, fmt.Errorf("Xvfb process not found in ps aux output") | ||
| } | ||
|
|
||
| func TestPlaywrightExecuteAPI(t *testing.T) { | ||
| image := headlessImage | ||
| name := containerName + "-playwright-api" | ||
|
|
||
| logger := slog.New(slog.NewTextHandler(t.Output(), &slog.HandlerOptions{Level: slog.LevelInfo})) | ||
| baseCtx := logctx.AddToContext(context.Background(), logger) | ||
|
|
||
| if _, err := exec.LookPath("docker"); err != nil { | ||
| require.NoError(t, err, "docker not available: %v", err) | ||
| } | ||
|
|
||
| // Clean slate | ||
| _ = stopContainer(baseCtx, name) | ||
|
|
||
| env := map[string]string{} | ||
|
|
||
| // Start container | ||
| _, exitCh, err := runContainer(baseCtx, image, name, env) | ||
| require.NoError(t, err, "failed to start container: %v", err) | ||
| defer stopContainer(baseCtx, name) | ||
|
|
||
| ctx, cancel := context.WithTimeout(baseCtx, 2*time.Minute) | ||
| defer cancel() | ||
|
|
||
| require.NoError(t, waitHTTPOrExit(ctx, apiBaseURL+"/spec.yaml", exitCh), "api not ready: %v", err) | ||
|
|
||
| client, err := apiClient() | ||
| require.NoError(t, err) | ||
|
|
||
| // Test simple Playwright script that navigates to a page and returns the title | ||
| playwrightCode := ` | ||
| await page.goto('https://example.com'); | ||
| const title = await page.title(); | ||
| return title; | ||
| ` | ||
|
|
||
| logger.Info("[test]", "action", "executing playwright code") | ||
| req := instanceoapi.ExecutePlaywrightCodeJSONRequestBody{ | ||
| Code: playwrightCode, | ||
| } | ||
|
|
||
| rsp, err := client.ExecutePlaywrightCodeWithResponse(ctx, req) | ||
| require.NoError(t, err, "playwright execute request error: %v", err) | ||
| require.Equal(t, http.StatusOK, rsp.StatusCode(), "unexpected status for playwright execute: %s body=%s", rsp.Status(), string(rsp.Body)) | ||
| require.NotNil(t, rsp.JSON200, "expected JSON200 response, got nil") | ||
|
|
||
| // Log the full response for debugging | ||
| if !rsp.JSON200.Success { | ||
| var errorMsg string | ||
| if rsp.JSON200.Error != nil { | ||
| errorMsg = *rsp.JSON200.Error | ||
| } | ||
| var stdout, stderr string | ||
| if rsp.JSON200.Stdout != nil { | ||
| stdout = *rsp.JSON200.Stdout | ||
| } | ||
| if rsp.JSON200.Stderr != nil { | ||
| stderr = *rsp.JSON200.Stderr | ||
| } | ||
| logger.Error("[test]", "error", errorMsg, "stdout", stdout, "stderr", stderr) | ||
| } | ||
|
|
||
| require.True(t, rsp.JSON200.Success, "expected success=true, got success=false. Error: %s", func() string { | ||
| if rsp.JSON200.Error != nil { | ||
| return *rsp.JSON200.Error | ||
| } | ||
| return "nil" | ||
| }()) | ||
|
Contributor
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. @tembo test case still failing, please fix === RUN TestPlaywrightExecuteAPI
Contributor
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. @tembo still failing: === RUN TestPlaywrightExecuteAPI i think the problem is that we install playwright globally in the dockerfile, but are we configuring the execution of this script to point to that global location when resolving node_modules? |
||
| require.NotNil(t, rsp.JSON200.Result, "expected result to be non-nil") | ||
|
|
||
| // Verify the result contains "Example Domain" (the title of example.com) | ||
| resultBytes, err := json.Marshal(rsp.JSON200.Result) | ||
| require.NoError(t, err, "failed to marshal result: %v", err) | ||
| resultStr := string(resultBytes) | ||
| logger.Info("[test]", "result", resultStr) | ||
| require.Contains(t, resultStr, "Example Domain", "expected result to contain 'Example Domain'") | ||
|
|
||
| logger.Info("[test]", "result", "playwright execute API test passed") | ||
| } | ||
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.
Bug: Node Module Path Mismatch Causes Runtime Failures
The
NODE_PATHis set to/usr/lib/node_modules, but global Node.js packages (like TypeScript and Playwright) are installed in/usr/local/lib/node_modules. This path mismatch prevents Node.js from resolving these modules, leading to runtime failures for the Playwright executor.Additional Locations (1)
images/chromium-headful/Dockerfile#L178-L179