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  }