diff --git a/Gopkg.lock b/Gopkg.lock index f2ba034310..bf1abbdbd4 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -25,6 +25,22 @@ revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" version = "v1.0.0" +[[projects]] + branch = "master" + digest = "1:65587005c6fa4293c0b8a2e457e689df7fda48cc5e1f5449ea2c1e7784551558" + name = "github.com/go-logr/logr" + packages = ["."] + pruneopts = "" + revision = "9fb12b3b21c5415d16ac18dc5cd42c1cfdd40c4e" + +[[projects]] + branch = "master" + digest = "1:ce43ad4015e7cdad3f0e8f2c8339439dd4470859a828d2a6988b0f713699e94a" + name = "github.com/go-logr/zapr" + packages = ["."] + pruneopts = "" + revision = "7536572e8d55209135cd5e7ccf7fce43dca217ab" + [[projects]] digest = "1:6e73003ecd35f4487a5e88270d3ca0a81bc80dc88053ac7e4dcfec5fba30d918" name = "github.com/gogo/protobuf" @@ -44,6 +60,14 @@ pruneopts = "" revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" +[[projects]] + branch = "master" + digest = "1:9854532d7b2fee9414d4fcd8d8bccd6b1c1e1663d8ec0337af63a19aaf4a778e" + name = "github.com/golang/groupcache" + packages = ["lru"] + pruneopts = "" + revision = "6f2cf27854a4a29e3811b0371547be335d411b8b" + [[projects]] digest = "1:3dd078fda7500c341bc26cfbc6c6a34614f295a2457149fc1045cab767cbcf18" name = "github.com/golang/protobuf" @@ -74,6 +98,14 @@ pruneopts = "" revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1" +[[projects]] + digest = "1:5247b135b5492aa232a731acdcb52b08f32b874cb398f21ab460396eadbe866b" + name = "github.com/google/uuid" + packages = ["."] + pruneopts = "" + revision = "d460ce9f8df2e77fb1ba55ca87fafed96c607494" + version = "v1.0.0" + [[projects]] digest = "1:16b2837c8b3cf045fa2cdc82af0cf78b19582701394484ae76b2c3bc3c99ad73" name = "github.com/googleapis/gnostic" @@ -132,6 +164,14 @@ revision = "1624edc4454b8682399def8740d46db5e4362ba4" version = "v1.1.5" +[[projects]] + branch = "master" + digest = "1:58050e2bc9621cc6b68c1da3e4a0d1c40ad1f89062b9855c26521fd42a97a106" + name = "github.com/mattbaird/jsonpatch" + packages = ["."] + pruneopts = "" + revision = "81af80346b1a01caae0cbc27fd3c1ba5b11e189f" + [[projects]] digest = "1:63722a4b1e1717be7b98fc686e0b30d5e7f734b9e93d7dee86293b6deab7ea28" name = "github.com/matttproud/golang_protobuf_extensions" @@ -164,6 +204,14 @@ pruneopts = "" revision = "cca7078d478f8520f85629ad7c68962d31ed7682" +[[projects]] + digest = "1:a5484d4fa43127138ae6e7b2299a6a52ae006c7f803d98d717f60abf3e97192e" + name = "github.com/pborman/uuid" + packages = ["."] + pruneopts = "" + revision = "adf5a7427709b9deb95d29d3fa8a2bf9cfd388f1" + version = "v1.2" + [[projects]] branch = "master" digest = "1:c24598ffeadd2762552269271b3b1510df2d83ee6696c1e543a0ff653af494bc" @@ -213,7 +261,7 @@ [[projects]] branch = "master" - digest = "1:e04aaa0e8f8da0ed3d6c0700bd77eda52a47f38510063209d72d62f0ef807d5e" + digest = "1:5a57ea878c9a40657ebe7916eca6bea7c76808f5acb68fd42ea5e204dd35f6f7" name = "github.com/prometheus/procfs" packages = [ ".", @@ -222,7 +270,7 @@ "xfs", ] pruneopts = "" - revision = "05ee40e3a273f7245e8777337fc7b46e533a9a92" + revision = "418d78d0b9a7b7de3a6bbc8a23def624cc977bb2" [[projects]] digest = "1:3962f553b77bf6c03fc07cd687a22dd3b00fe11aa14d31194f5505f5bb65cdc8" @@ -256,6 +304,37 @@ revision = "9a97c102cda95a86cec2345a6f09f55a939babf5" version = "v1.0.2" +[[projects]] + digest = "1:74f86c458e82e1c4efbab95233e0cf51b7cc02dc03193be9f62cd81224e10401" + name = "go.uber.org/atomic" + packages = ["."] + pruneopts = "" + revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289" + version = "v1.3.2" + +[[projects]] + digest = "1:22c7effcb4da0eacb2bb1940ee173fac010e9ef3c691f5de4b524d538bd980f5" + name = "go.uber.org/multierr" + packages = ["."] + pruneopts = "" + revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a" + version = "v1.1.0" + +[[projects]] + digest = "1:246f378f80fba6fcf0f191c486b6613265abd2bc0f2fa55a36b928c67352021e" + name = "go.uber.org/zap" + packages = [ + ".", + "buffer", + "internal/bufferpool", + "internal/color", + "internal/exit", + "zapcore", + ] + pruneopts = "" + revision = "ff33455a0e382e8a81d14dd7c922020b6b5e7982" + version = "v1.9.1" + [[projects]] branch = "master" digest = "1:61a86f0be8b466d6e3fbdabb155aaa4006137cb5e3fd3b949329d103fa0ceb0f" @@ -266,7 +345,7 @@ [[projects]] branch = "master" - digest = "1:fbdbb6cf8db3278412c9425ad78b26bb8eb788181f26a3ffb3e4f216b314f86a" + digest = "1:6846191f608c0dd6109f37ca46b784f9630191ff13f86ae974135c05a4c92971" name = "golang.org/x/net" packages = [ "context", @@ -278,18 +357,18 @@ "idna", ] pruneopts = "" - revision = "26e67e76b6c3f6ce91f7c52def5af501b4e0f3a2" + revision = "f04abc6bdfa7a0171a8a0c9fd2ada9391044d056" [[projects]] branch = "master" - digest = "1:ed900376500543ca05f2a2383e1f541b4606f19cd22f34acb81b17a0b90c7f3e" + digest = "1:18ebd6e65d5223778b5fc92bbf2afffb54c6ac3889bbc362df692b965f932fae" name = "golang.org/x/sys" packages = [ "unix", "windows", ] pruneopts = "" - revision = "d0be0721c37eeb5299f245a996a483160fc36940" + revision = "b09afc3d579e346c4a7e4705953acaf6f9e551bd" [[projects]] digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4" @@ -342,6 +421,7 @@ digest = "1:2fe7efa9ea3052443378383d27c15ba088d03babe69a89815ce7fe9ec1d9aeb4" name = "k8s.io/api" packages = [ + "admission/v1beta1", "admissionregistration/v1alpha1", "admissionregistration/v1beta1", "apps/v1", @@ -423,16 +503,20 @@ "pkg/util/httpstream", "pkg/util/intstr", "pkg/util/json", + "pkg/util/mergepatch", "pkg/util/net", "pkg/util/proxy", "pkg/util/runtime", "pkg/util/sets", + "pkg/util/strategicpatch", + "pkg/util/uuid", "pkg/util/validation", "pkg/util/validation/field", "pkg/util/wait", "pkg/util/yaml", "pkg/version", "pkg/watch", + "third_party/forked/golang/json", "third_party/forked/golang/netutil", "third_party/forked/golang/reflect", ] @@ -492,8 +576,11 @@ "tools/clientcmd/api", "tools/clientcmd/api/latest", "tools/clientcmd/api/v1", + "tools/leaderelection", + "tools/leaderelection/resourcelock", "tools/metrics", "tools/pager", + "tools/record", "tools/reference", "transport", "util/buffer", @@ -509,12 +596,40 @@ revision = "1f13a808da65775f22cbf47862c4e5898d8f4ca1" version = "kubernetes-1.11.2" +[[projects]] + branch = "master" + digest = "1:951bc2047eea6d316a17850244274554f26fd59189360e45f4056b424dadf2c1" + name = "k8s.io/kube-openapi" + packages = ["pkg/util/proto"] + pruneopts = "" + revision = "e3762e86a74c878ffed47484592986685639c2cd" + [[projects]] digest = "1:207e862b55f9399362c667ce30f1ac8b2180d282bc7bb5749fc81122944aa2b5" name = "sigs.k8s.io/controller-runtime" packages = [ + "pkg/cache", + "pkg/cache/internal", "pkg/client", "pkg/client/apiutil", + "pkg/controller", + "pkg/event", + "pkg/handler", + "pkg/internal/controller", + "pkg/internal/recorder", + "pkg/leaderelection", + "pkg/manager", + "pkg/patch", + "pkg/predicate", + "pkg/reconcile", + "pkg/recorder", + "pkg/runtime/inject", + "pkg/runtime/log", + "pkg/source", + "pkg/source/internal", + "pkg/webhook/admission", + "pkg/webhook/admission/types", + "pkg/webhook/types", ] pruneopts = "" revision = "79f06014d49b565e2d980fcd9519b33874ddb8d6" @@ -558,6 +673,12 @@ "k8s.io/client-go/transport", "k8s.io/client-go/util/workqueue", "sigs.k8s.io/controller-runtime/pkg/client", + "sigs.k8s.io/controller-runtime/pkg/controller", + "sigs.k8s.io/controller-runtime/pkg/event", + "sigs.k8s.io/controller-runtime/pkg/handler", + "sigs.k8s.io/controller-runtime/pkg/manager", + "sigs.k8s.io/controller-runtime/pkg/reconcile", + "sigs.k8s.io/controller-runtime/pkg/source", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/pkg/ansible/controller/controller.go b/pkg/ansible/controller/controller.go new file mode 100644 index 0000000000..51b1b14eff --- /dev/null +++ b/pkg/ansible/controller/controller.go @@ -0,0 +1,91 @@ +// Copyright 2018 The Operator-SDK 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. + +package controller + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/operator-framework/operator-sdk/pkg/ansible/events" + "github.com/operator-framework/operator-sdk/pkg/ansible/runner" + + "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/controller" + crthandler "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +// Options - options for your controller +type Options struct { + EventHandlers []events.EventHandler + LoggingLevel events.LogLevel + Runner runner.Runner + Namespace string + GVK schema.GroupVersionKind + // StopChannel is used to deal with the bug: + // https://github.com/kubernetes-sigs/controller-runtime/issues/103 + StopChannel <-chan struct{} +} + +// Add - Creates a new ansible operator controller and adds it to the manager +func Add(mgr manager.Manager, options Options) { + logrus.Infof("Watching %s/%v, %s, %s", options.GVK.Group, options.GVK.Version, options.GVK.Kind, options.Namespace) + if options.EventHandlers == nil { + options.EventHandlers = []events.EventHandler{} + } + eventHandlers := append(options.EventHandlers, events.NewLoggingEventHandler(options.LoggingLevel)) + + aor := &AnsibleOperatorReconciler{ + Client: mgr.GetClient(), + GVK: options.GVK, + Runner: options.Runner, + EventHandlers: eventHandlers, + } + + // Register the GVK with the schema + mgr.GetScheme().AddKnownTypeWithName(options.GVK, &unstructured.Unstructured{}) + metav1.AddToGroupVersion(mgr.GetScheme(), schema.GroupVersion{ + Group: options.GVK.Group, + Version: options.GVK.Version, + }) + + //Create new controller runtime controller and set the controller to watch GVK. + c, err := controller.New(fmt.Sprintf("%v-controller", strings.ToLower(options.GVK.Kind)), mgr, controller.Options{ + Reconciler: aor, + }) + if err != nil { + log.Fatal(err) + } + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(options.GVK) + if err := c.Watch(&source.Kind{Type: u}, &crthandler.EnqueueRequestForObject{}); err != nil { + log.Fatal(err) + } + + r := NewReconcileLoop(time.Minute*1, options.GVK, mgr.GetClient()) + r.Stop = options.StopChannel + cs := &source.Channel{Source: r.Source} + cs.InjectStopChannel(options.StopChannel) + if err := c.Watch(cs, &crthandler.EnqueueRequestForObject{}); err != nil { + log.Fatal(err) + } + r.Start() +} diff --git a/pkg/ansible/controller/reconcile.go b/pkg/ansible/controller/reconcile.go new file mode 100644 index 0000000000..0db01ab103 --- /dev/null +++ b/pkg/ansible/controller/reconcile.go @@ -0,0 +1,173 @@ +// Copyright 2018 The Operator-SDK 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. + +package controller + +import ( + "context" + "encoding/json" + "errors" + "os" + + "github.com/operator-framework/operator-sdk/pkg/ansible/events" + "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/kubeconfig" + "github.com/operator-framework/operator-sdk/pkg/ansible/runner" + "github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi" + + "github.com/sirupsen/logrus" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// AnsibleOperatorReconciler - object to reconcile runner requests +type AnsibleOperatorReconciler struct { + GVK schema.GroupVersionKind + Runner runner.Runner + Client client.Client + EventHandlers []events.EventHandler +} + +// Reconcile - handle the event. +func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(r.GVK) + err := r.Client.Get(context.TODO(), request.NamespacedName, u) + if apierrors.IsNotFound(err) { + return reconcile.Result{}, nil + } + if err != nil { + return reconcile.Result{}, err + } + + deleted := u.GetDeletionTimestamp() != nil + finalizer, finalizerExists := r.Runner.GetFinalizer() + pendingFinalizers := u.GetFinalizers() + // If the resource is being deleted we don't want to add the finalizer again + if finalizerExists && !deleted && !contains(pendingFinalizers, finalizer) { + logrus.Debugf("Adding finalizer %s to resource", finalizer) + finalizers := append(pendingFinalizers, finalizer) + u.SetFinalizers(finalizers) + err := r.Client.Update(context.TODO(), u) + return reconcile.Result{}, err + } + if !contains(pendingFinalizers, finalizer) && deleted { + logrus.Info("Resource is terminated, skipping reconcilation") + return reconcile.Result{}, nil + } + + s := u.Object["spec"] + _, ok := s.(map[string]interface{}) + if !ok { + logrus.Warnf("spec was not found") + u.Object["spec"] = map[string]interface{}{} + r.Client.Update(context.TODO(), u) + return reconcile.Result{Requeue: true}, nil + } + ownerRef := metav1.OwnerReference{ + APIVersion: u.GetAPIVersion(), + Kind: u.GetKind(), + Name: u.GetName(), + UID: u.GetUID(), + } + + kc, err := kubeconfig.Create(ownerRef, "http://localhost:8888", u.GetNamespace()) + if err != nil { + return reconcile.Result{}, err + } + defer os.Remove(kc.Name()) + eventChan, err := r.Runner.Run(u, kc.Name()) + if err != nil { + return reconcile.Result{}, err + } + + // iterate events from ansible, looking for the final one + statusEvent := eventapi.StatusJobEvent{} + for event := range eventChan { + for _, eHandler := range r.EventHandlers { + go eHandler.Handle(u, event) + } + if event.Event == "playbook_on_stats" { + // convert to StatusJobEvent; would love a better way to do this + data, err := json.Marshal(event) + if err != nil { + return reconcile.Result{}, err + } + err = json.Unmarshal(data, &statusEvent) + if err != nil { + return reconcile.Result{}, err + } + } + } + if statusEvent.Event == "" { + err := errors.New("did not receive playbook_on_stats event") + logrus.Error(err.Error()) + return reconcile.Result{}, err + } + + // We only want to update the CustomResource once, so we'll track changes and do it at the end + var needsUpdate bool + runSuccessful := true + for _, count := range statusEvent.EventData.Failures { + if count > 0 { + runSuccessful = false + break + } + } + // The finalizer has run successfully, time to remove it + if deleted && finalizerExists && runSuccessful { + finalizers := []string{} + for _, pendingFinalizer := range pendingFinalizers { + if pendingFinalizer != finalizer { + finalizers = append(finalizers, pendingFinalizer) + } + } + u.SetFinalizers(finalizers) + needsUpdate = true + } + + statusMap, ok := u.Object["status"].(map[string]interface{}) + if !ok { + u.Object["status"] = ResourceStatus{ + Status: NewStatusFromStatusJobEvent(statusEvent), + } + logrus.Infof("adding status for the first time") + needsUpdate = true + } else { + // Need to conver the map[string]interface into a resource status. + if update, status := UpdateResourceStatus(statusMap, statusEvent); update { + u.Object["status"] = status + needsUpdate = true + } + } + if needsUpdate { + err = r.Client.Update(context.TODO(), u) + } + if !runSuccessful { + return reconcile.Result{Requeue: true}, err + } + return reconcile.Result{}, err +} + +func contains(l []string, s string) bool { + for _, elem := range l { + if elem == s { + return true + } + } + return false +} diff --git a/pkg/ansible/controller/source.go b/pkg/ansible/controller/source.go new file mode 100644 index 0000000000..f8556d7d86 --- /dev/null +++ b/pkg/ansible/controller/source.go @@ -0,0 +1,78 @@ +// Copyright 2018 The Operator-SDK 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. + +package controller + +import ( + "context" + "time" + + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +// ReconcileLoop - new loop +type ReconcileLoop struct { + Source chan event.GenericEvent + Stop <-chan struct{} + GVK schema.GroupVersionKind + Interval time.Duration + Client client.Client +} + +// NewReconcileLoop - loop for a GVK. +// The reconcilation loop is needed because the resync period +// for the informer is not suitable for this use case. +func NewReconcileLoop(interval time.Duration, gvk schema.GroupVersionKind, c client.Client) ReconcileLoop { + s := make(chan event.GenericEvent, 1025) + return ReconcileLoop{ + Source: s, + GVK: gvk, + Interval: interval, + Client: c, + } +} + +// Start - start the reconcile loop +func (r *ReconcileLoop) Start() { + go func() { + ticker := time.NewTicker(r.Interval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + // List all object for the GVK + ul := &unstructured.UnstructuredList{} + ul.SetGroupVersionKind(r.GVK) + err := r.Client.List(context.Background(), nil, ul) + if err != nil { + logrus.Warningf("unable to list resources for GV: %v during reconcilation", r.GVK) + continue + } + for _, u := range ul.Items { + e := event.GenericEvent{ + Meta: &u, + Object: &u, + } + r.Source <- e + } + case <-r.Stop: + return + } + } + }() +} diff --git a/pkg/ansible/controller/types.go b/pkg/ansible/controller/types.go new file mode 100644 index 0000000000..1dd148bbd2 --- /dev/null +++ b/pkg/ansible/controller/types.go @@ -0,0 +1,125 @@ +// Copyright 2018 The Operator-SDK 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. + +package controller + +import ( + "github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi" +) + +const ( + host = "localhost" +) + +type Status struct { + Ok int `json:"ok"` + Changed int `json:"changed"` + Skipped int `json:"skipped"` + Failures int `json:"failures"` + TimeOfCompletion eventapi.EventTime `json:"completion"` +} + +func NewStatusFromStatusJobEvent(je eventapi.StatusJobEvent) Status { + // ok events. + o := 0 + changed := 0 + skipped := 0 + failures := 0 + if v, ok := je.EventData.Changed[host]; ok { + changed = v + } + if v, ok := je.EventData.Ok[host]; ok { + o = v + } + if v, ok := je.EventData.Skipped[host]; ok { + skipped = v + } + if v, ok := je.EventData.Failures[host]; ok { + failures = v + } + return Status{ + Ok: o, + Changed: changed, + Skipped: skipped, + Failures: failures, + TimeOfCompletion: je.Created, + } +} + +func IsStatusEqual(s1, s2 Status) bool { + return (s1.Ok == s2.Ok && s1.Changed == s2.Changed && s1.Skipped == s2.Skipped && s1.Failures == s2.Failures) +} + +func NewStatusFromMap(sm map[string]interface{}) Status { + //Create Old top level status + // ok events. + o := 0 + changed := 0 + skipped := 0 + failures := 0 + e := eventapi.EventTime{} + if v, ok := sm["changed"]; ok { + changed = int(v.(int64)) + } + if v, ok := sm["ok"]; ok { + o = int(v.(int64)) + } + if v, ok := sm["skipped"]; ok { + skipped = int(v.(int64)) + } + if v, ok := sm["failures"]; ok { + failures = int(v.(int64)) + } + if v, ok := sm["completion"]; ok { + s := v.(string) + e.UnmarshalJSON([]byte(s)) + } + return Status{ + Ok: o, + Changed: changed, + Skipped: skipped, + Failures: failures, + TimeOfCompletion: e, + } +} + +type ResourceStatus struct { + Status `json:",inline"` + FailureMessage string `json:"reason,omitempty"` + History []Status `json:"history,omitempty"` +} + +func UpdateResourceStatus(sm map[string]interface{}, je eventapi.StatusJobEvent) (bool, ResourceStatus) { + newStatus := NewStatusFromStatusJobEvent(je) + oldStatus := NewStatusFromMap(sm) + // Don't update the status if new status and old status are equal. + if IsStatusEqual(newStatus, oldStatus) { + return false, ResourceStatus{} + } + + history := []Status{} + h, ok := sm["history"] + if ok { + hi := h.([]interface{}) + for _, m := range hi { + ma := m.(map[string]interface{}) + history = append(history, NewStatusFromMap(ma)) + } + } + history = append(history, oldStatus) + return true, ResourceStatus{ + Status: newStatus, + History: history, + } +} diff --git a/pkg/ansible/events/log_events.go b/pkg/ansible/events/log_events.go new file mode 100644 index 0000000000..37422c106b --- /dev/null +++ b/pkg/ansible/events/log_events.go @@ -0,0 +1,73 @@ +// Copyright 2018 The Operator-SDK 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. + +package events + +import ( + "github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// LogLevel - Levelt for the logging to take place. +type LogLevel int + +const ( + // Tasks - only log the high level tasks. + Tasks LogLevel = iota + + // Everything - log every event. + Everything + + // Nothing - this will log nothing. + Nothing +) + +// EventHandler - knows how to handle job events. +type EventHandler interface { + Handle(*unstructured.Unstructured, eventapi.JobEvent) +} + +type loggingEventHandler struct { + LogLevel LogLevel +} + +func (l loggingEventHandler) Handle(u *unstructured.Unstructured, e eventapi.JobEvent) { + log := logrus.WithFields(logrus.Fields{ + "component": "logging_event_handler", + "name": u.GetName(), + "namespace": u.GetNamespace(), + "gvk": u.GroupVersionKind().String(), + "event_type": e.Event, + }) + t, ok := e.EventData["task"] + if ok { + log = log.WithField("task", t) + } + switch l.LogLevel { + case Everything: + log.Infof("event: %#v", e.EventData) + case Tasks: + if ok { + log.Infof("event: %#v", e.EventData) + } + } +} + +// NewLoggingEventHandler - Creates a Logging Event Handler to log events. +func NewLoggingEventHandler(l LogLevel) EventHandler { + return loggingEventHandler{ + LogLevel: l, + } +}