You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by as...@apache.org on 2021/02/18 16:11:31 UTC

[camel-k] branch master updated (e9f7bfc -> 70f7946)

This is an automated email from the ASF dual-hosted git repository.

astefanutti pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/camel-k.git.


    from e9f7bfc  Updated CHANGELOG.md
     new cf48cd2  feat: Use server-side apply to create and patch owned resources
     new 158c28b  fix(openapi): Clear managed fields from OpenAPI generated ConfigMap to support server-side apply
     new 70f7946  chore: Fallback to client-side apply if necessary

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 pkg/trait/deployer.go          | 114 ++++++++++++++++++++++++++++-------------
 pkg/trait/openapi.go           |  12 +++--
 pkg/util/kubernetes/replace.go |  22 +++-----
 pkg/util/patch/patch.go        |  61 ++++++++--------------
 4 files changed, 114 insertions(+), 95 deletions(-)


[camel-k] 02/03: fix(openapi): Clear managed fields from OpenAPI generated ConfigMap to support server-side apply

Posted by as...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

astefanutti pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/camel-k.git

commit 158c28b4db7f3091bcac3dee199c33cc2a9a2920
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Tue Feb 16 19:32:03 2021 +0100

    fix(openapi): Clear managed fields from OpenAPI generated ConfigMap to support server-side apply
---
 pkg/trait/openapi.go | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/pkg/trait/openapi.go b/pkg/trait/openapi.go
index 99d94bc..ba23835 100644
--- a/pkg/trait/openapi.go
+++ b/pkg/trait/openapi.go
@@ -26,18 +26,18 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/apache/camel-k/pkg/util"
-	"github.com/apache/camel-k/pkg/util/digest"
 	"github.com/pkg/errors"
-	k8serrors "k8s.io/apimachinery/pkg/api/errors"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	corev1 "k8s.io/api/core/v1"
+	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
-	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
+	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"github.com/apache/camel-k/pkg/util"
 	"github.com/apache/camel-k/pkg/util/defaults"
+	"github.com/apache/camel-k/pkg/util/digest"
 	"github.com/apache/camel-k/pkg/util/gzip"
 	"github.com/apache/camel-k/pkg/util/kubernetes"
 	"github.com/apache/camel-k/pkg/util/maven"
@@ -171,6 +171,8 @@ func (t *openAPITrait) generateOpenAPIConfigMap(e *Environment, resource v1.Reso
 		// ConfigMap already exists and matches the source
 		// Re-adding it to update its revision
 		cm.ResourceVersion = ""
+		// Clear the managed fields to support server-side apply
+		cm.ManagedFields = nil
 		e.Resources.Add(&cm)
 		return nil
 	}


[camel-k] 03/03: chore: Fallback to client-side apply if necessary

Posted by as...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

astefanutti pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/camel-k.git

commit 70f7946cb5a4cdc224c7c25e49b232867a4f815e
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Wed Feb 17 18:31:10 2021 +0100

    chore: Fallback to client-side apply if necessary
---
 pkg/trait/deployer.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 76 insertions(+), 6 deletions(-)

