forked from hashicorp/consul-template
/
renderer.go
146 lines (125 loc) · 3.29 KB
/
renderer.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
package manager
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
)
type RenderInput struct {
Backup bool
Contents []byte
Dry bool
DryStream io.Writer
Path string
Perms os.FileMode
}
type RenderResult struct {
DidRender bool
WouldRender bool
}
// Render atomically renders a file contents to disk, returning a result of
// whether it would have rendered and actually did render.
func Render(i *RenderInput) (*RenderResult, error) {
existing, err := ioutil.ReadFile(i.Path)
if err != nil && !os.IsNotExist(err) {
return nil, errors.Wrap(err, "failed reading file")
}
if bytes.Equal(existing, i.Contents) {
return &RenderResult{
DidRender: false,
WouldRender: true,
}, nil
}
if i.Dry {
fmt.Fprintf(i.DryStream, "> %s\n%s", i.Path, i.Contents)
} else {
if err := AtomicWrite(i.Path, i.Contents, i.Perms, i.Backup); err != nil {
return nil, errors.Wrap(err, "failed writing file")
}
}
return &RenderResult{
DidRender: true,
WouldRender: true,
}, nil
}
// AtomicWrite accepts a destination path and the template contents. It writes
// the template contents to a TempFile on disk, returning if any errors occur.
//
// If the parent destination directory does not exist, it will be created
// automatically with permissions 0755. To use a different permission, create
// the directory first or use `chmod` in a Command.
//
// If the destination path exists, all attempts will be made to preserve the
// existing file permissions. If those permissions cannot be read, an error is
// returned. If the file does not exist, it will be created automatically with
// permissions 0644. To use a different permission, create the destination file
// first or use `chmod` in a Command.
//
// If no errors occur, the Tempfile is "renamed" (moved) to the destination
// path.
func AtomicWrite(path string, contents []byte, perms os.FileMode, backup bool) error {
if path == "" {
return fmt.Errorf("missing destination")
}
parent := filepath.Dir(path)
if _, err := os.Stat(parent); os.IsNotExist(err) {
if err := os.MkdirAll(parent, 0755); err != nil {
return err
}
}
f, err := ioutil.TempFile(parent, "")
if err != nil {
return err
}
defer os.Remove(f.Name())
if _, err := f.Write(contents); err != nil {
return err
}
if err := f.Sync(); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
if err := os.Chmod(f.Name(), perms); err != nil {
return err
}
// If we got this far, it means we are about to save the file. Copy the
// current contents of the file onto disk (if it exists) so we have a backup.
if backup {
if _, err := os.Stat(path); !os.IsNotExist(err) {
if err := copyFile(path, path+".bak"); err != nil {
return err
}
}
}
if err := os.Rename(f.Name(), path); err != nil {
return err
}
return nil
}
// copyFile copies the file at src to the path at dst. Any errors that occur
// are returned.
func copyFile(src, dst string) error {
s, err := os.Open(src)
if err != nil {
return err
}
defer s.Close()
stat, err := s.Stat()
if err != nil {
return err
}
d, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, stat.Mode())
if err != nil {
return err
}
if _, err := io.Copy(d, s); err != nil {
d.Close()
return err
}
return d.Close()
}