...

Source file src/sigs.k8s.io/kustomize/api/internal/localizer/util.go

Documentation: sigs.k8s.io/kustomize/api/internal/localizer

     1  // Copyright 2022 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package localizer
     5  
     6  import (
     7  	"log"
     8  	"net/url"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"sigs.k8s.io/kustomize/api/ifc"
    13  	"sigs.k8s.io/kustomize/api/internal/git"
    14  	"sigs.k8s.io/kustomize/kyaml/errors"
    15  	"sigs.k8s.io/kustomize/kyaml/filesys"
    16  )
    17  
    18  const (
    19  	// DstPrefix prefixes the target and ref, if target is remote, in the default localize destination directory name
    20  	DstPrefix = "localized"
    21  
    22  	// LocalizeDir is the name of the localize directories used to store remote content in the localize destination
    23  	LocalizeDir = "localized-files"
    24  
    25  	// FileSchemeDir is the name of the directory immediately inside LocalizeDir used to store file-schemed repos
    26  	FileSchemeDir = "file-schemed"
    27  )
    28  
    29  // establishScope returns the effective scope given localize arguments and targetLdr at rawTarget. For remote rawTarget,
    30  // the effective scope is the downloaded repo.
    31  func establishScope(rawScope string, rawTarget string, targetLdr ifc.Loader, fSys filesys.FileSystem) (filesys.ConfirmedDir, error) {
    32  	if repo := targetLdr.Repo(); repo != "" {
    33  		if rawScope != "" {
    34  			return "", errors.Errorf("scope %q specified for remote localize target %q", rawScope, rawTarget)
    35  		}
    36  		return filesys.ConfirmedDir(repo), nil
    37  	}
    38  	// default scope
    39  	if rawScope == "" {
    40  		return filesys.ConfirmedDir(targetLdr.Root()), nil
    41  	}
    42  	scope, err := filesys.ConfirmDir(fSys, rawScope)
    43  	if err != nil {
    44  		return "", errors.WrapPrefixf(err, "unable to establish localize scope")
    45  	}
    46  	if !filesys.ConfirmedDir(targetLdr.Root()).HasPrefix(scope) {
    47  		return scope, errors.Errorf("localize scope %q does not contain target %q at %q", rawScope, rawTarget,
    48  			targetLdr.Root())
    49  	}
    50  	return scope, nil
    51  }
    52  
    53  // createNewDir returns the localize destination directory or error. Note that spec is nil if targetLdr is at local
    54  // target.
    55  func createNewDir(rawNewDir string, targetLdr ifc.Loader, spec *git.RepoSpec, fSys filesys.FileSystem) (filesys.ConfirmedDir, error) {
    56  	if rawNewDir == "" {
    57  		rawNewDir = defaultNewDir(targetLdr, spec)
    58  	}
    59  	if fSys.Exists(rawNewDir) {
    60  		return "", errors.Errorf("localize destination %q already exists", rawNewDir)
    61  	}
    62  	// destination directory must sit in an existing directory
    63  	if err := fSys.Mkdir(rawNewDir); err != nil {
    64  		return "", errors.WrapPrefixf(err, "unable to create localize destination directory")
    65  	}
    66  	newDir, err := filesys.ConfirmDir(fSys, rawNewDir)
    67  	if err != nil {
    68  		if errCleanup := fSys.RemoveAll(newDir.String()); errCleanup != nil {
    69  			log.Printf("%s", errors.WrapPrefixf(errCleanup, "unable to clean localize destination"))
    70  		}
    71  		return "", errors.WrapPrefixf(err, "unable to establish localize destination")
    72  	}
    73  
    74  	return newDir, nil
    75  }
    76  
    77  // defaultNewDir calculates the default localize destination directory name from targetLdr at the localize target
    78  // and spec of target, which is nil if target is local
    79  func defaultNewDir(targetLdr ifc.Loader, spec *git.RepoSpec) string {
    80  	targetDir := filepath.Base(targetLdr.Root())
    81  	if repo := targetLdr.Repo(); repo != "" {
    82  		// kustomize doesn't download repo into repo-named folder
    83  		// must find repo folder name from url
    84  		if repo == targetLdr.Root() {
    85  			targetDir = urlBase(spec.RepoPath)
    86  		}
    87  		return strings.Join([]string{DstPrefix, targetDir, strings.ReplaceAll(spec.Ref, "/", "-")}, "-")
    88  	}
    89  	// special case for local target directory since destination directory cannot have "/" in name
    90  	if targetDir == string(filepath.Separator) {
    91  		return DstPrefix
    92  	}
    93  	return strings.Join([]string{DstPrefix, targetDir}, "-")
    94  }
    95  
    96  // urlBase is the url equivalent of filepath.Base
    97  func urlBase(url string) string {
    98  	cleaned := strings.TrimRight(url, "/")
    99  	i := strings.LastIndex(cleaned, "/")
   100  	if i < 0 {
   101  		return cleaned
   102  	}
   103  	return cleaned[i+1:]
   104  }
   105  
   106  // hasRef checks if repoURL has ref query string parameter
   107  func hasRef(repoURL string) bool {
   108  	repoSpec, err := git.NewRepoSpecFromURL(repoURL)
   109  	if err != nil {
   110  		log.Fatalf("unable to parse validated root url: %s", err)
   111  	}
   112  	return repoSpec.Ref != ""
   113  }
   114  
   115  // cleanFilePath returns file cleaned, where file is a relative path to root on fSys
   116  func cleanFilePath(fSys filesys.FileSystem, root filesys.ConfirmedDir, file string) string {
   117  	abs := root.Join(file)
   118  	dir, f, err := fSys.CleanedAbs(abs)
   119  	if err != nil {
   120  		log.Fatalf("cannot clean validated file path %q: %s", abs, err)
   121  	}
   122  	locPath, err := filepath.Rel(root.String(), dir.Join(f))
   123  	if err != nil {
   124  		log.Fatalf("cannot find path from parent %q to file %q: %s", root, dir.Join(f), err)
   125  	}
   126  	return locPath
   127  }
   128  
   129  // locFilePath converts a URL to its localized form, e.g.
   130  // https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/api/krusty/testdata/localize/simple/service.yaml ->
   131  // localized-files/raw.githubusercontent.com/kubernetes-sigs/kustomize/master/api/krusty/testdata/localize/simple/service.yaml.
   132  //
   133  // fileURL must be a validated file URL.
   134  func locFilePath(fileURL string) string {
   135  	// File urls must have http or https scheme, so it is safe to use url.Parse.
   136  	u, err := url.Parse(fileURL)
   137  	if err != nil {
   138  		log.Panicf("cannot parse validated file url %q: %s", fileURL, err)
   139  	}
   140  
   141  	// HTTP requests use the escaped path, so we use it here. Escaped paths also help us
   142  	// preserve percent-encoding in the original path, in the absence of illegal characters,
   143  	// in case they have special meaning to the host.
   144  	// Extraneous '..' parent directory dot-segments should be removed.
   145  	path := filepath.Join(string(filepath.Separator), filepath.FromSlash(u.EscapedPath()))
   146  
   147  	// We intentionally exclude userinfo and port.
   148  	// Raw github urls are the only type of file urls kustomize officially accepts.
   149  	// In this case, the path already consists of org, repo, version, and path in repo, in order,
   150  	// so we can use it as is.
   151  	return filepath.Join(LocalizeDir, u.Hostname(), path)
   152  }
   153  
   154  // locRootPath returns the relative localized path of the validated root url rootURL, where the local copy of its repo
   155  // is at repoDir and the copy of its root is at root on fSys.
   156  func locRootPath(rootURL, repoDir string, root filesys.ConfirmedDir, fSys filesys.FileSystem) (string, error) {
   157  	repoSpec, err := git.NewRepoSpecFromURL(rootURL)
   158  	if err != nil {
   159  		log.Panicf("cannot parse validated repo url %q: %s", rootURL, err)
   160  	}
   161  	host, err := parseHost(repoSpec)
   162  	if err != nil {
   163  		return "", errors.WrapPrefixf(err, "unable to parse host of remote root %q", rootURL)
   164  	}
   165  	repo, err := filesys.ConfirmDir(fSys, repoDir)
   166  	if err != nil {
   167  		log.Panicf("unable to establish validated repo download location %q: %s", repoDir, err)
   168  	}
   169  	// calculate from copy instead of url to straighten symlinks
   170  	inRepo, err := filepath.Rel(repo.String(), root.String())
   171  	if err != nil {
   172  		log.Panicf("cannot find path from %q to child directory %q: %s", repo, root, err)
   173  	}
   174  	// the git-server-side directory name conventionally (but not universally) ends in .git, which
   175  	// is conventionally stripped from the client-side directory name used for the clone.
   176  	localRepoPath := strings.TrimSuffix(repoSpec.RepoPath, ".git")
   177  
   178  	// We do not need to escape RepoPath, a path on the git server.
   179  	// However, like git, we clean dot-segments from RepoPath.
   180  	// Git does not allow ref value to contain dot-segments.
   181  	return filepath.Join(LocalizeDir,
   182  		host,
   183  		filepath.Join(string(filepath.Separator), filepath.FromSlash(localRepoPath)),
   184  		filepath.FromSlash(repoSpec.Ref),
   185  		inRepo), nil
   186  }
   187  
   188  // parseHost returns the localize directory path corresponding to repoSpec.Host
   189  func parseHost(repoSpec *git.RepoSpec) (string, error) {
   190  	var target string
   191  	switch scheme, _, _ := strings.Cut(repoSpec.Host, "://"); scheme {
   192  	case "gh:":
   193  		// 'gh' was meant to be a local github.com shorthand, in which case
   194  		// the .gitconfig file could map it to any host. See origin here:
   195  		// https://github.com/kubernetes-sigs/kustomize/blob/kustomize/v4.5.7/api/internal/git/repospec.go#L203
   196  		// We give it a special host directory here under the assumption
   197  		// that we are unlikely to have another host simply named 'gh'.
   198  		return "gh", nil
   199  	case "file":
   200  		// We put file-scheme repos under a special directory to avoid
   201  		// colluding local absolute paths with hosts.
   202  		return FileSchemeDir, nil
   203  	case "https", "http", "ssh":
   204  		target = repoSpec.Host
   205  	default:
   206  		// We must have relative ssh url; in other words, the url has scp-like syntax.
   207  		// We attach a scheme to avoid url.Parse errors.
   208  		target = "ssh://" + repoSpec.Host
   209  	}
   210  	// url.Parse will not recognize ':' delimiter that both RepoSpec and git accept.
   211  	target = strings.TrimSuffix(target, ":")
   212  	u, err := url.Parse(target)
   213  	if err != nil {
   214  		return "", errors.Wrap(err)
   215  	}
   216  	// strip scheme, userinfo, port, and any trailing slashes.
   217  	return u.Hostname(), nil
   218  }
   219  

View as plain text