fileloader.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. /*
  2. Copyright 2018 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package loader
  14. import (
  15. "fmt"
  16. "log"
  17. "path/filepath"
  18. "strings"
  19. "sigs.k8s.io/kustomize/pkg/fs"
  20. "sigs.k8s.io/kustomize/pkg/git"
  21. "sigs.k8s.io/kustomize/pkg/ifc"
  22. )
  23. // fileLoader is a kustomization's interface to files.
  24. //
  25. // The directory in which a kustomization file sits
  26. // is referred to below as the kustomization's root.
  27. //
  28. // An instance of fileLoader has an immutable root,
  29. // and offers a `New` method returning a new loader
  30. // with a new root.
  31. //
  32. // A kustomization file refers to two kinds of files:
  33. //
  34. // * supplemental data paths
  35. //
  36. // `Load` is used to visit these paths.
  37. //
  38. // They must terminate in or below the root.
  39. //
  40. // They hold things like resources, patches,
  41. // data for ConfigMaps, etc.
  42. //
  43. // * bases; other kustomizations
  44. //
  45. // `New` is used to load bases.
  46. //
  47. // A base can be either a remote git repo URL, or
  48. // a directory specified relative to the current
  49. // root. In the former case, the repo is locally
  50. // cloned, and the new loader is rooted on a path
  51. // in that clone.
  52. //
  53. // As loaders create new loaders, a root history
  54. // is established, and used to disallow:
  55. //
  56. // - A base that is a repository that, in turn,
  57. // specifies a base repository seen previously
  58. // in the loading stack (a cycle).
  59. //
  60. // - An overlay depending on a base positioned at
  61. // or above it. I.e. '../foo' is OK, but '.',
  62. // '..', '../..', etc. are disallowed. Allowing
  63. // such a base has no advantages and encourages
  64. // cycles, particularly if some future change
  65. // were to introduce globbing to file
  66. // specifications in the kustomization file.
  67. //
  68. // These restrictions assure that kustomizations
  69. // are self-contained and relocatable, and impose
  70. // some safety when relying on remote kustomizations,
  71. // e.g. a ConfigMap generator specified to read
  72. // from /etc/passwd will fail.
  73. //
  74. type fileLoader struct {
  75. // Loader that spawned this loader.
  76. // Used to avoid cycles.
  77. referrer *fileLoader
  78. // An absolute, cleaned path to a directory.
  79. // The Load function reads from this directory,
  80. // or directories below it.
  81. root fs.ConfirmedDir
  82. // If this is non-nil, the files were
  83. // obtained from the given repository.
  84. repoSpec *git.RepoSpec
  85. // File system utilities.
  86. fSys fs.FileSystem
  87. // Used to clone repositories.
  88. cloner git.Cloner
  89. // Used to clean up, as needed.
  90. cleaner func() error
  91. }
  92. // NewFileLoaderAtCwd returns a loader that loads from ".".
  93. func NewFileLoaderAtCwd(fSys fs.FileSystem) *fileLoader {
  94. return newLoaderOrDie(fSys, ".")
  95. }
  96. // NewFileLoaderAtRoot returns a loader that loads from "/".
  97. func NewFileLoaderAtRoot(fSys fs.FileSystem) *fileLoader {
  98. return newLoaderOrDie(fSys, string(filepath.Separator))
  99. }
  100. // Root returns the absolute path that is prepended to any
  101. // relative paths used in Load.
  102. func (l *fileLoader) Root() string {
  103. return l.root.String()
  104. }
  105. func newLoaderOrDie(fSys fs.FileSystem, path string) *fileLoader {
  106. root, err := demandDirectoryRoot(fSys, path)
  107. if err != nil {
  108. log.Fatalf("unable to make loader at '%s'; %v", path, err)
  109. }
  110. return newLoaderAtConfirmedDir(
  111. root, fSys, nil, git.ClonerUsingGitExec)
  112. }
  113. // newLoaderAtConfirmedDir returns a new fileLoader with given root.
  114. func newLoaderAtConfirmedDir(
  115. root fs.ConfirmedDir, fSys fs.FileSystem,
  116. referrer *fileLoader, cloner git.Cloner) *fileLoader {
  117. return &fileLoader{
  118. root: root,
  119. referrer: referrer,
  120. fSys: fSys,
  121. cloner: cloner,
  122. cleaner: func() error { return nil },
  123. }
  124. }
  125. // Assure that the given path is in fact a directory.
  126. func demandDirectoryRoot(
  127. fSys fs.FileSystem, path string) (fs.ConfirmedDir, error) {
  128. if path == "" {
  129. return "", fmt.Errorf(
  130. "loader root cannot be empty")
  131. }
  132. d, f, err := fSys.CleanedAbs(path)
  133. if err != nil {
  134. return "", fmt.Errorf(
  135. "absolute path error in '%s' : %v", path, err)
  136. }
  137. if f != "" {
  138. return "", fmt.Errorf(
  139. "got file '%s', but '%s' must be a directory to be a root",
  140. f, path)
  141. }
  142. return d, nil
  143. }
  144. // New returns a new Loader, rooted relative to current loader,
  145. // or rooted in a temp directory holding a git repo clone.
  146. func (l *fileLoader) New(path string) (ifc.Loader, error) {
  147. if path == "" {
  148. return nil, fmt.Errorf("new root cannot be empty")
  149. }
  150. repoSpec, err := git.NewRepoSpecFromUrl(path)
  151. if err == nil {
  152. // Treat this as git repo clone request.
  153. if err := l.errIfRepoCycle(repoSpec); err != nil {
  154. return nil, err
  155. }
  156. return newLoaderAtGitClone(repoSpec, l.fSys, l.referrer, l.cloner)
  157. }
  158. if filepath.IsAbs(path) {
  159. return nil, fmt.Errorf("new root '%s' cannot be absolute", path)
  160. }
  161. root, err := demandDirectoryRoot(l.fSys, l.root.Join(path))
  162. if err != nil {
  163. return nil, err
  164. }
  165. if err := l.errIfGitContainmentViolation(root); err != nil {
  166. return nil, err
  167. }
  168. if err := l.errIfArgEqualOrHigher(root); err != nil {
  169. return nil, err
  170. }
  171. return newLoaderAtConfirmedDir(
  172. root, l.fSys, l, l.cloner), nil
  173. }
  174. // newLoaderAtGitClone returns a new Loader pinned to a temporary
  175. // directory holding a cloned git repo.
  176. func newLoaderAtGitClone(
  177. repoSpec *git.RepoSpec, fSys fs.FileSystem,
  178. referrer *fileLoader, cloner git.Cloner) (ifc.Loader, error) {
  179. err := cloner(repoSpec)
  180. if err != nil {
  181. return nil, err
  182. }
  183. root, f, err := fSys.CleanedAbs(repoSpec.AbsPath())
  184. if err != nil {
  185. return nil, err
  186. }
  187. // We don't know that the path requested in repoSpec
  188. // is a directory until we actually clone it and look
  189. // inside. That just happened, hence the error check
  190. // is here.
  191. if f != "" {
  192. return nil, fmt.Errorf(
  193. "'%s' refers to file '%s'; expecting directory",
  194. repoSpec.AbsPath(), f)
  195. }
  196. return &fileLoader{
  197. root: root,
  198. referrer: referrer,
  199. repoSpec: repoSpec,
  200. fSys: fSys,
  201. cloner: cloner,
  202. cleaner: repoSpec.Cleaner(fSys),
  203. }, nil
  204. }
  205. func (l *fileLoader) errIfGitContainmentViolation(
  206. base fs.ConfirmedDir) error {
  207. containingRepo := l.containingRepo()
  208. if containingRepo == nil {
  209. return nil
  210. }
  211. if !base.HasPrefix(containingRepo.CloneDir()) {
  212. return fmt.Errorf(
  213. "security; bases in kustomizations found in "+
  214. "cloned git repos must be within the repo, "+
  215. "but base '%s' is outside '%s'",
  216. base, containingRepo.CloneDir())
  217. }
  218. return nil
  219. }
  220. // Looks back through referrers for a git repo, returning nil
  221. // if none found.
  222. func (l *fileLoader) containingRepo() *git.RepoSpec {
  223. if l.repoSpec != nil {
  224. return l.repoSpec
  225. }
  226. if l.referrer == nil {
  227. return nil
  228. }
  229. return l.referrer.containingRepo()
  230. }
  231. // errIfArgEqualOrHigher tests whether the argument,
  232. // is equal to or above the root of any ancestor.
  233. func (l *fileLoader) errIfArgEqualOrHigher(
  234. candidateRoot fs.ConfirmedDir) error {
  235. if l.root.HasPrefix(candidateRoot) {
  236. return fmt.Errorf(
  237. "cycle detected: candidate root '%s' contains visited root '%s'",
  238. candidateRoot, l.root)
  239. }
  240. if l.referrer == nil {
  241. return nil
  242. }
  243. return l.referrer.errIfArgEqualOrHigher(candidateRoot)
  244. }
  245. // TODO(monopole): Distinguish branches?
  246. // I.e. Allow a distinction between git URI with
  247. // path foo and tag bar and a git URI with the same
  248. // path but a different tag?
  249. func (l *fileLoader) errIfRepoCycle(newRepoSpec *git.RepoSpec) error {
  250. // TODO(monopole): Use parsed data instead of Raw().
  251. if l.repoSpec != nil &&
  252. strings.HasPrefix(l.repoSpec.Raw(), newRepoSpec.Raw()) {
  253. return fmt.Errorf(
  254. "cycle detected: URI '%s' referenced by previous URI '%s'",
  255. newRepoSpec.Raw(), l.repoSpec.Raw())
  256. }
  257. if l.referrer == nil {
  258. return nil
  259. }
  260. return l.referrer.errIfRepoCycle(newRepoSpec)
  261. }
  262. // Load returns content of file at the given relative path,
  263. // else an error. The path must refer to a file in or
  264. // below the current root.
  265. func (l *fileLoader) Load(path string) ([]byte, error) {
  266. if filepath.IsAbs(path) {
  267. return nil, l.loadOutOfBounds(path)
  268. }
  269. d, f, err := l.fSys.CleanedAbs(l.root.Join(path))
  270. if err != nil {
  271. return nil, err
  272. }
  273. if f == "" {
  274. return nil, fmt.Errorf(
  275. "'%s' must be a file (got d='%s')", path, d)
  276. }
  277. if !d.HasPrefix(l.root) {
  278. return nil, l.loadOutOfBounds(path)
  279. }
  280. return l.fSys.ReadFile(d.Join(f))
  281. }
  282. func (l *fileLoader) loadOutOfBounds(path string) error {
  283. return fmt.Errorf(
  284. "security; file '%s' is not in or below '%s'",
  285. path, l.root)
  286. }
  287. // Cleanup runs the cleaner.
  288. func (l *fileLoader) Cleanup() error {
  289. return l.cleaner()
  290. }