Skip to content

Commit

Permalink
Patch instead of update (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
pepov authored Jan 26, 2021
1 parent 2e8f3e3 commit 44952e0
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 16 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ module github.com/banzaicloud/terraform-provider-k8s
go 1.13

require (
github.com/evanphx/json-patch v4.5.0+incompatible
github.com/hashicorp/terraform-plugin-sdk v1.15.0
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.3.3
github.com/pkg/errors v0.8.1
go.uber.org/zap v1.15.0 // indirect
k8s.io/apimachinery v0.18.6
k8s.io/client-go v0.18.6
Expand Down
110 changes: 110 additions & 0 deletions k8s/patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright The Helm Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Adapted from https://github.com/helm/helm/blob/master/pkg/kube/client.go
// and https://github.com/helm/helm/blob/master/pkg/kube/converter.go

package k8s

import (
"context"
"log"
"sync"

jsonpatch "github.com/evanphx/json-patch"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var k8sNativeScheme *runtime.Scheme
var k8sNativeSchemeOnce sync.Once

func patch(c client.Client, target, original, current *unstructured.Unstructured) error {
patch, patchType, err := createPatch(target, original, current)
if err != nil {
return errors.Wrap(err, "failed to create patch")
}

if patch == nil || string(patch) == "{}" {
log.Printf("Looks like there are no changes for %s %q", target.GroupVersionKind().String(), target.GetName())
return nil
}

// send patch to server
if err = c.Patch(context.TODO(), current, client.RawPatch(patchType, patch)); err != nil {
return errors.Wrapf(err, "cannot patch %q with kind %s", target.GroupVersionKind().String(), target.GetName())
}
return nil
}

func createPatch(target, original, current *unstructured.Unstructured) ([]byte, types.PatchType, error) {
oldData, err := json.Marshal(current)
if err != nil {
return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing current configuration")
}
originalData, err := json.Marshal(original)
if err != nil {
return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing original configuration")
}
newData, err := json.Marshal(target)
if err != nil {
return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing target configuration")
}

versionedObject := convert(target)

// Unstructured objects, such as CRDs, may not have an not registered error
// returned from ConvertToVersion. Anything that's unstructured should
// use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported
// on objects like CRDs.
_, isUnstructured := versionedObject.(runtime.Unstructured)

if isUnstructured {
// fall back to generic JSON merge patch
patch, err := jsonpatch.CreateMergePatch(oldData, newData)
return patch, types.MergePatchType, err
}

patchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject)
if err != nil {
return nil, types.StrategicMergePatchType, errors.Wrap(err, "unable to create patch metadata from object")
}

patch, err := strategicpatch.CreateThreeWayMergePatch(originalData, newData, oldData, patchMeta, true)
return patch, types.StrategicMergePatchType, err
}

func convert(obj *unstructured.Unstructured) runtime.Object {
s := kubernetesNativeScheme()
if obj, err := runtime.ObjectConvertor(s).ConvertToVersion(obj, obj.GroupVersionKind().GroupVersion()); err == nil {
return obj
}
return obj
}

// kubernetesNativeScheme returns a clean *runtime.Scheme with _only_ Kubernetes
// native resources added to it. This is required to break free of custom resources
// that may have been added to scheme.Scheme due to Helm being used as a package in
// combination with e.g. a versioned kube client. If we would not do this, the client
// may attempt to perform e.g. a 3-way-merge strategy patch for custom resources.
func kubernetesNativeScheme() *runtime.Scheme {
k8sNativeSchemeOnce.Do(func() {
k8sNativeScheme = runtime.NewScheme()
scheme.AddToScheme(k8sNativeScheme)
})
return k8sNativeScheme
}
4 changes: 3 additions & 1 deletion k8s/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,9 @@ func providerConfigure(d *schema.ResourceData, terraformVersion string) (interfa
return nil, fmt.Errorf("Failed to configure: %s", err)
}

return &ProviderConfig{c}, nil
return &ProviderConfig{
RuntimeClient: c,
}, nil
}

func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) {
Expand Down
39 changes: 24 additions & 15 deletions k8s/resource_k8s_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,54 +258,62 @@ func resourceK8sManifestRead(d *schema.ResourceData, config interface{}) error {
}

func resourceK8sManifestUpdate(d *schema.ResourceData, config interface{}) error {
namespace, _, _, name, err := idParts(d.Id())
namespace, _, _, _, err := idParts(d.Id())
if err != nil {
return err
}

content := d.Get("content").(string)
originalData, newData := d.GetChange("content")

object, err := contentToObject(content)
log.Printf("[DEBUG] Original vs modified: %s %s", originalData, newData)

modified, err := contentToObject(newData.(string))
if err != nil {
return err
}

objectNamespace := object.GetNamespace()
original, err := contentToObject(originalData.(string))
if err != nil {
return err
}

objectNamespace := modified.GetNamespace()

if namespace == "" && objectNamespace == "" {
object.SetNamespace("default")
modified.SetNamespace("default")
} else if objectNamespace == "" {
// TODO: which namespace should have a higher precedence?
object.SetNamespace(namespace)
modified.SetNamespace(namespace)
}

objectKey, err := client.ObjectKeyFromObject(object)
objectKey, err := client.ObjectKeyFromObject(modified)
if err != nil {
log.Printf("[DEBUG] Received error: %#v", err)
return err
}

copy := object.DeepCopy()
current := modified.DeepCopy()

client := config.(*ProviderConfig).RuntimeClient

err = client.Get(context.Background(), objectKey, copy)
err = client.Get(context.Background(), objectKey, current)
if err != nil {
log.Printf("[DEBUG] Received error: %#v", err)
return err
}

object.SetResourceVersion(copy.DeepCopy().GetResourceVersion())
modified.SetResourceVersion(current.DeepCopy().GetResourceVersion())

log.Printf("[INFO] Updating object %s", name)
err = client.Update(context.Background(), object)
if err != nil {
current.SetResourceVersion("")
original.SetResourceVersion("")

if err := patch(config.(*ProviderConfig).RuntimeClient, modified, original, current); err != nil {
log.Printf("[DEBUG] Received error: %#v", err)
return err
}
log.Printf("[INFO] Updated object: %#v", object)
log.Printf("[INFO] Updated object: %#v", modified)

return waitForReadyStatus(d, client, object, d.Timeout(schema.TimeoutUpdate))
return waitForReadyStatus(d, client, modified, d.Timeout(schema.TimeoutUpdate))
}

func resourceK8sManifestDelete(d *schema.ResourceData, config interface{}) error {
Expand Down Expand Up @@ -435,3 +443,4 @@ func contentToObject(content string) (*unstructured.Unstructured, error) {
}
}
}

0 comments on commit 44952e0

Please sign in to comment.