github.com/replicatedcom/ship@v0.50.0/pkg/patch/patcher.go (about)

     1  package patch
     2  
     3  import (
     4  	"encoding/json"
     5  	"path"
     6  	"path/filepath"
     7  	"reflect"
     8  	"strconv"
     9  
    10  	"github.com/ghodss/yaml"
    11  	"github.com/go-kit/kit/log"
    12  	"github.com/go-kit/kit/log/level"
    13  	"github.com/pkg/errors"
    14  	"github.com/spf13/afero"
    15  	yamlv3 "gopkg.in/yaml.v3"
    16  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    17  	"k8s.io/client-go/kubernetes/scheme"
    18  	kustomizepatch "sigs.k8s.io/kustomize/pkg/patch"
    19  	k8stypes "sigs.k8s.io/kustomize/pkg/types"
    20  
    21  	"github.com/replicatedhq/ship/pkg/api"
    22  	"github.com/replicatedhq/ship/pkg/util"
    23  )
    24  
    25  const PATCH_TOKEN = "TO_BE_MODIFIED"
    26  
    27  const TempYamlPath = "temp.yaml"
    28  
    29  type Patcher interface {
    30  	CreateTwoWayMergePatch(original, modified []byte) ([]byte, error)
    31  	MergePatches(original []byte, path []string, step api.Kustomize, resource string) ([]byte, error)
    32  	ApplyPatch(patch []byte, step api.Kustomize, resource string) ([]byte, error)
    33  	ModifyField(original []byte, path []string) ([]byte, error)
    34  	RunKustomize(kustomizationPath string) ([]byte, error)
    35  }
    36  
    37  type ShipPatcher struct {
    38  	Logger log.Logger
    39  	FS     afero.Afero
    40  }
    41  
    42  func NewShipPatcher(logger log.Logger, fs afero.Afero) Patcher {
    43  	return &ShipPatcher{
    44  		Logger: logger,
    45  		FS:     fs,
    46  	}
    47  }
    48  
    49  func (p *ShipPatcher) writeHeaderToPatch(originalJSON, patchJSON []byte) ([]byte, error) {
    50  	original := map[string]interface{}{}
    51  	patch := map[string]interface{}{}
    52  
    53  	err := json.Unmarshal(originalJSON, &original)
    54  	if err != nil {
    55  		return nil, errors.Wrap(err, "unmarshal original json")
    56  	}
    57  
    58  	err = json.Unmarshal(patchJSON, &patch)
    59  	if err != nil {
    60  		return nil, errors.Wrap(err, "unmarshal patch json")
    61  	}
    62  
    63  	originalAPIVersion, ok := original["apiVersion"]
    64  	if !ok {
    65  		return nil, errors.New("no apiVersion key present in original")
    66  	}
    67  
    68  	originalKind, ok := original["kind"]
    69  	if !ok {
    70  		return nil, errors.New("no kind key present in original")
    71  	}
    72  
    73  	originalMetadata, ok := original["metadata"]
    74  	if !ok {
    75  		return nil, errors.New("no metadata key present in original")
    76  	}
    77  
    78  	patch["apiVersion"] = originalAPIVersion
    79  	patch["kind"] = originalKind
    80  	patch["metadata"] = originalMetadata
    81  
    82  	modifiedPatch, err := json.Marshal(patch)
    83  	if err != nil {
    84  		return nil, errors.Wrap(err, "marshal modified patch json")
    85  	}
    86  
    87  	return modifiedPatch, nil
    88  }
    89  
    90  func (p *ShipPatcher) CreateTwoWayMergePatch(original, modified []byte) ([]byte, error) {
    91  	debug := level.Debug(log.With(p.Logger, "struct", "patcher", "handler", "createTwoWayMergePatch"))
    92  
    93  	debug.Log("event", "convert.original")
    94  	originalJSON, err := yaml.YAMLToJSON(original)
    95  	if err != nil {
    96  		return nil, errors.Wrap(err, "convert original file to json")
    97  	}
    98  
    99  	debug.Log("event", "convert.modified")
   100  	modifiedJSON, err := yaml.YAMLToJSON(modified)
   101  	if err != nil {
   102  		return nil, errors.Wrap(err, "convert modified file to json")
   103  	}
   104  
   105  	debug.Log("event", "createKubeResource.original")
   106  	r, err := util.NewKubernetesResource(originalJSON)
   107  	if err != nil {
   108  		return nil, errors.Wrap(err, "create kube resource with original json")
   109  	}
   110  
   111  	versionedObj, err := scheme.Scheme.New(util.ToGroupVersionKind(r.Id().Gvk()))
   112  	if err != nil {
   113  		return nil, errors.Wrap(err, "read group, version kind from kube resource")
   114  	}
   115  
   116  	patchBytes, err := strategicpatch.CreateTwoWayMergePatch(originalJSON, modifiedJSON, versionedObj)
   117  	if err != nil {
   118  		return nil, errors.Wrap(err, "create two way merge patch")
   119  	}
   120  
   121  	modifiedPatchJSON, err := p.writeHeaderToPatch(originalJSON, patchBytes)
   122  	if err != nil {
   123  		return nil, errors.Wrap(err, "write original header to patch")
   124  	}
   125  
   126  	patch, err := yaml.JSONToYAML(modifiedPatchJSON)
   127  	if err != nil {
   128  		return nil, errors.Wrap(err, "convert merge patch json to yaml")
   129  	}
   130  
   131  	return patch, nil
   132  }
   133  
   134  func (p *ShipPatcher) MergePatches(original []byte, path []string, step api.Kustomize, resource string) ([]byte, error) {
   135  	debug := level.Debug(log.With(p.Logger, "struct", "patcher", "handler", "mergePatches"))
   136  
   137  	debug.Log("event", "applyPatch")
   138  	modified, err := p.ApplyPatch(original, step, resource)
   139  	if err != nil {
   140  		return nil, errors.Wrap(err, "apply patch")
   141  	}
   142  
   143  	debug.Log("event", "modifyField")
   144  	dirtied, err := p.ModifyField(modified, path)
   145  	if err != nil {
   146  		return nil, errors.Wrap(err, "dirty modified")
   147  	}
   148  
   149  	debug.Log("event", "readOriginal")
   150  	originalYaml, err := p.FS.ReadFile(resource)
   151  	if err != nil {
   152  		return nil, errors.Wrap(err, "read original yaml")
   153  	}
   154  
   155  	debug.Log("event", "createNewPatch")
   156  	finalPatch, err := p.CreateTwoWayMergePatch(originalYaml, dirtied)
   157  	if err != nil {
   158  		return nil, errors.Wrap(err, "create patch")
   159  	}
   160  
   161  	return finalPatch, nil
   162  }
   163  
   164  func (p *ShipPatcher) ApplyPatch(patch []byte, step api.Kustomize, resource string) ([]byte, error) {
   165  	debug := level.Debug(log.With(p.Logger, "struct", "patcher", "handler", "applyPatch"))
   166  
   167  	defer p.deleteTempKustomization(step)
   168  
   169  	if err := p.FS.MkdirAll(step.TempRenderPath(), 0777); err != nil {
   170  		return nil, errors.Wrap(err, "ensure temp patch overlay dir exists")
   171  	}
   172  
   173  	debug.Log("event", "writeFile.tempPatch")
   174  	if err := p.FS.WriteFile(path.Join(step.TempRenderPath(), TempYamlPath), patch, 0755); err != nil {
   175  		return nil, errors.Wrap(err, "write temp patch overlay")
   176  	}
   177  
   178  	debug.Log("event", "relPath")
   179  	relativePathToResource, err := filepath.Rel(step.TempRenderPath(), resource)
   180  	if err != nil {
   181  		return nil, errors.Wrap(err, "failed to find relative path")
   182  	}
   183  
   184  	kustomizationYaml := k8stypes.Kustomization{
   185  		Resources:             []string{relativePathToResource},
   186  		PatchesStrategicMerge: []kustomizepatch.StrategicMerge{TempYamlPath},
   187  	}
   188  
   189  	kustomizationYamlBytes, err := yamlv3.Marshal(kustomizationYaml)
   190  	if err != nil {
   191  		return nil, errors.Wrap(err, "marshal kustomization yaml")
   192  	}
   193  
   194  	debug.Log("event", "writeFile.tempKustomizationYaml")
   195  	if err := p.FS.WriteFile(path.Join(step.TempRenderPath(), "kustomization.yaml"), kustomizationYamlBytes, 0755); err != nil {
   196  		return nil, errors.Wrap(err, "write temp kustomization yaml")
   197  	}
   198  
   199  	debug.Log("event", "run.kustomizeBuild")
   200  	merged, err := p.RunKustomize(step.TempRenderPath())
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  
   205  	return merged, nil
   206  }
   207  
   208  func (p *ShipPatcher) deleteTempKustomization(step api.Kustomize) error {
   209  	debug := level.Debug(log.With(p.Logger, "struct", "patcher", "handler", "deleteTempKustomization"))
   210  
   211  	tempKustomizationPath := path.Join(step.TempRenderPath(), "kustomization.yaml")
   212  
   213  	debug.Log("event", "remove.tempKustomizationYaml")
   214  	err := p.FS.Remove(tempKustomizationPath)
   215  	if err != nil {
   216  		return errors.Wrap(err, "remove temp base kustomization.yaml")
   217  	}
   218  
   219  	err = p.FS.Remove(path.Join(step.TempRenderPath(), TempYamlPath))
   220  	if err != nil {
   221  		return errors.Wrap(err, "remove temp patch yaml")
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  func (p *ShipPatcher) ModifyField(original []byte, path []string) ([]byte, error) {
   228  	debug := level.Debug(log.With(p.Logger, "struct", "patcher", "handler", "modifyField"))
   229  
   230  	originalMap := map[string]interface{}{}
   231  
   232  	debug.Log("event", "convert original yaml to json")
   233  	originalJSON, err := yaml.YAMLToJSON(original)
   234  	if err != nil {
   235  		return nil, errors.Wrap(err, "original yaml to json")
   236  	}
   237  
   238  	debug.Log("event", "unmarshal original yaml")
   239  	if err := json.Unmarshal(originalJSON, &originalMap); err != nil {
   240  		return nil, errors.Wrap(err, "unmarshal original yaml")
   241  	}
   242  
   243  	debug.Log("event", "modify field")
   244  	modified, err := p.modifyField(originalMap, []string{}, path)
   245  	if err != nil {
   246  		return nil, errors.Wrap(err, "error modifying value")
   247  	}
   248  
   249  	debug.Log("event", "marshal modified")
   250  	modifiedJSON, err := json.Marshal(modified)
   251  	if err != nil {
   252  		return nil, errors.Wrap(err, "marshal modified json")
   253  	}
   254  
   255  	debug.Log("event", "convert modified yaml to json")
   256  	modifiedYAML, err := yaml.JSONToYAML(modifiedJSON)
   257  	if err != nil {
   258  		return nil, errors.Wrap(err, "modified json to yaml")
   259  	}
   260  
   261  	return modifiedYAML, nil
   262  }
   263  
   264  func (p *ShipPatcher) modifyField(original interface{}, current []string, path []string) (interface{}, error) {
   265  	originalType := reflect.TypeOf(original)
   266  	if original == nil {
   267  		return nil, nil
   268  	}
   269  	switch originalType.Kind() {
   270  	case reflect.Map:
   271  		typedOriginal, ok := original.(map[string]interface{})
   272  		modifiedMap := make(map[string]interface{})
   273  		if !ok {
   274  			return nil, errors.New("error asserting map")
   275  		}
   276  		for key, value := range typedOriginal {
   277  			modifiedValue, err := p.modifyField(value, append(current, key), path)
   278  			if err != nil {
   279  				return nil, err
   280  			}
   281  			modifiedMap[key] = modifiedValue
   282  		}
   283  		return modifiedMap, nil
   284  	case reflect.Slice:
   285  		typedOriginal, ok := original.([]interface{})
   286  		modifiedSlice := make([]interface{}, len(typedOriginal))
   287  		if !ok {
   288  			return nil, errors.New("error asserting slice")
   289  		}
   290  		for key, value := range typedOriginal {
   291  			modifiedValue, err := p.modifyField(value, append(current, strconv.Itoa(key)), path)
   292  			if err != nil {
   293  				return nil, err
   294  			}
   295  			modifiedSlice[key] = modifiedValue
   296  		}
   297  		return modifiedSlice, nil
   298  	default:
   299  		for idx := range path {
   300  			if current[idx] != path[idx] {
   301  				return original, nil
   302  			}
   303  		}
   304  		return PATCH_TOKEN, nil
   305  	}
   306  }