Skip to content

Commit 9562ec2

Browse files
authored
Merge pull request #90 from techpivot/main
Add new opt-in flag to specify the output_file_mode to produce more deterministic behavior across operating systems.
2 parents 6cf95ea + 8c38af7 commit 9562ec2

File tree

7 files changed

+144
-28
lines changed

7 files changed

+144
-28
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 2.2.0 (Unreleased)
2+
3+
ENHANCEMENTS:
4+
5+
* New opt-in flag to specify the `output_file_mode` to produce more deterministic behavior across operating systems. ([#90](https://github.com/hashicorp/terraform-provider-archive/pull/90))
6+
17
## 2.1.0 (February 19, 2021)
28

39
Binary releases of this provider now include the darwin-arm64 platform. This version contains no further changes.

internal/provider/archiver.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,18 @@ type Archiver interface {
1010
ArchiveFile(infilename string) error
1111
ArchiveDir(indirname string, excludes []string) error
1212
ArchiveMultiple(content map[string][]byte) error
13+
SetOutputFileMode(outputFileMode string)
1314
}
1415

15-
type ArchiverBuilder func(filepath string) Archiver
16+
type ArchiverBuilder func(outputPath string) Archiver
1617

1718
var archiverBuilders = map[string]ArchiverBuilder{
1819
"zip": NewZipArchiver,
1920
}
2021

21-
func getArchiver(archiveType string, filepath string) Archiver {
22+
func getArchiver(archiveType string, outputPath string) Archiver {
2223
if builder, ok := archiverBuilders[archiveType]; ok {
23-
return builder(filepath)
24+
return builder(outputPath)
2425
}
2526
return nil
2627
}

internal/provider/data_source_archive_file.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ func dataSourceFile() *schema.Resource {
113113
ForceNew: true,
114114
Description: "MD5 of output file",
115115
},
116+
"output_file_mode": {
117+
Type: schema.TypeString,
118+
Optional: true,
119+
Default: "",
120+
ForceNew: true,
121+
},
116122
},
117123
}
118124
}
@@ -141,13 +147,12 @@ func dataSourceFileRead(d *schema.ResourceData, meta interface{}) error {
141147

142148
sha1, base64sha256, md5, err := genFileShas(outputPath)
143149
if err != nil {
144-
145150
return fmt.Errorf("could not generate file checksum sha256: %s", err)
146151
}
152+
147153
d.Set("output_sha", sha1)
148154
d.Set("output_base64sha256", base64sha256)
149155
d.Set("output_md5", md5)
150-
151156
d.Set("output_size", fi.Size())
152157
d.SetId(d.Get("output_sha").(string))
153158

@@ -171,6 +176,11 @@ func archive(d *schema.ResourceData) error {
171176
return fmt.Errorf("archive type not supported: %s", archiveType)
172177
}
173178

179+
outputFileMode := d.Get("output_file_mode").(string)
180+
if outputFileMode != "" {
181+
archiver.SetOutputFileMode(outputFileMode)
182+
}
183+
174184
if dir, ok := d.GetOk("source_dir"); ok {
175185
if excludes, ok := d.GetOk("excludes"); ok {
176186
excludeList := expandStringList(excludes.(*schema.Set).List())

internal/provider/data_source_archive_file_test.go

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"io/ioutil"
66
"os"
77
"path/filepath"
8-
"regexp"
98
"testing"
109

1110
r "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
@@ -28,19 +27,14 @@ func TestAccArchiveFile_Basic(t *testing.T) {
2827
testAccArchiveFileExists(f, &fileSize),
2928
r.TestCheckResourceAttrPtr("data.archive_file.foo", "output_size", &fileSize),
3029

31-
// We just check the hashes for syntax rather than exact
32-
// content since we don't want to break if the archive
33-
// library starts generating different bytes that are
34-
// functionally equivalent.
35-
r.TestMatchResourceAttr(
36-
"data.archive_file.foo", "output_base64sha256",
37-
regexp.MustCompile(`^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$`),
30+
r.TestCheckResourceAttr(
31+
"data.archive_file.foo", "output_base64sha256", "P7VckxoEiUO411WN3nwuS/yOBL4zsbVWkQU9E1I5H6c=",
3832
),
39-
r.TestMatchResourceAttr(
40-
"data.archive_file.foo", "output_md5", regexp.MustCompile(`^[0-9a-f]{32}$`),
33+
r.TestCheckResourceAttr(
34+
"data.archive_file.foo", "output_md5", "ea35f0444ea9a3d5641d8760bc2815cc",
4135
),
42-
r.TestMatchResourceAttr(
43-
"data.archive_file.foo", "output_sha", regexp.MustCompile(`^[0-9a-f]{40}$`),
36+
r.TestCheckResourceAttr(
37+
"data.archive_file.foo", "output_sha", "019c79c4dc14dbe1edb3e467b2de6a6aad148717",
4438
),
4539
),
4640
},
@@ -49,13 +43,31 @@ func TestAccArchiveFile_Basic(t *testing.T) {
4943
Check: r.ComposeTestCheckFunc(
5044
testAccArchiveFileExists(f, &fileSize),
5145
r.TestCheckResourceAttrPtr("data.archive_file.foo", "output_size", &fileSize),
46+
r.TestCheckResourceAttr(
47+
"data.archive_file.foo", "output_base64sha256", "UTE4f5cWfaR6p0HfOrLILxgvF8UUwiJTjTRwjQTgdWs=",
48+
),
49+
r.TestCheckResourceAttr(
50+
"data.archive_file.foo", "output_md5", "59fbc9e62af3cbc2f588f97498240dae",
51+
),
52+
r.TestCheckResourceAttr(
53+
"data.archive_file.foo", "output_sha", "ce4ee1450ab93ac86e11446649e44cea907b6568",
54+
),
5255
),
5356
},
5457
{
5558
Config: testAccArchiveFileDirConfig(f),
5659
Check: r.ComposeTestCheckFunc(
5760
testAccArchiveFileExists(f, &fileSize),
5861
r.TestCheckResourceAttrPtr("data.archive_file.foo", "output_size", &fileSize),
62+
r.TestCheckResourceAttr(
63+
"data.archive_file.foo", "output_base64sha256", "ydB8wtq8nK9vQ77VH6YTwoHmyljK46jW+uIJSwCzNpo=",
64+
),
65+
r.TestCheckResourceAttr(
66+
"data.archive_file.foo", "output_md5", "b73f64a383716070aa4a29563b8b14d4",
67+
),
68+
r.TestCheckResourceAttr(
69+
"data.archive_file.foo", "output_sha", "76d20a402eefd1cfbdc47886abd4e0909616c191",
70+
),
5971
),
6072
},
6173
{
@@ -103,19 +115,21 @@ data "archive_file" "foo" {
103115
func testAccArchiveFileFileConfig(outputPath string) string {
104116
return fmt.Sprintf(`
105117
data "archive_file" "foo" {
106-
type = "zip"
107-
source_file = "test-fixtures/test-file.txt"
108-
output_path = "%s"
118+
type = "zip"
119+
source_file = "test-fixtures/test-file.txt"
120+
output_path = "%s"
121+
output_file_mode = "0666"
109122
}
110123
`, filepath.ToSlash(outputPath))
111124
}
112125

113126
func testAccArchiveFileDirConfig(outputPath string) string {
114127
return fmt.Sprintf(`
115128
data "archive_file" "foo" {
116-
type = "zip"
117-
source_dir = "test-fixtures/test-dir"
118-
output_path = "%s"
129+
type = "zip"
130+
source_dir = "test-fixtures/test-dir"
131+
output_path = "%s"
132+
output_file_mode = "0666"
119133
}
120134
`, filepath.ToSlash(outputPath))
121135
}

internal/provider/zip_archiver.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import (
77
"os"
88
"path/filepath"
99
"sort"
10+
"strconv"
1011
"time"
1112
)
1213

1314
type ZipArchiver struct {
14-
filepath string
15-
filewriter *os.File
16-
writer *zip.Writer
15+
filepath string
16+
outputFileMode string // Default value "" means unset
17+
filewriter *os.File
18+
writer *zip.Writer
1719
}
1820

1921
func NewZipArchiver(filepath string) Archiver {
@@ -62,6 +64,14 @@ func (a *ZipArchiver) ArchiveFile(infilename string) error {
6264
// fh.Modified alone isn't enough when using a zero value
6365
fh.SetModTime(time.Time{})
6466

67+
if a.outputFileMode != "" {
68+
filemode, err := strconv.ParseUint(a.outputFileMode, 0, 32)
69+
if err != nil {
70+
return fmt.Errorf("error parsing output_file_mode value: %s", a.outputFileMode)
71+
}
72+
fh.SetMode(os.FileMode(filemode))
73+
}
74+
6575
f, err := a.writer.CreateHeader(fh)
6676
if err != nil {
6777
return fmt.Errorf("error creating file inside archive: %s", err)
@@ -137,6 +147,14 @@ func (a *ZipArchiver) ArchiveDir(indirname string, excludes []string) error {
137147
// fh.Modified alone isn't enough when using a zero value
138148
fh.SetModTime(time.Time{})
139149

150+
if a.outputFileMode != "" {
151+
filemode, err := strconv.ParseUint(a.outputFileMode, 0, 32)
152+
if err != nil {
153+
return fmt.Errorf("error parsing output_file_mode value: %s", a.outputFileMode)
154+
}
155+
fh.SetMode(os.FileMode(filemode))
156+
}
157+
140158
f, err := a.writer.CreateHeader(fh)
141159
if err != nil {
142160
return fmt.Errorf("error creating file inside archive: %s", err)
@@ -178,6 +196,10 @@ func (a *ZipArchiver) ArchiveMultiple(content map[string][]byte) error {
178196
return nil
179197
}
180198

199+
func (a *ZipArchiver) SetOutputFileMode(outputFileMode string) {
200+
a.outputFileMode = outputFileMode
201+
}
202+
181203
func (a *ZipArchiver) open() error {
182204
f, err := os.Create(a.filepath)
183205
if err != nil {

internal/provider/zip_archiver_test.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io/ioutil"
77
"os"
88
"path/filepath"
9+
"strconv"
910
"testing"
1011
"time"
1112
)
@@ -33,6 +34,30 @@ func TestZipArchiver_File(t *testing.T) {
3334
"test-file.txt": []byte("This is test content"),
3435
})
3536
}
37+
38+
func TestZipArchiver_FileMode(t *testing.T) {
39+
file, err := ioutil.TempFile("", "archive-file-mode-test.zip")
40+
if err != nil {
41+
t.Fatal(err)
42+
}
43+
44+
var (
45+
zipFilePath = file.Name()
46+
toZipPath = filepath.FromSlash("./test-fixtures/test-file.txt")
47+
)
48+
49+
stringArray := [5]string{"0444", "0644", "0666", "0744", "0777"}
50+
for _, element := range stringArray {
51+
archiver := NewZipArchiver(zipFilePath)
52+
archiver.SetOutputFileMode(element)
53+
if err := archiver.ArchiveFile(toZipPath); err != nil {
54+
t.Fatalf("unexpected error: %s", err)
55+
}
56+
57+
ensureFileMode(t, zipFilePath, element)
58+
}
59+
}
60+
3661
func TestZipArchiver_FileModified(t *testing.T) {
3762
var (
3863
zipFilePath = filepath.FromSlash("archive-file.zip")
@@ -63,7 +88,7 @@ func TestZipArchiver_FileModified(t *testing.T) {
6388

6489
actualContents, err := ioutil.ReadFile(zipFilePath)
6590
if err != nil {
66-
t.Fatalf("unexpecte error: %s", err)
91+
t.Fatalf("unexpected error: %s", err)
6792
}
6893

6994
if !bytes.Equal(expectedContents, actualContents) {
@@ -126,10 +151,10 @@ func TestZipArchiver_Multiple(t *testing.T) {
126151
}
127152

128153
ensureContents(t, zipfilepath, content)
129-
130154
}
131155

132156
func ensureContents(t *testing.T, zipfilepath string, wants map[string][]byte) {
157+
t.Helper()
133158
r, err := zip.OpenReader(zipfilepath)
134159
if err != nil {
135160
t.Fatalf("could not open zip file: %s", err)
@@ -145,6 +170,7 @@ func ensureContents(t *testing.T, zipfilepath string, wants map[string][]byte) {
145170
}
146171

147172
func ensureContent(t *testing.T, wants map[string][]byte, got *zip.File) {
173+
t.Helper()
148174
want, ok := wants[got.Name]
149175
if !ok {
150176
t.Errorf("additional file in zip: %s", got.Name)
@@ -167,3 +193,28 @@ func ensureContent(t *testing.T, wants map[string][]byte, got *zip.File) {
167193
t.Errorf("mismatched content\ngot\n%s\nwant\n%s", gotContent, wantContent)
168194
}
169195
}
196+
197+
func ensureFileMode(t *testing.T, zipfilepath string, outputFileMode string) {
198+
t.Helper()
199+
r, err := zip.OpenReader(zipfilepath)
200+
if err != nil {
201+
t.Fatalf("could not open zip file: %s", err)
202+
}
203+
defer r.Close()
204+
205+
filemode, err := strconv.ParseUint(outputFileMode, 0, 32)
206+
if err != nil {
207+
t.Fatalf("error parsing outputFileMode value: %s", outputFileMode)
208+
}
209+
var osfilemode = os.FileMode(filemode)
210+
211+
for _, cf := range r.File {
212+
if cf.FileInfo().IsDir() {
213+
continue
214+
}
215+
216+
if cf.Mode() != osfilemode {
217+
t.Fatalf("Expected filemode \"%s\" but was \"%s\"", osfilemode, cf.Mode())
218+
}
219+
}
220+
}

website/docs/d/archive_file.html.markdown

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ data "archive_file" "dotfiles" {
3838
filename = ".ssh/config"
3939
}
4040
}
41+
42+
# Archive a file to be used with Lambda using consistent file mode
43+
44+
data "archive_file" "lambda_my_function" {
45+
type = "zip"
46+
source_file = "${path.module}/../lambda/my-function/index.js"
47+
output_file_mode = "0666"
48+
output_path = "${path.module}/files/lambda-my-function.js.zip"
49+
}
50+
4151
```
4252

4353
~> **Note regarding symbolic links**: Due to a bug, the `archive_file` data
@@ -58,6 +68,8 @@ NOTE: One of `source`, `source_content_filename` (with `source_content`), `sourc
5868

5969
* `output_path` - (Required) The output of the archive file.
6070

71+
* `output_file_mode` (Optional) String that specifies the octal file mode for all archived files. For example: `"0666"`. Setting this will ensure that cross platform usage of this module will not vary the modes of archived files (and ultimately checksums) resulting in more deterministic behavior.
72+
6173
* `source_content` - (Optional) Add only this content to the archive with `source_content_filename` as the filename.
6274

6375
* `source_content_filename` - (Optional) Set this as the filename when using `source_content`.

0 commit comments

Comments
 (0)