// A Dagger module for managing Infrastructure as Code (IaC) workflows using OpenTofu. package main import ( "context" "fmt" "strings" "dagger/iac/internal/dagger" ) type Iac struct{} // Run all static validation and formatting checks on the OpenTofu code. // This implements Dagger's standard "Checks" design pattern, verifying structure and styling without running speculative plans. func (m *Iac) Check( ctx context.Context, // The directory containing the OpenTofu code. source *dagger.Directory, // Optional GCP credentials JSON (service account key or local ADC json) passed as a Secret. // +optional gcpCreds *dagger.Secret, // Optional GCP Project ID to inject as an environment variable. // +optional projectID string, // Optional custom Docker/OCI image. Defaults to alpine:3. // +optional baseImage string, ) (string, error) { container := m.baseContainer(source, gcpCreds, projectID, baseImage) // Sequentially execute 'tofu init', 'tofu fmt -check', 'tofu validate', and 'validator' res, err := container. WithExec([]string{"tofu", "init", "-no-color"}). WithExec([]string{"tofu", "fmt", "-no-color", "-check"}). WithExec([]string{"tofu", "validate", "-no-color"}). WithExec([]string{"validator", "-gitignore", "-groupby", "pass-fail", "."}). Stdout(ctx) if err != nil { return "", fmt.Errorf("static checks failed: %w\n%s", err, res) } return "All static checks (tofu fmt, tofu validate & validator) passed successfully! 🎉", nil } // Perform the complete Pull Request (CI) check workflow. // This runs TFLint, Format checking, Static Validation, and Speculative Planning sequentially, // posting individual VCS commit status checks, and finally posts or updates a single consolidated feedback comment. func (m *Iac) PRWorkflow( ctx context.Context, // The directory containing the OpenTofu code. source *dagger.Directory, // Optional GCP credentials JSON (service account key or local ADC json) passed as a Secret. // +optional gcpCreds *dagger.Secret, // Optional GCP Project ID to inject as an environment variable. // +optional projectID string, // Optional custom Docker/OCI image. Defaults to alpine:3. // +optional baseImage string, // Optional VCS platform provider: "github", "github-enterprise", or "gitea". Defaults to "github". // +optional vcsType string, // Optional VCS Token (Personal Access Token or GitHub Action Token) for posting statuses and comments. // +optional vcsToken *dagger.Secret, // Optional VCS repository name formatted as "owner/repo" (e.g. "jswank/google-cloud-cli"). // +optional vcsRepo string, // Optional Git Commit SHA to register status checks against. // +optional vcsSha string, // Optional target URL callback to attach to status checks (e.g., Cloud Build url). // +optional vcsTargetURL string, // Optional base URL of the private Gitea or GitHub Enterprise server instance. // +optional vcsBaseURL string, // Optional Pull Request / Issue Number to post consolidated Markdown feedback to. // +optional vcsPRNumber int, // Optional Build ID label to attach to the comment footer (e.g. Cloud Build build ID). // +optional buildID string, ) (string, error) { if vcsType == "" { vcsType = "github" } if buildID == "" { buildID = "local-run" } // 1. Mark all individual status checks as pending in the VCS _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/tflint", "pending", "Waiting...", vcsTargetURL) _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/fmt", "pending", "Waiting...", vcsTargetURL) _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/validate", "pending", "Waiting...", vcsTargetURL) _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/plan", "pending", "Waiting...", vcsTargetURL) // 2. Setup the base execution container container := m.baseContainer(source, gcpCreds, projectID, baseImage) // 3. Initialize OpenTofu (required before running validate, plan, etc.) initContainer := container.WithExec([]string{"tofu", "init", "-no-color"}) _, err := initContainer.Stdout(ctx) if err != nil { // If tofu init itself fails, mark validate and plan as failed immediately _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/validate", "failure", "Init failed", vcsTargetURL) _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/plan", "failure", "Init failed", vcsTargetURL) return "", fmt.Errorf("tofu init failed: %w", err) } // 4. Run CI Steps Sequentially to Capture Outputs and Exit Codes // We wrap commands in thin shell executors to write outputs and exit codes to the local workspace filesystem. execContainer := initContainer. WithExec([]string{"sh", "-c", "tflint --init && tflint > /workspace/tflint-output.txt 2>&1; echo $? > /workspace/tflint.exit"}). WithExec([]string{"sh", "-c", "tofu fmt -no-color -check > /workspace/fmt-output.txt 2>&1; echo $? > /workspace/fmt.exit"}). WithExec([]string{"sh", "-c", "tofu validate -no-color > /workspace/validate-output.txt 2>&1; echo $? > /workspace/validate.exit"}). WithExec([]string{"sh", "-c", "tofu plan -no-color -detailed-exitcode > /workspace/plan-output.txt 2>&1; echo $? > /workspace/plan.exit"}) // 5. Ingest Outputs and Exit Codes from Container Sandbox tflintOut, _ := execContainer.File("/workspace/tflint-output.txt").Contents(ctx) tflintCodeRaw, _ := execContainer.File("/workspace/tflint.exit").Contents(ctx) tflintCode := strings.TrimSpace(tflintCodeRaw) fmtOut, _ := execContainer.File("/workspace/fmt-output.txt").Contents(ctx) fmtCodeRaw, _ := execContainer.File("/workspace/fmt.exit").Contents(ctx) fmtCode := strings.TrimSpace(fmtCodeRaw) validateOut, _ := execContainer.File("/workspace/validate-output.txt").Contents(ctx) validateCodeRaw, _ := execContainer.File("/workspace/validate.exit").Contents(ctx) validateCode := strings.TrimSpace(validateCodeRaw) planOut, _ := execContainer.File("/workspace/plan-output.txt").Contents(ctx) planCodeRaw, _ := execContainer.File("/workspace/plan.exit").Contents(ctx) planCode := strings.TrimSpace(planCodeRaw) // 6. Post final success/failure commit status checks based on actual execution exit codes if tflintCode == "0" { _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/tflint", "success", "TFLint passed", vcsTargetURL) } else { _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/tflint", "failure", "TFLint found static issues", vcsTargetURL) } if fmtCode == "0" { _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/fmt", "success", "Format check passed", vcsTargetURL) } else { _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/fmt", "failure", "Formatting issues detected", vcsTargetURL) } if validateCode == "0" { _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/validate", "success", "Validation passed", vcsTargetURL) } else { _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/validate", "failure", "Static validation failed", vcsTargetURL) } if planCode == "0" || planCode == "2" { _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/plan", "success", "Speculative plan completed", vcsTargetURL) } else { _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, "ci/plan", "failure", "Plan generation failed", vcsTargetURL) } // 7. Assemble and post/update a single consolidated comment directly on Gitea/GitHub if vcsPRNumber > 0 { commentBody := m.buildConsolidatedPRComment( tflintCode, fmtCode, validateCode, planCode, tflintOut, fmtOut, validateOut, planOut, buildID, vcsTargetURL, ) _ = m.postOrUpdatePRComment(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsPRNumber, commentBody) } // Return consolidated logs for direct display in the GCB console stdout := fmt.Sprintf("--- TFLINT OUT ---\n%s\n--- FMT OUT ---\n%s\n--- VALIDATE OUT ---\n%s\n--- PLAN OUT ---\n%s\n", tflintOut, fmtOut, validateOut, planOut) if tflintCode != "0" || fmtCode != "0" || validateCode != "0" || (planCode != "0" && planCode != "2") { return stdout, fmt.Errorf("CI pipeline failed some gating steps [tflint: %s, fmt: %s, validate: %s, plan: %s]", tflintCode, fmtCode, validateCode, planCode) } return stdout, nil } // Perform OpenTofu initialization and validation (tofu validate). func (m *Iac) Validate( ctx context.Context, // The directory containing the OpenTofu code. source *dagger.Directory, // Optional GCP credentials JSON (service account key or local ADC json) passed as a Secret. // +optional gcpCreds *dagger.Secret, // Optional GCP Project ID to inject as an environment variable. // +optional projectID string, // Optional custom Docker/OCI image containing OpenTofu. Defaults to ghcr.io/opentofu/opentofu:1.8.8. // +optional baseImage string, // Optional VCS platform provider: "github", "github-enterprise", or "gitea". Defaults to "github". // +optional vcsType string, // Optional VCS Token (Personal Access Token or GitHub Action Token) for posting statuses. // +optional vcsToken *dagger.Secret, // Optional VCS repository name formatted as "owner/repo" (e.g. "jswank/google-cloud-cli"). // +optional vcsRepo string, // Optional Git Commit SHA to register status checks against. // +optional vcsSha string, // Optional target URL callback to attach to status checks (e.g., Cloud Build url). // +optional vcsTargetURL string, // Optional base URL of the private Gitea or GitHub Enterprise server instance (e.g. "https://github.mycompany.com"). // +optional vcsBaseURL string, // Optional Pull Request / Issue Number to post rich feedback comments to. // +optional vcsPRNumber int, ) (string, error) { if vcsType == "" { vcsType = "github" } contextName := "ci/validate" _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, contextName, "pending", "Running OpenTofu static validation...", vcsTargetURL) container := m.baseContainer(source, gcpCreds, projectID, baseImage) res, err := container. WithExec([]string{"tofu", "init", "-no-color"}). WithExec([]string{"tofu", "validate", "-no-color"}). Stdout(ctx) if err != nil { _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, contextName, "failure", "Validation failed!", vcsTargetURL) // Post a detailed error comment to the PR if vcsPRNumber > 0 { commentBody := m.buildValidationComment(vcsRepo, vcsSha, res, err) _ = m.postOrUpdatePRComment(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsPRNumber, commentBody) } return "", fmt.Errorf("validation failed: %w", err) } _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, contextName, "success", "Validation succeeded.", vcsTargetURL) // Post a success comment to the PR if vcsPRNumber > 0 { commentBody := m.buildValidationComment(vcsRepo, vcsSha, res, nil) _ = m.postOrUpdatePRComment(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsPRNumber, commentBody) } return res, nil } // Perform OpenTofu planning (tofu plan). Returns the plan output text. func (m *Iac) Plan( ctx context.Context, // The directory containing the OpenTofu code. source *dagger.Directory, // Optional GCP credentials JSON (service account key or local ADC json) passed as a Secret. // +optional gcpCreds *dagger.Secret, // Optional GCP Project ID to inject as an environment variable. // +optional projectID string, // Optional custom Docker/OCI image containing OpenTofu. // +optional baseImage string, // Optional VCS platform provider: "github", "github-enterprise", or "gitea". Defaults to "github". // +optional vcsType string, // Optional VCS Token (Personal Access Token or GitHub Action Token) for posting statuses. // +optional vcsToken *dagger.Secret, // Optional VCS repository name formatted as "owner/repo" (e.g. "jswank/google-cloud-cli"). // +optional vcsRepo string, // Optional Git Commit SHA to register status checks against. // +optional vcsSha string, // Optional target URL callback to attach to status checks (e.g., Cloud Build url). // +optional vcsTargetURL string, // Optional base URL of the private Gitea or GitHub Enterprise server instance. // +optional vcsBaseURL string, // Optional Pull Request / Issue Number to post rich feedback comments to. // +optional vcsPRNumber int, ) (string, error) { if vcsType == "" { vcsType = "github" } contextName := "ci/plan" _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, contextName, "pending", "Generating speculative plan...", vcsTargetURL) container := m.baseContainer(source, gcpCreds, projectID, baseImage) res, err := container. WithExec([]string{"tofu", "init", "-no-color"}). WithExec([]string{"tofu", "plan", "-no-color"}). Stdout(ctx) if err != nil { _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, contextName, "failure", "Plan failed!", vcsTargetURL) // Post a detailed error comment to the PR if vcsPRNumber > 0 { commentBody := m.buildPlanComment(vcsRepo, vcsSha, res, err) _ = m.postOrUpdatePRComment(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsPRNumber, commentBody) } return "", fmt.Errorf("planning failed: %w", err) } _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, contextName, "success", "Plan generated successfully.", vcsTargetURL) // Post a rich success summary comment to the PR if vcsPRNumber > 0 { commentBody := m.buildPlanComment(vcsRepo, vcsSha, res, nil) _ = m.postOrUpdatePRComment(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsPRNumber, commentBody) } return res, nil } // Perform OpenTofu planning and export the plan binary file. func (m *Iac) PlanExport( ctx context.Context, // The directory containing the OpenTofu code. source *dagger.Directory, // Optional GCP credentials JSON (service account key or local ADC json) passed as a Secret. // +optional gcpCreds *dagger.Secret, // Optional GCP Project ID to inject as an environment variable. // +optional projectID string, // Optional custom Docker/OCI image containing OpenTofu. // +optional baseImage string, // Optional VCS platform provider: "github", "github-enterprise", or "gitea". Defaults to "github". // +optional vcsType string, // Optional VCS Token (Personal Access Token or GitHub Action Token) for posting statuses. // +optional vcsToken *dagger.Secret, // Optional VCS repository name formatted as "owner/repo" (e.g. "jswank/google-cloud-cli"). // +optional vcsRepo string, // Optional Git Commit SHA to register status checks against. // +optional vcsSha string, // Optional target URL callback to attach to status checks (e.g., Cloud Build url). // +optional vcsTargetURL string, // Optional base URL of the private Gitea or GitHub Enterprise server instance. // +optional vcsBaseURL string, ) (*dagger.File, error) { if vcsType == "" { vcsType = "github" } contextName := "cd/plan-export" _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, contextName, "pending", "Generating and exporting plan file...", vcsTargetURL) container := m.baseContainer(source, gcpCreds, projectID, baseImage) planFile := "/workspace/tfplan" container = container. WithExec([]string{"tofu", "init", "-no-color"}). WithExec([]string{"tofu", "plan", "-no-color", "-out=" + planFile}) file := container.File(planFile) _, err := container.Stdout(ctx) if err != nil { _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, contextName, "failure", "Plan export failed!", vcsTargetURL) return nil, fmt.Errorf("planning and exporting failed: %w", err) } _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, contextName, "success", "Plan exported successfully.", vcsTargetURL) return file, nil } // Perform OpenTofu applying (tofu apply). func (m *Iac) Apply( ctx context.Context, // The directory containing the OpenTofu code. source *dagger.Directory, // Optional pre-generated plan file from PlanExport. If provided, apply will use this plan. // +optional planFile *dagger.File, // Optional GCP credentials JSON (service account key or local ADC json) passed as a Secret. // +optional gcpCreds *dagger.Secret, // Optional GCP Project ID to inject as an environment variable. // +optional projectID string, // Optional custom Docker/OCI image containing OpenTofu. // +optional baseImage string, // Optional VCS platform provider: "github", "github-enterprise", or "gitea". Defaults to "github". // +optional vcsType string, // Optional VCS Token (Personal Access Token or GitHub Action Token) for posting statuses. // +optional vcsToken *dagger.Secret, // Optional VCS repository name formatted as "owner/repo" (e.g. "jswank/google-cloud-cli"). // +optional vcsRepo string, // Optional Git Commit SHA to register status checks against. // +optional vcsSha string, // Optional target URL callback to attach to status checks (e.g., Cloud Build url). // +optional vcsTargetURL string, // Optional base URL of the private Gitea or GitHub Enterprise server instance. // +optional vcsBaseURL string, ) (string, error) { if vcsType == "" { vcsType = "github" } contextName := "cd/apply" _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, contextName, "pending", "Applying infrastructure changes...", vcsTargetURL) container := m.baseContainer(source, gcpCreds, projectID, baseImage) container = container.WithExec([]string{"tofu", "init", "-no-color"}) if planFile != nil { mountedPlanPath := "/workspace/tfplan" container = container. WithFile(mountedPlanPath, planFile). WithExec([]string{"tofu", "apply", "-no-color", mountedPlanPath}) } else { container = container.WithExec([]string{"tofu", "apply", "-no-color", "-auto-approve"}) } res, err := container.Stdout(ctx) if err != nil { _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, contextName, "failure", "Apply failed!", vcsTargetURL) return "", fmt.Errorf("applying failed: %w", err) } _ = m.postVCSStatus(ctx, vcsToken, vcsType, vcsBaseURL, vcsRepo, vcsSha, contextName, "success", "Infrastructure applied successfully.", vcsTargetURL) return res, nil }