github.com/lablabs/operator-sdk@v0.8.2/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/metrics" 31 "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/kubeconfig" 32 "github.com/operator-framework/operator-sdk/pkg/ansible/runner" 33 "github.com/operator-framework/operator-sdk/pkg/ansible/runner/eventapi" 34 35 v1 "k8s.io/api/core/v1" 36 apierrors "k8s.io/apimachinery/pkg/api/errors" 37 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 38 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 39 "k8s.io/apimachinery/pkg/runtime/schema" 40 "k8s.io/apimachinery/pkg/types" 41 "sigs.k8s.io/controller-runtime/pkg/client" 42 "sigs.k8s.io/controller-runtime/pkg/reconcile" 43 logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" 44 ) 45 46 const ( 47 // ReconcilePeriodAnnotation - annotation used by a user to specify the reconciliation interval for the CR. 48 // To use create a CR with an annotation "ansible.operator-sdk/reconcile-period: 30s" or some other valid 49 // Duration. This will override the operators/or controllers reconcile period for that particular CR. 50 ReconcilePeriodAnnotation = "ansible.operator-sdk/reconcile-period" 51 ) 52 53 // AnsibleOperatorReconciler - object to reconcile runner requests 54 type AnsibleOperatorReconciler struct { 55 GVK schema.GroupVersionKind 56 Runner runner.Runner 57 Client client.Client 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 // Need to get the unstructured object after ansible 191 // this needs to hit the API 192 err = r.Client.Get(context.TODO(), request.NamespacedName, u) 193 if apierrors.IsNotFound(err) { 194 return reconcile.Result{}, nil 195 } 196 if err != nil { 197 return reconcile.Result{}, err 198 } 199 200 // try to get the updated finalizers 201 pendingFinalizers = u.GetFinalizers() 202 203 // We only want to update the CustomResource once, so we'll track changes 204 // and do it at the end 205 runSuccessful := len(failureMessages) == 0 206 // The finalizer has run successfully, time to remove it 207 if deleted && finalizerExists && runSuccessful { 208 finalizers := []string{} 209 for _, pendingFinalizer := range pendingFinalizers { 210 if pendingFinalizer != finalizer { 211 finalizers = append(finalizers, pendingFinalizer) 212 } 213 } 214 u.SetFinalizers(finalizers) 215 err := r.Client.Update(context.TODO(), u) 216 if err != nil { 217 logger.Error(err, "Failed to remove finalizer") 218 return reconcileResult, err 219 } 220 } 221 if r.ManageStatus { 222 err = r.markDone(u, request.NamespacedName, statusEvent, failureMessages) 223 if err != nil { 224 logger.Error(err, "Failed to mark status done") 225 } 226 } 227 return reconcileResult, err 228 } 229 230 func (r *AnsibleOperatorReconciler) markRunning(u *unstructured.Unstructured, namespacedName types.NamespacedName) error { 231 // Get the latest resource to prevent updating a stale status 232 err := r.Client.Get(context.TODO(), namespacedName, u) 233 if err != nil { 234 return err 235 } 236 statusInterface := u.Object["status"] 237 statusMap, _ := statusInterface.(map[string]interface{}) 238 crStatus := ansiblestatus.CreateFromMap(statusMap) 239 240 // If there is no current status add that we are working on this resource. 241 errCond := ansiblestatus.GetCondition(crStatus, ansiblestatus.FailureConditionType) 242 243 if errCond != nil { 244 errCond.Status = v1.ConditionFalse 245 ansiblestatus.SetCondition(&crStatus, *errCond) 246 } 247 // If the condition is currently running, making sure that the values are correct. 248 // If they are the same a no-op, if they are different then it is a good thing we 249 // are updating it. 250 c := ansiblestatus.NewCondition( 251 ansiblestatus.RunningConditionType, 252 v1.ConditionTrue, 253 nil, 254 ansiblestatus.RunningReason, 255 ansiblestatus.RunningMessage, 256 ) 257 ansiblestatus.SetCondition(&crStatus, *c) 258 u.Object["status"] = crStatus.GetJSONMap() 259 err = r.Client.Status().Update(context.TODO(), u) 260 if err != nil { 261 return err 262 } 263 return nil 264 } 265 266 // markError - used to alert the user to the issues during the validation of a reconcile run. 267 // i.e Annotations that could be incorrect 268 func (r *AnsibleOperatorReconciler) markError(u *unstructured.Unstructured, namespacedName types.NamespacedName, failureMessage string) error { 269 logger := logf.Log.WithName("markError") 270 metrics.ReconcileFailed(r.GVK.String()) 271 // Get the latest resource to prevent updating a stale status 272 err := r.Client.Get(context.TODO(), namespacedName, u) 273 if apierrors.IsNotFound(err) { 274 logger.Info("Resource not found, assuming it was deleted", err) 275 return nil 276 } 277 if err != nil { 278 return err 279 } 280 statusInterface := u.Object["status"] 281 statusMap, ok := statusInterface.(map[string]interface{}) 282 // If the map is not available create one. 283 if !ok { 284 statusMap = map[string]interface{}{} 285 } 286 crStatus := ansiblestatus.CreateFromMap(statusMap) 287 288 sc := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType) 289 if sc != nil { 290 sc.Status = v1.ConditionFalse 291 ansiblestatus.SetCondition(&crStatus, *sc) 292 } 293 294 c := ansiblestatus.NewCondition( 295 ansiblestatus.FailureConditionType, 296 v1.ConditionTrue, 297 nil, 298 ansiblestatus.FailedReason, 299 failureMessage, 300 ) 301 ansiblestatus.SetCondition(&crStatus, *c) 302 // This needs the status subresource to be enabled by default. 303 u.Object["status"] = crStatus.GetJSONMap() 304 305 return r.Client.Status().Update(context.TODO(), u) 306 } 307 308 func (r *AnsibleOperatorReconciler) markDone(u *unstructured.Unstructured, namespacedName types.NamespacedName, statusEvent eventapi.StatusJobEvent, failureMessages eventapi.FailureMessages) error { 309 logger := logf.Log.WithName("markDone") 310 // Get the latest resource to prevent updating a stale status 311 err := r.Client.Get(context.TODO(), namespacedName, u) 312 if apierrors.IsNotFound(err) { 313 logger.Info("Resource not found, assuming it was deleted", err) 314 return nil 315 } 316 if err != nil { 317 return err 318 } 319 statusInterface := u.Object["status"] 320 statusMap, _ := statusInterface.(map[string]interface{}) 321 crStatus := ansiblestatus.CreateFromMap(statusMap) 322 323 runSuccessful := len(failureMessages) == 0 324 ansibleStatus := ansiblestatus.NewAnsibleResultFromStatusJobEvent(statusEvent) 325 326 if !runSuccessful { 327 metrics.ReconcileFailed(r.GVK.String()) 328 sc := ansiblestatus.GetCondition(crStatus, ansiblestatus.RunningConditionType) 329 sc.Status = v1.ConditionFalse 330 ansiblestatus.SetCondition(&crStatus, *sc) 331 c := ansiblestatus.NewCondition( 332 ansiblestatus.FailureConditionType, 333 v1.ConditionTrue, 334 ansibleStatus, 335 ansiblestatus.FailedReason, 336 strings.Join(failureMessages, "\n"), 337 ) 338 ansiblestatus.SetCondition(&crStatus, *c) 339 } else { 340 metrics.ReconcileSucceeded(r.GVK.String()) 341 c := ansiblestatus.NewCondition( 342 ansiblestatus.RunningConditionType, 343 v1.ConditionTrue, 344 ansibleStatus, 345 ansiblestatus.SuccessfulReason, 346 ansiblestatus.SuccessfulMessage, 347 ) 348 // Remove the failure condition if set, because this completed successfully. 349 ansiblestatus.RemoveCondition(&crStatus, ansiblestatus.FailureConditionType) 350 ansiblestatus.SetCondition(&crStatus, *c) 351 } 352 // This needs the status subresource to be enabled by default. 353 u.Object["status"] = crStatus.GetJSONMap() 354 355 return r.Client.Status().Update(context.TODO(), u) 356 } 357 358 func contains(l []string, s string) bool { 359 for _, elem := range l { 360 if elem == s { 361 return true 362 } 363 } 364 return false 365 }