diff --git a/cmd/helm/root.go b/cmd/helm/root.go index 285c80021..51a7391d7 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -163,6 +163,8 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string } actionConfig.RegistryClient = registryClient + actionConfig.UseThreeWayMergePatchForUnstructured = settings.UseThreeWayMergePatchForUnstructured + // Add subcommands cmd.AddCommand( // chart commands diff --git a/go.mod b/go.mod index 752614539..3f470e9d3 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/docker/distribution v2.7.1+incompatible github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible github.com/docker/go-units v0.4.0 + github.com/evanphx/json-patch v4.9.0+incompatible github.com/gobwas/glob v0.2.3 github.com/gofrs/flock v0.8.0 github.com/gosuri/uitable v0.0.4 diff --git a/pkg/action/action.go b/pkg/action/action.go index f093ed7f8..36cd70f22 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -94,6 +94,10 @@ type Configuration struct { // Capabilities describes the capabilities of the Kubernetes cluster. Capabilities *chartutil.Capabilities + // UseThreeWayMergePatchForUnstructured controls whether to use three way merge patch + // for unstructured (CR, CRD etc.) objects. + UseThreeWayMergePatchForUnstructured bool + Log func(string, ...interface{}) } @@ -365,6 +369,7 @@ func (cfg *Configuration) recordRelease(r *release.Release) { func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log DebugLog) error { kc := kube.New(getter) kc.Log = log + kc.UseThreeWayMergePatchForUnstructured = cfg.UseThreeWayMergePatchForUnstructured lazyClient := &lazyClient{ namespace: namespace, diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index ee60d981f..a314eecbb 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -68,6 +68,9 @@ type EnvSettings struct { PluginsDirectory string // MaxHistory is the max release history maintained. MaxHistory int + // UseThreeWayMergePatchForUnstructured controls whether to use three way merge patch + // for unstructured (CR, CRD etc.) objects. + UseThreeWayMergePatchForUnstructured bool } func New() *EnvSettings { @@ -86,6 +89,7 @@ func New() *EnvSettings { RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), } env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG")) + env.UseThreeWayMergePatchForUnstructured, _ = strconv.ParseBool(os.Getenv("HELM_USE_THREE_WAY_MERGE_PATCH_FOR_UNSTRUCTURED")) // bind to kubernetes config flags env.config = &genericclioptions.ConfigFlags{ @@ -115,6 +119,7 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&s.RegistryConfig, "registry-config", s.RegistryConfig, "path to the registry config file") fs.StringVar(&s.RepositoryConfig, "repository-config", s.RepositoryConfig, "path to the file containing repository names and URLs") fs.StringVar(&s.RepositoryCache, "repository-cache", s.RepositoryCache, "path to the file containing cached repository indexes") + fs.BoolVar(&s.UseThreeWayMergePatchForUnstructured, "use-three-way-merge-patch-for-unstructured", s.UseThreeWayMergePatchForUnstructured, "use three way merge patch for unstructured (CR, CRD etc.) objects") } func envOr(name, def string) string { diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 8c0a79102..e41670174 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -27,6 +27,7 @@ import ( "sync" "time" + jsonpatch "github.com/evanphx/json-patch" "github.com/pkg/errors" batch "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" @@ -69,6 +70,10 @@ type Client struct { // Namespace allows to bypass the kubeconfig file for the choice of the namespace Namespace string + // UseThreeWayMergePatchForUnstructured controls whether to use three way merge patch + // for unstructured (CR, CRD etc.) objects + UseThreeWayMergePatchForUnstructured bool + kubeClient *kubernetes.Clientset } @@ -418,7 +423,7 @@ func deleteResource(info *resource.Info) error { return err } -func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.PatchType, error) { +func createPatch(target *resource.Info, current runtime.Object, useThreeWayMergePatchForUnstructured bool) ([]byte, types.PatchType, error) { oldData, err := json.Marshal(current) if err != nil { return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing current configuration") @@ -446,7 +451,7 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P // Unstructured objects, such as CRDs, may not have an not registered error // returned from ConvertToVersion. Anything that's unstructured should - // use the jsonmergepatch.CreateThreeWayJSONMergePatch. Strategic Merge Patch is not supported + // use generic JSON merge patch. Strategic Merge Patch is not supported // on objects like CRDs. _, isUnstructured := versionedObject.(runtime.Unstructured) @@ -455,13 +460,17 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P if isUnstructured || isCRD { // fall back to generic JSON merge patch - // from https://github.com/kubernetes/kubectl/blob/b83b2ec7d15f286720bccf7872b5c72372cb8e80/pkg/cmd/apply/patcher.go#L129 - preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"), - mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")} - patch, err := jsonmergepatch.CreateThreeWayJSONMergePatch(oldData, newData, currentData, preconditions...) - if err != nil && mergepatch.IsPreconditionFailed(err) { - err = fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed") + if useThreeWayMergePatchForUnstructured { + // from https://github.com/kubernetes/kubectl/blob/b83b2ec7d15f286720bccf7872b5c72372cb8e80/pkg/cmd/apply/patcher.go#L129 + preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"), + mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")} + patch, err := jsonmergepatch.CreateThreeWayJSONMergePatch(oldData, newData, currentData, preconditions...) + if err != nil && mergepatch.IsPreconditionFailed(err) { + err = fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed") + } + return patch, types.MergePatchType, err } + patch, err := jsonpatch.CreateMergePatch(oldData, newData) return patch, types.MergePatchType, err } @@ -490,7 +499,7 @@ func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, } c.Log("Replaced %q with kind %s for kind %s", target.Name, currentObj.GetObjectKind().GroupVersionKind().Kind, kind) } else { - patch, patchType, err := createPatch(target, currentObj) + patch, patchType, err := createPatch(target, currentObj, c.UseThreeWayMergePatchForUnstructured) if err != nil { return errors.Wrap(err, "failed to create patch") }