From 42f5b43072d1fa135fff57833f1fecb8eb39550d Mon Sep 17 00:00:00 2001 From: SataQiu Date: Tue, 12 Mar 2019 13:39:22 +0800 Subject: [PATCH] fix main.go and add controller logic --- Gopkg.lock | 35 +- cmd/manager/main.go | 4 +- pkg/controller/tomcat/internal/sync/common.go | 5 + .../tomcat/internal/sync/deployment.go | 66 ++++ .../tomcat/internal/sync/service.go | 51 +++ pkg/controller/tomcat/tomcat_controller.go | 108 ++---- pkg/scheme/tomcat/template.go | 5 +- vendor/github.com/go-test/deep/LICENSE | 21 ++ vendor/github.com/go-test/deep/deep.go | 352 ++++++++++++++++++ vendor/github.com/iancoleman/strcase/LICENSE | 22 ++ vendor/github.com/iancoleman/strcase/camel.go | 75 ++++ .../github.com/iancoleman/strcase/numbers.go | 38 ++ vendor/github.com/iancoleman/strcase/snake.go | 94 +++++ .../mergo/transformers/transformers.go | 168 +++++++++ .../kube-operator-helper/syncer/external.go | 65 ++++ .../kube-operator-helper/syncer/interface.go | 51 +++ .../kube-operator-helper/syncer/object.go | 132 +++++++ .../kube-operator-helper/syncer/syncer.go | 80 ++++ 18 files changed, 1297 insertions(+), 75 deletions(-) create mode 100644 pkg/controller/tomcat/internal/sync/common.go create mode 100644 pkg/controller/tomcat/internal/sync/deployment.go create mode 100644 pkg/controller/tomcat/internal/sync/service.go create mode 100644 vendor/github.com/go-test/deep/LICENSE create mode 100644 vendor/github.com/go-test/deep/deep.go create mode 100644 vendor/github.com/iancoleman/strcase/LICENSE create mode 100644 vendor/github.com/iancoleman/strcase/camel.go create mode 100644 vendor/github.com/iancoleman/strcase/numbers.go create mode 100644 vendor/github.com/iancoleman/strcase/snake.go create mode 100644 vendor/github.com/kube-incubator/kube-operator-helper/mergo/transformers/transformers.go create mode 100644 vendor/github.com/kube-incubator/kube-operator-helper/syncer/external.go create mode 100644 vendor/github.com/kube-incubator/kube-operator-helper/syncer/interface.go create mode 100644 vendor/github.com/kube-incubator/kube-operator-helper/syncer/object.go create mode 100644 vendor/github.com/kube-incubator/kube-operator-helper/syncer/syncer.go diff --git a/Gopkg.lock b/Gopkg.lock index efa4712..c3e62a2 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -129,6 +129,14 @@ revision = "1d29f06aebd59ccdf11ae04aa0334ded96e2d909" version = "v0.18.0" +[[projects]] + digest = "1:3c04690dd1d3fad9a36c492c32863fad91bcf822fd95d5c4cae97be1e9abc361" + name = "github.com/go-test/deep" + packages = ["."] + pruneopts = "NT" + revision = "6592d9cc0a499ad2d5f574fde80a2b5c5cc3b4f5" + version = "v1.0.1" + [[projects]] digest = "1:9059915429f7f3a5f18cfa6b7cab9a28721d7ac6db4079a62044aa229eb7f2a8" name = "github.com/gobuffalo/envy" @@ -236,6 +244,14 @@ revision = "7087cb70de9f7a8bc0a10c375cb0d2280a8edf9c" version = "v0.5.1" +[[projects]] + branch = "master" + digest = "1:c13aea0b77243f1fcd0253583c90e180010a3a5bbdf43f61a3daddc48d88b0b2" + name = "github.com/iancoleman/strcase" + packages = ["."] + pruneopts = "NT" + revision = "3605ed457bf7f8caa1371b4fafadadc026673479" + [[projects]] digest = "1:aaa38889f11896ee3644d77e17dc7764cc47f5f3d3b488268df2af2b52541c5f" name = "github.com/imdario/mergo" @@ -260,6 +276,17 @@ revision = "1624edc4454b8682399def8740d46db5e4362ba4" version = "v1.1.5" +[[projects]] + branch = "master" + digest = "1:d48a0280c7ac9ef4cc4ad3d195f487ab36eb3c17363a86ee15d40058c3075650" + name = "github.com/kube-incubator/kube-operator-helper" + packages = [ + "mergo/transformers", + "syncer", + ] + pruneopts = "NT" + revision = "a5c85660322da9745bf83b2962c89c313771af3f" + [[projects]] branch = "master" digest = "1:4925ec3736ef6c299cfcf61597782e3d66ec13114f7476019d04c742a7be55d0" @@ -908,20 +935,25 @@ analyzer-version = 1 input-imports = [ "github.com/go-openapi/spec", + "github.com/imdario/mergo", + "github.com/kube-incubator/kube-operator-helper/mergo/transformers", + "github.com/kube-incubator/kube-operator-helper/syncer", "github.com/operator-framework/operator-sdk/pkg/k8sutil", "github.com/operator-framework/operator-sdk/pkg/leader", "github.com/operator-framework/operator-sdk/pkg/log/zap", "github.com/operator-framework/operator-sdk/pkg/metrics", "github.com/operator-framework/operator-sdk/version", "github.com/spf13/pflag", + "k8s.io/api/apps/v1", "k8s.io/api/core/v1", "k8s.io/apimachinery/pkg/api/errors", "k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/apimachinery/pkg/labels", "k8s.io/apimachinery/pkg/runtime", "k8s.io/apimachinery/pkg/runtime/schema", - "k8s.io/apimachinery/pkg/types", + "k8s.io/apimachinery/pkg/util/intstr", "k8s.io/client-go/plugin/pkg/client/auth/gcp", + "k8s.io/client-go/tools/record", "k8s.io/code-generator/cmd/client-gen", "k8s.io/code-generator/cmd/conversion-gen", "k8s.io/code-generator/cmd/deepcopy-gen", @@ -934,7 +966,6 @@ "sigs.k8s.io/controller-runtime/pkg/client", "sigs.k8s.io/controller-runtime/pkg/client/config", "sigs.k8s.io/controller-runtime/pkg/controller", - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil", "sigs.k8s.io/controller-runtime/pkg/handler", "sigs.k8s.io/controller-runtime/pkg/manager", "sigs.k8s.io/controller-runtime/pkg/reconcile", diff --git a/cmd/manager/main.go b/cmd/manager/main.go index b1755b6..3fe907f 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -7,8 +7,8 @@ import ( "os" "runtime" - "github.com/kube-incubator/tomcat-operator/tomcat-operator/pkg/apis" - "github.com/kube-incubator/tomcat-operator/tomcat-operator/pkg/controller" + "github.com/kube-incubator/tomcat-operator/pkg/apis" + "github.com/kube-incubator/tomcat-operator/pkg/controller" "github.com/operator-framework/operator-sdk/pkg/k8sutil" "github.com/operator-framework/operator-sdk/pkg/leader" diff --git a/pkg/controller/tomcat/internal/sync/common.go b/pkg/controller/tomcat/internal/sync/common.go new file mode 100644 index 0000000..dfe8c6b --- /dev/null +++ b/pkg/controller/tomcat/internal/sync/common.go @@ -0,0 +1,5 @@ +package sync + +var controllerLabels = map[string]string{ + "app.kubernetes.io/managed-by": "tomcat-operator.apache.org", +} diff --git a/pkg/controller/tomcat/internal/sync/deployment.go b/pkg/controller/tomcat/internal/sync/deployment.go new file mode 100644 index 0000000..1cceb55 --- /dev/null +++ b/pkg/controller/tomcat/internal/sync/deployment.go @@ -0,0 +1,66 @@ +package sync + +import ( + "fmt" + "reflect" + + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/imdario/mergo" + "github.com/kube-incubator/kube-operator-helper/mergo/transformers" + "github.com/kube-incubator/kube-operator-helper/syncer" + "github.com/kube-incubator/tomcat-operator/pkg/scheme/tomcat" +) + +var ( + oneReplica int32 = 1 +) + +// NewDeploymentSyncer returns a new sync.Interface for reconciling tomcat Deployment +func NewDeploymentSyncer(tc *tomcat.Tomcat, c client.Client, scheme *runtime.Scheme) syncer.Interface { + objLabels := tc.ComponentLabels(tomcat.TomcatDeployment) + + obj := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.ComponentName(tomcat.TomcatDeployment), + Namespace: tc.Namespace, + }, + } + + return syncer.NewObjectSyncer("Deployment", tc.Unwrap(), obj, c, scheme, func(existing runtime.Object) error { + out := existing.(*appsv1.Deployment) + out.Labels = labels.Merge(labels.Merge(out.Labels, objLabels), controllerLabels) + + template := tc.TomcatServerPodTemplateSpec() + + out.Spec.Template.ObjectMeta = template.ObjectMeta + + selector := metav1.SetAsLabelSelector(tc.TomcatServerPodLabels()) + if !reflect.DeepEqual(selector, out.Spec.Selector) { + if out.ObjectMeta.CreationTimestamp.IsZero() { + out.Spec.Selector = selector + } else { + return fmt.Errorf("deployment selector is immutable") + } + } + + err := mergo.Merge(&out.Spec.Template.Spec, template.Spec, mergo.WithTransformers(transformers.PodSpec)) + if err != nil { + return err + } + + if tc.Spec.Replicas != nil { + out.Spec.Replicas = tc.Spec.Replicas + } + + if out.Spec.Replicas == nil { + out.Spec.Replicas = &oneReplica + } + + return nil + }) +} diff --git a/pkg/controller/tomcat/internal/sync/service.go b/pkg/controller/tomcat/internal/sync/service.go new file mode 100644 index 0000000..4306a27 --- /dev/null +++ b/pkg/controller/tomcat/internal/sync/service.go @@ -0,0 +1,51 @@ +package sync + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kube-incubator/kube-operator-helper/syncer" + "github.com/kube-incubator/tomcat-operator/pkg/scheme/tomcat" +) + +// NewServiceSyncer returns a new sync.Interface for reconciling Tomcat Service +func NewServiceSyncer(tc *tomcat.Tomcat, c client.Client, scheme *runtime.Scheme) syncer.Interface { + objLabels := tc.ComponentLabels(tomcat.TomcatDeployment) + + obj := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.Name, + Namespace: tc.Namespace, + }, + } + + return syncer.NewObjectSyncer("Service", tc.Unwrap(), obj, c, scheme, func(existing runtime.Object) error { + out := existing.(*corev1.Service) + out.Labels = labels.Merge(labels.Merge(out.Labels, objLabels), controllerLabels) + + selector := tc.TomcatServerPodLabels() + if !labels.Equals(selector, out.Spec.Selector) { + if out.ObjectMeta.CreationTimestamp.IsZero() { + out.Spec.Selector = selector + } else { + return fmt.Errorf("service selector is immutable") + } + } + + if len(out.Spec.Ports) != 1 { + out.Spec.Ports = make([]corev1.ServicePort, 1) + } + + out.Spec.Ports[0].Name = "http" + out.Spec.Ports[0].Port = *tc.Spec.ServicePort + out.Spec.Ports[0].TargetPort = intstr.FromInt(tomcat.TomcatHTTPPort) + + return nil + }) +} diff --git a/pkg/controller/tomcat/tomcat_controller.go b/pkg/controller/tomcat/tomcat_controller.go index 1ec903a..4dccd8a 100644 --- a/pkg/controller/tomcat/tomcat_controller.go +++ b/pkg/controller/tomcat/tomcat_controller.go @@ -3,30 +3,27 @@ package tomcat import ( "context" - tomcatv1alpha1 "github.com/kube-incubator/tomcat-operator/pkg/apis/tomcat/v1alpha1" - + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/kube-incubator/kube-operator-helper/syncer" + tomcatv1alpha1 "github.com/kube-incubator/tomcat-operator/pkg/apis/tomcat/v1alpha1" + "github.com/kube-incubator/tomcat-operator/pkg/controller/tomcat/internal/sync" + "github.com/kube-incubator/tomcat-operator/pkg/scheme/tomcat" ) var log = logf.Log.WithName("controller_tomcat") -/** -* USER ACTION REQUIRED: This is a scaffold file intended for the user to modify with their own Controller -* business logic. Delete these comments after modifying this file.* - */ - // Add creates a new Tomcat Controller and adds it to the Manager. The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(mgr manager.Manager) error { @@ -35,7 +32,7 @@ func Add(mgr manager.Manager) error { // newReconciler returns a new reconcile.Reconciler func newReconciler(mgr manager.Manager) reconcile.Reconciler { - return &ReconcileTomcat{client: mgr.GetClient(), scheme: mgr.GetScheme()} + return &ReconcileTomcat{client: mgr.GetClient(), scheme: mgr.GetScheme(), recorder: mgr.GetRecorder("tomcat-controller")} } // add adds a new Controller to mgr with r as the reconcile.Reconciler @@ -52,14 +49,20 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return err } - // TODO(user): Modify this to be the types you create that are owned by the primary resource - // Watch for changes to secondary resource Pods and requeue the owner Tomcat - err = c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForOwner{ - IsController: true, - OwnerType: &tomcatv1alpha1.Tomcat{}, - }) - if err != nil { - return err + // Watch for changes to the resources that owned by the primary resource + subresources := []runtime.Object{ + &appsv1.Deployment{}, + &corev1.Service{}, + } + + for _, subresource := range subresources { + err = c.Watch(&source.Kind{Type: subresource}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &tomcatv1alpha1.Tomcat{}, + }) + if err != nil { + return err + } } return nil @@ -71,14 +74,13 @@ var _ reconcile.Reconciler = &ReconcileTomcat{} type ReconcileTomcat struct { // This client, initialized using mgr.Client() above, is a split client // that reads objects from the cache and writes to the apiserver - client client.Client - scheme *runtime.Scheme + client client.Client + scheme *runtime.Scheme + recorder record.EventRecorder } // Reconcile reads that state of the cluster for a Tomcat object and makes changes based on the state read // and what is in the Tomcat.Spec -// TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates -// a Pod as an example // Note: // The Controller will requeue the Request to be processed again if the returned error is non-nil or // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. @@ -87,8 +89,8 @@ func (r *ReconcileTomcat) Reconcile(request reconcile.Request) (reconcile.Result reqLogger.Info("Reconciling Tomcat") // Fetch the Tomcat instance - instance := &tomcatv1alpha1.Tomcat{} - err := r.client.Get(context.TODO(), request.NamespacedName, instance) + tomcat := tomcat.New(&tomcatv1alpha1.Tomcat{}) + err := r.client.Get(context.TODO(), request.NamespacedName, tomcat.Unwrap()) if err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. @@ -100,54 +102,22 @@ func (r *ReconcileTomcat) Reconcile(request reconcile.Request) (reconcile.Result return reconcile.Result{}, err } - // Define a new Pod object - pod := newPodForCR(instance) + r.scheme.Default(tomcat.Unwrap()) + tomcat.SetDefaults() - // Set Tomcat instance as the owner and controller - if err := controllerutil.SetControllerReference(instance, pod, r.scheme); err != nil { - return reconcile.Result{}, err - } - - // Check if this Pod already exists - found := &corev1.Pod{} - err = r.client.Get(context.TODO(), types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, found) - if err != nil && errors.IsNotFound(err) { - reqLogger.Info("Creating a new Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name) - err = r.client.Create(context.TODO(), pod) - if err != nil { - return reconcile.Result{}, err - } - - // Pod created successfully - don't requeue - return reconcile.Result{}, nil - } else if err != nil { - return reconcile.Result{}, err + syncers := []syncer.Interface{ + sync.NewDeploymentSyncer(tomcat, r.client, r.scheme), + sync.NewServiceSyncer(tomcat, r.client, r.scheme), } - // Pod already exists - don't requeue - reqLogger.Info("Skip reconcile: Pod already exists", "Pod.Namespace", found.Namespace, "Pod.Name", found.Name) - return reconcile.Result{}, nil + return reconcile.Result{}, r.sync(syncers) } -// newPodForCR returns a busybox pod with the same name/namespace as the cr -func newPodForCR(cr *tomcatv1alpha1.Tomcat) *corev1.Pod { - labels := map[string]string{ - "app": cr.Name, - } - return &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: cr.Name + "-pod", - Namespace: cr.Namespace, - Labels: labels, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "busybox", - Image: "busybox", - Command: []string{"sleep", "3600"}, - }, - }, - }, +func (r *ReconcileTomcat) sync(syncers []syncer.Interface) error { + for _, s := range syncers { + if err := syncer.Sync(context.TODO(), s, r.recorder); err != nil { + return err + } } + return nil } diff --git a/pkg/scheme/tomcat/template.go b/pkg/scheme/tomcat/template.go index 581def4..e69d5c0 100644 --- a/pkg/scheme/tomcat/template.go +++ b/pkg/scheme/tomcat/template.go @@ -5,7 +5,8 @@ import ( ) const ( - tomcatHTTPPort = 8080 + // TomcatHTTPPort is the default open port of tomcat container + TomcatHTTPPort = 8080 ) // TomcatServerPodTemplateSpec generates a pod template spec suitable for use in Tomcat deployment @@ -20,7 +21,7 @@ func (tomcat *Tomcat) TomcatServerPodTemplateSpec() (out corev1.PodTemplateSpec) Ports: []corev1.ContainerPort{ { Name: "http", - ContainerPort: int32(tomcatHTTPPort), + ContainerPort: int32(TomcatHTTPPort), }, }, }, diff --git a/vendor/github.com/go-test/deep/LICENSE b/vendor/github.com/go-test/deep/LICENSE new file mode 100644 index 0000000..228ef16 --- /dev/null +++ b/vendor/github.com/go-test/deep/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright 2015-2017 Daniel Nichter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/go-test/deep/deep.go b/vendor/github.com/go-test/deep/deep.go new file mode 100644 index 0000000..4ea14cb --- /dev/null +++ b/vendor/github.com/go-test/deep/deep.go @@ -0,0 +1,352 @@ +// Package deep provides function deep.Equal which is like reflect.DeepEqual but +// returns a list of differences. This is helpful when comparing complex types +// like structures and maps. +package deep + +import ( + "errors" + "fmt" + "log" + "reflect" + "strings" +) + +var ( + // FloatPrecision is the number of decimal places to round float values + // to when comparing. + FloatPrecision = 10 + + // MaxDiff specifies the maximum number of differences to return. + MaxDiff = 10 + + // MaxDepth specifies the maximum levels of a struct to recurse into. + MaxDepth = 10 + + // LogErrors causes errors to be logged to STDERR when true. + LogErrors = false + + // CompareUnexportedFields causes unexported struct fields, like s in + // T{s int}, to be comparsed when true. + CompareUnexportedFields = false +) + +var ( + // ErrMaxRecursion is logged when MaxDepth is reached. + ErrMaxRecursion = errors.New("recursed to MaxDepth") + + // ErrTypeMismatch is logged when Equal passed two different types of values. + ErrTypeMismatch = errors.New("variables are different reflect.Type") + + // ErrNotHandled is logged when a primitive Go kind is not handled. + ErrNotHandled = errors.New("cannot compare the reflect.Kind") +) + +type cmp struct { + diff []string + buff []string + floatFormat string +} + +var errorType = reflect.TypeOf((*error)(nil)).Elem() + +// Equal compares variables a and b, recursing into their structure up to +// MaxDepth levels deep, and returns a list of differences, or nil if there are +// none. Some differences may not be found if an error is also returned. +// +// If a type has an Equal method, like time.Equal, it is called to check for +// equality. +func Equal(a, b interface{}) []string { + aVal := reflect.ValueOf(a) + bVal := reflect.ValueOf(b) + c := &cmp{ + diff: []string{}, + buff: []string{}, + floatFormat: fmt.Sprintf("%%.%df", FloatPrecision), + } + if a == nil && b == nil { + return nil + } else if a == nil && b != nil { + c.saveDiff(b, "") + } else if a != nil && b == nil { + c.saveDiff(a, "") + } + if len(c.diff) > 0 { + return c.diff + } + + c.equals(aVal, bVal, 0) + if len(c.diff) > 0 { + return c.diff // diffs + } + return nil // no diffs +} + +func (c *cmp) equals(a, b reflect.Value, level int) { + if level > MaxDepth { + logError(ErrMaxRecursion) + return + } + + // Check if one value is nil, e.g. T{x: *X} and T.x is nil + if !a.IsValid() || !b.IsValid() { + if a.IsValid() && !b.IsValid() { + c.saveDiff(a.Type(), "") + } else if !a.IsValid() && b.IsValid() { + c.saveDiff("", b.Type()) + } + return + } + + // If differenet types, they can't be equal + aType := a.Type() + bType := b.Type() + if aType != bType { + c.saveDiff(aType, bType) + logError(ErrTypeMismatch) + return + } + + // Primitive https://golang.org/pkg/reflect/#Kind + aKind := a.Kind() + bKind := b.Kind() + + // If both types implement the error interface, compare the error strings. + // This must be done before dereferencing because the interface is on a + // pointer receiver. + if aType.Implements(errorType) && bType.Implements(errorType) { + if a.Elem().IsValid() && b.Elem().IsValid() { // both err != nil + aString := a.MethodByName("Error").Call(nil)[0].String() + bString := b.MethodByName("Error").Call(nil)[0].String() + if aString != bString { + c.saveDiff(aString, bString) + } + return + } + } + + // Dereference pointers and interface{} + if aElem, bElem := (aKind == reflect.Ptr || aKind == reflect.Interface), + (bKind == reflect.Ptr || bKind == reflect.Interface); aElem || bElem { + + if aElem { + a = a.Elem() + } + + if bElem { + b = b.Elem() + } + + c.equals(a, b, level+1) + return + } + + // Types with an Equal(), like time.Time. + eqFunc := a.MethodByName("Equal") + if eqFunc.IsValid() { + retVals := eqFunc.Call([]reflect.Value{b}) + if !retVals[0].Bool() { + c.saveDiff(a, b) + } + return + } + + switch aKind { + + ///////////////////////////////////////////////////////////////////// + // Iterable kinds + ///////////////////////////////////////////////////////////////////// + + case reflect.Struct: + /* + The variables are structs like: + type T struct { + FirstName string + LastName string + } + Type = .T, Kind = reflect.Struct + + Iterate through the fields (FirstName, LastName), recurse into their values. + */ + for i := 0; i < a.NumField(); i++ { + if aType.Field(i).PkgPath != "" && !CompareUnexportedFields { + continue // skip unexported field, e.g. s in type T struct {s string} + } + + c.push(aType.Field(i).Name) // push field name to buff + + // Get the Value for each field, e.g. FirstName has Type = string, + // Kind = reflect.String. + af := a.Field(i) + bf := b.Field(i) + + // Recurse to compare the field values + c.equals(af, bf, level+1) + + c.pop() // pop field name from buff + + if len(c.diff) >= MaxDiff { + break + } + } + case reflect.Map: + /* + The variables are maps like: + map[string]int{ + "foo": 1, + "bar": 2, + } + Type = map[string]int, Kind = reflect.Map + + Or: + type T map[string]int{} + Type = .T, Kind = reflect.Map + + Iterate through the map keys (foo, bar), recurse into their values. + */ + + if a.IsNil() || b.IsNil() { + if a.IsNil() && !b.IsNil() { + c.saveDiff("", b) + } else if !a.IsNil() && b.IsNil() { + c.saveDiff(a, "") + } + return + } + + if a.Pointer() == b.Pointer() { + return + } + + for _, key := range a.MapKeys() { + c.push(fmt.Sprintf("map[%s]", key)) + + aVal := a.MapIndex(key) + bVal := b.MapIndex(key) + if bVal.IsValid() { + c.equals(aVal, bVal, level+1) + } else { + c.saveDiff(aVal, "") + } + + c.pop() + + if len(c.diff) >= MaxDiff { + return + } + } + + for _, key := range b.MapKeys() { + if aVal := a.MapIndex(key); aVal.IsValid() { + continue + } + + c.push(fmt.Sprintf("map[%s]", key)) + c.saveDiff("", b.MapIndex(key)) + c.pop() + if len(c.diff) >= MaxDiff { + return + } + } + case reflect.Array: + n := a.Len() + for i := 0; i < n; i++ { + c.push(fmt.Sprintf("array[%d]", i)) + c.equals(a.Index(i), b.Index(i), level+1) + c.pop() + if len(c.diff) >= MaxDiff { + break + } + } + case reflect.Slice: + if a.IsNil() || b.IsNil() { + if a.IsNil() && !b.IsNil() { + c.saveDiff("", b) + } else if !a.IsNil() && b.IsNil() { + c.saveDiff(a, "") + } + return + } + + if a.Pointer() == b.Pointer() { + return + } + + aLen := a.Len() + bLen := b.Len() + n := aLen + if bLen > aLen { + n = bLen + } + for i := 0; i < n; i++ { + c.push(fmt.Sprintf("slice[%d]", i)) + if i < aLen && i < bLen { + c.equals(a.Index(i), b.Index(i), level+1) + } else if i < aLen { + c.saveDiff(a.Index(i), "") + } else { + c.saveDiff("", b.Index(i)) + } + c.pop() + if len(c.diff) >= MaxDiff { + break + } + } + + ///////////////////////////////////////////////////////////////////// + // Primitive kinds + ///////////////////////////////////////////////////////////////////// + + case reflect.Float32, reflect.Float64: + // Avoid 0.04147685731961082 != 0.041476857319611 + // 6 decimal places is close enough + aval := fmt.Sprintf(c.floatFormat, a.Float()) + bval := fmt.Sprintf(c.floatFormat, b.Float()) + if aval != bval { + c.saveDiff(a.Float(), b.Float()) + } + case reflect.Bool: + if a.Bool() != b.Bool() { + c.saveDiff(a.Bool(), b.Bool()) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if a.Int() != b.Int() { + c.saveDiff(a.Int(), b.Int()) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if a.Uint() != b.Uint() { + c.saveDiff(a.Uint(), b.Uint()) + } + case reflect.String: + if a.String() != b.String() { + c.saveDiff(a.String(), b.String()) + } + + default: + logError(ErrNotHandled) + } +} + +func (c *cmp) push(name string) { + c.buff = append(c.buff, name) +} + +func (c *cmp) pop() { + if len(c.buff) > 0 { + c.buff = c.buff[0 : len(c.buff)-1] + } +} + +func (c *cmp) saveDiff(aval, bval interface{}) { + if len(c.buff) > 0 { + varName := strings.Join(c.buff, ".") + c.diff = append(c.diff, fmt.Sprintf("%s: %v != %v", varName, aval, bval)) + } else { + c.diff = append(c.diff, fmt.Sprintf("%v != %v", aval, bval)) + } +} + +func logError(err error) { + if LogErrors { + log.Println(err) + } +} diff --git a/vendor/github.com/iancoleman/strcase/LICENSE b/vendor/github.com/iancoleman/strcase/LICENSE new file mode 100644 index 0000000..3e87ff7 --- /dev/null +++ b/vendor/github.com/iancoleman/strcase/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Ian Coleman +Copyright (c) 2018 Ma_124, + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, Subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or Substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/iancoleman/strcase/camel.go b/vendor/github.com/iancoleman/strcase/camel.go new file mode 100644 index 0000000..7c2e2b7 --- /dev/null +++ b/vendor/github.com/iancoleman/strcase/camel.go @@ -0,0 +1,75 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 Ian Coleman + * Copyright (c) 2018 Ma_124, + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, Subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or Substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package strcase + +import ( + "strings" +) + +// Converts a string to CamelCase +func toCamelInitCase(s string, initCase bool) string { + s = addWordBoundariesToNumbers(s) + s = strings.Trim(s, " ") + n := "" + capNext := initCase + for _, v := range s { + if v >= 'A' && v <= 'Z' { + n += string(v) + } + if v >= '0' && v <= '9' { + n += string(v) + } + if v >= 'a' && v <= 'z' { + if capNext { + n += strings.ToUpper(string(v)) + } else { + n += string(v) + } + } + if v == '_' || v == ' ' || v == '-' { + capNext = true + } else { + capNext = false + } + } + return n +} + +// Converts a string to CamelCase +func ToCamel(s string) string { + return toCamelInitCase(s, true) +} + +// Converts a string to lowerCamelCase +func ToLowerCamel(s string) string { + if s == "" { + return s + } + if r := rune(s[0]); r >= 'A' && r <= 'Z' { + s = strings.ToLower(string(r)) + s[1:] + } + return toCamelInitCase(s, false) +} diff --git a/vendor/github.com/iancoleman/strcase/numbers.go b/vendor/github.com/iancoleman/strcase/numbers.go new file mode 100644 index 0000000..fdf07cb --- /dev/null +++ b/vendor/github.com/iancoleman/strcase/numbers.go @@ -0,0 +1,38 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 Ian Coleman + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, Subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or Substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package strcase + +import ( + "regexp" +) + +var numberSequence = regexp.MustCompile(`([a-zA-Z])(\d+)([a-zA-Z]?)`) +var numberReplacement = []byte(`$1 $2 $3`) + +func addWordBoundariesToNumbers(s string) string { + b := []byte(s) + b = numberSequence.ReplaceAll(b, numberReplacement) + return string(b) +} diff --git a/vendor/github.com/iancoleman/strcase/snake.go b/vendor/github.com/iancoleman/strcase/snake.go new file mode 100644 index 0000000..1d2f520 --- /dev/null +++ b/vendor/github.com/iancoleman/strcase/snake.go @@ -0,0 +1,94 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 Ian Coleman + * Copyright (c) 2018 Ma_124, + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, Subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or Substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// Package strcase converts strings to snake_case or CamelCase +package strcase + +import ( + "strings" +) + +// Converts a string to snake_case +func ToSnake(s string) string { + return ToDelimited(s, '_') +} + +// Converts a string to SCREAMING_SNAKE_CASE +func ToScreamingSnake(s string) string { + return ToScreamingDelimited(s, '_', true) +} + +// Converts a string to kebab-case +func ToKebab(s string) string { + return ToDelimited(s, '-') +} + +// Converts a string to SCREAMING-KEBAB-CASE +func ToScreamingKebab(s string) string { + return ToScreamingDelimited(s, '-', true) +} + +// Converts a string to delimited.snake.case (in this case `del = '.'`) +func ToDelimited(s string, del uint8) string { + return ToScreamingDelimited(s, del, false) +} + +// Converts a string to SCREAMING.DELIMITED.SNAKE.CASE (in this case `del = '.'; screaming = true`) or delimited.snake.case (in this case `del = '.'; screaming = false`) +func ToScreamingDelimited(s string, del uint8, screaming bool) string { + s = addWordBoundariesToNumbers(s) + s = strings.Trim(s, " ") + n := "" + for i, v := range s { + // treat acronyms as words, eg for JSONData -> JSON is a whole word + nextCaseIsChanged := false + if i+1 < len(s) { + next := s[i+1] + if (v >= 'A' && v <= 'Z' && next >= 'a' && next <= 'z') || (v >= 'a' && v <= 'z' && next >= 'A' && next <= 'Z') { + nextCaseIsChanged = true + } + } + + if i > 0 && n[len(n)-1] != del && nextCaseIsChanged { + // add underscore if next letter case type is changed + if v >= 'A' && v <= 'Z' { + n += string(del) + string(v) + } else if v >= 'a' && v <= 'z' { + n += string(v) + string(del) + } + } else if v == ' ' || v == '_' || v == '-' { + // replace spaces/underscores with delimiters + n += string(del) + } else { + n = n + string(v) + } + } + + if screaming { + n = strings.ToUpper(n) + } else { + n = strings.ToLower(n) + } + return n +} diff --git a/vendor/github.com/kube-incubator/kube-operator-helper/mergo/transformers/transformers.go b/vendor/github.com/kube-incubator/kube-operator-helper/mergo/transformers/transformers.go new file mode 100644 index 0000000..69dc1cb --- /dev/null +++ b/vendor/github.com/kube-incubator/kube-operator-helper/mergo/transformers/transformers.go @@ -0,0 +1,168 @@ +/* +Copyright 2018 Pressinfra SRL. + +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. +*/ + +// Package transformers provide mergo transformers for Kubernetes objects +package transformers + +import ( + "fmt" + "reflect" + + "github.com/imdario/mergo" + + corev1 "k8s.io/api/core/v1" +) + +// TransformerMap is a mergo.Transformers implementation +type TransformerMap map[reflect.Type]func(dst, src reflect.Value) error + +// PodSpec mergo transformers for corev1.PodSpec +var PodSpec TransformerMap + +func init() { + PodSpec = TransformerMap{ + reflect.TypeOf([]corev1.Container{}): PodSpec.MergeListByKey("Name", mergo.WithOverride), + reflect.TypeOf([]corev1.ContainerPort{}): PodSpec.MergeListByKey("ContainerPort", mergo.WithOverride), + reflect.TypeOf([]corev1.EnvVar{}): PodSpec.MergeListByKey("Name", mergo.WithOverride), + reflect.TypeOf(corev1.EnvVar{}): PodSpec.OverrideFields("Value", "ValueFrom"), + reflect.TypeOf(corev1.VolumeSource{}): PodSpec.NilOtherFields(), + reflect.TypeOf([]corev1.Toleration{}): PodSpec.MergeListByKey("Key", mergo.WithOverride), + reflect.TypeOf([]corev1.Volume{}): PodSpec.MergeListByKey("Name", mergo.WithOverride), + reflect.TypeOf([]corev1.LocalObjectReference{}): PodSpec.MergeListByKey("Name", mergo.WithOverride), + reflect.TypeOf([]corev1.HostAlias{}): PodSpec.MergeListByKey("IP", mergo.WithOverride), + reflect.TypeOf([]corev1.VolumeMount{}): PodSpec.MergeListByKey("MountPath", mergo.WithOverride), + } +} + +// Transformer implements mergo.Tansformers interface for TransformenrMap +func (s TransformerMap) Transformer(t reflect.Type) func(dst, src reflect.Value) error { + if fn, ok := s[t]; ok { + return fn + } + return nil +} + +func (s *TransformerMap) mergeByKey(key string, dst, elem reflect.Value, opts ...func(*mergo.Config)) error { + elemKey := elem.FieldByName(key) + for i := 0; i < dst.Len(); i++ { + dstKey := dst.Index(i).FieldByName(key) + if elemKey.Kind() != dstKey.Kind() { + return fmt.Errorf("cannot merge when key type differs") + } + eq := eq(key, elem, dst.Index(i)) + if eq { + opts = append(opts, mergo.WithTransformers(s)) + return mergo.Merge(dst.Index(i).Addr().Interface(), elem.Interface(), opts...) + } + } + dst.Set(reflect.Append(dst, elem)) + return nil +} + +func eq(key string, a, b reflect.Value) bool { + aKey := a.FieldByName(key) + bKey := b.FieldByName(key) + if aKey.Kind() != bKey.Kind() { + return false + } + eq := false + switch aKey.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + eq = aKey.Int() == bKey.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + eq = aKey.Uint() == bKey.Uint() + case reflect.String: + eq = aKey.String() == bKey.String() + case reflect.Float32, reflect.Float64: + eq = aKey.Float() == bKey.Float() + } + return eq +} + +func indexByKey(key string, v reflect.Value, list reflect.Value) (int, bool) { + for i := 0; i < list.Len(); i++ { + if eq(key, v, list.Index(i)) { + return i, true + } + } + return -1, false +} + +// MergeListByKey merges two list by element key (eg. merge []corev1.Container +// by name). If mergo.WithAppendSlice options is passed, the list is extended, +// while elemnts with same name are merged. If not, the list is filtered to +// elements in src +func (s *TransformerMap) MergeListByKey(key string, opts ...func(*mergo.Config)) func(_, _ reflect.Value) error { + conf := &mergo.Config{} + for _, opt := range opts { + opt(conf) + } + return func(dst, src reflect.Value) error { + entries := reflect.MakeSlice(src.Type(), src.Len(), src.Len()) + for i := 0; i < src.Len(); i++ { + elem := src.Index(i) + err := s.mergeByKey(key, dst, elem, opts...) + if err != nil { + return err + } + j, found := indexByKey(key, elem, dst) + if found { + entries.Index(i).Set(dst.Index(j)) + } + } + if !conf.AppendSlice { + dst.SetLen(entries.Len()) + dst.SetCap(entries.Cap()) + dst.Set(entries) + } + + return nil + } +} + +// NilOtherFields nils all fields not defined in src +func (s *TransformerMap) NilOtherFields(opts ...func(*mergo.Config)) func(_, _ reflect.Value) error { + return func(dst, src reflect.Value) error { + for i := 0; i < dst.NumField(); i++ { + dstField := dst.Type().Field(i) + srcValue := src.FieldByName(dstField.Name) + dstValue := dst.FieldByName(dstField.Name) + + if srcValue.Kind() == reflect.Ptr && srcValue.IsNil() { + dstValue.Set(srcValue) + } else { + if dstValue.Kind() == reflect.Ptr && dstValue.IsNil() { + dstValue.Set(srcValue) + } else { + opts = append(opts, mergo.WithTransformers(s)) + return mergo.Merge(dstValue.Interface(), srcValue.Interface(), opts...) + } + } + } + return nil + } +} + +// OverrideFields when merging override fields even if they are zero values (eg. nil or empty list) +func (s *TransformerMap) OverrideFields(fields ...string) func(_, _ reflect.Value) error { + return func(dst, src reflect.Value) error { + for _, field := range fields { + srcValue := src.FieldByName(field) + dst.FieldByName(field).Set(srcValue) + } + return nil + } +} diff --git a/vendor/github.com/kube-incubator/kube-operator-helper/syncer/external.go b/vendor/github.com/kube-incubator/kube-operator-helper/syncer/external.go new file mode 100644 index 0000000..710139c --- /dev/null +++ b/vendor/github.com/kube-incubator/kube-operator-helper/syncer/external.go @@ -0,0 +1,65 @@ +/* +Copyright 2018 Pressinfra SRL. + +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. +*/ + +package syncer + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +type externalSyncer struct { + name string + obj interface{} + owner runtime.Object + syncFn func(context.Context, interface{}) (controllerutil.OperationResult, error) +} + +func (s *externalSyncer) GetObject() interface{} { return s.obj } +func (s *externalSyncer) GetOwner() runtime.Object { return s.owner } +func (s *externalSyncer) Sync(ctx context.Context) (SyncResult, error) { + var err error + result := SyncResult{} + result.Operation, err = s.syncFn(ctx, s.obj) + + if err != nil { + result.SetEventData(eventWarning, basicEventReason(s.name, err), + fmt.Sprintf("%T failed syncing: %s", s.obj, err)) + log.Error(err, string(result.Operation), "kind", fmt.Sprintf("%T", s.obj)) + } else { + result.SetEventData(eventNormal, basicEventReason(s.name, err), + fmt.Sprintf("%T successfully %s", s.obj, result.Operation)) + log.V(1).Info(string(result.Operation), "kind", fmt.Sprintf("%T", s.obj)) + } + + return result, err +} + +// NewExternalSyncer creates a new syncer which syncs a generic object +// persisting it's state into and external store The name is used for logging +// and event emitting purposes and should be an valid go identifier in upper +// camel case. (eg. GiteaRepo) +func NewExternalSyncer(name string, owner runtime.Object, obj interface{}, syncFn func(context.Context, interface{}) (controllerutil.OperationResult, error)) Interface { + return &externalSyncer{ + name: name, + obj: obj, + owner: owner, + syncFn: syncFn, + } +} diff --git a/vendor/github.com/kube-incubator/kube-operator-helper/syncer/interface.go b/vendor/github.com/kube-incubator/kube-operator-helper/syncer/interface.go new file mode 100644 index 0000000..bfa2346 --- /dev/null +++ b/vendor/github.com/kube-incubator/kube-operator-helper/syncer/interface.go @@ -0,0 +1,51 @@ +/* +Copyright 2018 Pressinfra SRL. + +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. +*/ + +package syncer + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// SyncResult is a result of an Sync call +type SyncResult struct { + Operation controllerutil.OperationResult + EventType string + EventReason string + EventMessage string +} + +// SetEventData sets event data on an SyncResult +func (r *SyncResult) SetEventData(eventType, reason, message string) { + r.EventType = eventType + r.EventReason = reason + r.EventMessage = message +} + +// Interface represents a syncer. A syncer persists an object +// (known as subject), into a store (kubernetes apiserver or generic stores) +// and records kubernetes events +type Interface interface { + // GetObject returns the object for which sync applies + GetObject() interface{} + // GetOwner returns the object owner or nil if object does not have one + GetOwner() runtime.Object + // Sync persists data into the external store + Sync(context.Context) (SyncResult, error) +} diff --git a/vendor/github.com/kube-incubator/kube-operator-helper/syncer/object.go b/vendor/github.com/kube-incubator/kube-operator-helper/syncer/object.go new file mode 100644 index 0000000..c1c6f31 --- /dev/null +++ b/vendor/github.com/kube-incubator/kube-operator-helper/syncer/object.go @@ -0,0 +1,132 @@ +/* +Copyright 2018 Pressinfra SRL. + +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. +*/ + +package syncer + +import ( + "context" + "fmt" + + "github.com/go-test/deep" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +var ( + errOwnerDeleted = fmt.Errorf("owner is deleted") +) + +// ObjectSyncer is a syncer.Interface for syncing kubernetes.Objects only by +// passing a SyncFn +type ObjectSyncer struct { + Owner runtime.Object + Obj runtime.Object + SyncFn controllerutil.MutateFn + Name string + Client client.Client + Scheme *runtime.Scheme + previousObject runtime.Object +} + +// GetObject returns the ObjectSyncer subject +func (s *ObjectSyncer) GetObject() interface{} { return s.Obj } + +// GetOwner returns the ObjectSyncer owner +func (s *ObjectSyncer) GetOwner() runtime.Object { return s.Owner } + +// Sync does the actual syncing and implements the syncer.Inteface Sync method +func (s *ObjectSyncer) Sync(ctx context.Context) (SyncResult, error) { + result := SyncResult{} + + key, err := getKey(s.Obj) + if err != nil { + return result, err + } + + result.Operation, err = controllerutil.CreateOrUpdate(ctx, s.Client, s.Obj, s.mutateFn()) + + // check deep diff + diff := deep.Equal(s.previousObject, s.Obj) + + // don't pass to user error for owner deletion, just don't create the object + if err == errOwnerDeleted { + log.Info(string(result.Operation), "key", key, "kind", fmt.Sprintf("%T", s.Obj), "error", err) + err = nil + } else if err != nil { + result.SetEventData(eventWarning, basicEventReason(s.Name, err), + fmt.Sprintf("%T %s failed syncing: %s", s.Obj, key, err)) + log.Error(err, string(result.Operation), "key", key, "kind", fmt.Sprintf("%T", s.Obj), "diff", diff) + } else { + result.SetEventData(eventNormal, basicEventReason(s.Name, err), + fmt.Sprintf("%T %s %s successfully", s.Obj, key, result.Operation)) + log.V(1).Info(string(result.Operation), "key", key, "kind", fmt.Sprintf("%T", s.Obj), "diff", diff) + } + + return result, err +} + +// Given an ObjectSyncer, returns a controllerutil.MutateFn which also sets the +// owner reference if the subject has one +func (s *ObjectSyncer) mutateFn() controllerutil.MutateFn { + return func(existing runtime.Object) error { + s.previousObject = existing.DeepCopyObject() + err := s.SyncFn(existing) + if err != nil { + return err + } + if s.Owner != nil { + existingMeta, ok := existing.(metav1.Object) + if !ok { + return fmt.Errorf("%T is not a metav1.Object", existing) + } + ownerMeta, ok := s.Owner.(metav1.Object) + if !ok { + return fmt.Errorf("%T is not a metav1.Object", s.Owner) + } + + // set owner reference only if owner resource is not being deleted, otherwise the owner + // reference will be reset in case of deleting with cascade=false. + if ownerMeta.GetDeletionTimestamp().IsZero() { + err := controllerutil.SetControllerReference(ownerMeta, existingMeta, s.Scheme) + if err != nil { + return err + } + } else if ctime := existingMeta.GetCreationTimestamp(); ctime.IsZero() { + // the owner is deleted, don't recreate the resource if does not exist, because gc + // will not delete it again because has no owner reference set + return errOwnerDeleted + } + } + return nil + } +} + +// NewObjectSyncer creates a new kubernetes.Object syncer for a given object +// with an owner and persists data using controller-runtime's CreateOrUpdate. +// The name is used for logging and event emitting purposes and should be an +// valid go identifier in upper camel case. (eg. MysqlStatefulSet) +func NewObjectSyncer(name string, owner, obj runtime.Object, c client.Client, scheme *runtime.Scheme, syncFn controllerutil.MutateFn) Interface { + return &ObjectSyncer{ + Owner: owner, + Obj: obj, + SyncFn: syncFn, + Name: name, + Client: c, + Scheme: scheme, + } +} diff --git a/vendor/github.com/kube-incubator/kube-operator-helper/syncer/syncer.go b/vendor/github.com/kube-incubator/kube-operator-helper/syncer/syncer.go new file mode 100644 index 0000000..f914aa3 --- /dev/null +++ b/vendor/github.com/kube-incubator/kube-operator-helper/syncer/syncer.go @@ -0,0 +1,80 @@ +/* +Copyright 2018 Pressinfra SRL. + +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. +*/ + +package syncer + +import ( + "context" + "fmt" + + "github.com/iancoleman/strcase" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" +) + +var log = logf.Log.WithName("syncer") + +const ( + eventNormal = "Normal" + eventWarning = "Warning" +) + +func getKey(obj runtime.Object) (types.NamespacedName, error) { + key := types.NamespacedName{} + objMeta, ok := obj.(metav1.Object) + if !ok { + return key, fmt.Errorf("%T is not a metav1.Object", obj) + } + + key.Name = objMeta.GetName() + key.Namespace = objMeta.GetNamespace() + return key, nil +} + +func basicEventReason(objKindName string, err error) string { + if err != nil { + return fmt.Sprintf("%sSyncFailed", strcase.ToCamel(objKindName)) + } + return fmt.Sprintf("%sSyncSuccessfull", strcase.ToCamel(objKindName)) +} + +// Sync mutates the subject of the syncer interface using controller-runtime +// CreateOrUpdate method, when obj is not nil. It takes care of setting owner +// references and recording kubernetes events where appropriate +func Sync(ctx context.Context, syncer Interface, recorder record.EventRecorder) error { + result, err := syncer.Sync(ctx) + owner := syncer.GetOwner() + + if recorder != nil && owner != nil && result.EventType != "" && result.EventReason != "" && result.EventMessage != "" { + if err != nil || result.Operation != controllerutil.OperationResultNone { + recorder.Eventf(owner, result.EventType, result.EventReason, result.EventMessage) + } + } + + return err +} + +// WithoutOwner partially implements implements the syncer interface for the case the subject has no owner +type WithoutOwner struct{} + +// GetOwner implementation of syncer interface for the case the subject has no owner +func (*WithoutOwner) GetOwner() runtime.Object { + return nil +}