Skip to content

Commit 053dba4

Browse files
committed
add benchmark
1 parent 3ec1c52 commit 053dba4

File tree

5 files changed

+194
-18
lines changed

5 files changed

+194
-18
lines changed

sandbox-sidecar/src/routes/runRoutes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ export function createRunRouter(
1717
router.post("/api/v1/sandboxes/runs", (req, res, next) => {
1818
try {
1919
const parsed = runRequestSchema.parse(req.body);
20+
21+
// Debug: log received metadata including AWS region
22+
console.log("Received run request metadata:", {
23+
hasMetadata: !!parsed.metadata,
24+
awsRegion: parsed.metadata?.AWS_REGION || "(not set)",
25+
awsKeyLength: parsed.metadata?.AWS_ACCESS_KEY_ID?.length || 0,
26+
});
27+
2028
const payload: SandboxRunPayload = {
2129
operation: parsed.operation,
2230
runId: parsed.run_id,

sandbox-sidecar/src/runners/e2bRunner.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -136,25 +136,33 @@ export class E2BSandboxRunner implements SandboxRunner {
136136
stderr: applyResult.stderr.slice(-500),
137137
}, "terraform apply output (last 500 chars)");
138138

139-
// Use terraform show to get state - works regardless of workspace configuration
140-
// This handles both: terraform.tfstate and terraform.tfstate.d/<workspace>/terraform.tfstate
139+
// Read the actual terraform.tfstate file (not terraform show -json which is different format)
140+
// Check both standard location and workspace location
141141
let stateBase64 = "";
142142

143143
try {
144-
const showResult = await this.runTerraformCommand(
145-
sandbox,
146-
workDir,
147-
["show", "-json"],
148-
undefined,
149-
undefined,
150-
metadata,
151-
);
144+
// Try standard location first
145+
let statePath = `${workDir}/terraform.tfstate`;
146+
let stateContent: string | null = null;
152147

153-
if (showResult.stdout && showResult.stdout.trim() !== "{}") {
154-
stateBase64 = Buffer.from(showResult.stdout, "utf8").toString("base64");
155-
logger.info({ stateSize: showResult.stdout.length }, "captured state via terraform show");
148+
try {
149+
stateContent = await sandbox.files.read(statePath);
150+
logger.info({ path: statePath }, "found state file at standard location");
151+
} catch {
152+
// Try workspace location - find the workspace state directory
153+
const lsResult = await sandbox.commands.run(`find ${workDir} -name "terraform.tfstate" -type f 2>/dev/null | head -1`);
154+
const foundPath = lsResult.stdout.trim();
155+
if (foundPath) {
156+
stateContent = await sandbox.files.read(foundPath);
157+
logger.info({ path: foundPath }, "found state file at workspace location");
158+
}
159+
}
160+
161+
if (stateContent && stateContent.trim()) {
162+
stateBase64 = Buffer.from(stateContent, "utf8").toString("base64");
163+
logger.info({ stateSize: stateContent.length }, "captured terraform.tfstate file");
156164
} else {
157-
logger.info("terraform show returned empty state");
165+
logger.info("no terraform.tfstate file found");
158166
}
159167
} catch (err) {
160168
// State doesn't exist - this is OK for empty applies or destroys
@@ -226,6 +234,12 @@ export class E2BSandboxRunner implements SandboxRunner {
226234
envs.AWS_REGION = metadata.AWS_REGION || "us-east-1";
227235
// Also set default region for AWS SDK
228236
envs.AWS_DEFAULT_REGION = envs.AWS_REGION;
237+
logger.info({
238+
region: envs.AWS_REGION,
239+
keyLength: envs.AWS_ACCESS_KEY_ID.length,
240+
}, "AWS credentials injected into terraform environment");
241+
} else {
242+
logger.warn("No AWS credentials in metadata - AWS resources will fail");
229243
}
230244

231245
return envs;

taco/internal/github/commands.go

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,17 @@ func (e *CommandExecutor) Execute(ctx context.Context, req *ExecuteRequest) *Com
8585

8686
// 3. Load existing state if any
8787
var state []byte
88+
logger.Info("Looking for existing state", slog.String("unit_id", unitID))
8889
if meta, err := e.unitRepo.Get(ctx, unitID); err == nil && meta != nil {
90+
logger.Info("Unit found, downloading state...")
8991
if stateData, err := e.store.Download(ctx, unitID); err == nil {
9092
state = stateData
9193
logger.Info("Loaded existing state", slog.Int("size", len(state)))
94+
} else {
95+
logger.Warn("Failed to download state", slog.String("error", err.Error()))
9296
}
97+
} else {
98+
logger.Info("No existing unit/state found", slog.String("error", fmt.Sprintf("%v", err)))
9399
}
94100

95101
// 4. Get terraform version from options or use default
@@ -140,7 +146,12 @@ func (e *CommandExecutor) Execute(ctx context.Context, req *ExecuteRequest) *Com
140146
if metadata["AWS_REGION"] == "" {
141147
metadata["AWS_REGION"] = "us-east-1"
142148
}
143-
// Don't log that credentials are present - security best practice
149+
// Log that credentials are configured (not the values)
150+
logger.Info("AWS credentials configured for sandbox",
151+
slog.String("region", metadata["AWS_REGION"]),
152+
slog.Int("key_length", len(awsKey)))
153+
} else {
154+
logger.Warn("AWS_ACCESS_KEY_ID not set - AWS resources will fail")
144155
}
145156

146157
// 8. Execute based on action
@@ -151,6 +162,8 @@ func (e *CommandExecutor) Execute(ctx context.Context, req *ExecuteRequest) *Com
151162
result = e.executeApply(ctx, logger, req, runID, unitID, archive, state, tfVersion, engine, workingDir, metadata, totalStart, false)
152163
case "destroy":
153164
result = e.executeApply(ctx, logger, req, runID, unitID, archive, state, tfVersion, engine, workingDir, metadata, totalStart, true)
165+
case "benchmark":
166+
result = e.executeBenchmark(ctx, logger, req, runID, unitID, archive, tfVersion, engine, workingDir, metadata, totalStart)
154167
default:
155168
result.Error = fmt.Sprintf("Unknown action: %s", req.Command.Action)
156169
}
@@ -303,12 +316,21 @@ func (e *CommandExecutor) executeApply(
303316
}
304317

305318
// Save the new state
319+
logger.Info("Apply result received",
320+
slog.Int("state_size", len(applyResult.State)),
321+
slog.Int("logs_size", len(applyResult.Logs)),
322+
slog.Bool("is_destroy", isDestroy))
323+
306324
if len(applyResult.State) > 0 && !isDestroy {
307325
if err := e.saveState(ctx, unitID, applyResult.State); err != nil {
308326
logger.Warn("Failed to save state", slog.String("error", err.Error()))
309327
} else {
310-
logger.Info("State saved", slog.Int("size", len(applyResult.State)))
328+
logger.Info("State saved successfully",
329+
slog.String("unit_id", unitID),
330+
slog.Int("size", len(applyResult.State)))
311331
}
332+
} else if !isDestroy {
333+
logger.Warn("No state returned from apply - state will not persist!")
312334
}
313335

314336
// For destroy, clean up the state
@@ -339,6 +361,125 @@ func (e *CommandExecutor) executeApply(
339361
return result
340362
}
341363

364+
// executeBenchmark runs apply followed by destroy in a single flow
365+
// This keeps state in the sandbox and ensures resources are cleaned up
366+
func (e *CommandExecutor) executeBenchmark(
367+
ctx context.Context,
368+
logger *slog.Logger,
369+
req *ExecuteRequest,
370+
runID, unitID string,
371+
archive []byte,
372+
tfVersion, engine, workingDir string,
373+
metadata map[string]string,
374+
totalStart time.Time,
375+
) *CommandResult {
376+
result := &CommandResult{
377+
Command: req.Command,
378+
Success: false,
379+
}
380+
381+
if e.sandbox == nil {
382+
result.Error = "Sandbox provider not configured"
383+
result.Timing.Total = time.Since(totalStart)
384+
return result
385+
}
386+
387+
// Generate a config version ID for the sandbox
388+
configVersionID := fmt.Sprintf("cv-%s", uuid.New().String()[:8])
389+
390+
logger.Info("Starting benchmark: apply + destroy cycle",
391+
slog.String("run_id", runID),
392+
slog.String("engine", engine),
393+
slog.String("version", tfVersion))
394+
395+
var allLogs strings.Builder
396+
397+
// Phase 1: Apply
398+
applyStart := time.Now()
399+
applyReq := &sandbox.ApplyRequest{
400+
RunID: runID,
401+
PlanID: uuid.New().String(),
402+
OrgID: "github-benchmark",
403+
UnitID: unitID,
404+
ConfigurationVersionID: configVersionID,
405+
IsDestroy: false,
406+
TerraformVersion: tfVersion,
407+
Engine: engine,
408+
WorkingDirectory: workingDir,
409+
ConfigArchive: archive,
410+
State: nil, // Fresh apply
411+
Metadata: metadata,
412+
}
413+
414+
applyResult, err := e.sandbox.ExecuteApply(ctx, applyReq)
415+
result.Timing.Apply = time.Since(applyStart)
416+
417+
if err != nil {
418+
result.Error = fmt.Sprintf("Apply phase failed: %v", err)
419+
result.Timing.Total = time.Since(totalStart)
420+
logger.Error("Benchmark apply failed", slog.String("error", err.Error()))
421+
return result
422+
}
423+
424+
allLogs.WriteString("=== APPLY PHASE ===\n")
425+
allLogs.WriteString(applyResult.Logs)
426+
allLogs.WriteString("\n\n")
427+
428+
logger.Info("Benchmark apply completed",
429+
slog.Duration("duration", result.Timing.Apply),
430+
slog.Int("state_size", len(applyResult.State)))
431+
432+
// Phase 2: Destroy (using state from apply)
433+
destroyStart := time.Now()
434+
destroyReq := &sandbox.ApplyRequest{
435+
RunID: runID + "-destroy",
436+
PlanID: uuid.New().String(),
437+
OrgID: "github-benchmark",
438+
UnitID: unitID,
439+
ConfigurationVersionID: configVersionID,
440+
IsDestroy: true,
441+
TerraformVersion: tfVersion,
442+
Engine: engine,
443+
WorkingDirectory: workingDir,
444+
ConfigArchive: archive,
445+
State: applyResult.State, // Use state from apply
446+
Metadata: metadata,
447+
}
448+
449+
destroyResult, err := e.sandbox.ExecuteApply(ctx, destroyReq)
450+
result.Timing.Destroy = time.Since(destroyStart)
451+
452+
if err != nil {
453+
result.Error = fmt.Sprintf("Destroy phase failed (resources may be orphaned!): %v", err)
454+
result.Timing.Total = time.Since(totalStart)
455+
logger.Error("Benchmark destroy failed", slog.String("error", err.Error()))
456+
return result
457+
}
458+
459+
allLogs.WriteString("=== DESTROY PHASE ===\n")
460+
allLogs.WriteString(destroyResult.Logs)
461+
462+
logger.Info("Benchmark destroy completed",
463+
slog.Duration("duration", result.Timing.Destroy))
464+
465+
// Success!
466+
result.Success = true
467+
result.Output = allLogs.String()
468+
result.Summary = fmt.Sprintf("Apply: %.2fs | Destroy: %.2fs | Total: %.2fs",
469+
result.Timing.Apply.Seconds(),
470+
result.Timing.Destroy.Seconds(),
471+
time.Since(totalStart).Seconds())
472+
473+
result.Timing.Total = time.Since(totalStart)
474+
475+
logger.Info("Benchmark completed successfully",
476+
slog.Duration("apply", result.Timing.Apply),
477+
slog.Duration("destroy", result.Timing.Destroy),
478+
slog.Duration("total", result.Timing.Total))
479+
480+
return result
481+
}
482+
342483
func (e *CommandExecutor) saveState(ctx context.Context, unitID string, state []byte) error {
343484
// Check if unit exists, create if not
344485
if _, err := e.unitRepo.Get(ctx, unitID); err != nil {

taco/internal/github/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ type TimingBreakdown struct {
128128
Clone time.Duration `json:"clone"`
129129
Init time.Duration `json:"init"`
130130
Execute time.Duration `json:"execute"` // plan, apply, or destroy time
131+
Apply time.Duration `json:"apply,omitempty"` // for benchmark: apply phase
132+
Destroy time.Duration `json:"destroy,omitempty"` // for benchmark: destroy phase
131133
Total time.Duration `json:"total"`
132134
}
133135

taco/internal/github/webhook.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ func ParseCommand(text string) *Command {
218218

219219
// Validate action
220220
switch action {
221-
case "plan", "apply", "destroy":
221+
case "plan", "apply", "destroy", "benchmark":
222222
cmd := &Command{
223223
Action: action,
224224
Options: make(map[string]string),
@@ -273,6 +273,12 @@ func FormatResult(result *CommandResult) string {
273273
} else {
274274
sb.WriteString("## ❌ OpenTaco Destroy Failed\n\n")
275275
}
276+
case "benchmark":
277+
if result.Success {
278+
sb.WriteString("## ✅ OpenTaco Benchmark Complete\n\n")
279+
} else {
280+
sb.WriteString("## ❌ OpenTaco Benchmark Failed\n\n")
281+
}
276282
}
277283

278284
// Timing breakdown
@@ -282,7 +288,9 @@ func FormatResult(result *CommandResult) string {
282288
if result.Timing.Clone > 0 {
283289
sb.WriteString(fmt.Sprintf("| Clone | %.2fs |\n", result.Timing.Clone.Seconds()))
284290
}
285-
sb.WriteString(fmt.Sprintf("| Init | %.2fs |\n", result.Timing.Init.Seconds()))
291+
if result.Timing.Init > 0 {
292+
sb.WriteString(fmt.Sprintf("| Init | %.2fs |\n", result.Timing.Init.Seconds()))
293+
}
286294

287295
switch result.Command.Action {
288296
case "plan":
@@ -291,6 +299,9 @@ func FormatResult(result *CommandResult) string {
291299
sb.WriteString(fmt.Sprintf("| Apply | %.2fs |\n", result.Timing.Execute.Seconds()))
292300
case "destroy":
293301
sb.WriteString(fmt.Sprintf("| Destroy | %.2fs |\n", result.Timing.Execute.Seconds()))
302+
case "benchmark":
303+
sb.WriteString(fmt.Sprintf("| Apply | %.2fs |\n", result.Timing.Apply.Seconds()))
304+
sb.WriteString(fmt.Sprintf("| Destroy | %.2fs |\n", result.Timing.Destroy.Seconds()))
294305
}
295306

296307
sb.WriteString("\n")

0 commit comments

Comments
 (0)