Skip to content

Commit 74632f2

Browse files
authored
Merge pull request #19 from gruntwork-io/rename-blocks
Rename and improve blocks
2 parents 7139206 + dcd33f2 commit 74632f2

99 files changed

Lines changed: 3824 additions & 2549 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursorrules

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
11
The Go server runs on port 7825 by default.
22

3-
We use bun, so prefer that to npm.
3+
We use bun, so prefer that to npm.
4+
5+
We use https://taskfile.dev/ over makefiles. Look for the Taskfile.yml to see what command can you can run.
6+
7+
Docs are located in /docs
8+
9+
The frontend is located in /web
10+
11+
The backend is spread across /api, /browser, and /cmd. When the user requests something, make sure you know if they're asking to update the docs, frontend, or backend.

api/boilerplate_render.go

Lines changed: 85 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,28 @@ func determineOutputDirectory(cliOutputPath string, apiRequestOutputPath *string
101101
return outputDir, nil
102102
}
103103

104-
// This handler renders a boilerplate template with the provided variables.
104+
// prepareOutputDirectory determines the output directory path and creates it if needed.
105+
// This is shared logic used by both HandleBoilerplateRender and HandleBoilerplateRenderInline.
106+
//
107+
// Parameters:
108+
// - cliOutputPath: The base output path from CLI flag
109+
// - apiOutputPath: Optional subdirectory from API request (validated for security)
110+
//
111+
// Returns the absolute output directory path or an error.
112+
func prepareOutputDirectory(cliOutputPath string, apiOutputPath *string) (string, error) {
113+
// Determine the final output directory path
114+
outputDir, err := determineOutputDirectory(cliOutputPath, apiOutputPath)
115+
if err != nil {
116+
return "", fmt.Errorf("failed to determine output directory: %w", err)
117+
}
118+
119+
// Create the output directory if it doesn't exist
120+
if err := os.MkdirAll(outputDir, 0755); err != nil {
121+
return "", fmt.Errorf("failed to create output directory: %w", err)
122+
}
123+
124+
return outputDir, nil
125+
}
105126

106127
// HandleBoilerplateRender renders a boilerplate template with the provided variables
107128
func HandleBoilerplateRender(runbookPath string, cliOutputPath string) gin.HandlerFunc {
@@ -140,33 +161,13 @@ func HandleBoilerplateRender(runbookPath string, cliOutputPath string) gin.Handl
140161
return
141162
}
142163

143-
// Determine the output directory
144-
outputDir, err := determineOutputDirectory(cliOutputPath, req.OutputPath)
164+
// Prepare the output directory (determine path and create it)
165+
// Files accumulate across multiple template renders in the same runbook
166+
outputDir, err := prepareOutputDirectory(cliOutputPath, req.OutputPath)
145167
if err != nil {
146-
slog.Error("Failed to determine output directory", "error", err)
147-
c.JSON(http.StatusBadRequest, gin.H{
148-
"error": "Invalid output path",
149-
"details": err.Error(),
150-
})
151-
return
152-
}
153-
154-
// Create the output directory if it doesn't exist
155-
if err := os.MkdirAll(outputDir, 0755); err != nil {
156-
slog.Error("Failed to create output directory", "error", err)
157-
c.JSON(http.StatusInternalServerError, gin.H{
158-
"error": "Failed to create output directory",
159-
"details": err.Error(),
160-
})
161-
return
162-
}
163-
164-
// Clear any existing files in the output directory before rendering
165-
// This ensures each render starts fresh without leftover files from previous renders
166-
if err := deleteDirectoryContents(outputDir); err != nil {
167-
slog.Error("Failed to clear output directory", "error", err)
168+
slog.Error("Failed to prepare output directory", "error", err)
168169
c.JSON(http.StatusInternalServerError, gin.H{
169-
"error": "Failed to clear output directory",
170+
"error": "Failed to prepare output directory",
170171
"details": err.Error(),
171172
})
172173
return
@@ -318,7 +319,7 @@ func preConvertJSONTypes(value any, variableType bpVariables.BoilerplateType) an
318319
}
319320

