Skip to content

currently only creates a file in memory but can scaffold to a real t…#12

Merged
joshuasilva414 merged 7 commits into
devfrom
feat/tf-scaffold
Mar 6, 2026
Merged

currently only creates a file in memory but can scaffold to a real t…#12
joshuasilva414 merged 7 commits into
devfrom
feat/tf-scaffold

Conversation

@deeg454

@deeg454 deeg454 commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

What

  • takes in a list of graph nodes and converts it to a intermediate representation of terraform objects and creates a terraform file with a map of those objects

Why

  • We need backend starter code to be used for taking our intermediate representation and turning it into an actual terraform file

Files Added/ Modified

  • napkin-backend/compiler
  • compiler/target.go
  • compiler/tf_writer.go
  • compiler/tf_target.go
  • compiler/tf_target_test.go
  • compiler/tf_writer_test.go

Resolves

Summary by CodeRabbit

  • New Features
    • Added a compilation workflow that converts internal infrastructure definitions into Terraform configuration and writes an output .tf file on startup, including provider and resource blocks with quoted attributes.
  • Tests
    • Added unit tests validating compilation from the internal graph to Terraform structures and verifying generated .tf file content.

@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ed4c06d6-8053-4897-8895-55265fafd7ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds exported IR and Terraform model types and a Target interface, implements TerraformTarget to compile an IR into a TFFile, adds a writer that emits HCL to disk, unit/integration tests for compile and write, and invokes compilation in main to produce output.tf.

Changes

Cohort / File(s) Summary
IR & Target API
napkin-backend/compiler/target.go
Adds exported IR representation and TF model types (NodeClass, GraphNode, IR, TFBlock, TFFile, TFResource, TFModule, TFOutput) and the Target interface (Compile(ir IR) (*TFFile, error)).
Terraform target implementation
napkin-backend/compiler/tf_target.go
Adds TerraformTarget with Compile that iterates IR.Nodes, logs nodes, maps node fields/attributes into TFBlocks with class-specific label rules, builds initial terraform { required_providers ... } block, and returns a TFFile. Includes var _ Target = (*TerraformTarget)(nil).
Writer (HCL emitter)
napkin-backend/compiler/tf_writer.go
Adds WriteTerraformFile(tf *TFFile, path string) error and an unexported recursive writeBlock that renders TFBlocks into HCL (labels, attributes, nested blocks) and writes the result to disk via os.WriteFile.
Tests for compiler & writer
napkin-backend/compiler/tf_target_test.go, napkin-backend/compiler/tr_writer_test.go, napkin-backend/compiler/tf_run _test.go
Adds unit test for TerraformTarget.Compile asserting a single node becomes expected resource; integration test for WriteTerraformFile writing/reading a temp .tf; and a placeholder package file tf_run _test.go.
Main integration & output
napkin-backend/main.go, napkin-backend/output.tf
Main now constructs a sample IR (aws provider + aws_instance), calls the compiler and writer to produce output.tf. output.tf added showing required_providers, provider, and resource blocks.

Sequence Diagram(s)

sequenceDiagram
    participant Main as Application
    participant TFTarget as TerraformTarget
    participant Writer as WriteTerraformFile
    participant FS as FileSystem

    Main->>TFTarget: Compile(ir IR)
    Note over TFTarget: Iterate ir.Nodes\nCreate TFBlock entries (Class, Labels, Attributes)
    TFTarget-->>Main: TFFile
    Main->>Writer: WriteTerraformFile(tf, path)
    Note over Writer: Recursively render TFBlock -> HCL string
    Writer->>FS: os.WriteFile(path, data)
    FS-->>Writer: Success / Error
    Writer-->>Main: return error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇
I hop through nodes and stitch each field with care,
Attributes and labels placed with flair,
I spill HCL lines onto disk so bright,
A tiny rabbit builder, coding through the night,
Output.tf done — a carrot-coded delight!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title is incomplete and appears truncated mid-sentence with an ellipsis, making it unclear and unprofessional. Complete the title with a clear, full sentence summarizing the main change, e.g., 'Scaffold Terraform file generation from IR graph nodes' or similar.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/tf-scaffold

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Feb 24, 2026

Copy link
Copy Markdown

🚀 Preview deployed to: https://napkin-pr-12.fly.dev

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
napkin-backend/compiler/target.go (1)

3-7: Attributes map[string]string limits expressible Terraform values.

