Skip to content

Commit 297f865

Browse files
committed
fix for windwos gomplate
1 parent fd97aa4 commit 297f865

File tree

5 files changed

+94
-47
lines changed

5 files changed

+94
-47
lines changed

atmos.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ settings:
399399
- "./README.yaml"
400400
# To Do: template can be a remote URL/github, using this local for testing
401401
template:
402-
- "./build-harness/templates/README.md.gotmpl"
402+
- "./README.md.gotmpl"
403403
# The final README
404404
output: "README.md"
405405
terraform:

go.mod

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.sum

+10-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/exec/file_utils.go

+47
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ package exec
33
import (
44
"fmt"
55
"io"
6+
"net/url"
67
"os"
8+
"path/filepath"
9+
"runtime"
10+
"strings"
711

812
"github.com/cloudposse/atmos/pkg/schema"
913
u "github.com/cloudposse/atmos/pkg/utils"
@@ -63,3 +67,46 @@ func printOrWriteToFile(
6367

6468
return nil
6569
}
70+
71+
// toFileURL converts a local filesystem path into a "file://" URL in a way
72+
// that won't confuse Gomplate on Windows or Linux.
73+
//
74+
// On Windows, e.g. localPath = "D:\Temp\foo.json" => "file://D:/Temp/foo.json"
75+
// On Linux, e.g. localPath = "/tmp/foo.json" => "file:///tmp/foo.json"
76+
func toFileURL(localPath string) (string, error) {
77+
pathSlashed := filepath.ToSlash(localPath)
78+
79+
if runtime.GOOS == "windows" {
80+
// If pathSlashed is "/D:/Temp/foo.json", remove the leading slash => "D:/Temp/foo.json"
81+
// Then prepend "file://"
82+
if strings.HasPrefix(pathSlashed, "/") {
83+
pathSlashed = strings.TrimPrefix(pathSlashed, "/") // e.g. "D:/Temp/foo.json"
84+
}
85+
return "file://" + pathSlashed, nil // e.g. "file://D:/Temp/foo.json"
86+
}
87+
88+
// Non-Windows: a path like "/tmp/foo.json" => "file:///tmp/foo.json"
89+
// If it doesn't start with '/', make it absolute
90+
if !strings.HasPrefix(pathSlashed, "/") {
91+
pathSlashed = "/" + pathSlashed
92+
}
93+
return "file://" + pathSlashed, nil
94+
}
95+
96+
func fixWindowsFileURL(rawURL string) (*url.URL, error) {
97+
u, err := url.Parse(rawURL)
98+
if err != nil {
99+
return nil, fmt.Errorf("failed to parse URL %q: %w", rawURL, err)
100+
}
101+
if runtime.GOOS == "windows" && u.Scheme == "file" {
102+
if len(u.Host) > 0 {
103+
u.Path = u.Host + u.Path
104+
u.Host = ""
105+
}
106+
if strings.HasPrefix(u.Path, "/") && len(u.Path) > 2 && u.Path[2] == ':' {
107+
u.Path = strings.TrimPrefix(u.Path, "/") // => "D:/Temp/foo.json"
108+
}
109+
}
110+
111+
return u, nil
112+
}

internal/exec/template_utils.go

+34-33
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8-
"net/url"
98
"os"
109
"text/template"
1110
"text/template/parse"
@@ -234,57 +233,50 @@ func ProcessTmplWithDatasourcesGomplate(
234233
ignoreMissingTemplateValues bool,
235234
) (string, error) {
236235

237-
/* Since there's no API method for this in Gomplate 3.11.8, have to set up env var
238-
The .Option("missingkey=default") approach isn't used to avoid having our own render logic.
239-
Instead we rely on standard gomplate.NewRenderer()
240-
*/
241236
if ignoreMissingTemplateValues {
242237
os.Setenv("GOMPLATE_MISSINGKEY", "default")
243238
defer os.Unsetenv("GOMPLATE_MISSINGKEY")
244239
}
245240

246-
// Write merged data to a temporary JSON file
247-
// Required to make no changes to the original templates used with build-harness
248-
241+
// 1) Write the 'inner' data
249242
rawJSON, err := json.Marshal(mergedData)
250243
if err != nil {
251244
return "", fmt.Errorf("failed to marshal merged data to JSON: %w", err)
252245
}
253-
254246
tmpfile, err := os.CreateTemp("", "gomplate-data-*.json")
255247
if err != nil {
256248
return "", fmt.Errorf("failed to create temp data file for gomplate: %w", err)
257249
}
258250
tmpName := tmpfile.Name()
259251
defer os.Remove(tmpName)
260252

261-
if _, err = tmpfile.Write(rawJSON); err != nil {
262-
_ = tmpfile.Close()
253+
if _, err := tmpfile.Write(rawJSON); err != nil {
254+
tmpfile.Close()
263255
return "", fmt.Errorf("failed to write JSON to temp file: %w", err)
264256
}
265-
if err = tmpfile.Close(); err != nil {
257+
if err := tmpfile.Close(); err != nil {
266258
return "", fmt.Errorf("failed to close temp data file: %w", err)
267259
}
268260

269-
// This is the file URL, it is referenced in .Env.README_YAML
270-
fileURL, err := url.Parse("file://" + tmpName)
261+
fileURL, err := toFileURL(tmpName)
271262
if err != nil {
272-
return "", fmt.Errorf("failed to parse temp file path: %w", err)
263+
return "", fmt.Errorf("failed to convert temp file path to file URL: %w", err)
273264
}
274265

275-
// Build the top-level data object that includes Env
276-
topLevel := make(map[string]interface{})
277-
278-
// Add .Env.README_YAML to point to fileURL
279-
topLevel["Env"] = map[string]interface{}{
280-
"README_YAML": fileURL.String(),
266+
finalFileUrl, err := fixWindowsFileURL(fileURL)
267+
if err != nil {
268+
return "", err
281269
}
282270

283-
// Could be refactored later to avoid the second temp file, but this is more straighforward
284-
271+
// 2) Write the 'outer' top-level
272+
topLevel := map[string]interface{}{
273+
"Env": map[string]interface{}{
274+
"README_YAML": fileURL,
275+
},
276+
}
285277
outerJSON, err := json.Marshal(topLevel)
286278
if err != nil {
287-
return "", fmt.Errorf("failed to marshal top-level data: %w", err)
279+
return "", err
288280
}
289281

290282
tmpfile2, err := os.CreateTemp("", "gomplate-top-level-*.json")
@@ -295,30 +287,40 @@ func ProcessTmplWithDatasourcesGomplate(
295287
defer os.Remove(tmpName2)
296288

297289
if _, err = tmpfile2.Write(outerJSON); err != nil {
298-
_ = tmpfile2.Close()
299-
return "", fmt.Errorf("failed to write top-level JSON to temp file: %w", err)
290+
tmpfile2.Close()
291+
return "", fmt.Errorf("failed to write top-level JSON: %w", err)
300292
}
301293
if err = tmpfile2.Close(); err != nil {
302-
return "", fmt.Errorf("failed to close top-level temp data file: %w", err)
294+
return "", fmt.Errorf("failed to close top-level JSON: %w", err)
303295
}
304296

305-
topLevelFileURL, err := url.Parse("file://" + tmpName2)
297+
topLevelFileURL, err := toFileURL(tmpName2)
306298
if err != nil {
307-
return "", fmt.Errorf("failed to parse top-level temp file path: %w", err)
299+
return "", fmt.Errorf("failed to convert top-level temp file path to file URL: %w", err)
308300
}
309301

310-
// Build the gomplate Options to point the entire "dot" context at the second file
302+
// This step is crucial on Windows:
303+
finalTopLevelFileURL, err := fixWindowsFileURL(topLevelFileURL)
304+
if err != nil {
305+
return "", err
306+
}
307+
308+
// 3) Construct Gomplate Options
311309
opts := gomplate.Options{
312310
Context: map[string]gomplate.Datasource{
313311
".": {
314-
URL: topLevelFileURL,
312+
URL: finalTopLevelFileURL,
313+
},
314+
"config": {
315+
URL: finalFileUrl,
315316
},
316317
},
317318
LDelim: "{{",
318319
RDelim: "}}",
319320
Funcs: template.FuncMap{},
320321
}
321322

323+
// 4) Render
322324
renderer := gomplate.NewRenderer(opts)
323325

324326
var buf bytes.Buffer
@@ -328,8 +330,7 @@ func ProcessTmplWithDatasourcesGomplate(
328330
Writer: &buf,
329331
}
330332

331-
err = renderer.RenderTemplates(context.Background(), []gomplate.Template{tpl})
332-
if err != nil {
333+
if err := renderer.RenderTemplates(context.Background(), []gomplate.Template{tpl}); err != nil {
333334
return "", fmt.Errorf("failed to render template: %w", err)
334335
}
335336

0 commit comments

Comments
 (0)