Skip to content

Commit ef43e3d

Browse files
datasource: add file datasource
The file datasource is meant to be used to pre-create a file that can then be used elsewhere in the build process by its path. This is useful for example when building a configuration file from a template, so then the resulting file can be referenced by components which only accept file paths.
1 parent 1a1fada commit ef43e3d

File tree

10 files changed

+434
-0
lines changed

10 files changed

+434
-0
lines changed

command/plugin.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
filebuilder "github.com/hashicorp/packer/builder/file"
1717
nullbuilder "github.com/hashicorp/packer/builder/null"
18+
filedatasource "github.com/hashicorp/packer/datasource/file"
1819
hcppackerimagedatasource "github.com/hashicorp/packer/datasource/hcp-packer-image"
1920
hcppackeriterationdatasource "github.com/hashicorp/packer/datasource/hcp-packer-iteration"
2021
httpdatasource "github.com/hashicorp/packer/datasource/http"
@@ -63,6 +64,7 @@ var PostProcessors = map[string]packersdk.PostProcessor{
6364
}
6465

6566
var Datasources = map[string]packersdk.Datasource{
67+
"file": new(filedatasource.Datasource),
6668
"hcp-packer-image": new(hcppackerimagedatasource.Datasource),
6769
"hcp-packer-iteration": new(hcppackeriterationdatasource.Datasource),
6870
"http": new(httpdatasource.Datasource),

datasource/file/data.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
//go:generate packer-sdc struct-markdown
5+
//go:generate packer-sdc mapstructure-to-hcl2 -type DatasourceOutput,Config
6+
package file
7+
8+
import (
9+
"fmt"
10+
"os"
11+
12+
"github.com/hashicorp/hcl/v2/hcldec"
13+
"github.com/hashicorp/packer-plugin-sdk/common"
14+
"github.com/hashicorp/packer-plugin-sdk/hcl2helper"
15+
"github.com/hashicorp/packer-plugin-sdk/template/config"
16+
"github.com/zclconf/go-cty/cty"
17+
)
18+
19+
type Config struct {
20+
common.PackerConfig `mapstructure:",squash"`
21+
// The contents of the file to create
22+
//
23+
// This is useful especially for files that involve templating so that
24+
// Packer can dynamically create files and expose them for later importing
25+
// as attributes in another entity.
26+
//
27+
// If no contents are specified, the resulting file will be empty.
28+
Contents string `mapstructure:"contents" required:"false"`
29+
// The file to write the contents to.
30+
Destination string `mapstructure:"destination" required:"true"`
31+
// Erase the destination if it exists.
32+
//
33+
// Default: `false`
34+
Force bool `mapstructure:"force" required:"false"`
35+
}
36+
37+
type Datasource struct {
38+
config Config
39+
}
40+
41+
type DatasourceOutput struct {
42+
// The path of the file created
43+
Path string `mapstructure:"path"`
44+
}
45+
46+
func (d *Datasource) ConfigSpec() hcldec.ObjectSpec {
47+
return d.config.FlatMapstructure().HCL2Spec()
48+
}
49+
50+
func (d *Datasource) Configure(raws ...interface{}) error {
51+
err := config.Decode(&d.config, nil, raws...)
52+
if err != nil {
53+
return err
54+
}
55+
56+
if d.config.Destination == "" {
57+
return fmt.Errorf("The `destination` must be specified.")
58+
}
59+
60+
return nil
61+
}
62+
63+
func (d *Datasource) OutputSpec() hcldec.ObjectSpec {
64+
return (&DatasourceOutput{}).FlatMapstructure().HCL2Spec()
65+
}
66+
67+
func (d *Datasource) Execute() (cty.Value, error) {
68+
nulVal := cty.NullVal(cty.EmptyObject)
69+
70+
_, err := os.Stat(d.config.Destination)
71+
if err == nil {
72+
if !d.config.Force {
73+
return nulVal, fmt.Errorf("destination file %q already exists", d.config.Destination)
74+
}
75+
}
76+
77+
dest, err := os.OpenFile(d.config.Destination, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
78+
if err != nil {
79+
return nulVal, fmt.Errorf("failed to create destination %q: %s", d.config.Destination, err)
80+
}
81+
82+
defer dest.Close()
83+
84+
written, err := dest.Write([]byte(d.config.Contents))
85+
if err != nil {
86+
defer os.Remove(d.config.Destination)
87+
return nulVal, fmt.Errorf("failed to write contents to %q: %s", d.config.Destination, err)
88+
}
89+
90+
if written != len(d.config.Contents) {
91+
defer os.Remove(d.config.Destination)
92+
return nulVal, fmt.Errorf(
93+
"failed to write contents to %q: expected to write %d bytes, but wrote %d instead",
94+
d.config.Destination,
95+
len(d.config.Contents),
96+
written)
97+
}
98+
99+
output := DatasourceOutput{
100+
Path: d.config.Destination,
101+
}
102+
103+
return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil
104+
}

datasource/file/data.hcl2spec.go

Lines changed: 74 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

datasource/file/data_acc_test.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package file
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"os/exec"
10+
"testing"
11+
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/hashicorp/packer-plugin-sdk/acctest"
14+
)
15+
16+
func TestFileDataSource(t *testing.T) {
17+
tests := []struct {
18+
name string
19+
template string
20+
createOutput bool
21+
expectError bool
22+
expectOutput string
23+
}{
24+
{
25+
"Success - write empty file",
26+
basicEmptyFileWrite,
27+
false,
28+
false,
29+
"",
30+
},
31+
{
32+
"Fail - write empty file, pre-existing output",
33+
basicEmptyFileWrite,
34+
true,
35+
true,
36+
"",
37+
},
38+
{
39+
"Success - write empty file, pre-existing output",
40+
basicEmptyFileWriteForce,
41+
true,
42+
false,
43+
"",
44+
},
45+
{
46+
"Success - write template to output",
47+
basicFileWithTemplateContents,
48+
false,
49+
false,
50+
"contents are 12345\n",
51+
},
52+
}
53+
54+
for _, tt := range tests {
55+
t.Run(tt.name, func(t *testing.T) {
56+
testCase := &acctest.PluginTestCase{
57+
Name: tt.name,
58+
Setup: func() error {
59+
return nil
60+
},
61+
Teardown: func() error {
62+
return nil
63+
},
64+
Template: tt.template,
65+
Type: "http",
66+
Check: func(buildCommand *exec.Cmd, logfile string) error {
67+
if buildCommand.ProcessState != nil {
68+
if buildCommand.ProcessState.ExitCode() != 0 && !tt.expectError {
69+
return fmt.Errorf("Bad exit code. Logfile: %s", logfile)
70+
}
71+
if tt.expectError && buildCommand.ProcessState.ExitCode() == 0 {
72+
return fmt.Errorf("Expected an error but succeeded.")
73+
}
74+
}
75+
76+
if tt.expectError {
77+
return nil
78+
}
79+
80+
outFile, err := os.ReadFile("output")
81+
if err != nil {
82+
return fmt.Errorf("failed to read output file: %s", err)
83+
}
84+
85+
diff := cmp.Diff(string(outFile), tt.expectOutput)
86+
if diff != "" {
87+
return fmt.Errorf("diff found in output: %s", diff)
88+
}
89+
90+
return nil
91+
},
92+
}
93+
94+
os.RemoveAll("output")
95+
if tt.createOutput {
96+
err := os.WriteFile("output", []byte{}, 0644)
97+
if err != nil {
98+
t.Fatalf("failed to pre-create output file: %s", err)
99+
}
100+
}
101+
102+
acctest.TestPlugin(t, testCase)
103+
104+
os.RemoveAll("output")
105+
})
106+
}
107+
}
108+
109+
var basicEmptyFileWrite string = `
110+
source "null" "test" {
111+
communicator = "none"
112+
}
113+
114+
data "file" "empty" {
115+
destination = "output"
116+
}
117+
118+
build {
119+
sources = [
120+
"source.null.test"
121+
]
122+
123+
provisioner "shell-local" {
124+
inline = [
125+
"set -ex",
126+
"test -f ${data.file.empty.path}",
127+
]
128+
}
129+
}
130+
`
131+
132+
var basicEmptyFileWriteForce string = `
133+
source "null" "test" {
134+
communicator = "none"
135+
}
136+
137+
data "file" "empty" {
138+
destination = "output"
139+
force = true
140+
}
141+
142+
build {
143+
sources = [
144+
"source.null.test"
145+
]
146+
147+
provisioner "shell-local" {
148+
inline = [
149+
"set -ex",
150+
"test -f ${data.file.empty.path}",
151+
]
152+
}
153+
}
154+
`
155+
156+
var basicFileWithTemplateContents string = `
157+
source "null" "test" {
158+
communicator = "none"
159+
}
160+
161+
data "file" "empty" {
162+
contents = templatefile("test-fixtures/template.pkrtpl.hcl", {
163+
"value" = "12345",
164+
})
165+
destination = "output"
166+
}
167+
168+
build {
169+
sources = [
170+
"source.null.test"
171+
]
172+
173+
provisioner "shell-local" {
174+
inline = [
175+
"set -ex",
176+
"test -f ${data.file.empty.path}",
177+
]
178+
}
179+
}
180+
`
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
contents are ${value}

0 commit comments

Comments
 (0)