diff --git a/pkg/trait/deployer.go b/pkg/trait/deployer.go
index d9fa584..a9a11d7 100644
--- a/pkg/trait/deployer.go
+++ b/pkg/trait/deployer.go
@@ -18,8 +18,14 @@ limitations under the License.
 package trait
 
 import (
+	"net/http"
+
 	"github.com/pkg/errors"
 
+	k8serrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
@@ -38,6 +44,8 @@ type deployerTrait struct {
 
 var _ ControllerStrategySelector = &deployerTrait{}
 
+var hasServerSideApply = true
+
 func newDeployerTrait() Trait {
 	return &deployerTrait{
 		BaseTrait: NewBaseTrait("deployer", 900),
@@ -75,13 +83,23 @@ func (t *deployerTrait) Apply(e *Environment) error {
 		// Register a post action that patches the resources generated by the traits
 		e.PostActions = append(e.PostActions, func(env *Environment) error {
 			for _, resource := range env.Resources.Items() {
-				target, err := patch.PositiveApplyPatch(resource)
-				if err != nil {
-					return err
+				// We assume that server-side apply is enabled by default.
+				// It is currently convoluted to check pro-actively whether server-side apply
+				// is enabled. This is possible to fetch the OpenAPI endpoint, which returns
+				// the entire server API document, then lookup the resource PATCH endpoint, and
+				// check its list of accepted MIME types.
+				// As a simpler solution, we fallback to client-side apply at the first
+				// 415 error, and assume server-side apply is not available globally.
+				if hasServerSideApply {
+					if err := t.serverSideApply(env, resource); err == nil {
+						continue
+					} else if isIncompatibleServerError(err) {
+						t.L.Info("Fallback to client-side apply to patch resources")
+						hasServerSideApply = false
+					}
 				}
-				err = env.Client.Patch(env.C, target, client.Apply, client.ForceOwnership, client.FieldOwner("camel-k-operator"))
-				if err != nil {
-					return errors.Wrapf(err, "error during apply resource: %v", resource)
+				if err := t.clientSideApply(env, resource); err != nil {
+					return err
 				}
 			}
 			return nil
@@ -91,6 +109,58 @@ func (t *deployerTrait) Apply(e *Environment) error {
 	return nil
 }
 
+func (t *deployerTrait) serverSideApply(env *Environment, resource runtime.Object) error {
+	target, err := patch.PositiveApplyPatch(resource)
+	if err != nil {
+		return err
+	}
+	err = env.Client.Patch(env.C, target, client.Apply, client.ForceOwnership, client.FieldOwner("camel-k-operator"))
+	if err != nil {
+		return errors.Wrapf(err, "error during apply resource: %v", resource)
+	}
+	return nil
+}
+
+func (t *deployerTrait) clientSideApply(env *Environment, resource runtime.Object) error {
+	err := env.Client.Create(env.C, resource)
+	if err == nil {
+		return nil
+	} else if !k8serrors.IsAlreadyExists(err) {
+		return errors.Wrapf(err, "error during create resource: %v", resource)
+	}
+	key, err := client.ObjectKeyFromObject(resource)
+	if err != nil {
+		return err
+	}
+	object := resource.DeepCopyObject()
+	err = env.Client.Get(env.C, key, object)
+	if err != nil {
+		return err
+	}
+	p, err := patch.PositiveMergePatch(object, resource)
+	if err != nil {
+		return err
+	} else if len(p) == 0 {
+		// Avoid triggering a patch request for nothing
+		return nil
+	}
+	err = env.Client.Patch(env.C, resource, client.RawPatch(types.MergePatchType, p))
+	if err != nil {
+		return errors.Wrapf(err, "error during patch resource: %v", resource)
+	}
+	return nil
+}
+
+func isIncompatibleServerError(err error) bool {
+	// 415: Unsupported media type means we're talking to a server which doesn't
+	// support server-side apply.
+	if _, ok := err.(*k8serrors.StatusError); !ok {
+		// Non-StatusError means the error isn't because the server is incompatible.
+		return false
+	}
+	return err.(*k8serrors.StatusError).Status().Code == http.StatusUnsupportedMediaType
+}
+
 func (t *deployerTrait) SelectControllerStrategy(e *Environment) (*ControllerStrategy, error) {
 	if t.Enabled != nil && !*t.Enabled {
 		return nil, nil


[camel-k] 01/03: feat: Use server-side apply to create and patch owned resources

Posted by as...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

astefanutti pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/camel-k.git

commit cf48cd2f7553a04c624c10520b5a80461f915ddf
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Mon Feb 15 18:16:58 2021 +0100

    feat: Use server-side apply to create and patch owned resources
    
    Server-side apply has been introduced to improve co-operation
    of controllers in defining the desire state for shared resources.
    
    We implemented a custom strategy relying on a combination of merge
    patch and pruning of nil values, but that fails to leverage the
    semantic provided by the strategic merge patch field markers,
    as strategic merge patch is not supported by CRDs, like Knative
    Service.
    
    With server-side apply, markers can be applied to data
    structures (Lists, Maps), to make valid and finer-grained merge
    strategies.
    
    This particularly helps cooperation of the Camel K operator with
    its Knative counterpart, into updating the Knative service pod
    template fields concurrently, such as the `Env []EnvVar` field,
    that declares the `+patchStrategy=merge` marker, which translates
    into the server-side apply `listType` and `+listMapKey` markers.
---
 pkg/trait/deployer.go          | 42 +++++------------------------
 pkg/util/kubernetes/replace.go | 22 ++++++---------
 pkg/util/patch/patch.go        | 61 +++++++++++++++---------------------------
 3 files changed, 36 insertions(+), 89 deletions(-)

diff --git a/pkg/trait/deployer.go b/pkg/trait/deployer.go
index 062b91d..d9fa584 100644
--- a/pkg/trait/deployer.go
+++ b/pkg/trait/deployer.go
@@ -19,12 +19,10 @@ package trait
 
 import (
 	"github.com/pkg/errors"
-	"k8s.io/apimachinery/pkg/types"
 
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
-	"github.com/apache/camel-k/pkg/util/kubernetes"
 	"github.com/apache/camel-k/pkg/util/patch"
 )
 
@@ -62,15 +60,6 @@ func (t *deployerTrait) Configure(e *Environment) (bool, error) {
 func (t *deployerTrait) Apply(e *Environment) error {
 	switch e.Integration.Status.Phase {
 
-	case v1.IntegrationPhaseNone, v1.IntegrationPhaseWaitingForPlatform, v1.IntegrationPhaseInitialization, v1.IntegrationPhaseDeploying:
-		// Register a post action that updates the resources generated by the traits
-		e.PostActions = append(e.PostActions, func(env *Environment) error {
-			if err := kubernetes.ReplaceResources(env.C, env.Client, env.Resources.Items()); err != nil {
-				return errors.Wrap(err, "error during replace resource")
-			}
-			return nil
-		})
-
 	case v1.IntegrationPhaseBuildingKit, v1.IntegrationPhaseResolvingKit:
 		if e.IntegrationKitInPhase(v1.IntegrationKitPhaseReady) {
 			e.PostProcessors = append(e.PostProcessors, func(environment *Environment) error {
@@ -80,38 +69,19 @@ func (t *deployerTrait) Apply(e *Environment) error {
 			})
 		}
 
-	case v1.IntegrationPhaseRunning, v1.IntegrationPhaseWaitingForBindings:
+	case v1.IntegrationPhaseNone, v1.IntegrationPhaseInitialization,
+		v1.IntegrationPhaseWaitingForPlatform, v1.IntegrationPhaseWaitingForBindings,
+		v1.IntegrationPhaseDeploying, v1.IntegrationPhaseRunning:
 		// Register a post action that patches the resources generated by the traits
 		e.PostActions = append(e.PostActions, func(env *Environment) error {
 			for _, resource := range env.Resources.Items() {
-				key, err := client.ObjectKeyFromObject(resource)
+				target, err := patch.PositiveApplyPatch(resource)
 				if err != nil {
 					return err
 				}
-
-				object := resource.DeepCopyObject()
-				err = env.Client.Get(env.C, key, object)
+				err = env.Client.Patch(env.C, target, client.Apply, client.ForceOwnership, client.FieldOwner("camel-k-operator"))
 				if err != nil {
-					return err
-				}
-
-				// If both objects have "ObjectMeta" and "Spec" fields and they contain all the expected fields
-				// (plus optional others), then avoid patching.
-				if !patch.ObjectMetaEqualDeepDerivative(object, resource) ||
-					!patch.SpecEqualDeepDerivative(object, resource) {
-
-					p, err := patch.PositiveMergePatch(object, resource)
-					if err != nil {
-						return err
-					} else if len(p) == 0 {
-						// Avoid triggering a patch request for nothing
-						continue
-					}
-
-					err = env.Client.Patch(env.C, resource, client.RawPatch(types.MergePatchType, p))
-					if err != nil {
-						return errors.Wrap(err, "error during patch resource")
-					}
+					return errors.Wrapf(err, "error during apply resource: %v", resource)
 				}
 			}
 			return nil
diff --git a/pkg/util/kubernetes/replace.go b/pkg/util/kubernetes/replace.go
index 55c8dcf..a9f045d 100644
--- a/pkg/util/kubernetes/replace.go
+++ b/pkg/util/kubernetes/replace.go
@@ -20,27 +20,21 @@ package kubernetes
 import (
 	"context"
 
-	"github.com/apache/camel-k/pkg/client"
-	routev1 "github.com/openshift/api/route/v1"
 	"github.com/pkg/errors"
+
 	corev1 "k8s.io/api/core/v1"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
-	serving "knative.dev/serving/pkg/apis/serving/v1"
+
 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
-)
 
-// ReplaceResources allows to completely replace a list of resources on Kubernetes, taking care of immutable fields and resource versions
-func ReplaceResources(ctx context.Context, c client.Client, objects []runtime.Object) error {
-	for _, object := range objects {
-		err := ReplaceResource(ctx, c, object)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
+	serving "knative.dev/serving/pkg/apis/serving/v1"
+
+	routev1 "github.com/openshift/api/route/v1"
+
+	"github.com/apache/camel-k/pkg/client"
+)
 
 // ReplaceResource allows to completely replace a resource on Kubernetes, taking care of immutable fields and resource versions
 func ReplaceResource(ctx context.Context, c client.Client, res runtime.Object) error {
diff --git a/pkg/util/patch/patch.go b/pkg/util/patch/patch.go
index e2c54b1..09c4265 100644
--- a/pkg/util/patch/patch.go
+++ b/pkg/util/patch/patch.go
@@ -21,7 +21,8 @@ import (
 	"reflect"
 
 	jsonpatch "github.com/evanphx/json-patch"
-	"k8s.io/apimachinery/pkg/api/equality"
+
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/json"
@@ -63,6 +64,26 @@ func PositiveMergePatch(source runtime.Object, target runtime.Object) ([]byte, e
 	return json.Marshal(positivePatch)
 }
 
+func PositiveApplyPatch(source runtime.Object) (runtime.Object, error) {
+	sourceJSON, err := json.Marshal(source)
+	if err != nil {
+		return nil, err
+	}
+
+	var positivePatch map[string]interface{}
+	err = json.Unmarshal(sourceJSON, &positivePatch)
+	if err != nil {
+		return nil, err
+	}
+
+	// The following is a work-around to remove null fields from the apply patch,
+	// so that ownership is not taken for non-managed fields.
+	// See https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/2155-clientgo-apply
+	removeNilValues(reflect.ValueOf(positivePatch), reflect.Value{})
+
+	return &unstructured.Unstructured{Object: positivePatch}, nil
+}
+
 func removeNilValues(v reflect.Value, parent reflect.Value) {
 	for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
 		v = v.Elem()
@@ -90,41 +111,3 @@ func removeNilValues(v reflect.Value, parent reflect.Value) {
 		}
 	}
 }
-
-func ObjectMetaEqualDeepDerivative(object runtime.Object, expected runtime.Object) (res bool) {
-	defer func() {
-		if r := recover(); r != nil {
-			res = false
-		}
-	}()
-
-	if expected == nil {
-		return true
-	} else if object == nil {
-		return false
-	}
-
-	objectMeta := reflect.ValueOf(object).Elem().FieldByName("ObjectMeta").Interface()
-	expectedMeta := reflect.ValueOf(expected).Elem().FieldByName("ObjectMeta").Interface()
-
-	return equality.Semantic.DeepDerivative(expectedMeta, objectMeta)
-}
-
-func SpecEqualDeepDerivative(object runtime.Object, expected runtime.Object) (res bool) {
-	defer func() {
-		if r := recover(); r != nil {
-			res = false
-		}
-	}()
-
-	if expected == nil {
-		return true
-	} else if object == nil {
-		return false
-	}
-
-	objectSpec := reflect.ValueOf(object).Elem().FieldByName("Spec").Interface()
-	expectedSpec := reflect.ValueOf(expected).Elem().FieldByName("Spec").Interface()
-
-	return equality.Semantic.DeepDerivative(expectedSpec, objectSpec)
-}