github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/pkg/kptfile/kptfileutil/util.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 kptfileutil 16 17 import ( 18 "bytes" 19 goerrors "errors" 20 "fmt" 21 "os" 22 "path/filepath" 23 "strings" 24 25 "github.com/GoogleContainerTools/kpt/internal/errors" 26 "github.com/GoogleContainerTools/kpt/internal/pkg" 27 "github.com/GoogleContainerTools/kpt/internal/types" 28 "github.com/GoogleContainerTools/kpt/internal/util/git" 29 kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" 30 "sigs.k8s.io/kustomize/kyaml/filesys" 31 "sigs.k8s.io/kustomize/kyaml/sets" 32 "sigs.k8s.io/kustomize/kyaml/yaml" 33 "sigs.k8s.io/kustomize/kyaml/yaml/merge3" 34 ) 35 36 func WriteFile(dir string, k interface{}) error { 37 const op errors.Op = "kptfileutil.WriteFile" 38 b, err := yaml.MarshalWithOptions(k, &yaml.EncoderOptions{SeqIndent: yaml.WideSequenceStyle}) 39 if err != nil { 40 return err 41 } 42 if _, err := os.Stat(filepath.Join(dir, kptfilev1.KptFileName)); err != nil && !goerrors.Is(err, os.ErrNotExist) { 43 return errors.E(op, errors.IO, types.UniquePath(dir), err) 44 } 45 46 // fyi: perm is ignored if the file already exists 47 err = os.WriteFile(filepath.Join(dir, kptfilev1.KptFileName), b, 0600) 48 if err != nil { 49 return errors.E(op, errors.IO, types.UniquePath(dir), err) 50 } 51 return nil 52 } 53 54 // ValidateInventory returns true and a nil error if the passed inventory 55 // is valid; otherwiste, false and the reason the inventory is not valid 56 // is returned. A valid inventory must have a non-empty namespace, name, 57 // and id. 58 func ValidateInventory(inv *kptfilev1.Inventory) (bool, error) { 59 const op errors.Op = "kptfileutil.ValidateInventory" 60 if inv == nil { 61 return false, errors.E(op, errors.MissingParam, 62 fmt.Errorf("kptfile missing inventory section")) 63 } 64 // Validate the name, namespace, and inventory id 65 if strings.TrimSpace(inv.Name) == "" { 66 return false, errors.E(op, errors.MissingParam, 67 fmt.Errorf("kptfile inventory empty name")) 68 } 69 if strings.TrimSpace(inv.Namespace) == "" { 70 return false, errors.E(op, errors.MissingParam, 71 fmt.Errorf("kptfile inventory empty namespace")) 72 } 73 if strings.TrimSpace(inv.InventoryID) == "" { 74 return false, errors.E(op, errors.MissingParam, 75 fmt.Errorf("kptfile inventory missing inventoryID")) 76 } 77 return true, nil 78 } 79 80 func Equal(kf1, kf2 *kptfilev1.KptFile) (bool, error) { 81 const op errors.Op = "kptfileutil.Equal" 82 kf1Bytes, err := yaml.Marshal(kf1) 83 if err != nil { 84 return false, errors.E(op, errors.YAML, err) 85 } 86 87 kf2Bytes, err := yaml.Marshal(kf2) 88 if err != nil { 89 return false, errors.E(op, errors.YAML, err) 90 } 91 92 return bytes.Equal(kf1Bytes, kf2Bytes), nil 93 } 94 95 // DefaultKptfile returns a new minimal Kptfile. 96 func DefaultKptfile(name string) *kptfilev1.KptFile { 97 return &kptfilev1.KptFile{ 98 ResourceMeta: yaml.ResourceMeta{ 99 TypeMeta: yaml.TypeMeta{ 100 APIVersion: kptfilev1.TypeMeta.APIVersion, 101 Kind: kptfilev1.TypeMeta.Kind, 102 }, 103 ObjectMeta: yaml.ObjectMeta{ 104 NameMeta: yaml.NameMeta{ 105 Name: name, 106 }, 107 }, 108 }, 109 } 110 } 111 112 // UpdateKptfileWithoutOrigin updates the Kptfile in the package specified by 113 // localPath with values from the package specified by updatedPath using a 3-way 114 // merge strategy, but where origin does not have any values. 115 // If updateUpstream is true, the values from the upstream and upstreamLock 116 // sections will also be copied into local. 117 func UpdateKptfileWithoutOrigin(localPath, updatedPath string, updateUpstream bool) error { 118 const op errors.Op = "kptfileutil.UpdateKptfileWithoutOrigin" 119 localKf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, localPath) 120 if err != nil { 121 if !goerrors.Is(err, os.ErrNotExist) { 122 return errors.E(op, types.UniquePath(localPath), err) 123 } 124 localKf = &kptfilev1.KptFile{} 125 } 126 127 updatedKf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, updatedPath) 128 if err != nil { 129 if !goerrors.Is(err, os.ErrNotExist) { 130 return errors.E(op, types.UniquePath(updatedPath), err) 131 } 132 updatedKf = &kptfilev1.KptFile{} 133 } 134 135 err = merge(localKf, updatedKf, &kptfilev1.KptFile{}) 136 if err != nil { 137 return err 138 } 139 140 if updateUpstream { 141 updateUpstreamAndUpstreamLock(localKf, updatedKf) 142 } 143 144 err = WriteFile(localPath, localKf) 145 if err != nil { 146 return errors.E(op, types.UniquePath(localPath), err) 147 } 148 return nil 149 } 150 151 // UpdateKptfile updates the Kptfile in the package specified by localPath with 152 // values from the packages specified in updatedPath using the package specified 153 // by originPath as the common ancestor. 154 // If updateUpstream is true, the values from the upstream and upstreamLock 155 // sections will also be copied into local. 156 func UpdateKptfile(localPath, updatedPath, originPath string, updateUpstream bool) error { 157 const op errors.Op = "kptfileutil.UpdateKptfile" 158 localKf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, localPath) 159 if err != nil { 160 if !goerrors.Is(err, os.ErrNotExist) { 161 return errors.E(op, types.UniquePath(localPath), err) 162 } 163 localKf = &kptfilev1.KptFile{} 164 } 165 166 updatedKf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, updatedPath) 167 if err != nil { 168 if !goerrors.Is(err, os.ErrNotExist) { 169 return errors.E(op, types.UniquePath(localPath), err) 170 } 171 updatedKf = &kptfilev1.KptFile{} 172 } 173 174 originKf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, originPath) 175 if err != nil { 176 if !goerrors.Is(err, os.ErrNotExist) { 177 return errors.E(op, types.UniquePath(localPath), err) 178 } 179 originKf = &kptfilev1.KptFile{} 180 } 181 182 err = merge(localKf, updatedKf, originKf) 183 if err != nil { 184 return err 185 } 186 187 if updateUpstream { 188 updateUpstreamAndUpstreamLock(localKf, updatedKf) 189 } 190 191 err = WriteFile(localPath, localKf) 192 if err != nil { 193 return errors.E(op, types.UniquePath(localPath), err) 194 } 195 return nil 196 } 197 198 // UpdateUpstreamLockFromGit updates the upstreamLock of the package specified 199 // by path by using the values from spec. It will also populate the commit 200 // field in upstreamLock using the latest commit of the git repo given 201 // by spec. 202 func UpdateUpstreamLockFromGit(path string, spec *git.RepoSpec) error { 203 const op errors.Op = "kptfileutil.UpdateUpstreamLockFromGit" 204 // read KptFile cloned with the package if it exists 205 kpgfile, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, path) 206 if err != nil { 207 return errors.E(op, types.UniquePath(path), err) 208 } 209 210 // populate the cloneFrom values so we know where the package came from 211 kpgfile.UpstreamLock = &kptfilev1.UpstreamLock{ 212 Type: kptfilev1.GitOrigin, 213 Git: &kptfilev1.GitLock{ 214 Repo: spec.OrgRepo, 215 Directory: spec.Path, 216 Ref: spec.Ref, 217 Commit: spec.Commit, 218 }, 219 } 220 err = WriteFile(path, kpgfile) 221 if err != nil { 222 return errors.E(op, types.UniquePath(path), err) 223 } 224 return nil 225 } 226 227 // merge merges the Kptfiles from various sources and updates localKf with output 228 // please refer to https://github.com/GoogleContainerTools/kpt/blob/main/docs/design-docs/03-pipeline-merge.md 229 // for related design 230 func merge(localKf, updatedKf, originalKf *kptfilev1.KptFile) error { 231 shouldAddSyntheticMergeName := shouldAddFnKey(localKf, updatedKf, originalKf) 232 if shouldAddSyntheticMergeName { 233 addNameForMerge(localKf, updatedKf, originalKf) 234 } 235 236 localBytes, err := yaml.Marshal(localKf) 237 if err != nil { 238 return err 239 } 240 241 updatedBytes, err := yaml.Marshal(updatedKf) 242 if err != nil { 243 return err 244 } 245 246 originalBytes, err := yaml.Marshal(originalKf) 247 if err != nil { 248 return err 249 } 250 251 mergedBytes, err := merge3.MergeStrings(string(localBytes), string(originalBytes), string(updatedBytes), true) 252 if err != nil { 253 return err 254 } 255 256 var mergedKf kptfilev1.KptFile 257 err = yaml.Unmarshal([]byte(mergedBytes), &mergedKf) 258 if err != nil { 259 return err 260 } 261 262 if shouldAddSyntheticMergeName { 263 removeFnKey(localKf, updatedKf, originalKf, &mergedKf) 264 } 265 266 // Copy the merged content into the local Kptfile struct. We don't copy 267 // name, namespace, Upstream or UpstreamLock, since we don't want those 268 // merged. 269 localKf.Annotations = mergedKf.Annotations 270 localKf.Labels = mergedKf.Labels 271 localKf.Info = mergedKf.Info 272 localKf.Pipeline = mergedKf.Pipeline 273 localKf.Inventory = mergedKf.Inventory 274 localKf.Status = mergedKf.Status 275 return nil 276 } 277 278 // shouldAddFnKey returns true iff all the functions from all sources 279 // doesn't have name field set and there are no duplicate function declarations, 280 // it means the user is unaware of name field, and we use image name or exec field 281 // value as mergeKey instead of name in such cases 282 func shouldAddFnKey(kfs ...*kptfilev1.KptFile) bool { 283 for _, kf := range kfs { 284 if kf == nil || kf.Pipeline == nil { 285 continue 286 } 287 if !shouldAddFnKeyUtil(kf.Pipeline.Mutators) || !shouldAddFnKeyUtil(kf.Pipeline.Validators) { 288 return false 289 } 290 } 291 return true 292 } 293 294 // shouldAddFnKeyUtil returns true iff all the functions from input list 295 // doesn't have name field set and there are no duplicate function declarations, 296 // it means the user is unaware of name field, and we use image name or exec field 297 // value as mergeKey instead of name in such cases 298 func shouldAddFnKeyUtil(fns []kptfilev1.Function) bool { 299 keySet := sets.String{} 300 for _, fn := range fns { 301 if fn.Name != "" { 302 return false 303 } 304 var key string 305 if fn.Exec != "" { 306 key = fn.Exec 307 } else { 308 key = strings.Split(fn.Image, ":")[0] 309 } 310 if keySet.Has(key) { 311 return false 312 } 313 keySet.Insert(key) 314 } 315 return true 316 } 317 318 // addNameForMerge adds name field for all the functions if empty 319 // name is primarily used as merge-key 320 func addNameForMerge(kfs ...*kptfilev1.KptFile) { 321 for _, kf := range kfs { 322 if kf == nil || kf.Pipeline == nil { 323 continue 324 } 325 for i, mutator := range kf.Pipeline.Mutators { 326 kf.Pipeline.Mutators[i] = addName(mutator) 327 } 328 for i, validator := range kf.Pipeline.Validators { 329 kf.Pipeline.Validators[i] = addName(validator) 330 } 331 } 332 } 333 334 // addName adds name field to the input function if empty 335 // name is nothing but image name in this case as we use it as fall back mergeKey 336 func addName(fn kptfilev1.Function) kptfilev1.Function { 337 if fn.Name != "" { 338 return fn 339 } 340 var key string 341 if fn.Exec != "" { 342 key = fn.Exec 343 } else { 344 parts := strings.Split(fn.Image, ":") 345 if len(parts) > 0 { 346 key = parts[0] 347 } 348 } 349 fn.Name = fmt.Sprintf("_kpt-merge_%s", key) 350 return fn 351 } 352 353 // removeFnKey remove the synthesized function name field before writing 354 func removeFnKey(kfs ...*kptfilev1.KptFile) { 355 for _, kf := range kfs { 356 if kf == nil || kf.Pipeline == nil { 357 continue 358 } 359 for i := range kf.Pipeline.Mutators { 360 if strings.HasPrefix(kf.Pipeline.Mutators[i].Name, "_kpt-merge_") { 361 kf.Pipeline.Mutators[i].Name = "" 362 } 363 } 364 for i := range kf.Pipeline.Validators { 365 if strings.HasPrefix(kf.Pipeline.Validators[i].Name, "_kpt-merge_") { 366 kf.Pipeline.Validators[i].Name = "" 367 } 368 } 369 } 370 } 371 372 func updateUpstreamAndUpstreamLock(localKf, updatedKf *kptfilev1.KptFile) { 373 if updatedKf.Upstream != nil { 374 localKf.Upstream = updatedKf.Upstream 375 } 376 377 if updatedKf.UpstreamLock != nil { 378 localKf.UpstreamLock = updatedKf.UpstreamLock 379 } 380 }