forked from lukasjarosch/skipper
-
Notifications
You must be signed in to change notification settings - Fork 0
/
file.go
246 lines (208 loc) · 6.06 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
package skipper
import (
"fmt"
"io"
"io/fs"
"path/filepath"
"strings"
"text/template"
"github.com/Masterminds/sprig/v3"
"github.com/spf13/afero"
"gopkg.in/yaml.v3"
)
var (
yamlFileExtensions = []string{".yml", ".yaml", ""}
)
// File is just an arbitrary description of a path and the data of the File to which Path points to.
// Note that the used filesystem is not relevant, only at the time of loading a File.
type File struct {
Path string
Mode fs.FileMode
Bytes []byte
}
func newFile(path string) (*File, error) {
if path == "" {
return nil, fmt.Errorf("path cannot be empty")
}
return &File{Path: path}, nil
}
// Exists returns true if the File exists in the given filesystem, false otherwise.
func (f *File) Exists(fs afero.Fs) bool {
exists, err := afero.Exists(fs, f.Path)
if err != nil {
return false
}
return exists
}
// Load will attempt to read the File from the given filesystem implementation.
// The loaded data is stored in `File.Bytes`
func (f *File) Load(fs afero.Fs) (err error) {
f.Bytes, err = afero.ReadFile(fs, f.Path)
if err != nil {
return fmt.Errorf("failed to Load %s: %w", f.Path, err)
}
info, err := fs.Stat(f.Path)
if err != nil {
return fmt.Errorf("unable to stat file %s: %w", f.Path, err)
}
f.Mode = info.Mode()
return nil
}
// YamlFileLoaderFunc is a function used to create specific types from a YamlFile and a relative path to that file.
type YamlFileLoaderFunc func(file *YamlFile, relativePath string) error
// YamlFileLoader is used to load Skipper specific yaml files from the inventory.
// It searches the basePath inside the given fileSystem for yaml files and loads them.
// Empty files are skipped.
// A path relative to the given pasePath is constructed.
// The loaded yaml file and the relative path are then passed to the given YamlFileLoaderFunc
// which is responsible for creating specific types from the YamlFile.
func YamlFileLoader(fileSystem afero.Fs, basePath string, loader YamlFileLoaderFunc) error {
yamlFiles, err := DiscoverYamlFiles(fileSystem, basePath)
if err != nil {
return err
}
for _, yamlFile := range yamlFiles {
err = yamlFile.Load(fileSystem)
if err != nil {
return err
}
// skip empty files
if len(yamlFile.Data) == 0 {
continue
}
relativePath := strings.ReplaceAll(yamlFile.Path, basePath, "")
relativePath = strings.TrimLeft(relativePath, "/")
err = loader(yamlFile, relativePath)
if err != nil {
return err
}
}
return nil
}
// YamlFile is what is used for all inventory-relevant files (classes, secrets and targets).
type YamlFile struct {
File
Data Data
}
// NewFile returns a newly initialized `YamlFile`.
func NewFile(path string) (*YamlFile, error) {
f, err := newFile(path)
if err != nil {
return nil, err
}
return &YamlFile{
File: *f,
}, nil
}
// CreateNewFile can be used to manually create a File inside the given filesystem.
// This is useful for dynamically creating classes or targets.
//
// The given path is attempted to be created and a file written.
func CreateNewYamlFile(fs afero.Fs, path string, data []byte) (*YamlFile, error) {
err := fs.MkdirAll(filepath.Dir(path), 0755)
if err != nil {
return nil, err
}
err = afero.WriteFile(fs, path, data, 0644)
if err != nil {
return nil, err
}
return NewFile(path)
}
// Load will first load the underlying raw file-data and then attempt to `yaml.Unmarshal` it into `Data`
// The resulting Data is stored in `YamlFile.Data`.
func (f *YamlFile) Load(fs afero.Fs) error {
err := f.File.Load(fs)
if err != nil {
return err
}
var d Data
if err := yaml.Unmarshal(f.Bytes, &d); err != nil {
return err
}
f.Data = d
return nil
}
// UnmarshalPath can be used to unmarshall only a sub-map of the Data inside [YamlFile].
// The function errors if the file has not been loaded.
func (f *YamlFile) UnmarshalPath(target interface{}, path ...interface{}) error {
if f.Data == nil {
return fmt.Errorf("yaml file not loaded, no data exists")
}
data, err := f.Data.GetPath(path...)
if err != nil {
return err
}
bytes, err := yaml.Marshal(data)
if err != nil {
return err
}
return yaml.Unmarshal(bytes, target)
}
// TemplateFile represents a file which is used as Template.
type TemplateFile struct {
File
tpl *template.Template
}
// NewTemplateFile creates a new TemplateFile at the given path.
// User-defined template funcs can be added to have them available in the templates.
// By default, the well-known sprig functions are always added (see: https://github.com/Masterminds/sprig).
func NewTemplateFile(path string, funcs map[string]any) (*TemplateFile, error) {
f, err := newFile(path)
if err != nil {
return nil, err
}
return &TemplateFile{
File: *f,
tpl: template.New(path).Funcs(sprig.TxtFuncMap()).Funcs(funcs),
}, nil
}
// Parse will attempt to Load and parse the template from the given filesystem.
func (tmpl *TemplateFile) Parse(fs afero.Fs) (err error) {
err = tmpl.File.Load(fs)
if err != nil {
return err
}
tmpl.tpl, err = tmpl.tpl.Parse(string(tmpl.Bytes))
if err != nil {
return fmt.Errorf("failed to parse template %s: %w", tmpl.Path, err)
}
return nil
}
// Execute renders the template file and writes the output into the passed `io.Writer`
// The passed contexData is what will be available inside the template.
func (tmpl *TemplateFile) Execute(out io.Writer, contextData any) (err error) {
return tmpl.tpl.Execute(out, contextData)
}
type SecretFile struct {
*YamlFile
Data SecretFileData
RelativePath string
}
func (sf *SecretFile) LoadSecretFileData(fs afero.Fs) error {
err := sf.File.Load(fs)
if err != nil {
return err
}
var d SecretFileData
if err := yaml.Unmarshal(sf.Bytes, &d); err != nil {
return err
}
sf.Data = d
return nil
}
type SecretFileList []*SecretFile
func NewSecretFile(file *YamlFile, relativeSecretPath string) (*SecretFile, error) {
return &SecretFile{
YamlFile: file,
RelativePath: relativeSecretPath,
}, nil
}
func (sfl SecretFileList) GetSecretFile(path string) *SecretFile {
for _, secretFile := range sfl {
if strings.EqualFold(secretFile.RelativePath, path) {
return secretFile
}
}
return nil
}