// // Copyright (c) 2019-2023 Red Hat, Inc. // This program and the accompanying materials are made // available under the terms of the Eclipse Public License 2.0 // which is available at https://www.eclipse.org/legal/epl-2.0/ // // SPDX-License-Identifier: EPL-2.0 // // Contributors: // Red Hat, Inc. - initial API and implementation // package deploy import ( "context" "fmt" "reflect" ctrl "sigs.k8s.io/controller-runtime" "github.com/eclipse-che/che-operator/pkg/common/chetypes" "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) var ( syncLog = ctrl.Log.WithName("sync") ) // Sync syncs the blueprint to the cluster in a generic (as much as Go allows) manner. // Returns true if object is up-to-date otherwise returns false func Sync(deployContext *chetypes.DeployContext, blueprint client.Object, diffOpts ...cmp.Option) (bool, error) { cli := getClientForObject(blueprint.GetNamespace(), deployContext) return SyncWithClient(cli, deployContext, blueprint, diffOpts...) } func SyncWithClient(cli client.Client, deployContext *chetypes.DeployContext, blueprint client.Object, diffOpts ...cmp.Option) (bool, error) { runtimeObject, ok := blueprint.(runtime.Object) if !ok { return false, fmt.Errorf("object %T is not a runtime.Object. Cannot sync it", runtimeObject) } // we will compare this object later with blueprint // we can't use runtimeObject.DeepCopyObject() actual, err := deployContext.ClusterAPI.Scheme.New(runtimeObject.GetObjectKind().GroupVersionKind()) if err != nil { return false, err } key := types.NamespacedName{Name: blueprint.GetName(), Namespace: blueprint.GetNamespace()} exists, err := GetWithClient(cli, key, actual.(client.Object)) if err != nil { return false, err } // set GroupVersionKind (it might be empty) actual.GetObjectKind().SetGroupVersionKind(runtimeObject.GetObjectKind().GroupVersionKind()) if !exists { return CreateWithClient(cli, deployContext, blueprint, false) } return UpdateWithClient(cli, deployContext, actual.(client.Object), blueprint, diffOpts...) } // Gets object by key. // Returns true if object exists otherwise returns false. func Get(deployContext *chetypes.DeployContext, key client.ObjectKey, actual client.Object) (bool, error) { cli := getClientForObject(key.Namespace, deployContext) return GetWithClient(cli, key, actual) } // Gets namespaced scope object by name // Returns true if object exists otherwise returns false. func GetNamespacedObject(deployContext *chetypes.DeployContext, name string, actual client.Object) (bool, error) { client := deployContext.ClusterAPI.Client key := types.NamespacedName{Name: name, Namespace: deployContext.CheCluster.Namespace} return GetWithClient(client, key, actual) } // Gets cluster scope object by name // Returns true if object exists otherwise returns false func GetClusterObject(deployContext *chetypes.DeployContext, name string, actual client.Object) (bool, error) { client := deployContext.ClusterAPI.NonCachingClient key := types.NamespacedName{Name: name} return GetWithClient(client, key, actual) } // Creates object. // Return true if a new object is created, false if it has been already created or error occurred. func CreateIfNotExists(deployContext *chetypes.DeployContext, blueprint client.Object) (isCreated bool, err error) { cli := getClientForObject(blueprint.GetNamespace(), deployContext) return CreateIfNotExistsWithClient(cli, deployContext, blueprint) } func CreateIfNotExistsWithClient(cli client.Client, deployContext *chetypes.DeployContext, blueprint client.Object) (isCreated bool, err error) { key := types.NamespacedName{Name: blueprint.GetName(), Namespace: blueprint.GetNamespace()} actual := blueprint.DeepCopyObject().(client.Object) exists, err := GetWithClient(cli, key, actual) if exists { return false, nil } else if err != nil { return false, err } return CreateWithClient(cli, deployContext, blueprint, false) } // Creates object. // Return true if a new object is created otherwise returns false. func Create(deployContext *chetypes.DeployContext, blueprint client.Object) (bool, error) { client := getClientForObject(blueprint.GetNamespace(), deployContext) return CreateWithClient(client, deployContext, blueprint, false) } // Deletes object. // Returns true if object deleted or not found otherwise returns false. func Delete(deployContext *chetypes.DeployContext, key client.ObjectKey, objectMeta client.Object) (bool, error) { client := getClientForObject(key.Namespace, deployContext) return DeleteByKeyWithClient(client, key, objectMeta) } func DeleteNamespacedObject(deployContext *chetypes.DeployContext, name string, objectMeta client.Object) (bool, error) { client := deployContext.ClusterAPI.Client key := types.NamespacedName{Name: name, Namespace: deployContext.CheCluster.Namespace} return DeleteByKeyWithClient(client, key, objectMeta) } func DeleteClusterObject(deployContext *chetypes.DeployContext, name string, objectMeta client.Object) (bool, error) { client := deployContext.ClusterAPI.NonCachingClient key := types.NamespacedName{Name: name} return DeleteByKeyWithClient(client, key, objectMeta) } // Updates object. // Returns true if object is up to date otherwiser return false func UpdateWithClient(client client.Client, deployContext *chetypes.DeployContext, actual client.Object, blueprint client.Object, diffOpts ...cmp.Option) (bool, error) { actualMeta, ok := actual.(metav1.Object) if !ok { return false, fmt.Errorf("object %T is not a metav1.Object. Cannot sync it", actualMeta) } diff := cmp.Diff(actual, blueprint, diffOpts...) if len(diff) > 0 { // don't print difference if there are no diffOpts mainly to avoid huge output if len(diffOpts) != 0 { fmt.Printf("Difference:\n%s", diff) } if isUpdateUsingDeleteCreate(actual.GetObjectKind().GroupVersionKind().Kind) { done, err := doDeleteIgnoreIfNotFound(context.TODO(), client, actual) if !done { return false, err } return CreateWithClient(client, deployContext, blueprint, false) } else { err := setOwnerReferenceIfNeeded(deployContext, blueprint) if err != nil { return false, err } // to be able to update, we need to set the resource version of the object that we know of blueprint.(metav1.Object).SetResourceVersion(actualMeta.GetResourceVersion()) err = client.Update(context.TODO(), blueprint) syncLog.Info("Object updated", "namespace", actual.GetNamespace(), "kind", GetObjectType(actual), "name", actual.GetName()) return false, err } } return true, nil } func CreateWithClient(client client.Client, deployContext *chetypes.DeployContext, blueprint client.Object, returnTrueIfAlreadyExists bool) (bool, error) { err := setOwnerReferenceIfNeeded(deployContext, blueprint) if err != nil { return false, err } err = client.Create(context.TODO(), blueprint) if err == nil { syncLog.Info("Object created", "namespace", blueprint.GetNamespace(), "kind", GetObjectType(blueprint), "name", blueprint.GetName()) return true, nil } else if errors.IsAlreadyExists(err) { return returnTrueIfAlreadyExists, nil } else { return false, err } } func DeleteByKeyWithClient(cli client.Client, key client.ObjectKey, objectMeta client.Object) (bool, error) { runtimeObject, ok := objectMeta.(runtime.Object) if !ok { return false, fmt.Errorf("object %T is not a runtime.Object. Cannot sync it", runtimeObject) } actual := runtimeObject.DeepCopyObject().(client.Object) exists, err := GetWithClient(cli, key, actual) if !exists { return true, nil } else if err != nil { return false, err } return doDeleteIgnoreIfNotFound(context.TODO(), cli, actual) } func GetWithClient(client client.Client, key client.ObjectKey, object client.Object) (bool, error) { err := client.Get(context.TODO(), key, object) if err == nil { return true, nil } else if errors.IsNotFound(err) { return false, nil } else { return false, err } } func isUpdateUsingDeleteCreate(kind string) bool { return "Service" == kind || "Ingress" == kind || "Route" == kind || "Job" == kind || "Secret" == kind } func setOwnerReferenceIfNeeded(deployContext *chetypes.DeployContext, blueprint metav1.Object) error { if shouldSetOwnerReferenceForObject(deployContext, blueprint) { return controllerutil.SetControllerReference(deployContext.CheCluster, blueprint, deployContext.ClusterAPI.Scheme) } return nil } func shouldSetOwnerReferenceForObject(deployContext *chetypes.DeployContext, blueprint metav1.Object) bool { // empty workspace (cluster scope object) or object in another namespace return blueprint.GetNamespace() == deployContext.CheCluster.Namespace } func getClientForObject(objectNamespace string, deployContext *chetypes.DeployContext) client.Client { // empty namespace (cluster scope object) or object in another namespace if deployContext.CheCluster.Namespace == objectNamespace { return deployContext.ClusterAPI.Client } return deployContext.ClusterAPI.NonCachingClient } func GetObjectType(obj interface{}) string { objType := reflect.TypeOf(obj).String() if reflect.TypeOf(obj).Kind().String() == "ptr" { objType = objType[1:] } return objType } // DeleteIgnoreIfNotFound deletes object. // Returns nil if object deleted or not found otherwise returns error. func DeleteIgnoreIfNotFound(context context.Context, cli client.Client, key client.ObjectKey, blueprint client.Object) error { runtimeObj, ok := blueprint.(runtime.Object) if !ok { return fmt.Errorf("object %T is not a runtime.Object. Cannot sync it", runtimeObj) } actual := runtimeObj.DeepCopyObject().(client.Object) exists, err := doGet(context, cli, key, actual) if exists { _, err := doDeleteIgnoreIfNotFound(context, cli, actual) return err } return err } // doCreate creates object. // Returns true if object created otherwise returns false. // Throws error if object cannot be created or already exists otherwise returns nil. func doCreate(context context.Context, client client.Client, deployContext *chetypes.DeployContext, blueprint client.Object) (bool, error) { err := setOwnerReferenceIfNeeded(deployContext, blueprint) if err != nil { return false, err } err = client.Create(context, blueprint) if err == nil { syncLog.Info("Object created", "namespace", blueprint.GetNamespace(), "kind", GetObjectType(blueprint), "name", blueprint.GetName()) return true, nil } else { return false, err } } // doDeleteIgnoreIfNotFound deletes object. // Returns true if object deleted or not found otherwise returns false. // Returns error if object cannot be deleted otherwise returns nil. func doDeleteIgnoreIfNotFound(context context.Context, cli client.Client, actual client.Object) (bool, error) { err := cli.Delete(context, actual) if err == nil { if errors.IsNotFound(err) { syncLog.Info("Object not found", "namespace", actual.GetNamespace(), "kind", GetObjectType(actual), "name", actual.GetName()) } else { syncLog.Info("Object deleted", "namespace", actual.GetNamespace(), "kind", GetObjectType(actual), "name", actual.GetName()) } return true, nil } else { return false, err } } // doGet gets object. // Returns true if object exists otherwise returns false. // Returns error if object cannot be retrieved otherwise returns nil. func doGet(context context.Context, cli client.Client, key client.ObjectKey, object client.Object) (bool, error) { err := cli.Get(context, key, object) if err == nil { return true, nil } else if errors.IsNotFound(err) { return false, nil } else { return false, err } }