Skip to content

Commit

Permalink
Adding status conditions
Browse files Browse the repository at this point in the history
  • Loading branch information
Shawn Hurley committed Oct 18, 2018
1 parent 956ccda commit c470efe
Show file tree
Hide file tree
Showing 7 changed files with 667 additions and 204 deletions.
114 changes: 60 additions & 54 deletions pkg/ansible/controller/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import (
"os"
"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"
Expand Down Expand Up @@ -80,7 +82,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")
Expand All @@ -96,34 +100,28 @@ 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.CreateStatusFromMap(statusMap)

// If the no current status add that we are working on this resourece.
errCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.FailureConditionType)
succCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType)
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{
Expand All @@ -145,11 +143,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 {
Expand All @@ -160,6 +159,9 @@ func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconc
return reconcile.Result{}, err
}
}
if event.Event == eventapi.EventRunnerOnFailed {
failureMessages = append(failureMessages, eventapi.GetFailedPlaybookMessage(event))
}
}
if statusEvent.Event == "" {
err := errors.New("did not receive playbook_on_stats event")
Expand All @@ -168,14 +170,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{}
Expand All @@ -185,31 +180,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.NewAnsibleStatusFromStatusJobEvent(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,
failureMessages.GetMessages(),
)
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 {
Expand Down
186 changes: 186 additions & 0 deletions pkg/ansible/controller/status/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// 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"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
host = "localhost"
)

// AnsibleStatus - encapsulation of the ansible result.
type AnsibleStatus struct {
Ok int `json:"ok"`
Changed int `json:"changed"`
Skipped int `json:"skipped"`
Failures int `json:"failures"`
TimeOfCompletion eventapi.EventTime `json:"completion"`
}

// NewAnsibleStatusFromStatusJobEvent - creates a Ansible status from job event.
func NewAnsibleStatusFromStatusJobEvent(je eventapi.StatusJobEvent) *AnsibleStatus {
// 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 &AnsibleStatus{
Ok: o,
Changed: changed,
Skipped: skipped,
Failures: failures,
TimeOfCompletion: je.Created,
}
}

// NewAnsibleStatusFromMap - creates a Ansible status from a job event.
func NewAnsibleStatusFromMap(sm map[string]interface{}) *AnsibleStatus {
//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 &AnsibleStatus{
Ok: o,
Changed: changed,
Skipped: skipped,
Failures: failures,
TimeOfCompletion: e,
}
}

// 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 meta.Time `json:"lastTransitionTime"`
AnsibleStatus *AnsibleStatus `json:"ansibleStatus"`
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 ansibleStatus *AnsibleStatus
if ok {
ansibleStatus = NewAnsibleStatusFromMap(asm)
}
ltts, ok := cm["lastTransitionTime"].(string)
ltt := meta.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 = meta.NewTime(t)
}
}
return Condition{
Type: ConditionType(ct),
Status: v1.ConditionStatus(status),
LastTransitionTime: ltt,
Reason: reason,
Message: message,
AnsibleStatus: ansibleStatus,
}
}

// Status - The status for custom resoureces managed by the operator-sdk.
type Status struct {
Conditions []Condition `json:"conditions"`
}

// CreateStatusFromMap - create a status from the map
func CreateStatusFromMap(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("unknow condition, remoiong condition: %v", ci)
continue
}
conditions = append(conditions, createConditionFromMap(cm))
}
return Status{Conditions: conditions}
}
Loading

0 comments on commit c470efe

Please sign in to comment.