This commit is contained in:
2025-08-31 12:24:50 -04:00
parent 6ec69103dd
commit 66b114f374
8 changed files with 321 additions and 81 deletions

View File

@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"regexp"
"strings"
"time"
@ -341,7 +343,15 @@ func (s *SimpleDockerRuntime) createContainer(ctx context.Context, function *dom
echo "const handler = require('/tmp/index.js').handler;
const input = process.env.FUNCTION_INPUT ? JSON.parse(process.env.FUNCTION_INPUT) : {};
const context = { functionName: '` + function.Name + `' };
handler(input, context).then(result => console.log(JSON.stringify(result))).catch(err => { console.error(err); process.exit(1); });" > /tmp/runner.js &&
console.log('<stdout>');
handler(input, context).then(result => {
console.log('</stdout>');
console.log('<result>' + JSON.stringify(result) + '</result>');
}).catch(err => {
console.log('</stdout>');
console.error('<result>{\"error\": \"' + err.message + '\"}</result>');
process.exit(1);
});" > /tmp/runner.js &&
node /tmp/runner.js
`}
case "python", "python3", "python3.9", "python3.10", "python3.11":
@ -350,8 +360,15 @@ func (s *SimpleDockerRuntime) createContainer(ctx context.Context, function *dom
echo "import json, os, sys; sys.path.insert(0, '/tmp'); from handler import handler;
input_data = json.loads(os.environ.get('FUNCTION_INPUT', '{}'));
context = {'function_name': '` + function.Name + `'};
result = handler(input_data, context);
print(json.dumps(result))" > /tmp/runner.py &&
print('<stdout>');
try:
result = handler(input_data, context);
print('</stdout>');
print('<result>' + json.dumps(result) + '</result>');
except Exception as e:
print('</stdout>');
print('<result>{\"error\": \"' + str(e) + '\"}</result>', file=sys.stderr);
sys.exit(1);" > /tmp/runner.py &&
python /tmp/runner.py
`}
default:
@ -386,20 +403,48 @@ func (s *SimpleDockerRuntime) getContainerLogs(ctx context.Context, containerID
logs, err := s.client.ContainerLogs(ctx, containerID, container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Tail: "50", // Get last 50 lines
Tail: "100", // Get last 100 lines
})
if err != nil {
return nil, fmt.Errorf("failed to get container logs: %w", err)
}
defer logs.Close()
// For simplicity, we'll return a placeholder
// In a real implementation, you'd parse the log output
return []string{
"Container logs would appear here",
"Function execution started",
"Function execution completed",
}, nil
// Read the actual logs content
logData, err := io.ReadAll(logs)
if err != nil {
return nil, fmt.Errorf("failed to read log data: %w", err)
}
// Parse Docker logs to remove binary headers
rawOutput := parseDockerLogs(logData)
// Parse the XML-tagged output to extract logs
parsedLogs, _, err := s.parseContainerOutput(rawOutput)
if err != nil {
s.logger.Warn("Failed to parse container output for logs", zap.Error(err))
// Fallback to raw output split by lines
lines := strings.Split(strings.TrimSpace(rawOutput), "\n")
cleanLines := make([]string, 0, len(lines))
for _, line := range lines {
if trimmed := strings.TrimSpace(line); trimmed != "" {
cleanLines = append(cleanLines, trimmed)
}
}
return cleanLines, nil
}
// If no logs were parsed from <stdout> tags, fallback to basic parsing
if len(parsedLogs) == 0 {
lines := strings.Split(strings.TrimSpace(rawOutput), "\n")
for _, line := range lines {
if trimmed := strings.TrimSpace(line); trimmed != "" && !strings.Contains(trimmed, "<result>") && !strings.Contains(trimmed, "</result>") {
parsedLogs = append(parsedLogs, trimmed)
}
}
}
return parsedLogs, nil
}
func (s *SimpleDockerRuntime) getContainerOutput(ctx context.Context, containerID string) (json.RawMessage, error) {
@ -415,36 +460,143 @@ func (s *SimpleDockerRuntime) getContainerOutput(ctx context.Context, containerI
defer logs.Close()
// Read the actual logs content
buf := make([]byte, 4096)
var output strings.Builder
for {
n, err := logs.Read(buf)
if n > 0 {
// Docker logs include 8-byte headers, skip them for stdout content
if n > 8 {
output.Write(buf[8:n])
logData, err := io.ReadAll(logs)
if err != nil {
return nil, fmt.Errorf("failed to read log data: %w", err)
}
// Parse Docker logs to remove binary headers
rawOutput := parseDockerLogs(logData)
// Parse the XML-tagged output to extract the result
_, result, err := s.parseContainerOutput(rawOutput)
if err != nil {
s.logger.Warn("Failed to parse container output for result", zap.Error(err))
// Fallback to legacy parsing
logContent := strings.TrimSpace(rawOutput)
if json.Valid([]byte(logContent)) && logContent != "" {
return json.RawMessage(logContent), nil
} else {
// Return the output wrapped in a JSON object
fallbackResult := map[string]interface{}{
"result": "Function executed successfully",
"output": logContent,
"timestamp": time.Now().UTC(),
}
}
if err != nil {
break
resultJSON, _ := json.Marshal(fallbackResult)
return json.RawMessage(resultJSON), nil
}
}
logContent := strings.TrimSpace(output.String())
// Try to parse as JSON first, if that fails, wrap in a JSON object
if json.Valid([]byte(logContent)) && logContent != "" {
return json.RawMessage(logContent), nil
} else {
// Return the output wrapped in a JSON object
result := map[string]interface{}{
// If no result was found in XML tags, provide a default success result
if result == nil {
defaultResult := map[string]interface{}{
"result": "Function executed successfully",
"output": logContent,
"message": "No result output found",
"timestamp": time.Now().UTC(),
}
resultJSON, _ := json.Marshal(result)
resultJSON, _ := json.Marshal(defaultResult)
return json.RawMessage(resultJSON), nil
}
return result, nil
}
// parseDockerLogs parses Docker log output which includes 8-byte headers
func parseDockerLogs(logData []byte) string {
var cleanOutput strings.Builder
for len(logData) > 8 {
// Docker log header: [STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4]
// Skip the first 8 bytes (header)
headerSize := 8
if len(logData) < headerSize {
break
}
// Extract size from bytes 4-7 (big endian)
size := int(logData[4])<<24 + int(logData[5])<<16 + int(logData[6])<<8 + int(logData[7])
if len(logData) < headerSize+size {
// If the remaining data is less than expected size, take what we have
size = len(logData) - headerSize
}
if size > 0 {
// Extract the actual log content
content := string(logData[headerSize : headerSize+size])
cleanOutput.WriteString(content)
}
// Move to next log entry
logData = logData[headerSize+size:]
}
return cleanOutput.String()
}
// parseContainerOutput parses container output that contains <stdout> and <result> XML tags
func (s *SimpleDockerRuntime) parseContainerOutput(rawOutput string) (logs []string, result json.RawMessage, err error) {
// Extract stdout content (logs) - use DOTALL flag for multiline matching
stdoutRegex := regexp.MustCompile(`(?s)<stdout>(.*?)</stdout>`)
stdoutMatch := stdoutRegex.FindStringSubmatch(rawOutput)
if len(stdoutMatch) > 1 {
stdoutContent := strings.TrimSpace(stdoutMatch[1])
if stdoutContent != "" {
// Split stdout content into lines for logs
lines := strings.Split(stdoutContent, "\n")
// Clean up empty lines and trim whitespace
cleanLogs := make([]string, 0, len(lines))
for _, line := range lines {
if trimmed := strings.TrimSpace(line); trimmed != "" {
cleanLogs = append(cleanLogs, trimmed)
}
}
logs = cleanLogs
}
}
// Extract result content - use DOTALL flag for multiline matching
resultRegex := regexp.MustCompile(`(?s)<result>(.*?)</result>`)
resultMatch := resultRegex.FindStringSubmatch(rawOutput)
if len(resultMatch) > 1 {
resultContent := strings.TrimSpace(resultMatch[1])
if resultContent != "" {
// Validate JSON
if json.Valid([]byte(resultContent)) {
result = json.RawMessage(resultContent)
} else {
// If not valid JSON, wrap it
wrappedResult := map[string]interface{}{
"output": resultContent,
}
resultJSON, _ := json.Marshal(wrappedResult)
result = json.RawMessage(resultJSON)
}
}
}
// If no result tag found, treat entire output as result (fallback for non-tagged output)
if result == nil {
// Remove any XML tags from the output for fallback
cleanOutput := regexp.MustCompile(`(?s)<[^>]*>`).ReplaceAllString(rawOutput, "")
cleanOutput = strings.TrimSpace(cleanOutput)
if cleanOutput != "" {
if json.Valid([]byte(cleanOutput)) {
result = json.RawMessage(cleanOutput)
} else {
// Wrap non-JSON output
wrappedResult := map[string]interface{}{
"output": cleanOutput,
}
resultJSON, _ := json.Marshal(wrappedResult)
result = json.RawMessage(resultJSON)
}
}
}
return logs, result, nil
}
func (s *SimpleDockerRuntime) cleanupContainer(ctx context.Context, containerID string) {