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