Skip to content

Commit 160aafe

Browse files
author
Shawn Hurley
authored
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.
1 parent e63ba2f commit 160aafe

File tree

7 files changed

+669
-204
lines changed

7 files changed

+669
-204
lines changed

pkg/ansible/controller/reconcile.go

Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@ import (
1919
"encoding/json"
2020
"errors"
2121
"os"
22+
"strings"
2223
"time"
2324

25+
ansiblestatus "github.com/operator-framework/operator-sdk/pkg/ansible/controller/status"
2426
"github.com/operator-framework/operator-sdk/pkg/ansible/events"
2527
"github.com/operator-framework/operator-sdk/pkg/ansible/proxy/kubeconfig"
2628
"github.com/operator-framework/operator-sdk/pkg/ansible/runner"
2729
"github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi"
2830

2931
"github.com/sirupsen/logrus"
32+
"k8s.io/api/core/v1"
3033
apierrors "k8s.io/apimachinery/pkg/api/errors"
3134
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3235
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -80,7 +83,9 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
8083
finalizers := append(pendingFinalizers, finalizer)
8184
u.SetFinalizers(finalizers)
8285
err := r.Client.Update(context.TODO(), u)
83-
return reconcileResult, err
86+
if err != nil {
87+
return reconcileResult, err
88+
}
8489
}
8590
if !contains(pendingFinalizers, finalizer) && deleted {
8691
logrus.Info("Resource is terminated, skipping reconcilation")
@@ -96,34 +101,32 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
96101
if err != nil {
97102
return reconcileResult, err
98103
}
99-
reconcileResult.Requeue = true
100-
return reconcileResult, nil
101104
}
102-
status := u.Object["status"]
103-
_, ok = status.(map[string]interface{})
104-
if !ok {
105-
logrus.Debugf("status was not found")
106-
u.Object["status"] = map[string]interface{}{}
105+
statusInterface := u.Object["status"]
106+
statusMap, _ := statusInterface.(map[string]interface{})
107+
crStatus := ansiblestatus.CreateFromMap(statusMap)
108+
109+
// If there is no current status add that we are working on this resource.
110+
errCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.FailureConditionType)
111+
succCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType)
112+
113+
// If the condition is currently running, making sure that the values are correct.
114+
// If they are the same a no-op, if they are different then it is a good thing we
115+
// are updating it.
116+
if (errCond == nil && succCond == nil) || (succCond != nil && succCond.Reason != ansiblestatus.SuccessfulReason) {
117+
c := ansiblestatus.NewCondition(
118+
ansiblestatus.RunningConditionType,
119+
v1.ConditionTrue,
120+
nil,
121+
ansiblestatus.RunningReason,
122+
ansiblestatus.RunningMessage,
123+
)
124+
ansiblestatus.SetCondition(&crStatus, *c)
125+
u.Object["status"] = crStatus
107126
err = r.Client.Update(context.TODO(), u)
108127
if err != nil {
109128
return reconcileResult, err
110129
}
111-
reconcileResult.Requeue = true
112-
return reconcileResult, nil
113-
}
114-
115-
// If status is an empty map we can assume CR was just created
116-
if len(u.Object["status"].(map[string]interface{})) == 0 {
117-
logrus.Debugf("Setting phase status to %v", StatusPhaseCreating)
118-
u.Object["status"] = ResourceStatus{
119-
Phase: StatusPhaseCreating,
120-
}
121-
err = r.Client.Update(context.TODO(), u)
122-
if err != nil {
123-
return reconcileResult, err
124-
}
125-
reconcileResult.Requeue = true
126-
return reconcileResult, nil
127130
}
128131