Real HCL resources use booleans, numbers, lists, and nested blocks — all coerced to strings here. Consider a map[string]any (or a typed value union) when the IR needs to represent non-string attribute values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@napkin-backend/compiler/target.go` around lines 3 - 7, TFResource currently
uses Attributes map[string]string which loses booleans, numbers, lists and
nested blocks; change Attributes to map[string]any (or a small typed union/value
struct) in the TFResource type and update any consumers/serializers (e.g. code
that marshals to HCL/JSON or reads Attributes in functions referencing
TFResource) to handle non-string types (booleans, numbers, []any,
map[string]any) accordingly so attribute values are preserved and correctly
emitted.
napkin-backend/main.go (1)

44-44: Inconsistent logging — use log.Println instead of fmt.Println.

All other output in main() goes through log (lines 55, 62, 64). Using fmt.Println here omits the timestamp and skips log routing.

♻️ Proposed fix
-	fmt.Println("Terraform file written to output.tf")
+	log.Println("Terraform file written to output.tf")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@napkin-backend/main.go` at line 44, Replace the fmt.Println call in main()
that prints "Terraform file written to output.tf" with log.Println so it uses
the same logging pipeline and timestamps as the other log calls; locate the
fmt.Println invocation in the main function and change it to use log.Println to
maintain consistent logging behavior across main().
napkin-backend/compiler/tf_target.go (1)

6-17: No validation of node.Type or node.ID — empty values produce malformed HCL.

An empty Type or ID silently emits resource "" "" { }, which is invalid Terraform. Returning an error here is cheap and prevents confusing downstream failures.

