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 }