forked from gruntwork-io/terragrunt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
file.go
365 lines (313 loc) · 11.7 KB
/
file.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
package util
import (
"encoding/gob"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"fmt"
"github.com/gruntwork-io/terragrunt/errors"
"github.com/mattn/go-zglob"
)
// Return true if the given file exists
func FileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// Return the canonical version of the given path, relative to the given base path. That is, if the given path is a
// relative path, assume it is relative to the given base path. A canonical path is an absolute path with all relative
// components (e.g. "../") fully resolved, which makes it safe to compare paths as strings.
func CanonicalPath(path string, basePath string) (string, error) {
if !filepath.IsAbs(path) {
path = JoinPath(basePath, path)
}
absPath, err := filepath.Abs(path)
if err != nil {
return "", err
}
return CleanPath(absPath), nil
}
// Return the canonical version of the given paths, relative to the given base path. That is, if a given path is a
// relative path, assume it is relative to the given base path. A canonical path is an absolute path with all relative
// components (e.g. "../") fully resolved, which makes it safe to compare paths as strings.
func CanonicalPaths(paths []string, basePath string) ([]string, error) {
canonicalPaths := []string{}
for _, path := range paths {
canonicalPath, err := CanonicalPath(path, basePath)
if err != nil {
return canonicalPaths, err
}
canonicalPaths = append(canonicalPaths, canonicalPath)
}
return canonicalPaths, nil
}
// Returns true if the given regex can be found in any of the files matched by the given glob
func Grep(regex *regexp.Regexp, glob string) (bool, error) {
// Ideally, we'd use a builin Go library like filepath.Glob here, but per https://github.com/golang/go/issues/11862,
// the current go implementation doesn't support treating ** as zero or more directories, just zero or one.
// So we use a third-party library.
matches, err := zglob.Glob(glob)
if err != nil {
return false, errors.WithStackTrace(err)
}
for _, match := range matches {
if IsDir(match) {
continue
}
bytes, err := ioutil.ReadFile(match)
if err != nil {
return false, errors.WithStackTrace(err)
}
if regex.Match(bytes) {
return true, nil
}
}
return false, nil
}
// Return true if the path points to a directory
func IsDir(path string) bool {
fileInfo, err := os.Stat(path)
return err == nil && fileInfo.IsDir()
}
// Return true if the path points to a file
func IsFile(path string) bool {
fileInfo, err := os.Stat(path)
return err == nil && !fileInfo.IsDir()
}
// Return the relative path you would have to take to get from basePath to path
func GetPathRelativeTo(path string, basePath string) (string, error) {
if path == "" {
path = "."
}
if basePath == "" {
basePath = "."
}
inputFolderAbs, err := filepath.Abs(basePath)
if err != nil {
return "", errors.WithStackTrace(err)
}
fileAbs, err := filepath.Abs(path)
if err != nil {
return "", errors.WithStackTrace(err)
}
relPath, err := filepath.Rel(inputFolderAbs, fileAbs)
if err != nil {
return "", errors.WithStackTrace(err)
}
return filepath.ToSlash(relPath), nil
}
// Return the contents of the file at the given path as a string
func ReadFileAsString(path string) (string, error) {
bytes, err := ioutil.ReadFile(path)
if err != nil {
return "", errors.WithStackTraceAndPrefix(err, "Error reading file at path %s", path)
}
return string(bytes), nil
}
// Copy the files and folders within the source folder into the destination folder. Note that hidden files and folders
// (those starting with a dot) will be skipped. Will create a specified manifest file that contains paths of all copied files.
func CopyFolderContents(source, destination, manifestFile string) error {
return CopyFolderContentsWithFilter(source, destination, manifestFile, func(path string) bool {
return !PathContainsHiddenFileOrFolder(path)
})
}
// Copy the files and folders within the source folder into the destination folder. Pass each file and folder through
// the given filter function and only copy it if the filter returns true. Will create a specified manifest file
// that contains paths of all copied files.
func CopyFolderContentsWithFilter(source, destination, manifestFile string, filter func(path string) bool) error {
if err := os.MkdirAll(destination, 0700); err != nil {
return errors.WithStackTrace(err)
}
manifest := newFileManifest(destination, manifestFile)
if err := manifest.Clean(); err != nil {
return errors.WithStackTrace(err)
}
if err := manifest.Create(); err != nil {
return errors.WithStackTrace(err)
}
defer manifest.Close()
// Why use filepath.Glob here? The original implementation used ioutil.ReadDir, but that method calls lstat on all
// the files/folders in the directory, including files/folders you may want to explicitly skip. The next attempt
// was to use filepath.Walk, but that doesn't work because it ignores symlinks. So, now we turn to filepath.Glob.
files, err := filepath.Glob(fmt.Sprintf("%s/*", source))
if err != nil {
return errors.WithStackTrace(err)
}
for _, file := range files {
fileRelativePath, err := GetPathRelativeTo(file, source)
if err != nil {
return err
}
if !filter(fileRelativePath) {
continue
}
dest := filepath.Join(destination, fileRelativePath)
if IsDir(file) {
info, err := os.Lstat(file)
if err != nil {
return errors.WithStackTrace(err)
}
if err := os.MkdirAll(dest, info.Mode()); err != nil {
return errors.WithStackTrace(err)
}
if err := CopyFolderContentsWithFilter(file, dest, manifestFile, filter); err != nil {
return err
}
if err := manifest.AddDirectory(dest); err != nil {
return err
}
} else {
parentDir := filepath.Dir(dest)
if err := os.MkdirAll(parentDir, 0700); err != nil {
return errors.WithStackTrace(err)
}
if err := CopyFile(file, dest); err != nil {
return err
}
if err := manifest.AddFile(dest); err != nil {
return err
}
}
}
return nil
}
// IsSymLink returns true if the given file is a symbolic link
// Per https://stackoverflow.com/a/18062079/2308858
func IsSymLink(path string) bool {
fileInfo, err := os.Lstat(path)
return err == nil && fileInfo.Mode()&os.ModeSymlink != 0
}
func PathContainsHiddenFileOrFolder(path string) bool {
pathParts := strings.Split(path, string(filepath.Separator))
for _, pathPart := range pathParts {
if strings.HasPrefix(pathPart, ".") && pathPart != "." && pathPart != ".." {
return true
}
}
return false
}
// Copy a file from source to destination
func CopyFile(source string, destination string) error {
contents, err := ioutil.ReadFile(source)
if err != nil {
return errors.WithStackTrace(err)
}
return WriteFileWithSamePermissions(source, destination, contents)
}
// Write a file to the given destination with the given contents using the same permissions as the file at source
func WriteFileWithSamePermissions(source string, destination string, contents []byte) error {
fileInfo, err := os.Stat(source)
if err != nil {
return errors.WithStackTrace(err)
}
return ioutil.WriteFile(destination, contents, fileInfo.Mode())
}
// Windows systems use \ as the path separator *nix uses /
// Use this function when joining paths to force the returned path to use / as the path separator
// This will improve cross-platform compatibility
func JoinPath(elem ...string) string {
return filepath.ToSlash(filepath.Join(elem...))
}
// Use this function when cleaning paths to ensure the returned path uses / as the path separator to improve cross-platform compatibility
func CleanPath(path string) string {
return filepath.ToSlash(filepath.Clean(path))
}
// Join two paths together with a double-slash between them, as this is what Terraform uses to identify where a "repo"
// ends and a path within the repo begins. Note: The Terraform docs only mention two forward-slashes, so it's not clear
// if on Windows those should be two back-slashes? https://www.terraform.io/docs/modules/sources.html
func JoinTerraformModulePath(modulesFolder string, path string) string {
cleanModulesFolder := strings.TrimRight(modulesFolder, `/\`)
cleanPath := strings.TrimLeft(path, `/\`)
return fmt.Sprintf("%s//%s", cleanModulesFolder, cleanPath)
}
// fileManifest represents a manifest with paths of all files copied by terragrunt.
// This allows to clean those files on subsequent runs.
// The problem is as follows: terragrunt copies the terraform source code first to "working directory" using go-getter,
// and then copies all files from the working directory to the above dir.
// It works fine on the first run, but if we delete a file from the current terragrunt directory, we want it
// to be cleaned in the "working directory" as well. Since we don't really know what can get copied by go-getter,
// we have to track all the files we touch in a manifest. This way we know exactly which files we need to clean on
// subsequent runs.
type fileManifest struct {
ManifestFolder string // this is a folder that has the manifest in it
ManifestFile string // this is the manifest file name
encoder *gob.Encoder
fileHandle *os.File
}
// fileManifestEntry represents an entry in the fileManifest.
// It uses a struct with IsDir flag so that we won't have to call Stat on every
// file to determine if it's a directory or a file
type fileManifestEntry struct {
Path string
IsDir bool
}
// Clean will recursively remove all files specified in the manifest
func (manifest *fileManifest) Clean() error {
return manifest.clean(filepath.Join(manifest.ManifestFolder, manifest.ManifestFile))
}
// clean cleans the files in the manifest. If it has a directory entry, then it recursively calls clean()
func (manifest *fileManifest) clean(manifestPath string) error {
// if manifest file doesn't exist, just exit
if !FileExists(manifestPath) {
return nil
}
file, err := os.Open(manifestPath)
if err != nil {
return err
}
defer file.Close()
decoder := gob.NewDecoder(file)
// decode paths one by one
for {
var manifestEntry fileManifestEntry
err = decoder.Decode(&manifestEntry)
if err != nil {
if err == io.EOF {
break
} else {
return err
}
}
if manifestEntry.IsDir {
// join the directory entry path with the manifest file name and call clean()
if err := manifest.clean(filepath.Join(manifestEntry.Path, manifest.ManifestFile)); err != nil {
return errors.WithStackTrace(err)
}
} else {
if err := os.Remove(manifestEntry.Path); err != nil && !os.IsNotExist(err) {
return errors.WithStackTrace(err)
}
}
}
// remove the manifest itself
// it will run after the close defer
defer os.Remove(manifestPath)
return nil
}
// Create will create the manifest file
func (manifest *fileManifest) Create() error {
fileHandle, err := os.OpenFile(filepath.Join(manifest.ManifestFolder, manifest.ManifestFile), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
manifest.fileHandle = fileHandle
manifest.encoder = gob.NewEncoder(manifest.fileHandle)
return nil
}
// AddFile will add the file path to the manifest file. Please make sure to run Create() before using this
func (manifest *fileManifest) AddFile(path string) error {
return manifest.encoder.Encode(fileManifestEntry{Path: path, IsDir: false})
}
// AddDirectory will add the directory path to the manifest file. Please make sure to run Create() before using this
func (manifest *fileManifest) AddDirectory(path string) error {
return manifest.encoder.Encode(fileManifestEntry{Path: path, IsDir: true})
}
// Close closes the manifest file handle
func (manifest *fileManifest) Close() error {
return manifest.fileHandle.Close()
}
func newFileManifest(manifestFolder string, manifestFile string) *fileManifest {
return &fileManifest{ManifestFolder: manifestFolder, ManifestFile: manifestFile}
}