github.com/verrazzano/verrazzano@v1.7.1/application-operator/controllers/loggingtrait/loggingtrait_controller.go (about) 1 // Copyright (c) 2021, 2022, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package loggingtrait 5 6 import ( 7 "context" 8 errors "errors" 9 "fmt" 10 "github.com/verrazzano/verrazzano/application-operator/controllers/clusters" 11 "github.com/verrazzano/verrazzano/pkg/constants" 12 vzlogInit "github.com/verrazzano/verrazzano/pkg/log" 13 "github.com/verrazzano/verrazzano/pkg/log/vzlog" 14 "os" 15 "strings" 16 17 oamv1alpha1 "github.com/verrazzano/verrazzano/application-operator/apis/oam/v1alpha1" 18 vznav "github.com/verrazzano/verrazzano/application-operator/controllers/navigation" 19 "go.uber.org/zap" 20 corev1 "k8s.io/api/core/v1" 21 k8serrors "k8s.io/apimachinery/pkg/api/errors" 22 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 "k8s.io/apimachinery/pkg/runtime" 25 "k8s.io/apimachinery/pkg/types" 26 ctrl "sigs.k8s.io/controller-runtime" 27 "sigs.k8s.io/controller-runtime/pkg/client" 28 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 29 "sigs.k8s.io/controller-runtime/pkg/reconcile" 30 ) 31 32 // Reconciler constants 33 const ( 34 loggingNamePart = "logging-stdout" 35 errLoggingResource = "cannot add logging sidecar to the resource" 36 configMapAPIVersion = "v1" 37 configMapKind = "ConfigMap" 38 loggingMountPath = "/fluentd/etc/custom.conf" 39 loggingKey = "custom.conf" 40 defaultMode int32 = 400 41 controllerName = "loggingtrait" 42 ) 43 44 // LoggingTraitReconciler reconciles a LoggingTrait object 45 type LoggingTraitReconciler struct { 46 client.Client 47 Log *zap.SugaredLogger 48 Scheme *runtime.Scheme 49 } 50 51 // +kubebuilder:rbac:groups=oam.verrazzano.io,resources=loggingtraits,verbs=get;list;watch;create;update;patch;delete 52 // +kubebuilder:rbac:groups=oam.verrazzano.io,resources=loggingtraits/status,verbs=get;update;patch 53 // +kubebuilder:rbac:groups=core.oam.dev,resources=containerizedworkloads,verbs=get;list; 54 // +kubebuilder:rbac:groups=core.oam.dev,resources=containerizedworkloads/status,verbs=get; 55 // +kubebuilder:rbac:groups=core.oam.dev,resources=workloaddefinitions,verbs=get;list;watch 56 // +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;update;patch;delete 57 // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;update;patch;delete 58 // +kubebuilder:rbac:groups=apps,resources=replicasets,verbs=get;list;watch;update;patch;delete 59 // +kubebuilder:rbac:groups=apps,resources=pods,verbs=get;list;watch;update;patch;delete 60 61 func (r *LoggingTraitReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 62 if ctx == nil { 63 return ctrl.Result{}, errors.New("context cannot be nil") 64 } 65 66 // We do not want any resource to get reconciled if it is in namespace kube-system 67 // This is due to a bug found in OKE, it should not affect functionality of any vz operators 68 // If this is the case then return success 69 if req.Namespace == constants.KubeSystem { 70 log := zap.S().With(vzlogInit.FieldResourceNamespace, req.Namespace, vzlogInit.FieldResourceName, req.Name, vzlogInit.FieldController, controllerName) 71 log.Infof("Logging trait resource %v should not be reconciled in kube-system namespace, ignoring", req.NamespacedName) 72 return reconcile.Result{}, nil 73 } 74 75 var err error 76 var trait *oamv1alpha1.LoggingTrait 77 if trait, err = r.fetchTrait(ctx, req.NamespacedName, zap.S()); err != nil || trait == nil { 78 return clusters.IgnoreNotFoundWithLog(err, zap.S()) 79 } 80 log, err := clusters.GetResourceLogger("loggingtrait", req.NamespacedName, trait) 81 if err != nil { 82 zap.S().Errorf("Failed to create controller logger for logging trait resource: %v", err) 83 return clusters.NewRequeueWithDelay(), nil 84 } 85 log.Oncef("Reconciling logging trait resource %v, generation %v", req.NamespacedName, trait.Generation) 86 87 res, err := r.doReconcile(ctx, trait, log) 88 if clusters.ShouldRequeue(res) { 89 return res, nil 90 } 91 // Never return an error since it has already been logged and we don't want the 92 // controller runtime to log again (with stack trace). Just re-queue if there is an error. 93 if err != nil { 94 return clusters.NewRequeueWithDelay(), nil 95 } 96 97 log.Oncef("Finished reconciling logging trait %v", req.NamespacedName) 98 99 return ctrl.Result{}, nil 100 } 101 102 // doReconcile performs the reconciliation operations for the logging trait 103 func (r *LoggingTraitReconciler) doReconcile(ctx context.Context, trait *oamv1alpha1.LoggingTrait, log vzlog.VerrazzanoLogger) (ctrl.Result, error) { 104 if trait.DeletionTimestamp.IsZero() { 105 result, supported, err := r.reconcileTraitCreateOrUpdate(ctx, log, trait) 106 if err != nil { 107 return result, err 108 } 109 if !supported { 110 // If the workload kind is not supported then delete the trait 111 log.Debugf("Deleting trait %s because workload is not supported", trait.Name) 112 113 err = r.Client.Delete(context.TODO(), trait, &client.DeleteOptions{}) 114 115 } 116 return result, err 117 } 118 119 return r.reconcileTraitDelete(ctx, log, trait) 120 } 121 122 // reconcileTraitDelete reconciles a logging trait that is being deleted. 123 func (r *LoggingTraitReconciler) reconcileTraitDelete(ctx context.Context, log vzlog.VerrazzanoLogger, trait *oamv1alpha1.LoggingTrait) (ctrl.Result, error) { 124 // Retrieve the workload the trait is related to 125 workload, err := vznav.FetchWorkloadFromTrait(ctx, r, log, trait) 126 if err != nil || workload == nil { 127 return reconcile.Result{}, err 128 } 129 if workload.GetKind() == "VerrazzanoCoherenceWorkload" || workload.GetKind() == "VerrazzanoWebLogicWorkload" { 130 return reconcile.Result{}, nil 131 } 132 133 // Retrieve the child resources of the workload 134 resources, err := vznav.FetchWorkloadChildren(ctx, r, log, workload) 135 if err != nil { 136 log.Errorw(fmt.Sprintf("Failed to retrieve the workloads child resources: %v", err), "workload", workload.UnstructuredContent()) 137 } 138 139 // If there are no child resources fallback to the workload 140 if len(resources) == 0 { 141 resources = append(resources, workload) 142 } 143 144 for _, resource := range resources { 145 isCombined := false 146 configMapName := loggingNamePart + "-" + resource.GetName() + "-" + strings.ToLower(resource.GetKind()) 147 148 if ok, containersFieldPath := locateContainersField(resource); ok { 149 resourceContainers, ok, err := unstructured.NestedSlice(resource.Object, containersFieldPath...) 150 if !ok || err != nil { 151 log.Errorf("Failed to gather resource containers: %v", err) 152 return reconcile.Result{}, err 153 } 154 155 var image string 156 if len(trait.Spec.LoggingImage) != 0 { 157 image = trait.Spec.LoggingImage 158 } else { 159 image = os.Getenv("DEFAULT_FLUENTD_IMAGE") 160 } 161 envFluentd := &corev1.EnvVar{ 162 Name: "FLUENTD_CONF", 163 Value: "custom.conf", 164 } 165 loggingContainer := &corev1.Container{ 166 Name: loggingNamePart, 167 Image: image, 168 ImagePullPolicy: corev1.PullPolicy(trait.Spec.ImagePullPolicy), 169 Env: []corev1.EnvVar{*envFluentd}, 170 } 171 172 repeatNo := 0 173 repeat := false 174 for i, resContainer := range resourceContainers { 175 if loggingContainer.Name == resContainer.(map[string]interface{})["name"] { 176 repeat = true 177 repeatNo = i 178 break 179 } 180 } 181 if repeat { 182 resourceContainers[repeatNo] = resourceContainers[len(resourceContainers)-1] 183 resourceContainers = resourceContainers[:len(resourceContainers)-1] 184 } 185 err = unstructured.SetNestedSlice(resource.Object, resourceContainers, containersFieldPath...) 186 if err != nil { 187 log.Errorf("Failed to set resource containers: %v", err) 188 return reconcile.Result{}, err 189 } 190 191 isCombined = true 192 193 } 194 195 if ok, volumesFieldPath := locateVolumesField(resource); ok { 196 resourceVolumes, ok, err := unstructured.NestedSlice(resource.Object, volumesFieldPath...) 197 if err != nil { 198 log.Errorf("Failed to gather resource volumes: %v", err) 199 return reconcile.Result{}, err 200 } else if !ok { 201 log.Debug("No volumes found") 202 } 203 204 loggingVolume := &corev1.Volume{ 205 Name: configMapName, 206 VolumeSource: corev1.VolumeSource{ 207 ConfigMap: &corev1.ConfigMapVolumeSource{ 208 LocalObjectReference: corev1.LocalObjectReference{ 209 Name: configMapName, 210 }, 211 DefaultMode: func(mode int32) *int32 { 212 return &mode 213 }(defaultMode), 214 }, 215 }, 216 } 217 218 repeatNo := 0 219 repeat := false 220 for i, resVolume := range resourceVolumes { 221 if loggingVolume.Name == resVolume.(map[string]interface{})["name"] { 222 log.Debugw("Volume was discarded because of duplicate names", "volume name", loggingVolume.Name) 223 repeat = true 224 repeatNo = i 225 break 226 } 227 } 228 if repeat { 229 resourceVolumes[repeatNo] = resourceVolumes[len(resourceVolumes)-1] 230 resourceVolumes = resourceVolumes[:len(resourceVolumes)-1] 231 } 232 233 err = unstructured.SetNestedSlice(resource.Object, resourceVolumes, volumesFieldPath...) 234 if err != nil { 235 log.Errorf("Failed to set resource containers: %v", err) 236 return reconcile.Result{}, err 237 } 238 239 isCombined = true 240 241 } 242 243 if isCombined { 244 // make a copy of the resource spec since resource.Object will get overwritten in CreateOrUpdate 245 // if the resource exists 246 specCopy, _, err := unstructured.NestedFieldCopy(resource.Object, "spec") 247 if err != nil { 248 log.Errorf("Failed to make a copy of the spec: %v", err) 249 return reconcile.Result{}, err 250 } 251 252 _, err = controllerutil.CreateOrUpdate(ctx, r.Client, resource, func() error { 253 return unstructured.SetNestedField(resource.Object, specCopy, "spec") 254 }) 255 if err != nil { 256 log.Errorf("Failed creating or updating resource: %v", err) 257 return reconcile.Result{}, err 258 } 259 log.Debugw("Successfully removed logging from resource", "resource GVK", resource.GroupVersionKind().String()) 260 } 261 262 r.deleteLoggingConfigMap(ctx, trait, resource) 263 264 } 265 266 return reconcile.Result{}, nil 267 } 268 269 // fetchTrait attempts to get a trait given a namespaced name. 270 // Will return nil for the trait and no error if the trait does not exist. 271 func (r *LoggingTraitReconciler) fetchTrait(ctx context.Context, name types.NamespacedName, log *zap.SugaredLogger) (*oamv1alpha1.LoggingTrait, error) { 272 var trait oamv1alpha1.LoggingTrait 273 log.Debugw("Fetch trait", "trait", name) 274 if err := r.Get(ctx, name, &trait); err != nil { 275 if k8serrors.IsNotFound(err) { 276 log.Debug("Trait has been deleted") 277 return nil, nil 278 } 279 log.Debug("Failed to fetch trait") 280 return nil, err 281 } 282 return &trait, nil 283 } 284 285 func (r *LoggingTraitReconciler) reconcileTraitCreateOrUpdate(ctx context.Context, log vzlog.VerrazzanoLogger, trait *oamv1alpha1.LoggingTrait) (ctrl.Result, bool, error) { 286 287 // Retrieve the workload the trait is related to 288 workload, err := vznav.FetchWorkloadFromTrait(ctx, r, log, trait) 289 if err != nil || workload == nil { 290 return reconcile.Result{}, true, err 291 } 292 if workload.GetKind() == "VerrazzanoCoherenceWorkload" || workload.GetKind() == "VerrazzanoWebLogicWorkload" { 293 return reconcile.Result{}, true, nil 294 } 295 // Retrieve the child resources of the workload 296 resources, err := vznav.FetchWorkloadChildren(ctx, r, log, workload) 297 if err != nil { 298 log.Errorw(fmt.Sprintf("Failed to retrieve the workloads child resources: %v", err), "workload", workload.UnstructuredContent()) 299 } 300 301 // If there are no child resources fallback to the workload 302 if len(resources) == 0 { 303 resources = append(resources, workload) 304 } 305 306 isFound := false 307 for _, resource := range resources { 308 isCombined := false 309 configMapName := loggingNamePart + "-" + resource.GetName() + "-" + strings.ToLower(resource.GetKind()) 310 311 if ok, containersFieldPath := locateContainersField(resource); ok { 312 resourceContainers, ok, err := unstructured.NestedSlice(resource.Object, containersFieldPath...) 313 if !ok || err != nil { 314 log.Errorf("Failed to gather resource containers: %v", err) 315 return reconcile.Result{}, true, err 316 } 317 loggingVolumeMount := &corev1.VolumeMount{ 318 MountPath: loggingMountPath, 319 Name: configMapName, 320 SubPath: loggingKey, 321 ReadOnly: true, 322 } 323 uLoggingVolumeMount, err := struct2Unmarshal(loggingVolumeMount) 324 if err != nil { 325 log.Errorf("Failed to unmarshal a volumeMount for logging: %v", err) 326 } 327 328 var volumeMountFieldPath = []string{"volumeMounts"} 329 var resourceVolumeMounts []interface{} 330 for _, resContainer := range resourceContainers { 331 volumeMounts, ok, err := unstructured.NestedSlice(resContainer.(map[string]interface{}), volumeMountFieldPath...) 332 if err != nil { 333 log.Errorf("Failed to gather resource container volumeMounts: %v", err) 334 return reconcile.Result{}, true, err 335 } else if !ok { 336 log.Debug("No volumeMounts found") 337 } 338 resourceVolumeMounts = appendSliceOfInterface(resourceVolumeMounts, volumeMounts) 339 340 } 341 iVolumeMount := -1 342 for i, cVolumeMount := range resourceVolumeMounts { 343 if cVolumeMount.(map[string]interface{})["mountPath"] == uLoggingVolumeMount.Object["mountPath"] { 344 iVolumeMount = i 345 } 346 } 347 if iVolumeMount == -1 { 348 resourceVolumeMounts = append(resourceVolumeMounts, uLoggingVolumeMount.Object) 349 } 350 envFluentd := &corev1.EnvVar{ 351 Name: "FLUENTD_CONF", 352 Value: "custom.conf", 353 } 354 loggingContainer := &corev1.Container{ 355 Name: loggingNamePart, 356 Image: trait.Spec.LoggingImage, 357 ImagePullPolicy: corev1.PullPolicy(trait.Spec.ImagePullPolicy), 358 Env: []corev1.EnvVar{*envFluentd}, 359 } 360 361 uLoggingContainer, err := struct2Unmarshal(loggingContainer) 362 if err != nil { 363 log.Errorf("Failed to unmarshal a container for logging: %v", err) 364 } 365 366 err = unstructured.SetNestedSlice(uLoggingContainer.Object, resourceVolumeMounts, volumeMountFieldPath...) 367 if err != nil { 368 log.Errorf("Failed to set container volumeMounts: %v", err) 369 return reconcile.Result{}, true, err 370 } 371 372 repeatNo := 0 373 repeat := false 374 for i, resContainer := range resourceContainers { 375 if loggingContainer.Name == resContainer.(map[string]interface{})["name"] { 376 repeat = true 377 repeatNo = i 378 break 379 } 380 } 381 if repeat { 382 resourceContainers[repeatNo] = uLoggingContainer.Object 383 } else { 384 resourceContainers = append(resourceContainers, uLoggingContainer.Object) 385 } 386 387 err = unstructured.SetNestedSlice(resource.Object, resourceContainers, containersFieldPath...) 388 if err != nil { 389 log.Errorf("Failed to set resource containers: %v", err) 390 return reconcile.Result{}, true, err 391 } 392 393 isCombined = true 394 isFound = true 395 396 } 397 398 if ok, volumesFieldPath := locateVolumesField(resource); ok { 399 resourceVolumes, ok, err := unstructured.NestedSlice(resource.Object, volumesFieldPath...) 400 if err != nil { 401 log.Errorf("Failed to gather resource volumes: %v", err) 402 return reconcile.Result{}, true, err 403 } else if !ok { 404 log.Debug("No volumes found") 405 } 406 407 loggingVolume := &corev1.Volume{ 408 Name: configMapName, 409 VolumeSource: corev1.VolumeSource{ 410 ConfigMap: &corev1.ConfigMapVolumeSource{ 411 LocalObjectReference: corev1.LocalObjectReference{ 412 Name: configMapName, 413 }, 414 DefaultMode: func(mode int32) *int32 { 415 return &mode 416 }(defaultMode), 417 }, 418 }, 419 } 420 uLoggingVolume, err := struct2Unmarshal(loggingVolume) 421 if err != nil { 422 log.Errorf("Failed unmarshalling logging volume: %v", err) 423 } 424 425 repeatNo := 0 426 repeat := false 427 for i, resVolume := range resourceVolumes { 428 if loggingVolume.Name == resVolume.(map[string]interface{})["name"] { 429 log.Debugw("Volume was discarded because of duplicate names", "volume name", loggingVolume.Name) 430 repeat = true 431 repeatNo = i 432 break 433 } 434 } 435 if repeat { 436 resourceVolumes[repeatNo] = uLoggingVolume.Object 437 } else { 438 resourceVolumes = append(resourceVolumes, uLoggingVolume.Object) 439 } 440 441 err = unstructured.SetNestedSlice(resource.Object, resourceVolumes, volumesFieldPath...) 442 if err != nil { 443 log.Errorf("Failed to set resource volumes: %v", err) 444 return reconcile.Result{}, true, err 445 } 446 447 isFound = true 448 isCombined = true 449 450 } 451 452 if isCombined { 453 if isFound { 454 455 r.ensureLoggingConfigMapExists(ctx, trait, resource) 456 } 457 // make a copy of the resource spec since resource.Object will get overwritten in CreateOrUpdate 458 // if the resource exists 459 specCopy, _, err := unstructured.NestedFieldCopy(resource.Object, "spec") 460 if err != nil { 461 log.Errorf("Failed to make a copy of the spec: %v", err) 462 r.deleteLoggingConfigMap(ctx, trait, resource) 463 return reconcile.Result{}, true, err 464 } 465 466 _, err = controllerutil.CreateOrUpdate(ctx, r.Client, resource, func() error { 467 return unstructured.SetNestedField(resource.Object, specCopy, "spec") 468 }) 469 if err != nil { 470 log.Errorf("Failed creating or updating resource: %v", err) 471 r.deleteLoggingConfigMap(ctx, trait, resource) 472 return reconcile.Result{}, true, err 473 } 474 log.Debugw("Successfully deploy logging to resource", "resource GVK", resource.GroupVersionKind().String()) 475 } 476 477 if !isFound { 478 log.Debugw("Cannot locate any resource", "total resources", len(resources)) 479 return reconcile.Result{}, false, fmt.Errorf(errLoggingResource) 480 } 481 482 } 483 484 return reconcile.Result{}, true, nil 485 } 486 487 // ensureLoggingConfigMapExists ensures that the FLUENTD configmap exists. If it already exists, there is nothing 488 // to do. If it doesn't exist, create it. 489 func (r *LoggingTraitReconciler) ensureLoggingConfigMapExists(ctx context.Context, trait *oamv1alpha1.LoggingTrait, resource *unstructured.Unstructured) error { 490 // check if configmap exists 491 configMapName := loggingNamePart + "-" + resource.GetName() + "-" + strings.ToLower(resource.GetKind()) 492 configMapExists, err := resourceExists(ctx, r, configMapAPIVersion, configMapKind, configMapName, resource.GetNamespace()) 493 if err != nil { 494 return err 495 } 496 497 if !configMapExists { 498 if err = r.Create(ctx, r.createLoggingConfigMap(trait, resource), &client.CreateOptions{}); err != nil { 499 return err 500 } 501 } 502 return err 503 } 504 505 // createLoggingConfigMap returns a configmap based on the logging trait 506 func (r *LoggingTraitReconciler) createLoggingConfigMap(trait *oamv1alpha1.LoggingTrait, resource *unstructured.Unstructured) *corev1.ConfigMap { 507 configMapName := loggingNamePart + "-" + resource.GetName() + "-" + strings.ToLower(resource.GetKind()) 508 data := make(map[string]string) 509 data["custom.conf"] = trait.Spec.LoggingConfig 510 configMap := &corev1.ConfigMap{ 511 ObjectMeta: metav1.ObjectMeta{ 512 Name: configMapName, 513 Namespace: resource.GetNamespace(), 514 Labels: resource.GetLabels(), 515 }, 516 Data: data, 517 } 518 controllerutil.SetControllerReference(resource, configMap, r.Scheme) 519 return configMap 520 } 521 522 func (r *LoggingTraitReconciler) deleteLoggingConfigMap(ctx context.Context, trait *oamv1alpha1.LoggingTrait, resource *unstructured.Unstructured) error { 523 // check if configmap exists 524 configMapExists, err := resourceExists(ctx, r, configMapAPIVersion, configMapKind, loggingNamePart+"-"+resource.GetName()+"-"+strings.ToLower(resource.GetKind()), resource.GetNamespace()) 525 if configMapExists { 526 return r.Delete(ctx, r.createLoggingConfigMap(trait, resource), &client.DeleteOptions{}) 527 } 528 return err 529 } 530 531 // resourceExists determines whether or not a resource of the given kind identified by the given name and namespace exists 532 func resourceExists(ctx context.Context, r client.Reader, apiVersion string, kind string, name string, namespace string) (bool, error) { 533 resources := unstructured.UnstructuredList{} 534 resources.SetAPIVersion(apiVersion) 535 resources.SetKind(kind) 536 options := []client.ListOption{client.InNamespace(namespace), client.MatchingFields{"metadata.name": name}} 537 err := r.List(ctx, &resources, options...) 538 return len(resources.Items) != 0, err 539 } 540 541 func (r *LoggingTraitReconciler) SetupWithManager(mgr ctrl.Manager) error { 542 return ctrl.NewControllerManagedBy(mgr). 543 For(&oamv1alpha1.LoggingTrait{}). 544 Complete(r) 545 }