/
readfile.go
236 lines (214 loc) · 6.53 KB
/
readfile.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
package file
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"strconv"
"strings"
"text/template"
"github.com/imdario/mergo"
"github.com/kong/deck/utils"
"sigs.k8s.io/yaml"
)
// getContent reads all the YAML and JSON files in the directory or the
// file, depending on the type of each item in filenames, merges the content of
// these files and renders a Content.
func getContent(filenames []string, mockEnvVars bool) (*Content, error) {
var workspaces, runtimeGroups []string
var res Content
var errs []error
for _, fileOrDir := range filenames {
readers, err := getReaders(fileOrDir)
if err != nil {
return nil, err
}
for filename, r := range readers {
content, err := readContent(r, mockEnvVars)
if err != nil {
errs = append(errs, fmt.Errorf("reading file %s: %w", filename, err))
continue
}
if content.Workspace != "" {
workspaces = append(workspaces, content.Workspace)
}
if content.Konnect != nil && len(content.Konnect.RuntimeGroupName) > 0 {
runtimeGroups = append(runtimeGroups, content.Konnect.RuntimeGroupName)
}
err = mergo.Merge(&res, content, mergo.WithAppendSlice)
if err != nil {
return nil, fmt.Errorf("merging file contents: %w", err)
}
}
}
if len(errs) > 0 {
return nil, utils.ErrArray{Errors: errs}
}
if err := validateWorkspaces(workspaces); err != nil {
return nil, err
}
if err := validateRuntimeGroups(runtimeGroups); err != nil {
return nil, err
}
return &res, nil
}
// getReaders returns back a map of filename:io.Reader representing all the
// YAML and JSON files in a directory. If fileOrDir is a single file, then it
// returns back the reader for the file.
// If fileOrDir is equal to "-" string, then it returns back a io.Reader
// for the os.Stdin file descriptor.
func getReaders(fileOrDir string) (map[string]io.Reader, error) {
// special case where `-` means stdin
if fileOrDir == "-" {
return map[string]io.Reader{"STDIN": os.Stdin}, nil
}
finfo, err := os.Stat(fileOrDir)
if err != nil {
return nil, fmt.Errorf("reading state file: %w", err)
}
var files []string
if finfo.IsDir() {
files, err = utils.ConfigFilesInDir(fileOrDir)
if err != nil {
return nil, fmt.Errorf("getting files from directory: %w", err)
}
} else {
files = append(files, fileOrDir)
}
res := make(map[string]io.Reader, len(files))
for _, file := range files {
f, err := os.Open(file)
if err != nil {
return nil, fmt.Errorf("opening file: %w", err)
}
res[file] = bufio.NewReader(f)
}
return res, nil
}
func hasLeadingSpace(fileContent string) bool {
if fileContent != "" && string(fileContent[0]) == " " {
return true
}
return false
}
// readContent reads all the byes until io.EOF and unmarshals the read
// bytes into Content.
func readContent(reader io.Reader, mockEnvVars bool) (*Content, error) {
var err error
contentBytes, err := ioutil.ReadAll(reader)
if err != nil {
return nil, err
}
renderedContent, err := renderTemplate(string(contentBytes), mockEnvVars)
if err != nil {
return nil, fmt.Errorf("parsing file: %w", err)
}
// go-yaml implementation fails at correctly parsing a file whose first
// character is a space, as shown in https://github.com/Kong/deck/issues/578
// If that is the case here, raise an error.
if hasLeadingSpace(renderedContent) {
return nil, fmt.Errorf("file must not begin with a whitespace")
}
renderedContentBytes := []byte(renderedContent)
err = validate(renderedContentBytes)
if err != nil {
return nil, fmt.Errorf("validating file content: %w", err)
}
var result Content
err = yamlUnmarshal(renderedContentBytes, &result)
if err != nil {
return nil, err
}
return &result, nil
}
// yamlUnmarshal is a wrapper around yaml.Unmarshal to ensure that the right
// yaml package is in use. Using ghodss/yaml ensures that no
// `map[interface{}]interface{}` is present in go-kong.Plugin.Configuration.
// If it is present, then it leads to a silent error. See Github Issue #144.
// The verification for this is done using a test.
func yamlUnmarshal(bytes []byte, v interface{}) error {
return yaml.Unmarshal(bytes, v)
}
func getPrefixedEnvVar(key string) (string, error) {
const envVarPrefix = "DECK_"
if !strings.HasPrefix(key, envVarPrefix) {
return "", fmt.Errorf("environment variables in the state file must "+
"be prefixed with 'DECK_', found: '%s'", key)
}
value, exists := os.LookupEnv(key)
if !exists {
return "", fmt.Errorf("environment variable '%s' present in state file but not set", key)
}
return value, nil
}
// getPrefixedEnvVarMocked is used when we mock the env variables while rendering a template.
// It will always return the name of the environment variable in this case.
func getPrefixedEnvVarMocked(key string) (string, error) {
const envVarPrefix = "DECK_"
if !strings.HasPrefix(key, envVarPrefix) {
return "", fmt.Errorf("environment variables in the state file must "+
"be prefixed with 'DECK_', found: '%s'", key)
}
return key, nil
}
func toBool(key string) (bool, error) {
return strconv.ParseBool(key)
}
// toBoolMocked is used when we mock the env variables while rendering a template.
// It will always return false in this case.
func toBoolMocked(_ string) (bool, error) {
return false, nil
}
func toInt(key string) (int, error) {
return strconv.Atoi(key)
}
// toIntMocked is used when we mock the env variables while rendering a template.
// It will always return 42 in this case.
func toIntMocked(_ string) (int, error) {
return 42, nil
}
func toFloat(key string) (float64, error) {
return strconv.ParseFloat(key, 64)
}
// toFloatMocked is used when we mock the env variables while rendering a template.
// It will always return 42 in this case.
func toFloatMocked(_ string) (float64, error) {
return 42, nil
}
func indent(spaces int, v string) string {
pad := strings.Repeat(" ", spaces)
return strings.Replace(v, "\n", "\n"+pad, -1)
}
func renderTemplate(content string, mockEnvVars bool) (string, error) {
var templateFuncs template.FuncMap
if mockEnvVars {
templateFuncs = template.FuncMap{
"env": getPrefixedEnvVarMocked,
"toBool": toBoolMocked,
"toInt": toIntMocked,
"toFloat": toFloatMocked,
"indent": indent,
}
} else {
templateFuncs = template.FuncMap{
"env": getPrefixedEnvVar,
"toBool": toBool,
"toInt": toInt,
"toFloat": toFloat,
"indent": indent,
}
}
t := template.New("state").Funcs(templateFuncs).Delims("${{", "}}")
t, err := t.Parse(content)
if err != nil {
return "", err
}
var buffer bytes.Buffer
err = t.Execute(&buffer, nil)
if err != nil {
return "", err
}
return buffer.String(), nil
}