github.com/jmrodri/operator-sdk@v0.5.0/pkg/ansible/controller/reconcile.go (about) 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 controller 16 17 import ( 18 "context" 19 "encoding/json" 20 "errors" 21 "math/rand" 22 "os" 23 "strconv" 24 "strings" 25 "time" 26 27 ansiblestatus "github.com/operator-framework/operator-sdk/pkg/ansible/controller/status" 28 "github.com/operator-framework/operator-sdk/pkg/ansible/events" 29 "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/kubeconfig" 30 "github.com/operator-framework/operator-sdk/pkg/ansible/runner" 31 "github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi" 32 33 v1 "k8s.io/api/core/v1" 34 apierrors "k8s.io/apimachinery/pkg/api/errors" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 37 "k8s.io/apimachinery/pkg/runtime/schema" 38 "k8s.io/apimachinery/pkg/types" 39 "sigs.k8s.io/controller-runtime/pkg/client" 40 "sigs.k8s.io/controller-runtime/pkg/reconcile" 41 logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" 42 ) 43 44 const ( 45 // ReconcilePeriodAnnotation - annotation used by a user to specify the reconcilation interval for the CR. 46 // To use create a CR with an annotation "ansible.operator-sdk/reconcile-period: 30s" or some other valid 47 // Duration. This will override the operators/or controllers reconcile period for that particular CR. 48 ReconcilePeriodAnnotation = "ansible.operator-sdk/reconcile-period" 49 ) 50 51 // AnsibleOperatorReconciler - object to reconcile runner requests 52 type AnsibleOperatorReconciler struct { 53 GVK schema.GroupVersionKind 54 Runner runner.Runner 55 Client client.Client 56 EventHandlers []events.EventHandler 57 ReconcilePeriod time.Duration 58 ManageStatus bool 59 } 60 61 // Reconcile - handle the event. 62 func (r *AnsibleOperatorReconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) { 63 u := &unstructured.Unstructured{} 64 u.SetGroupVersionKind(r.GVK) 65 err := r.Client.Get(context.TODO(), request.NamespacedName, u) 66 if apierrors.IsNotFound(err) { 67 return reconcile.Result{}, nil 68 } 69 if err != nil { 70 return reconcile.Result{}, err 71 } 72 73 ident := strconv.Itoa(rand.Int()) 74 logger := logf.Log.WithName("reconciler").WithValues( 75 "job", ident, 76 "name", u.GetName(), 77 "namespace", u.GetNamespace(), 78 ) 79 80 reconcileResult := reconcile.Result{RequeueAfter: r.ReconcilePeriod} 81 if ds, ok := u.GetAnnotations()[ReconcilePeriodAnnotation]; ok { 82 duration, err := time.ParseDuration(ds) 83 if err != nil { 84 return reconcileResult, err 85 } 86 reconcileResult.RequeueAfter = duration 87 } 88 89 deleted := u.GetDeletionTimestamp() != nil 90 finalizer, finalizerExists := r.Runner.GetFinalizer() 91 pendingFinalizers := u.GetFinalizers() 92 // If the resource is being deleted we don't want to add the finalizer again 93 if finalizerExists && !deleted && !contains(pendingFinalizers, finalizer) { 94 logger.V(1).Info("Adding finalizer to resource", "Finalizer", finalizer) 95 finalizers := append(pendingFinalizers, finalizer) 96 u.SetFinalizers(finalizers) 97 err := r.Client.Update(context.TODO(), u) 98 if err != nil { 99 return reconcileResult, err 100 } 101 } 102 if !contains(pendingFinalizers, finalizer) && deleted { 103 logger.Info("Resource is terminated, skipping reconcilation") 104 return reconcile.Result{}, nil 105 } 106 107 spec := u.Object["spec"] 108 _, ok := spec.(map[string]interface{}) 109 if !ok { 110 logger.V(1).Info("Spec was not found") 111 u.Object["spec"] = map[string]interface{}{} 112 err = r.Client.Update(context.TODO(), u) 113 if err != nil { 114 return reconcileResult, err 115 } 116 } 117 118 if r.ManageStatus { 119 err = r.markRunning(u, request.NamespacedName) 120 if err != nil { 121 return reconcileResult, err 122 } 123 } 124 125 ownerRef := metav1.OwnerReference{ 126 APIVersion: u.GetAPIVersion(), 127 Kind: u.GetKind(), 128 Name: u.GetName(), 129 UID: u.GetUID(), 130 } 131 132 kc, err := kubeconfig.Create(ownerRef, "http://localhost:8888", u.GetNamespace()) 133 if err != nil { 134 return reconcileResult, err 135 } 136 defer func() { 137 if err := os.Remove(kc.Name()); err != nil { 138 logger.Error(err, "Failed to remove generated kubeconfig file") 139 } 140 }() 141 result, err := r.Runner.Run(ident, u, kc.Name()) 142 if err != nil { 143 return reconcileResult, err 144 } 145 146 // iterate events from ansible, looking for the final one 147 statusEvent := eventapi.StatusJobEvent{} 148 failureMessages := eventapi.FailureMessages{} 149 for event := range result.Events() { 150 for _, eHandler := range r.EventHandlers { 151 go eHandler.Handle(ident, u, event) 152 } 153 if event.Event == eventapi.EventPlaybookOnStats { 154 // convert to StatusJobEvent; would love a better way to do this 155 data, err := json.Marshal(event) 156 if err != nil { 157 return reconcile.Result{}, err 158 } 159 err = json.Unmarshal(data, &statusEvent) 160 if err != nil { 161 return reconcile.Result{}, err 162 } 163 } 164 if event.Event == eventapi.EventRunnerOnFailed { 165 failureMessages = append(failureMessages, event.GetFailedPlaybookMessage()) 166 } 167 } 168 if statusEvent.Event == "" { 169 eventErr := errors.New("did not receive playbook_on_stats event") 170 stdout, err := result.Stdout() 171 if err != nil { 172 logger.Error(err, "Failed to get ansible-runner stdout") 173 return reconcileResult, err 174 } 175 logger.Error(eventErr, stdout) 176 return reconcileResult, eventErr 177 } 178 179 // We only want to update the CustomResource once, so we'll track changes and do it at the end 180 runSuccessful := len(failureMessages) == 0 181 // The finalizer has run successfully, time to remove it 182 if deleted && finalizerExists && runSuccessful { 183 finalizers := []string{} 184 for _, pendingFinalizer := range pendingFinalizers { 185 if pendingFinalizer != finalizer { 186 finalizers = append(finalizers, pendingFinalizer) 187 } 188 } 189 u.SetFinalizers(finalizers) 190 err := r.Client.Update(context.TODO(), u) 191 if err != nil { 192 return reconcileResult, err 193 } 194 } 195 if r.ManageStatus { 196 err = r.markDone(u, request.NamespacedName, statusEvent, failureMessages) 197 if err != nil { 198 logger.Error(err, "Failed to mark status done") 199 } 200 } 201 return reconcileResult, err 202 } 203 204 func (r *AnsibleOperatorReconciler) markRunning(u *unstructured.Unstructured, namespacedName types.NamespacedName) error { 205 // Get the latest resource to prevent updating a stale status 206 err := r.Client.Get(context.TODO(), namespacedName, u) 207 if err != nil { 208 return err 209 } 210 statusInterface := u.Object["status"] 211 statusMap, _ := statusInterface.(map[string]interface{}) 212 crStatus := ansiblestatus.CreateFromMap(statusMap) 213 214 // If there is no current status add that we are working on this resource. 215 errCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.FailureConditionType) 216 217 if errCond != nil { 218 errCond.Status = v1.ConditionFalse 219 ansiblestatus.SetCondition(&crStatus, *errCond) 220 } 221 // If the condition is currently running, making sure that the values are correct. 222 // If they are the same a no-op, if they are different then it is a good thing we 223 // are updating it. 224 c := ansiblestatus.NewCondition( 225 ansiblestatus.RunningConditionType, 226 v1.ConditionTrue, 227 nil, 228 ansiblestatus.RunningReason, 229 ansiblestatus.RunningMessage, 230 ) 231 ansiblestatus.SetCondition(&crStatus, *c) 232 u.Object["status"] = crStatus.GetJSONMap() 233 err = r.Client.Status().Update(context.TODO(), u) 234 if err != nil { 235 return err 236 } 237 return nil 238 } 239 240 func (r *AnsibleOperatorReconciler) markDone(u *unstructured.Unstructured, namespacedName types.NamespacedName, statusEvent eventapi.StatusJobEvent, failureMessages eventapi.FailureMessages) error { 241 logger := logf.Log.WithName("markDone") 242 // Get the latest resource to prevent updating a stale status 243 err := r.Client.Get(context.TODO(), namespacedName, u) 244 if apierrors.IsNotFound(err) { 245 logger.Info("Resource not found, assuming it was deleted", err) 246 return nil 247 } 248 if err != nil { 249 return err 250 } 251 statusInterface := u.Object["status"] 252 statusMap, _ := statusInterface.(map[string]interface{}) 253 crStatus := ansiblestatus.CreateFromMap(statusMap) 254 255 runSuccessful := len(failureMessages) == 0 256 ansibleStatus := ansiblestatus.NewAnsibleResultFromStatusJobEvent(statusEvent) 257 258 if !runSuccessful { 259 sc := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType) 260 sc.Status = v1.ConditionFalse 261 ansiblestatus.SetCondition(&crStatus, *sc) 262 c := ansiblestatus.NewCondition( 263 ansiblestatus.FailureConditionType, 264 v1.ConditionTrue, 265 ansibleStatus, 266 ansiblestatus.FailedReason, 267 strings.Join(failureMessages, "\n"), 268 ) 269 ansiblestatus.SetCondition(&crStatus, *c) 270 } else { 271 c := ansiblestatus.NewCondition( 272 ansiblestatus.RunningConditionType, 273 v1.ConditionTrue, 274 ansibleStatus, 275 ansiblestatus.SuccessfulReason, 276 ansiblestatus.SuccessfulMessage, 277 ) 278 // Remove the failure condition if set, because this completed successfully. 279 ansiblestatus.RemoveCondition(&crStatus, ansiblestatus.FailureConditionType) 280 ansiblestatus.SetCondition(&crStatus, *c) 281 } 282 // This needs the status subresource to be enabled by default. 283 u.Object["status"] = crStatus.GetJSONMap() 284 285 return r.Client.Status().Update(context.TODO(), u) 286 } 287 288 func contains(l []string, s string) bool { 289 for _, elem := range l { 290 if elem == s { 291 return true 292 } 293 } 294 return false 295 }