Skip to content

Commit 5619dbd

Browse files
committed
add vcs benchmark capability
1 parent 6309ae6 commit 5619dbd

File tree

9 files changed

+1504
-12
lines changed

9 files changed

+1504
-12
lines changed

sandbox-sidecar/src/runners/e2bRunner.ts

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,32 @@ export class E2BSandboxRunner implements SandboxRunner {
4747
appendLog?.(chunk);
4848
};
4949

50-
// Run terraform init
50+
// Run terraform init (with AWS creds if configured for benchmark)
51+
const metadata = job.payload.metadata;
5152
await this.runTerraformCommand(
5253
sandbox,
5354
workDir,
5455
["init", "-input=false", "-no-color"],
5556
logs,
5657
streamLog,
58+
metadata,
5759
);
5860

5961
// Run terraform plan
6062
const planArgs = ["plan", "-input=false", "-no-color", "-out=tfplan.binary"];
6163
if (job.payload.isDestroy) {
6264
planArgs.splice(1, 0, "-destroy");
6365
}
64-
await this.runTerraformCommand(sandbox, workDir, planArgs, logs, streamLog);
66+
await this.runTerraformCommand(sandbox, workDir, planArgs, logs, streamLog, metadata);
6567

6668
// Get plan JSON
6769
const showResult = await this.runTerraformCommand(
6870
sandbox,
6971
workDir,
7072
["show", "-json", "tfplan.binary"],
73+
undefined,
74+
undefined,
75+
metadata,
7176
);
7277

7378
const planJSON = showResult.stdout;
@@ -103,30 +108,61 @@ export class E2BSandboxRunner implements SandboxRunner {
103108
appendLog?.(chunk);
104109
};
105110

106-
// Run terraform init
111+
// Run terraform init (with AWS creds if configured for benchmark)
112+
const metadata = job.payload.metadata;
107113
await this.runTerraformCommand(
108114
sandbox,
109115
workDir,
110116
["init", "-input=false", "-no-color"],
111117
logs,
112118
streamLog,
119+
metadata,
113120
);
114121

115122
// Run terraform apply/destroy
116123
const applyCommand = job.payload.isDestroy ? "destroy" : "apply";
117-
await this.runTerraformCommand(
124+
const applyResult = await this.runTerraformCommand(
118125
sandbox,
119126
workDir,
120127
[applyCommand, "-auto-approve", "-input=false", "-no-color"],
121128
logs,
122129
streamLog,
130+
metadata,
123131
);
124132

125-
// Read the state file
126-
const statePath = `${workDir}/terraform.tfstate`;
127-
const stateContent = await sandbox.files.read(statePath);
133+
// Log the apply output for debugging
134+
logger.info({
135+
stdout: applyResult.stdout.slice(-500),
136+
stderr: applyResult.stderr.slice(-500),
137+
}, "terraform apply output (last 500 chars)");
138+
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
141+
let stateBase64 = "";
142+
143+
try {
144+
const showResult = await this.runTerraformCommand(
145+
sandbox,
146+
workDir,
147+
["show", "-json"],
148+
undefined,
149+
undefined,
150+
metadata,
151+
);
152+
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");
156+
} else {
157+
logger.info("terraform show returned empty state");
158+
}
159+
} catch (err) {
160+
// State doesn't exist - this is OK for empty applies or destroys
161+
logger.warn({ error: err }, "no state found after apply (may be empty apply)");
162+
}
163+
128164
const result: SandboxRunResult = {
129-
state: Buffer.from(stateContent, "utf8").toString("base64"),
165+
state: stateBase64,
130166
};
131167

132168
return { logs: logs.join(""), result };
@@ -169,6 +205,27 @@ export class E2BSandboxRunner implements SandboxRunner {
169205
return { sandbox, needsInstall };
170206
}
171207

208+
/**
209+
* Build environment variables for Terraform execution.
210+
* Includes AWS credentials if provided in metadata for benchmark runs.
211+
*/
212+
private buildTerraformEnvs(metadata?: Record<string, string>): Record<string, string> {
213+
const envs: Record<string, string> = {
214+
TF_IN_AUTOMATION: "1",
215+
};
216+
217+
// Inject AWS credentials if provided (for benchmark runs with real resources)
218+
if (metadata?.AWS_ACCESS_KEY_ID) {
219+
envs.AWS_ACCESS_KEY_ID = metadata.AWS_ACCESS_KEY_ID;
220+
envs.AWS_SECRET_ACCESS_KEY = metadata.AWS_SECRET_ACCESS_KEY || "";
221+
envs.AWS_REGION = metadata.AWS_REGION || "us-east-1";
222+
// Also set default region for AWS SDK
223+
envs.AWS_DEFAULT_REGION = envs.AWS_REGION;
224+
}
225+
226+
return envs;
227+
}
228+
172229