♻️ Proposed fix
 	for _, node := range ir.Nodes {
+		if node.Type == "" || node.ID == "" {
+			return nil, fmt.Errorf("compiler: node %q has empty Type or ID", node.ID)
+		}
 		resource := TFResource{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@napkin-backend/compiler/tf_target.go` around lines 6 - 17, The Compile method
on TerraformTarget should validate node.Type and node.ID before emitting
TFResource to avoid producing invalid HCL; in TerraformTarget.Compile (iterating
ir.Nodes) check that node.Type and node.ID are non-empty and, if either is
empty, return a descriptive error (including which field and the node reference)
instead of appending a resource, so malformed resource "" "" is never produced;
ensure the error is returned up to callers of Compile.
napkin-backend/compiler/tf_writer.go (1)

14-16: Non-deterministic attribute ordering — idempotent generation is broken.

Go map iteration order is randomised per run, so two calls with identical input can produce different files. This makes VCS diffs noisy and breaks any downstream tooling that relies on stable output (e.g., terraform fmt diffing, plan caching).

♻️ Proposed fix — sort keys before writing
+import (
+	"os"
+	"sort"
+	"strings"
+)

+		keys := make([]string, 0, len(v.Attributes))
+		for k := range v.Attributes {
+			keys = append(keys, k)
+		}
+		sort.Strings(keys)
+		for _, key := range keys {
+			value := v.Attributes[key]
 			builder.WriteString("  " + key + ` = "` + value + `"` + "\n")
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@napkin-backend/compiler/tf_writer.go` around lines 14 - 16, The loop over
v.Attributes is non-deterministic because Go map iteration is unordered; change
the code in tf_writer.go (the block that iterates v.Attributes) to collect the
map keys into a slice, sort that slice (e.g., with sort.Strings), and then
iterate the sorted keys to write builder.WriteString lines so attribute output
is stable and idempotent; update imports if needed to include "sort".
napkin-backend/compiler/tf_target_test.go (1)

40-42: Test only validates ami; the instance_type attribute in the fixture is unverified.

A regression that silently drops attributes beyond the first would pass undetected.

♻️ Proposed fix
 	if resource.Attributes["ami"] != "ami-123" {
 		t.Errorf("expected ami attribute to be ami-123")
 	}
+
+	if resource.Attributes["instance_type"] != "t2.micro" {
+		t.Errorf("expected instance_type attribute to be t2.micro, got %s", resource.Attributes["instance_type"])
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@napkin-backend/compiler/tf_target_test.go` around lines 40 - 42, The test
currently only asserts resource.Attributes["ami"] and misses verifying the
fixture's instance_type, allowing regressions that drop attributes to pass;
update the test in tf_target_test.go (the test that accesses
resource.Attributes) to also assert that resource.Attributes["instance_type"]
equals the expected fixture value and include a clear t.Errorf message on
failure (and optionally assert the Attributes map contains both keys) so the
test fails if instance_type is missing or incorrect.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@napkin-backend/compiler/tf_target.go`:
- Around line 9-15: The TFResource being built in the loop copies
node.Attributes by reference, so changes to TFResource.Attributes will mutate
the original IR node; update the assignment in the loop that constructs
TFResource (where TFResource{..., Attributes: node.Attributes} is set) to
deep-copy node.Attributes into a new map before assigning (e.g., clone/map-copy
all key→value entries, recursively if values are maps/slices), or add a helper
like cloneAttributes and use it; ensure the copy logic is used for each node in
the ir.Nodes iteration so TFResource.Attributes is independent of the original
GraphNode.

In `@napkin-backend/compiler/tf_writer.go`:
- Around line 8-19: WriteTerraformFile currently only serialises tf.Resources
and drops TFFile.Modules and TFFile.Outputs; update WriteTerraformFile to also
iterate over tf.Modules and tf.Outputs and append them to the strings.Builder
(preferably after resources) using the same style as resources: open the block
with module "<name>" { and output "<name>" { respectively, emit each block's
attributes/arguments, close the block, and separate with newlines. Reference the
WriteTerraformFile function and the TFFile.Modules and TFFile.Outputs fields
when making the change so modules and outputs are no longer silently omitted.
- Around line 14-16: The loop writing attribute values writes them verbatim
(builder.WriteString in the v.Attributes iteration) which breaks HCL when values
contain " or \; change it to use strconv.Quote on the attribute value (e.g.,
replace the literal `"` + value + `"` construction with strconv.Quote(value)) so
the string is properly escaped, and add the strconv import if missing.

In `@napkin-backend/compiler/tr_writer_test.go`:
- Around line 22-26: The temp file created with os.CreateTemp (variable tmp) is
never closed; add a deferred close to avoid leaking the file descriptor and to
allow removal on Windows—e.g. immediately after the nil err check, add a defer
that closes tmp and then removes the file (use defer func(){ _ = tmp.Close(); _
= os.Remove(tmp.Name()) }()) so Close runs before Remove; reference tmp,
os.CreateTemp and tmp.Name() when making the change.

In `@napkin-backend/main.go`:
- Around line 39-44: The code currently calls
compiler.WriteTerraformFile(tfFile, "output.tf") which writes to a CWD-relative
path; change this to accept a configurable output path (via a command-line flag
like flag.String("out", "output.tf", ...) or an environment variable fallback)
and pass that variable to compiler.WriteTerraformFile instead of the hardcoded
"output.tf"; also update the fmt.Println to print the resolved output path.
Ensure the flag/env logic runs in main before calling
compiler.WriteTerraformFile and reference the existing
compiler.WriteTerraformFile and the printed message for the replacement.

---

Nitpick comments:
In `@napkin-backend/compiler/target.go`:
- Around line 3-7: TFResource currently uses Attributes map[string]string which
loses booleans, numbers, lists and nested blocks; change Attributes to
map[string]any (or a small typed union/value struct) in the TFResource type and
update any consumers/serializers (e.g. code that marshals to HCL/JSON or reads
Attributes in functions referencing TFResource) to handle non-string types
(booleans, numbers, []any, map[string]any) accordingly so attribute values are
preserved and correctly emitted.

In `@napkin-backend/compiler/tf_target_test.go`:
- Around line 40-42: The test currently only asserts resource.Attributes["ami"]
and misses verifying the fixture's instance_type, allowing regressions that drop
attributes to pass; update the test in tf_target_test.go (the test that accesses
resource.Attributes) to also assert that resource.Attributes["instance_type"]
equals the expected fixture value and include a clear t.Errorf message on
failure (and optionally assert the Attributes map contains both keys) so the
test fails if instance_type is missing or incorrect.

In `@napkin-backend/compiler/tf_target.go`:
- Around line 6-17: The Compile method on TerraformTarget should validate
node.Type and node.ID before emitting TFResource to avoid producing invalid HCL;
in TerraformTarget.Compile (iterating ir.Nodes) check that node.Type and node.ID
are non-empty and, if either is empty, return a descriptive error (including
which field and the node reference) instead of appending a resource, so
malformed resource "" "" is never produced; ensure the error is returned up to
callers of Compile.

In `@napkin-backend/compiler/tf_writer.go`:
- Around line 14-16: The loop over v.Attributes is non-deterministic because Go
map iteration is unordered; change the code in tf_writer.go (the block that
iterates v.Attributes) to collect the map keys into a slice, sort that slice
(e.g., with sort.Strings), and then iterate the sorted keys to write
builder.WriteString lines so attribute output is stable and idempotent; update
imports if needed to include "sort".

In `@napkin-backend/main.go`:
- Line 44: Replace the fmt.Println call in main() that prints "Terraform file
written to output.tf" with log.Println so it uses the same logging pipeline and
timestamps as the other log calls; locate the fmt.Println invocation in the main
function and change it to use log.Println to maintain consistent logging
behavior across main().

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4f740ef and 2632713.

📒 Files selected for processing (7)
  • napkin-backend/compiler/target.go
  • napkin-backend/compiler/tf_target.go
  • napkin-backend/compiler/tf_target_test.go
  • napkin-backend/compiler/tf_writer.go
  • napkin-backend/compiler/tr_writer_test.go
  • napkin-backend/main.go
  • napkin-backend/napkin-backend

Comment thread napkin-backend/compiler/tf_target.go Outdated
Comment thread napkin-backend/compiler/tf_writer.go Outdated
Comment on lines +8 to +19
func WriteTerraformFile(tf *TFFile, path string) error {
var builder strings.Builder

for _, v := range tf.Resources {
builder.WriteString(`resource "` + v.Type + `" "` + v.Name + `" {` + "\n")

for key, value := range v.Attributes {
builder.WriteString(" " + key + ` = "` + value + `"` + "\n")
}

builder.WriteString("}\n\n")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

TFFile.Modules and TFFile.Outputs are silently dropped.

The writer only serialises tf.Resources; Modules and Outputs — both first-class fields of TFFile — are never emitted. Any caller that populates those fields will get a silently incomplete Terraform file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@napkin-backend/compiler/tf_writer.go` around lines 8 - 19, WriteTerraformFile
currently only serialises tf.Resources and drops TFFile.Modules and
TFFile.Outputs; update WriteTerraformFile to also iterate over tf.Modules and
tf.Outputs and append them to the strings.Builder (preferably after resources)
using the same style as resources: open the block with module "<name>" { and
output "<name>" { respectively, emit each block's attributes/arguments, close
the block, and separate with newlines. Reference the WriteTerraformFile function
and the TFFile.Modules and TFFile.Outputs fields when making the change so
modules and outputs are no longer silently omitted.

Comment thread napkin-backend/compiler/tf_writer.go Outdated
Comment thread napkin-backend/compiler/tr_writer_test.go
Comment thread napkin-backend/main.go
Comment on lines +39 to +44
err = compiler.WriteTerraformFile(tfFile, "output.tf")
if err != nil {
panic(err)
}

fmt.Println("Terraform file written to output.tf")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

"output.tf" is a relative path resolved against the process CWD.

This silently writes to different locations depending on how the binary is invoked (e.g., go run vs ./napkin-backend). Consider making the output path a command-line flag or env var.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@napkin-backend/main.go` around lines 39 - 44, The code currently calls
compiler.WriteTerraformFile(tfFile, "output.tf") which writes to a CWD-relative
path; change this to accept a configurable output path (via a command-line flag
like flag.String("out", "output.tf", ...) or an environment variable fallback)
and pass that variable to compiler.WriteTerraformFile instead of the hardcoded
"output.tf"; also update the fmt.Println to print the resolved output path.
Ensure the flag/env logic runs in main before calling
compiler.WriteTerraformFile and reference the existing
compiler.WriteTerraformFile and the printed message for the replacement.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@napkin-backend/compiler/tf_target.go`:
- Around line 10-16: The attributes map allocated in tf_target.go is never
populated, so copy all entries from the source node.Attributes into the newly
created attributes map before constructing TFResource; specifically iterate over
node.Attributes and assign each key/value into the local attributes map (the one
created with make) so TFResource.Attributes contains the original data when
returned by Compile (or the function that builds TFResource), preserving the
source GraphNode.Attributes rather than dropping them.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2632713 and 3cb3b20.

📒 Files selected for processing (3)
  • napkin-backend/compiler/tf_target.go
  • napkin-backend/compiler/tf_writer.go
  • napkin-backend/compiler/tr_writer_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • napkin-backend/compiler/tf_writer.go
  • napkin-backend/compiler/tr_writer_test.go

Comment thread napkin-backend/compiler/tf_target.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
napkin-backend/main.go (1)

48-53: ⚠️ Potential issue | 🟡 Minor

Make Terraform output path configurable instead of CWD-relative.

Line 48 writes to "output.tf" relative to process CWD, which is brittle across go run, container entrypoints, and service managers.

Suggested change
+import "flag"
@@
 func main() {
+	outPath := flag.String("out", "output.tf", "path to generated Terraform file")
+	flag.Parse()
@@
-	err = compiler.WriteTerraformFile(tfFile, "output.tf")
+	err = compiler.WriteTerraformFile(tfFile, *outPath)
 	if err != nil {
 		panic(err)
 	}
 
-	fmt.Println("Terraform file written to output.tf")
+	fmt.Printf("Terraform file written to %s\n", *outPath)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@napkin-backend/main.go` around lines 48 - 53, The code currently writes
Terraform to a hardcoded CWD-relative filename via
compiler.WriteTerraformFile(tfFile, "output.tf"); make the output path
configurable (e.g., add a CLI flag or env var) and pass that value into
WriteTerraformFile instead of the literal "output.tf". Update main.go to read a
new config (flag like tfOutputPath or env var such as NAPKIN_TF_OUTPUT),
validate/normalize it (use filepath.Abs or filepath.Join if you need to resolve
relative paths), and call compiler.WriteTerraformFile(tfFile, tfOutputPath);
ensure any default remains "output.tf" for backward compatibility.
🧹 Nitpick comments (1)
napkin-backend/compiler/tf_target.go (1)

32-32: Remove compile-loop debug printing from stdout.

Line 32 introduces noisy runtime output on every compile; prefer structured logging behind a debug flag (or remove entirely).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@napkin-backend/compiler/tf_target.go` at line 32, Remove the noisy fmt.Printf
call that prints node.ID/node.Type/node.Class to stdout in tf_target.go: delete
the fmt.Printf("ID=%s Type=%s Class=%s\n", node.ID, node.Type, node.Class) line
and, if you need the info for debugging, replace it with a structured debug log
behind the existing debug mechanism (e.g., use the project's logger.Debugf or
guard the output with a debug flag) so normal compiles produce no stdout noise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@napkin-backend/compiler/target.go`:
- Around line 27-43: The IR currently uses map[string]string for attributes (in
TFBlock, TFFile, GraphNode.Attributes and TFResource) which cannot represent
Terraform typed expressions (strings, numbers, booleans, lists, maps/objects)
nor distinguish arguments from nested block constructs; change the model to
replace map[string]string with a typed value system (e.g., introduce a
TFValue/Value type and ValueKind enum for
String/Number/Bool/List/Map/Object/Expression) and make TFBlock explicitly
separate arguments (Attributes map[string]TFValue) from nested blocks (Blocks
[]TFBlock with block type/name/labels preserved) so meta-arguments like
depends_on can be lists and complex nested structures (e.g., metadata_options)
can be objects; also update GraphNode.Attributes to map[string]TFValue and
TFResource to use TFValue for its attributes so code that serializes to HCL can
emit proper typed values vs nested blocks.

In `@napkin-backend/compiler/tf_target.go`:
- Around line 39-51: Before assigning block.Labels, validate that required node
fields are present: for ClassResource and ClassData ensure node.Type and node.ID
are non-empty, for ClassProvider ensure node.Type is non-empty, and for
ClassModule ensure node.ID is non-empty. If a required field is missing, return
a clear error (or propagate an error) instead of creating the block; update the
code around the switch that sets block.Labels (the references to node.Class,
ClassResource, ClassData, ClassProvider, ClassModule, block.Labels, node.Type,
node.ID) to perform these checks and error handling prior to assigning labels.

In `@napkin-backend/compiler/tf_writer.go`:
- Around line 29-33: WriteTerraformFile currently dereferences tf without
checking for nil which will panic; add an early guard at the top of
WriteTerraformFile to return a descriptive error (e.g., using fmt.Errorf or
errors.New) when tf == nil, before creating the strings.Builder or calling
writeBlock, so callers can handle the error instead of crashing; reference
TFFile and writeBlock to locate the function to modify.

In `@napkin-backend/main.go`:
- Around line 29-37: The scaffolded aws_instance resource (ID "web", Class
compiler.ClassResource, Type "aws_instance") must include IMDSv2 and encrypted
root volume settings; update the resource creation in main.go where the
Attributes map for that aws_instance is built to add a metadata_options block
with http_endpoint = "enabled" and http_tokens = "required", and add a
root_block_device block with encrypted = true so the generated Terraform
includes IMDSv2 enforcement and an encrypted root disk.

---

Duplicate comments:
In `@napkin-backend/main.go`:
- Around line 48-53: The code currently writes Terraform to a hardcoded
CWD-relative filename via compiler.WriteTerraformFile(tfFile, "output.tf"); make
the output path configurable (e.g., add a CLI flag or env var) and pass that
value into WriteTerraformFile instead of the literal "output.tf". Update main.go
to read a new config (flag like tfOutputPath or env var such as
NAPKIN_TF_OUTPUT), validate/normalize it (use filepath.Abs or filepath.Join if
you need to resolve relative paths), and call
compiler.WriteTerraformFile(tfFile, tfOutputPath); ensure any default remains
"output.tf" for backward compatibility.

---

Nitpick comments:
In `@napkin-backend/compiler/tf_target.go`:
- Line 32: Remove the noisy fmt.Printf call that prints
node.ID/node.Type/node.Class to stdout in tf_target.go: delete the
fmt.Printf("ID=%s Type=%s Class=%s\n", node.ID, node.Type, node.Class) line and,
if you need the info for debugging, replace it with a structured debug log
behind the existing debug mechanism (e.g., use the project's logger.Debugf or
guard the output with a debug flag) so normal compiles produce no stdout noise.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d88f9a1 and f317fe1.

📒 Files selected for processing (9)
  • napkin-backend/compiler/target.go
  • napkin-backend/compiler/tf_run _test.go
  • napkin-backend/compiler/tf_target.go
  • napkin-backend/compiler/tf_target_test.go
  • napkin-backend/compiler/tf_writer.go
  • napkin-backend/compiler/tr_writer_test.go
  • napkin-backend/main.go
  • napkin-backend/napkin-backend
  • napkin-backend/output.tf
✅ Files skipped from review due to trivial changes (1)
  • napkin-backend/compiler/tf_run _test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • napkin-backend/compiler/tf_target_test.go
  • napkin-backend/compiler/tr_writer_test.go

Comment on lines +27 to +43
type TFBlock struct {
Class string
Labels []string
Attributes map[string]string
Blocks []TFBlock
}
type TFFile struct {
Block []TFBlock
}

type GraphNode struct {
ID string
Class NodeClass
Type string
Attributes map[string]string
Edges []string // list of node IDs this node depends on
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Terraform HCL argument value types nested blocks aws_instance

💡 Result:

In Terraform HCL, there are two different schema-driven “shapes” inside a block:

  1. Arguments (aka attributes) use name = <expression> and the value is an expression that evaluates to a Terraform type (string, number, bool, list, map, object, etc.). What types are allowed is defined by the block’s schema (e.g., the provider’s resource schema). [1]

  2. Nested blocks use block_type { ... } and are not set with =. They represent structured sub-objects/collections defined by the schema, and can usually be repeated (list/set of objects) depending on how the provider defined it. [1] Provider schemas commonly model repeatable nested blocks as list/set “collections of structures”. [3]

How this applies to aws_instance

For resource "aws_instance" ..., things like root_block_device, ebs_block_device, and (historically) network_interface are nested blocks, not arguments—so this is invalid:

# INVALID (because it's a block, not an argument)
root_block_device = var.root_block_device

…and Terraform will error with “An argument named ... is not expected here. Did you mean to define a block of type ...?” (common example shown for root_block_device / ebs_block_device). [4]

Instead, you must write them as blocks, optionally using a dynamic block if you want to drive them from variables:

variable "root_block_device" {
  type = object({
    volume_size = number
    volume_type = string
  })
  default = null
}

resource "aws_instance" "this" {
  ami           = var.ami
  instance_type = var.instance_type

  dynamic "root_block_device" {
    for_each = var.root_block_device == null ? [] : [var.root_block_device]
    content {
      volume_size = root_block_device.value.volume_size
      volume_type = root_block_device.value.volume_type
    }
  }
}

“Attributes as blocks” edge case

Terraform documents a rare compatibility behavior where some resources support both a nested block and an argument with the same name (and []=[] can mean “purge all”). This is uncommon and mainly relevant for JSON syntax or module abstraction quirks. [2]

Note on network_interface for aws_instance

Recent AWS provider releases have deprecated the network_interface block on aws_instance (guidance: use primary_network_interface and aws_network_interface_attachment instead). So whether you should use that nested block depends on your provider version. [5]


🏁 Script executed:

# First, let's examine the target.go file to understand the current implementation
cd napkin-backend && find . -name "target.go" -type f

Repository: acmutsa/napkin

Length of output: 78


🏁 Script executed:

# Read the target.go file to see the current structure
cat -n napkin-backend/compiler/target.go

Repository: acmutsa/napkin

Length of output: 1263


🏁 Script executed:

# Search for usages of TFBlock and TFFile to understand how they're being used
rg "TFBlock|TFFile" napkin-backend --type go -A 2 -B 2

Repository: acmutsa/napkin

Length of output: 2880


IR attribute model cannot represent Terraform's type system and nested block semantics.

map[string]string attributes (lines 30, 41, and also in TFResource at line 15) cannot represent booleans, numbers, lists, maps, objects, or nested block constructs. In Terraform, arguments are expressions that evaluate to typed values (string, number, bool, list, map, object), and nested blocks (like root_block_device, ebs_block_device on aws_instance) are separate structural constructs using block_type { ... } syntax, not name = value assignments. The current model conflates these semantically distinct concepts. Additionally, meta-arguments like depends_on require list types, and complex nested structures like metadata_options require object/map representation.

The IR model will need to support typed values and distinguish between arguments and nested blocks to generate valid Terraform configuration for real resources.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@napkin-backend/compiler/target.go` around lines 27 - 43, The IR currently
uses map[string]string for attributes (in TFBlock, TFFile, GraphNode.Attributes
and TFResource) which cannot represent Terraform typed expressions (strings,
numbers, booleans, lists, maps/objects) nor distinguish arguments from nested
block constructs; change the model to replace map[string]string with a typed
value system (e.g., introduce a TFValue/Value type and ValueKind enum for
String/Number/Bool/List/Map/Object/Expression) and make TFBlock explicitly
separate arguments (Attributes map[string]TFValue) from nested blocks (Blocks
[]TFBlock with block type/name/labels preserved) so meta-arguments like
depends_on can be lists and complex nested structures (e.g., metadata_options)
can be objects; also update GraphNode.Attributes to map[string]TFValue and
TFResource to use TFValue for its attributes so code that serializes to HCL can
emit proper typed values vs nested blocks.

Comment on lines +39 to +51
switch node.Class {
case ClassResource:
block.Labels = []string{node.Type, node.ID}

case ClassData:
block.Labels = []string{node.Type, node.ID}

case ClassProvider:
block.Labels = []string{node.Type}

case ClassModule:
block.Labels = []string{node.ID}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate required node fields before assigning labels.

Line 39-Line 51 builds labels without checking required fields. Empty Type/ID will generate invalid Terraform blocks and defer failure to later stages.

Suggested change
 		switch node.Class {
 		case ClassResource:
+			if node.Type == "" || node.ID == "" {
+				return nil, fmt.Errorf("resource nodes require non-empty type and id (id=%q, type=%q)", node.ID, node.Type)
+			}
 			block.Labels = []string{node.Type, node.ID}
 
 		case ClassData:
+			if node.Type == "" || node.ID == "" {
+				return nil, fmt.Errorf("data nodes require non-empty type and id (id=%q, type=%q)", node.ID, node.Type)
+			}
 			block.Labels = []string{node.Type, node.ID}
 
 		case ClassProvider:
+			if node.Type == "" {
+				return nil, fmt.Errorf("provider nodes require non-empty type (id=%q)", node.ID)
+			}
 			block.Labels = []string{node.Type}
 
 		case ClassModule:
+			if node.ID == "" {
+				return nil, fmt.Errorf("module nodes require non-empty id")
+			}
 			block.Labels = []string{node.ID}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@napkin-backend/compiler/tf_target.go` around lines 39 - 51, Before assigning
block.Labels, validate that required node fields are present: for ClassResource
and ClassData ensure node.Type and node.ID are non-empty, for ClassProvider
ensure node.Type is non-empty, and for ClassModule ensure node.ID is non-empty.
If a required field is missing, return a clear error (or propagate an error)
instead of creating the block; update the code around the switch that sets
block.Labels (the references to node.Class, ClassResource, ClassData,
ClassProvider, ClassModule, block.Labels, node.Type, node.ID) to perform these
checks and error handling prior to assigning labels.

Comment on lines +29 to +33
func WriteTerraformFile(tf *TFFile, path string) error {
var builder strings.Builder

for _, block := range tf.Block {
writeBlock(&builder, block, "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard WriteTerraformFile against nil input.

Line 32 will panic when tf == nil. Return an explicit error instead so callers can handle it safely.

Suggested change
 import (
+	"errors"
 	"os"
 	"strconv"
 	"strings"
 )
@@
 func WriteTerraformFile(tf *TFFile, path string) error {
+	if tf == nil {
+		return errors.New("nil TFFile")
+	}
 	var builder strings.Builder
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@napkin-backend/compiler/tf_writer.go` around lines 29 - 33,
WriteTerraformFile currently dereferences tf without checking for nil which will
panic; add an early guard at the top of WriteTerraformFile to return a
descriptive error (e.g., using fmt.Errorf or errors.New) when tf == nil, before
creating the strings.Builder or calling writeBlock, so callers can handle the
error instead of crashing; reference TFFile and writeBlock to locate the
function to modify.

Comment thread napkin-backend/main.go
Comment on lines +29 to +37
{
ID: "web",
Class: compiler.ClassResource,
Type: "aws_instance",
Attributes: map[string]string{
"ami": "ami-123",
"instance_type": "t2.micro",
},
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Terraform aws_instance metadata_options http_tokens IMDSv2 requirement

💡 Result:

To make IMDSv2 required for a Terraform aws_instance, set the instance metadata option HttpTokens = required via metadata_options.http_tokens = "required".

resource "aws_instance" "example" {
  # ...

  metadata_options {
    http_endpoint = "enabled"   # keep IMDS reachable
    http_tokens   = "required"  # IMDSv2-only (IMDSv1 disabled)
  }
}

Key points:

  • In EC2, HttpTokens controls whether IMDSv2 is optional (IMDSv1 still works) or required (IMDSv1 disabled). [1]
  • If you require IMDSv2 (http_tokens = "required"), the IMDS endpoint must be enabled (http_endpoint = "enabled"), otherwise metadata is unreachable. [2]
  • Security tooling commonly flags Terraform aws_instance because IMDS tokens are often left optional unless you set this explicitly. [3]

Sources: [1] [2] [3]


🌐 Web query:

Terraform aws_instance root_block_device encrypted default encryption configuration

💡 Result:

What controls “default encryption” for aws_instance root volumes?

AWS account/Region setting (recommended baseline): “EBS encryption by default”
If you enable it, all newly created EBS volumes in that Region are always encrypted, and you can’t opt out per-volume in that Region. It doesn’t affect existing volumes. [1]
In Terraform this is managed with aws_ebs_encryption_by_default (and deleting that Terraform resource disables default encryption). [2]

# Regional (per-provider-region or per "region" arg) setting
resource "aws_ebs_encryption_by_default" "this" {
  enabled = true # defaults to true, but set explicitly for clarity
}

Optional: set the default KMS key used when no key is specified
By default, AWS uses an AWS-managed key for EBS if you don’t specify a CMK. Terraform can set a customer-managed key via aws_ebs_default_kms_key (note: this does not itself enable encryption-by-default). [3]

resource "aws_ebs_default_kms_key" "this" {
  key_arn = aws_kms_key.ebs.arn
}

How does aws_instance.root_block_device.encrypted interact with that?

In the aws_instance resource, the root_block_device block has:

  • encrypted (defaults to false in the Terraform schema)
  • kms_key_id (optional)
  • and Terraform notes these must be configured to perform drift detection, and changing encrypted/kms_key_id forces instance replacement. [4]
resource "aws_instance" "example" {
  ami           = var.ami
  instance_type = "t3.micro"

  root_block_device {
    encrypted  = true
    kms_key_id = aws_kms_key.ebs.arn # optional; omit to use the account/region default key
  }
}

Practical guidance

  • If you want encryption everywhere with minimal per-resource config: enable EBS encryption by default (+ optionally set the default CMK). [1][2][3]
  • If you need to enforce encryption (and/or a specific CMK) at the resource level and have Terraform track it: set root_block_device.encrypted = true (and kms_key_id if needed), but be aware changes require replacement. [4]

Sources: AWS EBS encryption-by-default behavior and constraints [1]; Terraform AWS provider docs for aws_ebs_encryption_by_default [2], aws_ebs_default_kms_key [3], and aws_instance.root_block_device encryption semantics [4].

[1] (docs.aws.amazon.com)
[2] (raw.githubusercontent.com)
[3] (raw.githubusercontent.com)
[4] (raw.githubusercontent.com)

Citations:


🏁 Script executed:

# Locate and examine the napkin-backend/main.go file
fd "main.go" napkin-backend/

Repository: acmutsa/napkin

Length of output: 80


🏁 Script executed:

# Check if the file exists and view the relevant lines around 29-37
if [ -f "napkin-backend/main.go" ]; then
  wc -l napkin-backend/main.go
  echo "---"
  cat -n napkin-backend/main.go | sed -n '20,45p'
fi

Repository: acmutsa/napkin

Length of output: 767


Add IMDSv2 enforcement and encrypted root block device to scaffolded aws_instance.

The aws_instance at lines 29–37 omits metadata_options (IMDSv2) and root_block_device (encryption), leaving the generated Terraform vulnerable to security scanning. Add:

metadata_options {
  http_endpoint = "enabled"
  http_tokens   = "required"
}
root_block_device {
  encrypted = true
}

These are standard Terraform attributes, not account-level settings, and should be included in the IR/writer path for any production-facing EC2 scaffolding.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@napkin-backend/main.go` around lines 29 - 37, The scaffolded aws_instance
resource (ID "web", Class compiler.ClassResource, Type "aws_instance") must
include IMDSv2 and encrypted root volume settings; update the resource creation
in main.go where the Attributes map for that aws_instance is built to add a
metadata_options block with http_endpoint = "enabled" and http_tokens =
"required", and add a root_block_device block with encrypted = true so the
generated Terraform includes IMDSv2 enforcement and an encrypted root disk.

@joshuasilva414 joshuasilva414 changed the base branch from main to dev March 6, 2026 17:45
@joshuasilva414 joshuasilva414 merged commit 366ff10 into dev Mar 6, 2026
2 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Mar 26, 2026
joshuasilva414 added a commit that referenced this pull request Mar 30, 2026
* refactor backend for better file structure

* Setup React Flow (#9)

* basic graph structure (#11)

* currently only creates a file in memory but can scaffold to a real t… (#12)

* currently only creates a file in memory but can scaffold to a real tf file

* slight refactor

* forgot to commit this

* created terraform and provider block

* remove unnecessary import

---------

Co-authored-by: Joshua Silva <72359611+joshuasilva414@users.noreply.github.com>
Co-authored-by: joshuasilva414 <joshuasilva414@gmail.com>

* initial analyzer design

---------

Co-authored-by: joshuasilva414 <joshuasilva414@gmail.com>
Co-authored-by: Eric Lee <155848001+Eric1305@users.noreply.github.com>
Co-authored-by: Adan Santos <116550868+adanrsantos@users.noreply.github.com>
Co-authored-by: Joshua Silva <72359611+joshuasilva414@users.noreply.github.com>
joshuasilva414 added a commit that referenced this pull request Apr 17, 2026
* Analyzer (#20)

* refactor backend for better file structure

* Setup React Flow (#9)

* basic graph structure (#11)

* currently only creates a file in memory but can scaffold to a real t… (#12)

* currently only creates a file in memory but can scaffold to a real tf file

* slight refactor

* forgot to commit this

* created terraform and provider block

* remove unnecessary import

---------

Co-authored-by: Joshua Silva <72359611+joshuasilva414@users.noreply.github.com>
Co-authored-by: joshuasilva414 <joshuasilva414@gmail.com>

* initial analyzer design

---------

Co-authored-by: joshuasilva414 <joshuasilva414@gmail.com>
Co-authored-by: Eric Lee <155848001+Eric1305@users.noreply.github.com>
Co-authored-by: Adan Santos <116550868+adanrsantos@users.noreply.github.com>
Co-authored-by: Joshua Silva <72359611+joshuasilva414@users.noreply.github.com>

* Merge branch 'dev' into dev-kaydo

* Implemented /compile Endpoint

---------

Co-authored-by: Joshua Silva <72359611+joshuasilva414@users.noreply.github.com>
Co-authored-by: diego <161160330+deeg454@users.noreply.github.com>
Co-authored-by: joshuasilva414 <joshuasilva414@gmail.com>
Co-authored-by: Eric Lee <155848001+Eric1305@users.noreply.github.com>
Co-authored-by: Adan Santos <116550868+adanrsantos@users.noreply.github.com>
@coderabbitai coderabbitai Bot mentioned this pull request May 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants