1
2
3
4 package loader
5
6 import (
7 "fmt"
8 "io"
9 "log"
10 "net/http"
11 "net/url"
12 "path/filepath"
13 "strings"
14
15 "sigs.k8s.io/kustomize/api/ifc"
16 "sigs.k8s.io/kustomize/api/internal/git"
17 "sigs.k8s.io/kustomize/kyaml/errors"
18 "sigs.k8s.io/kustomize/kyaml/filesys"
19 )
20
21
22
23 func IsRemoteFile(path string) bool {
24 u, err := url.Parse(path)
25 return err == nil && (u.Scheme == "http" || u.Scheme == "https")
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 type FileLoader struct {
80
81
82 referrer *FileLoader
83
84
85
86
87 root filesys.ConfirmedDir
88
89
90 loadRestrictor LoadRestrictorFunc
91
92
93
94 repoSpec *git.RepoSpec
95
96
97 fSys filesys.FileSystem
98
99
100 http *http.Client
101
102
103 cloner git.Cloner
104
105
106 cleaner func() error
107 }
108
109
110
111 func (fl *FileLoader) Repo() string {
112 if fl.repoSpec != nil {
113 return fl.repoSpec.Dir.String()
114 }
115 return ""
116 }
117
118
119
120 func (fl *FileLoader) Root() string {
121 return fl.root.String()
122 }
123
124 func NewLoaderOrDie(
125 lr LoadRestrictorFunc,
126 fSys filesys.FileSystem, path string) *FileLoader {
127 root, err := filesys.ConfirmDir(fSys, path)
128 if err != nil {
129 log.Fatalf("unable to make loader at '%s'; %v", path, err)
130 }
131 return newLoaderAtConfirmedDir(
132 lr, root, fSys, nil, git.ClonerUsingGitExec)
133 }
134
135
136 func newLoaderAtConfirmedDir(
137 lr LoadRestrictorFunc,
138 root filesys.ConfirmedDir, fSys filesys.FileSystem,
139 referrer *FileLoader, cloner git.Cloner) *FileLoader {
140 return &FileLoader{
141 loadRestrictor: lr,
142 root: root,
143 referrer: referrer,
144 fSys: fSys,
145 cloner: cloner,
146 cleaner: func() error { return nil },
147 }
148 }
149
150
151
152 func (fl *FileLoader) New(path string) (ifc.Loader, error) {
153 if path == "" {
154 return nil, errors.Errorf("new root cannot be empty")
155 }
156
157 repoSpec, err := git.NewRepoSpecFromURL(path)
158 if err == nil {
159
160 if err = fl.errIfRepoCycle(repoSpec); err != nil {
161 return nil, err
162 }
163 return newLoaderAtGitClone(
164 repoSpec, fl.fSys, fl, fl.cloner)
165 }
166
167 if filepath.IsAbs(path) {
168 return nil, fmt.Errorf("new root '%s' cannot be absolute", path)
169 }
170 root, err := filesys.ConfirmDir(fl.fSys, fl.root.Join(path))
171 if err != nil {
172 return nil, errors.WrapPrefixf(err, ErrRtNotDir.Error())
173 }
174 if err = fl.errIfGitContainmentViolation(root); err != nil {
175 return nil, err
176 }
177 if err = fl.errIfArgEqualOrHigher(root); err != nil {
178 return nil, err
179 }
180 return newLoaderAtConfirmedDir(
181 fl.loadRestrictor, root, fl.fSys, fl, fl.cloner), nil
182 }
183
184
185
186 func newLoaderAtGitClone(
187 repoSpec *git.RepoSpec, fSys filesys.FileSystem,
188 referrer *FileLoader, cloner git.Cloner) (ifc.Loader, error) {
189 cleaner := repoSpec.Cleaner(fSys)
190 err := cloner(repoSpec)
191 if err != nil {
192 cleaner()
193 return nil, err
194 }
195 root, f, err := fSys.CleanedAbs(repoSpec.AbsPath())
196 if err != nil {
197 cleaner()
198 return nil, err
199 }
200
201
202
203
204 if f != "" {
205 cleaner()
206 return nil, fmt.Errorf(
207 "'%s' refers to file '%s'; expecting directory",
208 repoSpec.AbsPath(), f)
209 }
210
211
212 if !root.HasPrefix(repoSpec.CloneDir()) {
213 _ = cleaner()
214 return nil, fmt.Errorf("%q refers to directory outside of repo %q", repoSpec.AbsPath(),
215 repoSpec.CloneDir())
216 }
217 return &FileLoader{
218
219 loadRestrictor: RestrictionRootOnly,
220 root: root,
221 referrer: referrer,
222 repoSpec: repoSpec,
223 fSys: fSys,
224 cloner: cloner,
225 cleaner: cleaner,
226 }, nil
227 }
228
229 func (fl *FileLoader) errIfGitContainmentViolation(
230 base filesys.ConfirmedDir) error {
231 containingRepo := fl.containingRepo()
232 if containingRepo == nil {
233 return nil
234 }
235 if !base.HasPrefix(containingRepo.CloneDir()) {
236 return fmt.Errorf(
237 "security; bases in kustomizations found in "+
238 "cloned git repos must be within the repo, "+
239 "but base '%s' is outside '%s'",
240 base, containingRepo.CloneDir())
241 }
242 return nil
243 }
244
245
246
247 func (fl *FileLoader) containingRepo() *git.RepoSpec {
248 if fl.repoSpec != nil {
249 return fl.repoSpec
250 }
251 if fl.referrer == nil {
252 return nil
253 }
254 return fl.referrer.containingRepo()
255 }
256
257
258
259 func (fl *FileLoader) errIfArgEqualOrHigher(
260 candidateRoot filesys.ConfirmedDir) error {
261 if fl.root.HasPrefix(candidateRoot) {
262 return fmt.Errorf(
263 "cycle detected: candidate root '%s' contains visited root '%s'",
264 candidateRoot, fl.root)
265 }
266 if fl.referrer == nil {
267 return nil
268 }
269 return fl.referrer.errIfArgEqualOrHigher(candidateRoot)
270 }
271
272
273
274
275
276 func (fl *FileLoader) errIfRepoCycle(newRepoSpec *git.RepoSpec) error {
277
278 if fl.repoSpec != nil &&
279 strings.HasPrefix(fl.repoSpec.Raw(), newRepoSpec.Raw()) {
280 return fmt.Errorf(
281 "cycle detected: URI '%s' referenced by previous URI '%s'",
282 newRepoSpec.Raw(), fl.repoSpec.Raw())
283 }
284 if fl.referrer == nil {
285 return nil
286 }
287 return fl.referrer.errIfRepoCycle(newRepoSpec)
288 }
289
290
291
292
293 func (fl *FileLoader) Load(path string) ([]byte, error) {
294 if IsRemoteFile(path) {
295 return fl.httpClientGetContent(path)
296 }
297 if !filepath.IsAbs(path) {
298 path = fl.root.Join(path)
299 }
300 path, err := fl.loadRestrictor(fl.fSys, fl.root, path)
301 if err != nil {
302 return nil, err
303 }
304 return fl.fSys.ReadFile(path)
305 }
306
307 func (fl *FileLoader) httpClientGetContent(path string) ([]byte, error) {
308 var hc *http.Client
309 if fl.http != nil {
310 hc = fl.http
311 } else {
312 hc = &http.Client{}
313 }
314 resp, err := hc.Get(path)
315 if err != nil {
316 return nil, errors.Wrap(err)
317 }
318 defer resp.Body.Close()
319
320 if resp.StatusCode < 200 || resp.StatusCode > 299 {
321 _, err = git.NewRepoSpecFromURL(path)
322 if err == nil {
323 return nil, errors.Errorf("URL is a git repository")
324 }
325 return nil, fmt.Errorf("%w: status code %d (%s)", ErrHTTP, resp.StatusCode, http.StatusText(resp.StatusCode))
326 }
327 content, err := io.ReadAll(resp.Body)
328 return content, errors.Wrap(err)
329 }
330
331
332 func (fl *FileLoader) Cleanup() error {
333 return fl.cleaner()
334 }
335
View as plain text