173230
private async installIacTool(sandbox: Sandbox, engine: string, version: string): Promise<void> {
174231
logger.info({ engine, version }, "installing IaC tool at runtime");
@@ -231,6 +288,20 @@ export class E2BSandboxRunner implements SandboxRunner {
231288
// Use gunzip + tar separately for better compatibility across tar versions
232289
await sandbox.commands.run(`cd ${workDir} && gunzip -c bundle.tar.gz | tar -x --exclude='terraform.tfstate' --exclude='terraform.tfstate.backup'`);
233290

291+
// Debug: List extracted files to understand archive structure
292+
const listResult = await sandbox.commands.run(`find ${workDir} -type f -name "*.tf" | head -20`);
293+
logger.info({
294+
tfFiles: listResult.stdout.trim().split('\n').filter(Boolean),
295+
workDir,
296+
workingDirectory: job.payload.workingDirectory || '(none)'
297+
}, "extracted terraform files");
298+
299+
// Also list all files for debugging
300+
const allFilesResult = await sandbox.commands.run(`ls -la ${workDir}`);
301+
logger.info({
302+
files: allFilesResult.stdout
303+
}, "workspace directory listing");
304+
234305
// Determine the execution directory
235306
const execDir = job.payload.workingDirectory
236307
? `${workDir}/${job.payload.workingDirectory}`
@@ -273,6 +344,7 @@ export class E2BSandboxRunner implements SandboxRunner {
273344
args: string[],
274345
logBuffer?: string[],
275346
appendLog?: (chunk: string) => void,
347+
metadata?: Record<string, string>,
276348
): Promise<{ stdout: string; stderr: string }> {
277349
const engine = (sandbox as any)._requestedEngine || "terraform";
278350
const binaryName = engine === "tofu" ? "tofu" : "terraform";
@@ -291,9 +363,7 @@ export class E2BSandboxRunner implements SandboxRunner {
291363

292364
const result = await sandbox.commands.run(cmdStr, {
293365
cwd,
294-
envs: {
295-
TF_IN_AUTOMATION: "1",
296-
},
366+
envs: this.buildTerraformEnvs(metadata),
297367
onStdout: pipeChunk,
298368
onStderr: pipeChunk,
299369
});

taco/cmd/statesman/main.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99
"os"
1010
"os/signal"
11+
"strings"
1112
"time"
1213

1314
"github.com/diggerhq/digger/opentaco/internal/analytics"
@@ -21,6 +22,7 @@ import (
2122
"github.com/diggerhq/digger/opentaco/internal/repositories"
2223
"github.com/diggerhq/digger/opentaco/internal/sandbox"
2324
"github.com/diggerhq/digger/opentaco/internal/storage"
25+
"github.com/google/uuid"
2426
"github.com/kelseyhightower/envconfig"
2527
"github.com/labstack/echo/v4"
2628
echomiddleware "github.com/labstack/echo/v4/middleware"
@@ -101,7 +103,16 @@ func main() {
101103
if err != nil {
102104
slog.Warn("Failed to list units from storage", "error", err)
103105
} else {
106+
syncedCount := 0
107+
skippedCount := 0
104108
for _, unit := range units {
109+
// Skip non-unit paths (config-versions, plans, runs, etc.)
110+
// Valid unit paths are: {org-uuid}/{unit-uuid}
111+
if !isValidUnitPath(unit.ID) {
112+
skippedCount++
113+
continue
114+
}
115+
105116
if err := queryStore.SyncEnsureUnit(context.Background(), unit.ID); err != nil {
106117
slog.Warn("Failed to sync unit", "unit_id", unit.ID, "error", err)
107118
continue
@@ -110,8 +121,9 @@ func main() {
110121
if err := queryStore.SyncUnitMetadata(context.Background(), unit.ID, unit.Size, unit.Updated); err != nil {
111122
slog.Warn("Failed to sync metadata for unit", "unit_id", unit.ID, "error", err)
112123
}
124+
syncedCount++
113125
}
114-
slog.Info("Synced units from storage to database", "count", len(units))
126+
slog.Info("Synced units from storage to database", "synced", syncedCount, "skipped_non_units", skippedCount)
115127
}
116128
} else {
117129
slog.Info("Query backend already has units, skipping sync", "count", len(existingUnits))
@@ -275,3 +287,22 @@ func main() {
275287
analytics.SendEssential("server_shutdown_complete")
276288
slog.Info("Server shutdown complete")
277289
}
290+
291+
// isValidUnitPath checks if a storage path matches the expected unit format: {org-uuid}/{unit-uuid}
292+
// This filters out TFE-related paths like config-versions/, plans/, runs/, etc.
293+
func isValidUnitPath(path string) bool {
294+
parts := strings.SplitN(strings.Trim(path, "/"), "/", 2)
295+
if len(parts) != 2 {
296+
return false
297+
}
298+
299+
// Both parts must be valid UUIDs
300+
if _, err := uuid.Parse(parts[0]); err != nil {
301+
return false
302+
}
303+
if _, err := uuid.Parse(parts[1]); err != nil {
304+
return false
305+
}
306+
307+
return true
308+
}

taco/cmd/statesman/statesman

40 MB
Binary file not shown.

taco/internal/api/routes.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import (
55
"fmt"
66
"log"
77
"net/http"
8+
"os"
89
"time"
910

1011
"github.com/diggerhq/digger/opentaco/internal/analytics"
12+
"github.com/diggerhq/digger/opentaco/internal/github"
1113
"github.com/diggerhq/digger/opentaco/internal/tfe"
1214

1315
authpkg "github.com/diggerhq/digger/opentaco/internal/auth"
@@ -362,6 +364,48 @@ func RegisterRoutes(e *echo.Echo, deps Dependencies) {
362364
})
363365
})
364366

367+
// Register GitHub webhook for benchmarks (if OPENTACO_GITHUB_TOKEN is set)
368+
RegisterGitHubWebhook(e, deps)
369+
365370
// Register webhook-authenticated internal routes (if OPENTACO_ENABLE_INTERNAL_ENDPOINTS is set)
366371
RegisterInternalRoutes(e, deps)
367372
}
373+
374+
// RegisterGitHubWebhook registers the GitHub webhook endpoint for benchmark operations.
375+
// This enables /opentaco plan, /opentaco apply, /opentaco destroy commands via PR comments.
376+
// Required env vars (BOTH must be set to enable):
377+
// - OPENTACO_GITHUB_TOKEN: GitHub personal access token or app token
378+
// - OPENTACO_GITHUB_WEBHOOK_SECRET: Secret for validating webhook signatures (required for security)
379+
func RegisterGitHubWebhook(e *echo.Echo, deps Dependencies) {
380+
githubToken := os.Getenv("OPENTACO_GITHUB_TOKEN")
381+
webhookSecret := os.Getenv("OPENTACO_GITHUB_WEBHOOK_SECRET")
382+
383+
// Require BOTH token and secret to enable - security by default
384+
if githubToken == "" || webhookSecret == "" {
385+
if githubToken != "" && webhookSecret == "" {
386+
log.Println("WARNING: OPENTACO_GITHUB_TOKEN set but OPENTACO_GITHUB_WEBHOOK_SECRET missing - webhook disabled for security")
387+
}
388+
return
389+
}
390+
391+
log.Println("Registering GitHub webhook endpoint at /webhooks/github")
392+
393+
// Create GitHub client
394+
ghClient := github.NewClient(githubToken)
395+
396+
// Create command executor with sandbox and storage
397+
executor := github.NewCommandExecutor(
398+
ghClient,
399+
deps.Sandbox,
400+
deps.Repository,
401+
deps.BlobStore,
402+
)
403+
404+
// Create webhook handler
405+
handler := github.NewWebhookHandler(ghClient, executor)
406+
407+
// Register the webhook endpoint (no auth required - uses webhook signature validation)
408+
e.POST("/webhooks/github", handler.HandleWebhook)
409+
410+
log.Println("GitHub webhook registered successfully")
411+
}

0 commit comments

Comments
 (0)