-
Notifications
You must be signed in to change notification settings - Fork 396
/
template.go
272 lines (239 loc) · 7.84 KB
/
template.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
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package template renders the static files under the "/templates/" directory.
package template
import (
"bytes"
"embed"
"fmt"
"io/fs"
"path"
"path/filepath"
"strings"
"text/template"
"github.com/aws/copilot-cli/internal/pkg/aws/s3"
"github.com/aws/copilot-cli/internal/pkg/template/artifactpath"
)
//go:embed templates templates/overrides/cdk/.gitignore
var templateFS embed.FS
// File names under "templates/".
const (
DNSCertValidatorFileName = "dns-cert-validator"
CertReplicatorFileName = "cert-replicator"
DNSDelegationFileName = "dns-delegation"
CustomDomainFileName = "custom-domain"
AppRunnerCustomDomainLambdaFileName = "custom-domain-app-runner"
customResourceRootPath = "custom-resources"
customResourceZippedScriptName = "index.js"
scriptDirName = "scripts"
)
// AddonsStackLogicalID is the logical ID for the addon stack resource in the main template.
const AddonsStackLogicalID = "AddonsStack"
// Groups of files that belong to the same stack.
var (
envCustomResourceFiles = []string{
DNSCertValidatorFileName,
CertReplicatorFileName,
DNSDelegationFileName,
CustomDomainFileName,
}
)
// Reader is the interface that wraps the Read method.
type Reader interface {
Read(path string) (*Content, error)
}
// Parser is the interface that wraps the Parse method.
type Parser interface {
Parse(path string, data interface{}, options ...ParseOption) (*Content, error)
}
// ReadParser is the interface that wraps the Read and Parse methods.
type ReadParser interface {
Reader
Parser
}
// Uploadable is an uploadable file.
type Uploadable struct {
name string
content []byte
path string
}
// Name returns the name of the custom resource script.
func (e Uploadable) Name() string {
return e.name
}
// Content returns the content of the custom resource script.
func (e Uploadable) Content() []byte {
return e.content
}
type fileToCompress struct {
name string
uploadables []Uploadable
}
type osFS interface {
fs.ReadDirFS
fs.ReadFileFS
}
// Template represents the "/templates/" directory that holds static files to be embedded in the binary.
type Template struct {
fs osFS
}
// New returns a Template object that can be used to parse files under the "/templates/" directory.
func New() *Template {
return &Template{
fs: templateFS,
}
}
// Read returns the contents of the template under "/templates/{path}".
func (t *Template) Read(path string) (*Content, error) {
s, err := t.read(path)
if err != nil {
return nil, err
}
return &Content{
Buffer: bytes.NewBufferString(s),
}, nil
}
// Parse parses the template under "/templates/{path}" with the specified data object and returns its content.
func (t *Template) Parse(path string, data interface{}, options ...ParseOption) (*Content, error) {
tpl, err := t.parse("template", path, options...)
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
if err := tpl.Execute(buf, data); err != nil {
return nil, fmt.Errorf("execute template %s: %w", path, err)
}
return &Content{buf}, nil
}
// UploadEnvironmentCustomResources uploads the environment custom resource scripts.
func (t *Template) UploadEnvironmentCustomResources(upload s3.CompressAndUploadFunc) (map[string]string, error) {
return t.uploadCustomResources(upload, envCustomResourceFiles)
}
func (t *Template) uploadCustomResources(upload s3.CompressAndUploadFunc, fileNames []string) (map[string]string, error) {
urls := make(map[string]string)
for _, name := range fileNames {
url, err := t.uploadFileToCompress(upload, fileToCompress{
name: path.Join(scriptDirName, name),
uploadables: []Uploadable{
{
name: customResourceZippedScriptName,
path: path.Join(customResourceRootPath, fmt.Sprintf("%s.js", name)),
},
},
})
if err != nil {
return nil, err
}
urls[name] = url
}
return urls, nil
}
func (t *Template) uploadFileToCompress(upload s3.CompressAndUploadFunc, file fileToCompress) (string, error) {
var contents []byte
var nameBinaries []s3.NamedBinary
for _, uploadable := range file.uploadables {
content, err := t.Read(uploadable.path)
if err != nil {
return "", err
}
uploadable.content = content.Bytes()
contents = append(contents, uploadable.content...)
nameBinaries = append(nameBinaries, uploadable)
}
// Prefix with a SHA256 checksum of the fileToCompress so that
// only new content gets a new URL. Otherwise, if two fileToCompress have the
// same content then the URL generated will be identical.
url, err := upload(artifactpath.MkdirSHA256(file.name, contents), nameBinaries...)
if err != nil {
return "", fmt.Errorf("upload %s: %w", file.name, err)
}
return url, nil
}
// ParseOption represents a functional option for the Parse method.
type ParseOption func(t *template.Template) *template.Template
// WithFuncs returns a template that can parse additional custom functions.
func WithFuncs(fns map[string]interface{}) ParseOption {
return func(t *template.Template) *template.Template {
return t.Funcs(fns)
}
}
// Content represents the parsed template.
type Content struct {
*bytes.Buffer
}
// MarshalBinary returns the contents as binary and implements the encoding.BinaryMarshaler interface.
func (c *Content) MarshalBinary() ([]byte, error) {
return c.Bytes(), nil
}
// newTextTemplate returns a named text/template with the "indent" and "include" functions.
func newTextTemplate(name string) *template.Template {
t := template.New(name)
t.Funcs(map[string]interface{}{
"include": func(name string, data interface{}) (string, error) {
// Taken from https://github.com/helm/helm/blob/8648ccf5d35d682dcd5f7a9c2082f0aaf071e817/pkg/engine/engine.go#L147-L154
buf := bytes.NewBuffer(nil)
if err := t.ExecuteTemplate(buf, name, data); err != nil {
return "", err
}
return buf.String(), nil
},
"indent": func(spaces int, s string) string {
// Taken from https://github.com/Masterminds/sprig/blob/48e6b77026913419ba1a4694dde186dc9c4ad74d/strings.go#L109-L112
pad := strings.Repeat(" ", spaces)
return pad + strings.Replace(s, "\n", "\n"+pad, -1)
},
})
return t
}
func (t *Template) read(path string) (string, error) {
dat, err := t.fs.ReadFile(filepath.ToSlash(filepath.Join("templates", path))) // We need to use "/" even on Windows with go:embed.
if err != nil {
return "", fmt.Errorf("read template %s: %w", path, err)
}
return string(dat), nil
}
// parse reads the file at path and returns a parsed text/template object with the given name.
func (t *Template) parse(name, path string, options ...ParseOption) (*template.Template, error) {
content, err := t.read(path)
if err != nil {
return nil, err
}
emptyTextTpl := newTextTemplate(name)
for _, opt := range options {
emptyTextTpl = opt(emptyTextTpl)
}
parsedTpl, err := emptyTextTpl.Parse(content)
if err != nil {
return nil, fmt.Errorf("parse template %s: %w", path, err)
}
return parsedTpl, nil
}
// WalkDirFunc is the type of the function called by any Walk functions while visiting each file under a directory.
type WalkDirFunc func(name string, content *Content) error
func (t *Template) walkDir(basePath, curPath string, data any, fn WalkDirFunc, parseOpts ...ParseOption) error {
entries, err := t.fs.ReadDir(path.Join("templates", curPath))
if err != nil {
return fmt.Errorf("read dir %q: %w", curPath, err)
}
for _, entry := range entries {
targetPath := path.Join(curPath, entry.Name())
if entry.IsDir() {
if err := t.walkDir(basePath, targetPath, data, fn); err != nil {
return err
}
continue
}
content, err := t.Parse(targetPath, data, parseOpts...)
if err != nil {
return err
}
relPath, err := filepath.Rel(basePath, targetPath)
if err != nil {
return err
}
if err := fn(relPath, content); err != nil {
return err
}
}
return nil
}