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