129132
ownerRef := metav1.OwnerReference{
@@ -145,11 +148,12 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
145148

146149
// iterate events from ansible, looking for the final one
147150
statusEvent := eventapi.StatusJobEvent{}
151+
failureMessages := eventapi.FailureMessages{}
148152
for event := range eventChan {
149153
for _, eHandler := range r.EventHandlers {
150154
go eHandler.Handle(u, event)
151155
}
152-
if event.Event == "playbook_on_stats" {
156+
if event.Event == eventapi.EventPlaybookOnStats {
153157
// convert to StatusJobEvent; would love a better way to do this
154158
data, err := json.Marshal(event)
155159
if err != nil {
@@ -160,6 +164,9 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
160164
return reconcile.Result{}, err
161165
}
162166
}
167+
if event.Event == eventapi.EventRunnerOnFailed {
168+
failureMessages = append(failureMessages, event.GetFailedPlaybookMessage())
169+
}
163170
}
164171
if statusEvent.Event == "" {
165172
err := errors.New("did not receive playbook_on_stats event")
@@ -168,14 +175,7 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
168175
}
169176

170177
// We only want to update the CustomResource once, so we'll track changes and do it at the end
171-
var needsUpdate bool
172-
runSuccessful := true
173-
for _, count := range statusEvent.EventData.Failures {
174-
if count > 0 {
175-
runSuccessful = false
176-
break
177-
}
178-
}
178+
runSuccessful := len(failureMessages) == 0
179179
// The finalizer has run successfully, time to remove it
180180
if deleted && finalizerExists && runSuccessful {
181181
finalizers := []string{}
@@ -185,31 +185,42 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
185185
}
186186
}
187187
u.SetFinalizers(finalizers)
188-
needsUpdate = true
189-
}
190-
191-
statusMap, ok := u.Object["status"].(map[string]interface{})
192-
if !ok {
193-
u.Object["status"] = ResourceStatus{
194-
Status: NewStatusFromStatusJobEvent(statusEvent),
195-
}
196-
logrus.Infof("adding status for the first time")
197-
needsUpdate = true
198-
} else {
199-
// Need to conver the map[string]interface into a resource status.
200-
if update, status := UpdateResourceStatus(statusMap, statusEvent); update {
201-
u.Object["status"] = status
202-
needsUpdate = true
188+
err := r.Client.Update(context.TODO(), u)
189+
if err != nil {
190+
return reconcileResult, err
203191
}
204192
}
205-
if needsUpdate {
206-
err = r.Client.Update(context.TODO(), u)
207-
}
193+
ansibleStatus := ansiblestatus.NewAnsibleResultFromStatusJobEvent(statusEvent)
194+
208195
if !runSuccessful {
209-
reconcileResult.Requeue = true
210-
return reconcileResult, err
211-
}
196+
sc := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType)
197+
sc.Status = v1.ConditionFalse
198+
ansiblestatus.SetCondition(&crStatus, *sc)
199+
c := ansiblestatus.NewCondition(
200+
ansiblestatus.FailureConditionType,
201+
v1.ConditionTrue,
202+
ansibleStatus,
203+
ansiblestatus.FailedReason,
204+
strings.Join(failureMessages, "\n"),
205+
)
206+
ansiblestatus.SetCondition(&crStatus, *c)
207+
} else {
208+
c := ansiblestatus.NewCondition(
209+
ansiblestatus.RunningConditionType,
210+
v1.ConditionTrue,
211+
ansibleStatus,
212+
ansiblestatus.SuccessfulReason,
213+
ansiblestatus.SuccessfulMessage,
214+
)
215+
// Remove the failure condition if set, because this completed successfully.
216+
ansiblestatus.RemoveCondition(&crStatus, ansiblestatus.FailureConditionType)
217+
ansiblestatus.SetCondition(&crStatus, *c)
218+
}
219+
// This needs the status subresource to be enabled by default.
220+
u.Object["status"] = crStatus
221+
err = r.Client.Update(context.TODO(), u)
212222
return reconcileResult, err
223+
213224
}
214225

