From 160aafefd6febaec65eae53f5e6714c1aa73ae81 Mon Sep 17 00:00:00 2001 From: Shawn Hurley Date: Thu, 1 Nov 2018 15:52:59 -0400 Subject: [PATCH] pkg/ansible - update status to include failure message on the status. (#639) **Description of the change:** Updates the ansible operator status to align better [conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#typical-status-properties) **Motivation for the change:** * Adding failure message to the user to see why the operator was unable to complete * Better align base operator to fit in with the conventions. --- pkg/ansible/controller/reconcile.go | 119 ++++---- pkg/ansible/controller/status/types.go | 167 +++++++++++ pkg/ansible/controller/status/utils.go | 93 ++++++ pkg/ansible/controller/status/utils_test.go | 300 ++++++++++++++++++++ pkg/ansible/controller/types.go | 136 --------- pkg/ansible/events/log_events.go | 19 +- pkg/ansible/runner/eventapi/types.go | 39 +++ 7 files changed, 669 insertions(+), 204 deletions(-) create mode 100644 pkg/ansible/controller/status/types.go create mode 100644 pkg/ansible/controller/status/utils.go create mode 100644 pkg/ansible/controller/status/utils_test.go delete mode 100644 pkg/ansible/controller/types.go diff --git a/pkg/ansible/controller/reconcile.go b/pkg/ansible/controller/reconcile.go index 3cbd969221..50198cf310 100644 --- a/pkg/ansible/controller/reconcile.go +++ b/pkg/ansible/controller/reconcile.go @@ -19,14 +19,17 @@ import ( "encoding/json" "errors" "os" + "strings" "time" + ansiblestatus "github.com/operator-framework/operator-sdk/pkg/ansible/controller/status" "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" + "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -80,7 +83,9 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc finalizers := append(pendingFinalizers, finalizer) u.SetFinalizers(finalizers) err := r.Client.Update(context.TODO(), u) - return reconcileResult, err + if err != nil { + return reconcileResult, err + } } if !contains(pendingFinalizers, finalizer) && deleted { logrus.Info("Resource is terminated, skipping reconcilation") @@ -96,34 +101,32 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc if err != nil { return reconcileResult, err } - reconcileResult.Requeue = true - return reconcileResult, nil } - status := u.Object["status"] - _, ok = status.(map[string]interface{}) - if !ok { - logrus.Debugf("status was not found") - u.Object["status"] = map[string]interface{}{} + statusInterface := u.Object["status"] + statusMap, _ := statusInterface.(map[string]interface{}) + crStatus := ansiblestatus.CreateFromMap(statusMap) + + // If there is no current status add that we are working on this resource. + errCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.FailureConditionType) + succCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType) + + // If the condition is currently running, making sure that the values are correct. + // If they are the same a no-op, if they are different then it is a good thing we + // are updating it. + if (errCond == nil && succCond == nil) || (succCond != nil && succCond.Reason != ansiblestatus.SuccessfulReason) { + c := ansiblestatus.NewCondition( + ansiblestatus.RunningConditionType, + v1.ConditionTrue, + nil, + ansiblestatus.RunningReason, + ansiblestatus.RunningMessage, + ) + ansiblestatus.SetCondition(&crStatus, *c) + u.Object["status"] = crStatus err = r.Client.Update(context.TODO(), u) if err != nil { return reconcileResult, err } - reconcileResult.Requeue = true - return reconcileResult, nil - } - - // If status is an empty map we can assume CR was just created - if len(u.Object["status"].(map[string]interface{})) == 0 { - logrus.Debugf("Setting phase status to %v", StatusPhaseCreating) - u.Object["status"] = ResourceStatus{ - Phase: StatusPhaseCreating, - } - err = r.Client.Update(context.TODO(), u) - if err != nil { - return reconcileResult, err - } - reconcileResult.Requeue = true - return reconcileResult, nil } ownerRef := metav1.OwnerReference{ @@ -145,11 +148,12 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc // iterate events from ansible, looking for the final one statusEvent := eventapi.StatusJobEvent{} + failureMessages := eventapi.FailureMessages{} for event := range eventChan { for _, eHandler := range r.EventHandlers { go eHandler.Handle(u, event) } - if event.Event == "playbook_on_stats" { + if event.Event == eventapi.EventPlaybookOnStats { // convert to StatusJobEvent; would love a better way to do this data, err := json.Marshal(event) if err != nil { @@ -160,6 +164,9 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc return reconcile.Result{}, err } } + if event.Event == eventapi.EventRunnerOnFailed { + failureMessages = append(failureMessages, event.GetFailedPlaybookMessage()) + } } if statusEvent.Event == "" { err := errors.New("did not receive playbook_on_stats event") @@ -168,14 +175,7 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc } // 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 - } - } + runSuccessful := len(failureMessages) == 0 // The finalizer has run successfully, time to remove it if deleted && finalizerExists && runSuccessful { finalizers := []string{} @@ -185,31 +185,42 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc } } 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 + err := r.Client.Update(context.TODO(), u) + if err != nil { + return reconcileResult, err } } - if needsUpdate { - err = r.Client.Update(context.TODO(), u) - } + ansibleStatus := ansiblestatus.NewAnsibleResultFromStatusJobEvent(statusEvent) + if !runSuccessful { - reconcileResult.Requeue = true - return reconcileResult, err - } + sc := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType) + sc.Status = v1.ConditionFalse + ansiblestatus.SetCondition(&crStatus, *sc) + c := ansiblestatus.NewCondition( + ansiblestatus.FailureConditionType, + v1.ConditionTrue, + ansibleStatus, + ansiblestatus.FailedReason, + strings.Join(failureMessages, "\n"), + ) + ansiblestatus.SetCondition(&crStatus, *c) + } else { + c := ansiblestatus.NewCondition( + ansiblestatus.RunningConditionType, + v1.ConditionTrue, + ansibleStatus, + ansiblestatus.SuccessfulReason, + ansiblestatus.SuccessfulMessage, + ) + // Remove the failure condition if set, because this completed successfully. + ansiblestatus.RemoveCondition(&crStatus, ansiblestatus.FailureConditionType) + ansiblestatus.SetCondition(&crStatus, *c) + } + // This needs the status subresource to be enabled by default. + u.Object["status"] = crStatus + err = r.Client.Update(context.TODO(), u) return reconcileResult, err + } func contains(l []string, s string) bool { diff --git a/pkg/ansible/controller/status/types.go b/pkg/ansible/controller/status/types.go new file mode 100644 index 0000000000..8da061710c --- /dev/null +++ b/pkg/ansible/controller/status/types.go @@ -0,0 +1,167 @@ +// 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 status + +import ( + "time" + + "github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi" + "github.com/sirupsen/logrus" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + host = "localhost" +) + +// AnsibleResult - encapsulation of the ansible result. +type AnsibleResult struct { + Ok int `json:"ok"` + Changed int `json:"changed"` + Skipped int `json:"skipped"` + Failures int `json:"failures"` + TimeOfCompletion eventapi.EventTime `json:"completion"` +} + +// NewAnsibleResultFromStatusJobEvent - creates a Ansible status from job event. +func NewAnsibleResultFromStatusJobEvent(je eventapi.StatusJobEvent) *AnsibleResult { + // ok events. + a := &AnsibleResult{TimeOfCompletion: je.Created} + if v, ok := je.EventData.Changed[host]; ok { + a.Changed = v + } + if v, ok := je.EventData.Ok[host]; ok { + a.Ok = v + } + if v, ok := je.EventData.Skipped[host]; ok { + a.Skipped = v + } + if v, ok := je.EventData.Failures[host]; ok { + a.Failures = v + } + return a +} + +// NewAnsibleResultFromMap - creates a Ansible status from a job event. +func NewAnsibleResultFromMap(sm map[string]interface{}) *AnsibleResult { + //Create Old top level status + // ok events. + a := &AnsibleResult{} + if v, ok := sm["changed"]; ok { + a.Changed = int(v.(int64)) + } + if v, ok := sm["ok"]; ok { + a.Ok = int(v.(int64)) + } + if v, ok := sm["skipped"]; ok { + a.Skipped = int(v.(int64)) + } + if v, ok := sm["failures"]; ok { + a.Failures = int(v.(int64)) + } + if v, ok := sm["completion"]; ok { + s := v.(string) + a.TimeOfCompletion.UnmarshalJSON([]byte(s)) + } + return a +} + +// ConditionType - type of condition +type ConditionType string + +const ( + // RunningConditionType - condition type of running. + RunningConditionType ConditionType = "Running" + // FailureConditionType - condition type of failure. + FailureConditionType ConditionType = "Failure" +) + +// Condition - the condition for the ansible operator. +type Condition struct { + Type ConditionType `json:"type"` + Status v1.ConditionStatus `json:"status"` + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + AnsibleResult *AnsibleResult `json:"ansibleResult,omitempty"` + Reason string `json:"reason"` + Message string `json:"message"` +} + +func createConditionFromMap(cm map[string]interface{}) Condition { + ct, ok := cm["type"].(string) + if !ok { + //If we do not find the string we are defaulting + // to make sure we can at least update the status. + ct = string(RunningConditionType) + } + status, ok := cm["status"].(string) + if !ok { + status = string(v1.ConditionTrue) + } + reason, ok := cm["reason"].(string) + if !ok { + reason = RunningReason + } + message, ok := cm["message"].(string) + if !ok { + message = RunningMessage + } + asm, ok := cm["ansibleStatus"].(map[string]interface{}) + var ansibleResult *AnsibleResult + if ok { + ansibleResult = NewAnsibleResultFromMap(asm) + } + ltts, ok := cm["lastTransitionTime"].(string) + ltt := metav1.Now() + if ok { + t, err := time.Parse("2006-01-02T15:04:05Z", ltts) + if err != nil { + logrus.Warningf("unable to parse time for status condition: %v", ltts) + } else { + ltt = metav1.NewTime(t) + } + } + return Condition{ + Type: ConditionType(ct), + Status: v1.ConditionStatus(status), + LastTransitionTime: ltt, + Reason: reason, + Message: message, + AnsibleResult: ansibleResult, + } +} + +// Status - The status for custom resources managed by the operator-sdk. +type Status struct { + Conditions []Condition `json:"conditions"` +} + +// CreateFromMap - create a status from the map +func CreateFromMap(statusMap map[string]interface{}) Status { + conditionsInterface, ok := statusMap["conditions"].([]interface{}) + if !ok { + return Status{Conditions: []Condition{}} + } + conditions := []Condition{} + for _, ci := range conditionsInterface { + cm, ok := ci.(map[string]interface{}) + if !ok { + logrus.Warningf("unknown condition, removing condition: %v", ci) + continue + } + conditions = append(conditions, createConditionFromMap(cm)) + } + return Status{Conditions: conditions} +} diff --git a/pkg/ansible/controller/status/utils.go b/pkg/ansible/controller/status/utils.go new file mode 100644 index 0000000000..e4789f13a9 --- /dev/null +++ b/pkg/ansible/controller/status/utils.go @@ -0,0 +1,93 @@ +// 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 status + +import ( + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // RunningReason - Condition is running + RunningReason = "Running" + // SuccessfulReason - Condition is running due to reconcile being successful + SuccessfulReason = "Successful" + // FailedReason - Condition is failed due to ansible failure + FailedReason = "Failed" + // UnknownFailedReason - Condition is unknown + UnknownFailedReason = "Unknown" +) + +const ( + // RunningMessage - message for running reason. + RunningMessage = "Running reconciliation" + // SuccessfulMessage - message for successful reason. + SuccessfulMessage = "Awaiting next reconciliation" +) + +// NewCondition - condition +func NewCondition(condType ConditionType, status v1.ConditionStatus, ansibleResult *AnsibleResult, reason, message string) *Condition { + return &Condition{ + Type: condType, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + AnsibleResult: ansibleResult, + } +} + +// GetCondition returns the condition with the provided type. +func GetCondition(status Status, condType ConditionType) *Condition { + for i := range status.Conditions { + c := status.Conditions[i] + if c.Type == condType { + return &c + } + } + return nil +} + +// SetCondition updates the scheduledReport to include the provided condition. If the condition that +// we are about to add already exists and has the same status and reason then we are not going to update. +func SetCondition(status *Status, condition Condition) { + currentCond := GetCondition(*status, condition.Type) + if currentCond != nil && currentCond.Status == condition.Status && currentCond.Reason == condition.Reason { + return + } + // Do not update lastTransitionTime if the status of the condition doesn't change. + if currentCond != nil && currentCond.Status == condition.Status { + condition.LastTransitionTime = currentCond.LastTransitionTime + } + newConditions := filterOutCondition(status.Conditions, condition.Type) + status.Conditions = append(newConditions, condition) +} + +// RemoveCondition removes the scheduledReport condition with the provided type. +func RemoveCondition(status *Status, condType ConditionType) { + status.Conditions = filterOutCondition(status.Conditions, condType) +} + +// filterOutCondition returns a new slice of scheduledReport conditions without conditions with the provided type. +func filterOutCondition(conditions []Condition, condType ConditionType) []Condition { + var newConditions []Condition + for _, c := range conditions { + if c.Type == condType { + continue + } + newConditions = append(newConditions, c) + } + return newConditions +} diff --git a/pkg/ansible/controller/status/utils_test.go b/pkg/ansible/controller/status/utils_test.go new file mode 100644 index 0000000000..3dc1d5c9a8 --- /dev/null +++ b/pkg/ansible/controller/status/utils_test.go @@ -0,0 +1,300 @@ +// 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 status + +import ( + "reflect" + "testing" + + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestNewCondition(t *testing.T) { + testCases := []struct { + name string + condType ConditionType + status v1.ConditionStatus + ansibleResult *AnsibleResult + reason string + message string + expectedCondtion Condition + }{ + { + name: "running condition creating", + condType: RunningConditionType, + status: v1.ConditionTrue, + ansibleResult: nil, + reason: RunningReason, + message: RunningMessage, + expectedCondtion: Condition{ + Type: RunningConditionType, + Status: v1.ConditionTrue, + Reason: RunningReason, + Message: RunningMessage, + }, + }, + { + name: "failure condition creating", + condType: FailureConditionType, + status: v1.ConditionFalse, + ansibleResult: &AnsibleResult{ + Changed: 0, + Failures: 1, + Ok: 10, + Skipped: 1, + }, + reason: FailedReason, + message: "invalid parameter", + expectedCondtion: Condition{ + Type: FailureConditionType, + Status: v1.ConditionFalse, + Reason: FailedReason, + Message: "invalid parameter", + AnsibleResult: &AnsibleResult{ + Changed: 0, + Failures: 1, + Ok: 10, + Skipped: 1, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ac := NewCondition(tc.condType, tc.status, tc.ansibleResult, tc.reason, tc.message) + tc.expectedCondtion.LastTransitionTime = ac.LastTransitionTime + if !reflect.DeepEqual(*ac, tc.expectedCondtion) { + t.Fatalf("condition did no match expected:\nActual: %#v\nExpected: %#v", *ac, tc.expectedCondtion) + } + }) + } +} + +func TestGetCondition(t *testing.T) { + testCases := []struct { + name string + condType ConditionType + status Status + expectedCondition *Condition + }{ + { + name: "find RunningCondition", + condType: RunningConditionType, + status: Status{ + Conditions: []Condition{ + Condition{ + Type: RunningConditionType, + }, + }, + }, + expectedCondition: &Condition{ + Type: RunningConditionType, + }, + }, + { + name: "did not find RunningCondition", + condType: RunningConditionType, + status: Status{ + Conditions: []Condition{ + Condition{ + Type: FailureConditionType, + }, + }, + }, + expectedCondition: nil, + }, + { + name: "find FailureCondition", + condType: FailureConditionType, + status: Status{ + Conditions: []Condition{ + Condition{ + Type: FailureConditionType, + }, + }, + }, + expectedCondition: &Condition{ + Type: FailureConditionType, + }, + }, + { + name: "did not find FailureCondition", + condType: FailureConditionType, + status: Status{ + Conditions: []Condition{ + Condition{ + Type: RunningConditionType, + }, + }, + }, + expectedCondition: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ac := GetCondition(tc.status, tc.condType) + if !reflect.DeepEqual(ac, tc.expectedCondition) { + t.Fatalf("condition did no match expected:\nActual: %#v\nExpected: %#v", ac, tc.expectedCondition) + } + }) + } +} + +func TestRemoveCondition(t *testing.T) { + testCases := []struct { + name string + condType ConditionType + status Status + expectedSize int + }{ + { + name: "remove RunningCondition", + condType: RunningConditionType, + status: Status{ + Conditions: []Condition{ + Condition{ + Type: RunningConditionType, + }, + }, + }, + expectedSize: 0, + }, + { + name: "did not find RunningCondition", + condType: RunningConditionType, + status: Status{ + Conditions: []Condition{ + Condition{ + Type: FailureConditionType, + }, + }, + }, + expectedSize: 1, + }, + { + name: "remove FailureCondition", + condType: FailureConditionType, + status: Status{ + Conditions: []Condition{ + Condition{ + Type: FailureConditionType, + }, + }, + }, + expectedSize: 0, + }, + { + name: "did not find FailureCondition", + condType: FailureConditionType, + status: Status{ + Conditions: []Condition{ + Condition{ + Type: RunningConditionType, + }, + }, + }, + expectedSize: 1, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + RemoveCondition(&tc.status, tc.condType) + if tc.expectedSize != len(tc.status.Conditions) { + t.Fatalf("conditions did no match expected size:\nActual: %#v\nExpected: %#v", len(tc.status.Conditions), tc.expectedSize) + } + }) + } +} + +func TestSetCondition(t *testing.T) { + lastTransitionTime := metav1.Now() + keeptMessage := SuccessfulMessage + testCases := []struct { + name string + status *Status + condition *Condition + expectedNewSize int + keepLastTransitionTime bool + keepMessage bool + }{ + { + name: "add new condition", + status: &Status{ + Conditions: []Condition{}, + }, + condition: NewCondition(RunningConditionType, v1.ConditionTrue, nil, RunningReason, RunningMessage), + expectedNewSize: 1, + keepLastTransitionTime: false, + }, + { + name: "update running condition", + status: &Status{ + Conditions: []Condition{ + Condition{ + Type: RunningConditionType, + Status: v1.ConditionTrue, + Reason: SuccessfulReason, + Message: SuccessfulMessage, + LastTransitionTime: lastTransitionTime, + }, + }, + }, + condition: NewCondition(RunningConditionType, v1.ConditionTrue, nil, RunningReason, RunningMessage), + expectedNewSize: 1, + keepLastTransitionTime: true, + }, + { + name: "do not update running condition", + status: &Status{ + Conditions: []Condition{ + Condition{ + Type: RunningConditionType, + Status: v1.ConditionTrue, + Reason: RunningReason, + Message: SuccessfulMessage, + LastTransitionTime: lastTransitionTime, + }, + }, + }, + condition: NewCondition(RunningConditionType, v1.ConditionTrue, nil, RunningReason, RunningMessage), + expectedNewSize: 1, + keepLastTransitionTime: true, + keepMessage: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + SetCondition(tc.status, *tc.condition) + if tc.expectedNewSize != len(tc.status.Conditions) { + t.Fatalf("new size of conditions did not match expected\nActual: %v\nExpected: %v", len(tc.status.Conditions), tc.expectedNewSize) + } + if tc.keepLastTransitionTime { + tc.condition.LastTransitionTime = lastTransitionTime + } + if tc.keepMessage { + tc.condition.Message = keeptMessage + } + ac := GetCondition(*tc.status, tc.condition.Type) + if !reflect.DeepEqual(ac, tc.condition) { + t.Fatalf("condition did not match expected:\nActual: %#v\nExpected: %#v", ac, tc.condition) + } + }) + } +} diff --git a/pkg/ansible/controller/types.go b/pkg/ansible/controller/types.go deleted file mode 100644 index fbd5f730e5..0000000000 --- a/pkg/ansible/controller/types.go +++ /dev/null @@ -1,136 +0,0 @@ -// 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" - StatusPhaseCreating = "Creating" - StatusPhaseRunning = "Running" - StatusPhaseFailed = "Failed" -) - -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"` - Phase string `json:"phase"` - 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) - phase := StatusPhaseRunning - // 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)) - } - } - - if newStatus.Failures > 0 { - phase = StatusPhaseFailed - } - - history = append(history, oldStatus) - return true, ResourceStatus{ - Status: newStatus, - Phase: phase, - History: history, - } -} diff --git a/pkg/ansible/events/log_events.go b/pkg/ansible/events/log_events.go index 15ddd6244b..098932865c 100644 --- a/pkg/ansible/events/log_events.go +++ b/pkg/ansible/events/log_events.go @@ -32,15 +32,6 @@ const ( // Nothing - this will log nothing. Nothing - - // Ansible Events - EventPlaybookOnTaskStart = "playbook_on_task_start" - EventRunnerOnOk = "runner_on_ok" - EventRunnerOnFailed = "runner_on_failed" - - // Ansible Task Actions - TaskActionSetFact = "set_fact" - TaskActionDebug = "debug" ) // EventHandler - knows how to handle job events. @@ -66,18 +57,18 @@ func (l loggingEventHandler) Handle(u *unstructured.Unstructured, e eventapi.Job // log only the following for the 'Tasks' LogLevel t, ok := e.EventData["task"] if ok { - setFactAction := e.EventData["task_action"] == TaskActionSetFact - debugAction := e.EventData["task_action"] == TaskActionDebug + setFactAction := e.EventData["task_action"] == eventapi.TaskActionSetFact + debugAction := e.EventData["task_action"] == eventapi.TaskActionDebug - if e.Event == EventPlaybookOnTaskStart && !setFactAction && !debugAction { + if e.Event == eventapi.EventPlaybookOnTaskStart && !setFactAction && !debugAction { log.Infof("[playbook task]: %s", e.EventData["name"]) return } - if e.Event == EventRunnerOnOk && debugAction { + if e.Event == eventapi.EventRunnerOnOk && debugAction { log.Infof("[playbook debug]: %v", e.EventData["task_args"]) return } - if e.Event == EventRunnerOnFailed { + if e.Event == eventapi.EventRunnerOnFailed { log.Errorf("[failed]: [playbook task] '%s' failed with task_args - %v", t, e.EventData["task_args"]) return diff --git a/pkg/ansible/runner/eventapi/types.go b/pkg/ansible/runner/eventapi/types.go index ffbf2ad743..f360a01121 100644 --- a/pkg/ansible/runner/eventapi/types.go +++ b/pkg/ansible/runner/eventapi/types.go @@ -20,6 +20,29 @@ import ( "time" ) +const ( + // Ansible Events + + // EventPlaybookOnTaskStart - playbook is starting to run a task. + EventPlaybookOnTaskStart = "playbook_on_task_start" + // EventRunnerOnOk - task finished with ok status. + EventRunnerOnOk = "runner_on_ok" + // EventRunnerOnFailed - task finished with failed status. + EventRunnerOnFailed = "runner_on_failed" + // EventPlaybookOnStats - playbook has finished running. + EventPlaybookOnStats = "playbook_on_stats" + + // Ansible Task Actions + + // TaskActionSetFact - task action of setting a fact. + TaskActionSetFact = "set_fact" + // TaskActionDebug - task action of printing a debug message. + TaskActionDebug = "debug" + + // defaultFailedMessage - Default failed playbook message + defaultFailedMessage = "unknown playbook failure" +) + // EventTime - time to unmarshal nano time. type EventTime struct { time.Time @@ -74,3 +97,19 @@ type StatsEventData struct { Failures map[string]int `json:"failures"` Skipped map[string]int `json:"skipped"` } + +// FailureMessages - failure messages from the event api +type FailureMessages []string + +// GetFailedPlaybookMessage - get the failure message from res.msg +func (je JobEvent) GetFailedPlaybookMessage() string { + message := defaultFailedMessage + result, ok := je.EventData["res"].(map[string]interface{}) + if !ok { + return message + } + if m, ok := result["msg"].(string); ok { + message = m + } + return message +}