github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/util/get/get.go (about)

     1  // Copyright 2019 Google LLC
     2  //
     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  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package get contains libraries for fetching packages.
    16  package get
    17  
    18  import (
    19  	"context"
    20  	goerrors "errors"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  
    26  	"github.com/GoogleContainerTools/kpt/internal/errors"
    27  	"github.com/GoogleContainerTools/kpt/internal/fnruntime"
    28  	"github.com/GoogleContainerTools/kpt/internal/hook"
    29  	"github.com/GoogleContainerTools/kpt/internal/pkg"
    30  	"github.com/GoogleContainerTools/kpt/internal/printer"
    31  	"github.com/GoogleContainerTools/kpt/internal/types"
    32  	"github.com/GoogleContainerTools/kpt/internal/util/addmergecomment"
    33  	"github.com/GoogleContainerTools/kpt/internal/util/attribution"
    34  	"github.com/GoogleContainerTools/kpt/internal/util/fetch"
    35  	"github.com/GoogleContainerTools/kpt/internal/util/pathutil"
    36  	"github.com/GoogleContainerTools/kpt/internal/util/stack"
    37  	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
    38  	"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
    39  	"sigs.k8s.io/kustomize/kyaml/filesys"
    40  	"sigs.k8s.io/kustomize/kyaml/kio"
    41  )
    42  
    43  // Command fetches a package from a git repository, copies it to a local
    44  // directory, and expands any remote subpackages.
    45  type Command struct {
    46  	// Git contains information about the git repo to fetch
    47  	Git *kptfilev1.Git
    48  
    49  	// Destination is the output directory to clone the package to.  Defaults to the name of the package --
    50  	// either the base repo name, or the base subdirectory name.
    51  	Destination string
    52  
    53  	// Name is the name to give the package.  Defaults to the destination.
    54  	Name string
    55  
    56  	// IsDeploymentInstance indicates if the package is forked for deployment.
    57  	// If forked package has defined deploy hooks, those will be executed post fork.
    58  	IsDeploymentInstance bool
    59  
    60  	// UpdateStrategy is the strategy that will be configured in the package
    61  	// Kptfile. This determines how changes will be merged when updating the
    62  	// package.
    63  	UpdateStrategy kptfilev1.UpdateStrategyType
    64  }
    65  
    66  // Run runs the Command.
    67  func (c Command) Run(ctx context.Context) error {
    68  	const op errors.Op = "get.Run"
    69  	if err := (&c).DefaultValues(); err != nil {
    70  		return errors.E(op, err)
    71  	}
    72  
    73  	if _, err := os.Stat(c.Destination); !goerrors.Is(err, os.ErrNotExist) {
    74  		return errors.E(op, errors.Exist, types.UniquePath(c.Destination), fmt.Errorf("destination directory already exists"))
    75  	}
    76  
    77  	err := os.MkdirAll(c.Destination, 0700)
    78  	if err != nil {
    79  		return errors.E(op, errors.IO, types.UniquePath(c.Destination), err)
    80  	}
    81  
    82  	// normalize path to a filepath
    83  	repoDir := c.Git.Directory
    84  	if !strings.HasSuffix(repoDir, "file://") {
    85  		// Convert from separator to slash and back.
    86  		// This ensures all separators are compatible with the local OS.
    87  		repoDir = filepath.FromSlash(filepath.ToSlash(repoDir))
    88  	}
    89  	c.Git.Directory = repoDir
    90  
    91  	kf := kptfileutil.DefaultKptfile(c.Name)
    92  	kf.Upstream = &kptfilev1.Upstream{
    93  		Type:           kptfilev1.GitOrigin,
    94  		Git:            c.Git,
    95  		UpdateStrategy: c.UpdateStrategy,
    96  	}
    97  
    98  	err = kptfileutil.WriteFile(c.Destination, kf)
    99  	if err != nil {
   100  		return cleanUpDirAndError(c.Destination, err)
   101  	}
   102  
   103  	absDestPath, _, err := pathutil.ResolveAbsAndRelPaths(c.Destination)
   104  	if err != nil {
   105  		return err
   106  	}
   107  	p, err := pkg.New(filesys.FileSystemOrOnDisk{}, absDestPath)
   108  	if err != nil {
   109  		return cleanUpDirAndError(c.Destination, err)
   110  	}
   111  
   112  	if err = c.fetchPackages(ctx, p); err != nil {
   113  		return cleanUpDirAndError(c.Destination, err)
   114  	}
   115  
   116  	inout := &kio.LocalPackageReadWriter{PackagePath: c.Destination, PreserveSeqIndent: true, WrapBareSeqNode: true}
   117  	amc := &addmergecomment.AddMergeComment{}
   118  	at := &attribution.Attributor{PackagePaths: []string{c.Destination}, CmdGroup: "pkg"}
   119  	// do not error out as this is best effort
   120  	_ = kio.Pipeline{
   121  		Inputs:  []kio.Reader{inout},
   122  		Filters: []kio.Filter{kio.FilterAll(amc), kio.FilterAll(at)},
   123  		Outputs: []kio.Writer{inout},
   124  	}.Execute()
   125  
   126  	if c.IsDeploymentInstance {
   127  		pr := printer.FromContextOrDie(ctx)
   128  		pr.Printf("\nCustomizing package for deployment.\n")
   129  		hookCmd := hook.Executor{}
   130  		hookCmd.RunnerOptions.InitDefaults()
   131  		hookCmd.PkgPath = c.Destination
   132  
   133  		builtinHooks := []kptfilev1.Function{
   134  			{
   135  				Image: fnruntime.FuncGenPkgContext,
   136  			},
   137  		}
   138  		if err := hookCmd.Execute(ctx, builtinHooks); err != nil {
   139  			return err
   140  		}
   141  		pr.Printf("\nCustomized package for deployment.\n")
   142  	}
   143  
   144  	return nil
   145  }
   146  
   147  // Fetches any remote subpackages referenced through the root package and its subpackages.
   148  // It will also handle situations where a remote subpackage references other remote subpackages.
   149  func (c Command) fetchPackages(ctx context.Context, rootPkg *pkg.Pkg) error {
   150  	const op errors.Op = "get.fetchPackages"
   151  	pr := printer.FromContextOrDie(ctx)
   152  	packageCount := 0
   153  	// Create a stack to keep track of all Kptfiles that needs to be checked
   154  	// for remote subpackages.
   155  	s := stack.NewPkgStack()
   156  	s.Push(rootPkg)
   157  
   158  	for s.Len() > 0 {
   159  		p := s.Pop()
   160  
   161  		kf, err := p.Kptfile()
   162  		if err != nil {
   163  			return errors.E(op, p.UniquePath, err)
   164  		}
   165  
   166  		if kf.Upstream != nil && kf.UpstreamLock == nil {
   167  			packageCount++
   168  			pr.PrintPackage(p, !(p == rootPkg))
   169  			pr.Printf("Fetching %s@%s\n", kf.Upstream.Git.Repo, kf.Upstream.Git.Ref)
   170  			err := (&fetch.Command{
   171  				Pkg: p,
   172  			}).Run(ctx)
   173  			if err != nil {
   174  				return errors.E(op, p.UniquePath, err)
   175  			}
   176  		}
   177  
   178  		subPkgs, err := p.DirectSubpackages()
   179  		if err != nil {
   180  			return errors.E(op, p.UniquePath, err)
   181  		}
   182  		for _, subPkg := range subPkgs {
   183  			s.Push(subPkg)
   184  		}
   185  	}
   186  	pr.Printf("\nFetched %d package(s).\n", packageCount)
   187  	return nil
   188  }
   189  
   190  // DefaultValues sets values to the default values if they were unspecified
   191  func (c *Command) DefaultValues() error {
   192  	const op errors.Op = "get.DefaultValues"
   193  	if c.Git == nil {
   194  		return errors.E(op, errors.MissingParam, fmt.Errorf("must specify git repo information"))
   195  	}
   196  	g := c.Git
   197  	if len(g.Repo) == 0 {
   198  		return errors.E(op, errors.MissingParam, fmt.Errorf("must specify repo"))
   199  	}
   200  	if len(g.Ref) == 0 {
   201  		return errors.E(op, errors.MissingParam, fmt.Errorf("must specify ref"))
   202  	}
   203  	if len(c.Destination) == 0 {
   204  		return errors.E(op, errors.MissingParam, fmt.Errorf("must specify destination"))
   205  	}
   206  	if len(g.Directory) == 0 {
   207  		return errors.E(op, errors.MissingParam, fmt.Errorf("must specify directory"))
   208  	}
   209  
   210  	if !filepath.IsAbs(c.Destination) {
   211  		return errors.E(op, errors.InvalidParam, fmt.Errorf("destination must be an absolute path"))
   212  	}
   213  
   214  	// default the name to the destination name
   215  	if len(c.Name) == 0 {
   216  		c.Name = filepath.Base(c.Destination)
   217  	}
   218  
   219  	// default the update strategy to resource-merge
   220  	if len(c.UpdateStrategy) == 0 {
   221  		c.UpdateStrategy = kptfilev1.ResourceMerge
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  func cleanUpDirAndError(destination string, err error) error {
   228  	const op errors.Op = "get.Run"
   229  	rmErr := os.RemoveAll(destination)
   230  	if rmErr != nil {
   231  		return errors.E(op, types.UniquePath(destination), err, rmErr)
   232  	}
   233  	return errors.E(op, types.UniquePath(destination), err)
   234  }