320321
// HandleBoilerplateRenderInline renders boilerplate templates provided directly in the request body
321-
func HandleBoilerplateRenderInline() gin.HandlerFunc {
322+
func HandleBoilerplateRenderInline(cliOutputPath string) gin.HandlerFunc {
322323
return func(c *gin.Context) {
323324
var req RenderInlineRequest
324325
if err := c.ShouldBindJSON(&req); err != nil {
@@ -376,21 +377,22 @@ func HandleBoilerplateRenderInline() gin.HandlerFunc {
376377
slog.Debug("Wrote template file", "path", fullPath)
377378
}
378379

379-
// Create a temporary output directory
380-
outputDir, err := os.MkdirTemp("", "boilerplate-output-*")
380+
// Always render to a temp directory first for inline templates.
381+
// This prevents boilerplate from clearing existing files in the persistent output.
382+
tempOutputDir, err := os.MkdirTemp("", "boilerplate-output-*")
381383
if err != nil {
382-
slog.Error("Failed to create output directory", "error", err)
384+
slog.Error("Failed to create temp output directory", "error", err)
383385
c.JSON(http.StatusInternalServerError, gin.H{
384386
"error": "Failed to create output directory",
385387
"details": err.Error(),
386388
})
387389
return
388390
}
389-
defer os.RemoveAll(outputDir) // Clean up output directory when done
390-
slog.Info("Created temporary output directory", "outputDir", outputDir)
391+
defer os.RemoveAll(tempOutputDir)
392+
slog.Info("Created temporary output directory", "outputDir", tempOutputDir)
391393

392-
// Render the template using the boilerplate package
393-
err = renderBoilerplateTemplate(tempDir, outputDir, req.Variables)
394+
// Render the template to the temp directory
395+
err = renderBoilerplateTemplate(tempDir, tempOutputDir, req.Variables)
394396
if err != nil {
395397
slog.Error("Failed to render boilerplate template", "error", err)
396398
c.JSON(http.StatusInternalServerError, gin.H{
@@ -402,8 +404,8 @@ func HandleBoilerplateRenderInline() gin.HandlerFunc {
402404

403405
slog.Info("Successfully rendered boilerplate template")
404406

405-
// Read all rendered files from the output directory
406-
renderedFiles, err := readAllFilesInDirectory(outputDir)
407+
// Read all rendered files from the temp output directory
408+
renderedFiles, err := readAllFilesInDirectory(tempOutputDir)
407409
if err != nil {
408410
slog.Error("Failed to read rendered files", "error", err)
409411
c.JSON(http.StatusInternalServerError, gin.H{
@@ -413,8 +415,53 @@ func HandleBoilerplateRenderInline() gin.HandlerFunc {
413415
return
414416
}
415417

416-
// Build file tree from the generated output
417-
fileTree, err := buildFileTreeWithRoot(outputDir, "")
418+
// If generateFile is true, copy rendered files to the persistent output directory
419+
// This merges with existing files instead of replacing them
420+
var persistentOutputDir string
421+
if req.GenerateFile {
422+
persistentOutputDir, err = prepareOutputDirectory(cliOutputPath, nil)
423+
if err != nil {
424+
slog.Error("Failed to prepare output directory", "error", err)
425+
c.JSON(http.StatusInternalServerError, gin.H{
426+
"error": "Failed to prepare output directory",
427+
"details": err.Error(),
428+
})
429+
return
430+
}
431+
432+
// Copy each rendered file to the persistent output (merging with existing files)
433+
for relPath, file := range renderedFiles {
434+
fullPath := filepath.Join(persistentOutputDir, relPath)
435+
436+
// Create parent directories if needed
437+
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
438+
slog.Error("Failed to create directory", "path", filepath.Dir(fullPath), "error", err)
439+
c.JSON(http.StatusInternalServerError, gin.H{
440+
"error": "Failed to create directory structure",
441+
"details": err.Error(),
442+
})
443+
return
444+
}
445+
446+
if err := os.WriteFile(fullPath, []byte(file.Content), 0644); err != nil {
447+
slog.Error("Failed to write file", "path", fullPath, "error", err)
448+
c.JSON(http.StatusInternalServerError, gin.H{
449+
"error": "Failed to write file",
450+
"details": err.Error(),
451+
})
452+
return
453+
}
454+
slog.Debug("Copied file to persistent output", "path", fullPath)
455+
}
456+
slog.Info("Successfully copied files to persistent output", "outputDir", persistentOutputDir)
457+
}
458+
459+
// Build file tree - use persistent dir if files were generated, otherwise temp
460+
fileTreeDir := tempOutputDir
461+
if persistentOutputDir != "" {
462+
fileTreeDir = persistentOutputDir
463+
}
464+
fileTree, err := buildFileTreeWithRoot(fileTreeDir, "")
418465
if err != nil {
419466
slog.Error("Failed to build file tree", "error", err)
420467
c.JSON(http.StatusInternalServerError, gin.H{

api/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func setupCommonRoutes(r *gin.Engine, runbookPath string, outputPath string, reg
3333
r.POST("/api/boilerplate/render", HandleBoilerplateRender(runbookPath, outputPath))
3434

3535
// API endpoint to render boilerplate templates from inline template files
36-
r.POST("/api/boilerplate/render-inline", HandleBoilerplateRenderInline())
36+
r.POST("/api/boilerplate/render-inline", HandleBoilerplateRenderInline(outputPath))
3737

3838
// API endpoint to get registered executables
3939
r.GET("/api/runbook/executables", HandleExecutablesRequest(registry))

api/types.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package api
22

3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
7+
38
// Core data types
49
// ---
510

@@ -44,12 +49,51 @@ type RenderResponse struct {
4449
FileTree []FileTreeNode `json:"fileTree"`
4550
}
4651

52+
// FlexibleBool is a boolean type that can be unmarshaled from both JSON boolean and string values.
53+
// This handles cases where MDX authors write generateFile="true" instead of generateFile={true}.
54+
type FlexibleBool bool
55+
56+
// UnmarshalJSON implements json.Unmarshaler for FlexibleBool
57+
func (fb *FlexibleBool) UnmarshalJSON(data []byte) error {
58+
// Try to unmarshal as bool first
59+
var b bool
60+
if err := json.Unmarshal(data, &b); err == nil {
61+
*fb = FlexibleBool(b)
62+
return nil
63+
}
64+
65+
// Try to unmarshal as string
66+
var s string
67+
if err := json.Unmarshal(data, &s); err == nil {
68+
switch s {
69+
case "true", "True", "TRUE", "1":
70+
*fb = true
71+
case "false", "False", "FALSE", "0", "":
72+
*fb = false
73+
default:
74+
return fmt.Errorf("invalid boolean string: %s", s)
75+
}
76+
return nil
77+
}
78+
79+
return fmt.Errorf("cannot unmarshal %s into FlexibleBool", string(data))
80+
}
81+
4782
// RenderInlineRequest represents a request to render template files provided in the request body
4883
type RenderInlineRequest struct {
4984
// Map of relative file paths to their contents
5085
// Example: {"boilerplate.yml": "...", "main.tf": "..."}
5186
TemplateFiles map[string]string `json:"templateFiles"`
5287
Variables map[string]any `json:"variables"`
88+
// GenerateFile indicates whether to write files to the persistent output directory.
89+
// When false (default), files are only rendered and returned in the response.
90+
// When true, files are also written to the output directory (CLI-configured path + OutputPath).
91+
// Accepts both boolean and string values (e.g., true, "true", "false").
92+
GenerateFile FlexibleBool `json:"generateFile,omitempty"`
93+
// OutputPath is an optional subdirectory within the CLI-configured output path.
94+
// Only used when GenerateFile is true.
95+
// SECURITY: Must be a relative path without ".." to prevent directory traversal attacks.
96+
OutputPath *string `json:"outputPath,omitempty"`
5397
}
5498

5599
// RenderInlineResponse represents the response from the inline render endpoint

0 commit comments

Comments
 (0)