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