215226
func contains(l []string, s string) bool {
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright 2018 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package status
16+
17+
import (
18+
"time"
19+
20+
"github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi"
21+
"github.com/sirupsen/logrus"
22+
"k8s.io/api/core/v1"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
)
25+
26+
const (
27+
host = "localhost"
28+
)
29+
30+
// AnsibleResult - encapsulation of the ansible result.
31+
type AnsibleResult struct {
32+
Ok int `json:"ok"`
33+
Changed int `json:"changed"`
34+
Skipped int `json:"skipped"`
35+
Failures int `json:"failures"`
36+
TimeOfCompletion eventapi.EventTime `json:"completion"`
37+
}
38+
39+
// NewAnsibleResultFromStatusJobEvent - creates a Ansible status from job event.
40+
func NewAnsibleResultFromStatusJobEvent(je eventapi.StatusJobEvent) *AnsibleResult {
41+
// ok events.
42+
a := &AnsibleResult{TimeOfCompletion: je.Created}
43+
if v, ok := je.EventData.Changed[host]; ok {
44+
a.Changed = v
45+
}
46+
if v, ok := je.EventData.Ok[host]; ok {
47+
a.Ok = v
48+
}
49+
if v, ok := je.EventData.Skipped[host]; ok {
50+
a.Skipped = v
51+
}
52+
if v, ok := je.EventData.Failures[host]; ok {
53+
a.Failures = v
54+
}
55+
return a
56+
}
57+
58+
// NewAnsibleResultFromMap - creates a Ansible status from a job event.
59+
func NewAnsibleResultFromMap(sm map[string]interface{}) *AnsibleResult {
60+
//Create Old top level status
61+
// ok events.
62+
a := &AnsibleResult{}
63+
if v, ok := sm["changed"]; ok {
64+
a.Changed = int(v.(int64))
65+
}
66+
if v, ok := sm["ok"]; ok {
67+
a.Ok = int(v.(int64))
68+
}
69+
if v, ok := sm["skipped"]; ok {
70+
a.Skipped = int(v.(int64))
71+
}
72+
if v, ok := sm["failures"]; ok {
73+
a.Failures = int(v.(int64))
74+
}
75+
if v, ok := sm["completion"]; ok {
76+
s := v.(string)
77+
a.TimeOfCompletion.UnmarshalJSON([]byte(s))
78+
}
79+
return a
80+
}
81+
82+
// ConditionType - type of condition
83+
type ConditionType string
84+
85+
const (
86+
// RunningConditionType - condition type of running.
87+
RunningConditionType ConditionType = "Running"
88+
// FailureConditionType - condition type of failure.
89+
FailureConditionType ConditionType = "Failure"
90+
)
91+
92+
// Condition - the condition for the ansible operator.
93+
type Condition struct {
94+
Type ConditionType `json:"type"`
95+
Status v1.ConditionStatus `json:"status"`
96+
LastTransitionTime metav1.Time `json:"lastTransitionTime"`
97+
AnsibleResult *AnsibleResult `json:"ansibleResult,omitempty"`
98+
Reason string `json:"reason"`
99+
Message string `json:"message"`
100+
}
101+
102+
func createConditionFromMap(cm map[string]interface{}) Condition {
103+
ct, ok := cm["type"].(string)
104+
if !ok {
105+
//If we do not find the string we are defaulting
106+
// to make sure we can at least update the status.
107+
ct = string(RunningConditionType)
108+
}
109+
status, ok := cm["status"].(string)
110+
if !ok {
111+
status = string(v1.ConditionTrue)
112+
}
113+
reason, ok := cm["reason"].(string)
114+
if !ok {
115+
reason = RunningReason
116+
}
117+
message, ok := cm["message"].(string)
118+
if !ok {
119+
message = RunningMessage
120+
}
121+
asm, ok := cm["ansibleStatus"].(map[string]interface{})
122+
var ansibleResult *AnsibleResult
123+
if ok {
124+
ansibleResult = NewAnsibleResultFromMap(asm)
125+
}
126+
ltts, ok := cm["lastTransitionTime"].(string)
127+
ltt := metav1.Now()
128+
if ok {
129+
t, err := time.Parse("2006-01-02T15:04:05Z", ltts)
130+
if err != nil {
131+
logrus.Warningf("unable to parse time for status condition: %v", ltts)
132+
} else {
133+
ltt = metav1.NewTime(t)
134+
}
135+
}
136+
return Condition{
137+
Type: ConditionType(ct),
138+
Status: v1.ConditionStatus(status),
139+
LastTransitionTime: ltt,
140+
Reason: reason,
141+
Message: message,
142+
AnsibleResult: ansibleResult,
143+
}
144+
}
145+
146+
// Status - The status for custom resources managed by the operator-sdk.
147+
type Status struct {
148+
Conditions []Condition `json:"conditions"`
149+
}
150+
151+
// CreateFromMap - create a status from the map
152+
func CreateFromMap(statusMap map[string]interface{}) Status {
153+
conditionsInterface, ok := statusMap["conditions"].([]interface{})
154+
if !ok {
155+
return Status{Conditions: []Condition{}}
156+
}
157+
conditions := []Condition{}
158+
for _, ci := range conditionsInterface {
159+
cm, ok := ci.(map[string]interface{})
160+
if !ok {
161+
logrus.Warningf("unknown condition, removing condition: %v", ci)
162+
continue
163+
}
164+
conditions = append(conditions, createConditionFromMap(cm))
165+
}
166+
return Status{Conditions: conditions}
167+
}

0 commit comments

Comments
 (0)