diff --git a/internal/provider/archiver.go b/internal/provider/archiver.go index 66faafb0..0b67d4b0 100644 --- a/internal/provider/archiver.go +++ b/internal/provider/archiver.go @@ -10,6 +10,7 @@ type Archiver interface { ArchiveFile(infilename string) error ArchiveDir(indirname string, excludes []string) error ArchiveMultiple(content map[string][]byte) error + ArchiveMultipleDirs(dirs []interface{}, rootDirs []interface{}, excludes []string) error SetOutputFileMode(outputFileMode string) } diff --git a/internal/provider/data_source_archive_file.go b/internal/provider/data_source_archive_file.go index f0df0dd9..73bcd563 100644 --- a/internal/provider/data_source_archive_file.go +++ b/internal/provider/data_source_archive_file.go @@ -26,6 +26,33 @@ func dataSourceFile() *schema.Resource { Required: true, ForceNew: true, }, + "source_dirs": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "dirs": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "root_dirs": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + ConflictsWith: []string{"source_file", "source_dir", "source_content", "source_content_filename"}, + }, "source": { Type: schema.TypeSet, Optional: true, @@ -44,7 +71,7 @@ func dataSourceFile() *schema.Resource { }, }, }, - ConflictsWith: []string{"source_file", "source_dir", "source_content", "source_content_filename"}, + ConflictsWith: []string{"source_file", "source_dir", "source_dirs", "source_content", "source_content_filename"}, Set: func(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) @@ -57,31 +84,31 @@ func dataSourceFile() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, - ConflictsWith: []string{"source_file", "source_dir"}, + ConflictsWith: []string{"source_file", "source_dir", "source_dirs"}, }, "source_content_filename": { Type: schema.TypeString, Optional: true, ForceNew: true, - ConflictsWith: []string{"source_file", "source_dir"}, + ConflictsWith: []string{"source_file", "source_dir", "source_dirs"}, }, "source_file": { Type: schema.TypeString, Optional: true, ForceNew: true, - ConflictsWith: []string{"source_content", "source_content_filename", "source_dir"}, + ConflictsWith: []string{"source_content", "source_content_filename", "source_dir", "source_dirs"}, }, "source_dir": { Type: schema.TypeString, Optional: true, ForceNew: true, - ConflictsWith: []string{"source_content", "source_content_filename", "source_file"}, + ConflictsWith: []string{"source_content", "source_content_filename", "source_file", "source_dirs"}, }, "excludes": { Type: schema.TypeSet, Optional: true, ForceNew: true, - ConflictsWith: []string{"source_content", "source_content_filename", "source_file"}, + ConflictsWith: []string{"source_content", "source_content_filename", "source_file", "source_dirs"}, Elem: &schema.Schema{ Type: schema.TypeString, }, @@ -212,6 +239,22 @@ func archive(d *schema.ResourceData) error { if err := archiver.ArchiveMultiple(content); err != nil { return fmt.Errorf("error archiving content: %s", err) } + } else if v, ok := d.GetOk("source_dirs"); ok { + vL := v.(*schema.Set).List() + for _, v := range vL { + src := v.(map[string]interface{}) + dirs := src["dirs"].([]interface{}) + rootDirs := src["root_dirs"].([]interface{}) + if excludes, ok := d.GetOk("excludes"); ok { + excludeList := expandStringList(excludes.(*schema.Set).List()) + + archiver.ArchiveMultipleDirs(dirs, rootDirs, excludeList) + return nil + } else { + archiver.ArchiveMultipleDirs(dirs, rootDirs, []string{""}) + return nil + } + } } else { return fmt.Errorf("one of 'source_dir', 'source_file', 'source_content_filename' must be specified") } diff --git a/internal/provider/zip_archiver.go b/internal/provider/zip_archiver.go index 087d0d9f..3505fc8b 100644 --- a/internal/provider/zip_archiver.go +++ b/internal/provider/zip_archiver.go @@ -94,6 +94,150 @@ func checkMatch(fileName string, excludes []string) (value bool) { return false } +func (a *ZipArchiver) ArchiveMultipleDirs(dirs []interface{}, rootDirs []interface{}, excludes []string) error { + if err := a.open(); err != nil { + return err + } + + defer a.close() + + // ensure exclusions are OS compatible paths + for i := range excludes { + excludes[i] = filepath.FromSlash(excludes[i]) + } + + for _, indirname := range rootDirs { + indirname := indirname.(string) + _, err := assertValidDir(indirname) + if err != nil { + return err + } + + filepath.Walk(indirname, func(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("error encountered during file walk: %s", err) + } + + relname, err := filepath.Rel(indirname, path) + if err != nil { + return fmt.Errorf("error relativizing file for archival: %s", err) + } + + isMatch := checkMatch(relname, excludes) + + if info.IsDir() { + if isMatch { + return filepath.SkipDir + } + return nil + } + + if isMatch { + return nil + } + + if err != nil { + return err + } + + fh, err := zip.FileInfoHeader(info) + if err != nil { + return fmt.Errorf("error creating file header: %s", err) + } + fh.Name = filepath.ToSlash(relname) + fh.Method = zip.Deflate + // fh.Modified alone isn't enough when using a zero value + fh.SetModTime(time.Time{}) + + if a.outputFileMode != "" { + filemode, err := strconv.ParseUint(a.outputFileMode, 0, 32) + if err != nil { + return fmt.Errorf("error parsing output_file_mode value: %s", a.outputFileMode) + } + fh.SetMode(os.FileMode(filemode)) + } + + f, err := a.writer.CreateHeader(fh) + if err != nil { + return fmt.Errorf("error creating file inside archive: %s", err) + } + content, err := ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("error reading file for archival: %s", err) + } + _, err = f.Write(content) + return err + }) + } + + for _, indirname := range dirs { + indirname := indirname.(string) + _, err := assertValidDir(indirname) + if err != nil { + return err + } + + filepath.Walk(indirname, func(path string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("error encountered during file walk: %s", err) + } + + relname, err := filepath.Rel(indirname, path) + if err != nil { + return fmt.Errorf("error relativizing file for archival: %s", err) + } + + relname = fmt.Sprintf("%s/%s", filepath.Base(indirname), relname) + + isMatch := checkMatch(relname, excludes) + + if info.IsDir() { + if isMatch { + return filepath.SkipDir + } + return nil + } + + if isMatch { + return nil + } + + if err != nil { + return err + } + + fh, err := zip.FileInfoHeader(info) + if err != nil { + return fmt.Errorf("error creating file header: %s", err) + } + fh.Name = filepath.ToSlash(relname) + fh.Method = zip.Deflate + // fh.Modified alone isn't enough when using a zero value + fh.SetModTime(time.Time{}) + + if a.outputFileMode != "" { + filemode, err := strconv.ParseUint(a.outputFileMode, 0, 32) + if err != nil { + return fmt.Errorf("error parsing output_file_mode value: %s", a.outputFileMode) + } + fh.SetMode(os.FileMode(filemode)) + } + + f, err := a.writer.CreateHeader(fh) + if err != nil { + return fmt.Errorf("error creating file inside archive: %s", err) + } + content, err := ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("error reading file for archival: %s", err) + } + _, err = f.Write(content) + return err + }) + } + return nil +} + func (a *ZipArchiver) ArchiveDir(indirname string, excludes []string) error { _, err := assertValidDir(indirname) if err != nil { diff --git a/website/docs/d/archive_file.html.markdown b/website/docs/d/archive_file.html.markdown index 161609de..50884728 100644 --- a/website/docs/d/archive_file.html.markdown +++ b/website/docs/d/archive_file.html.markdown @@ -61,7 +61,7 @@ data "archive_file" "lambda_my_function" { The following arguments are supported: -NOTE: One of `source`, `source_content_filename` (with `source_content`), `source_file`, or `source_dir` must be specified. +NOTE: One of `source`, `source_content_filename` (with `source_content`), `source_file`, `source_dir`, or `source_dirs` must be specified. * `type` - (Required) The type of archive to generate. NOTE: `zip` is supported. @@ -78,6 +78,8 @@ NOTE: One of `source`, `source_content_filename` (with `source_content`), `sourc * `source_dir` - (Optional) Package entire contents of this directory into the archive. +* `source_dirs` - (Optional) Specifies attributes of multiple directory listing into the archive + * `source` - (Optional) Specifies attributes of a single source file to include into the archive. * `excludes` - (Optional) Specify files to ignore when reading the `source_dir`. @@ -88,6 +90,12 @@ The `source` block supports the following: * `filename` - (Required) Set this as the filename when declaring a `source`. +The `source_dirs` block supports the following: + +* `dirs` - (Required) Add the list of directory as their own folder into the archive + +* `root_dirs` - (Required) Add the list of directory contents into the archive root level + ## Attributes Reference The following attributes are exported: