github.com/verrazzano/verrazzano@v1.7.1/application-operator/controllers/logging/fluentd.go (about)

     1  // Copyright (C) 2020, 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 logging
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"os"
    10  
    11  	"github.com/crossplane/oam-kubernetes-runtime/pkg/oam"
    12  	vzapi "github.com/verrazzano/verrazzano/application-operator/apis/oam/v1alpha1"
    13  	"go.uber.org/zap"
    14  	corev1 "k8s.io/api/core/v1"
    15  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    16  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    17  	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
    18  )
    19  
    20  const (
    21  	FluentdStdoutSidecarName = "fluentd-stdout-sidecar"
    22  	fluentdConfKey           = "fluentd.conf"
    23  	fluentdConfMountPath     = "/fluentd/etc/fluentd.conf"
    24  	configMapName            = "fluentd-config"
    25  	confVolume               = "fluentd-config-volume"
    26  
    27  	scratchVolMountPath = "/scratch"
    28  )
    29  
    30  // DefaultFluentdImage holds the default FLUENTD image that will be used if it is not specified in the logging logInfo
    31  var DefaultFluentdImage string
    32  
    33  func init() {
    34  	DefaultFluentdImage = os.Getenv("DEFAULT_FLUENTD_IMAGE")
    35  }
    36  
    37  // FluentdManager is a general interface to interact with FLUENTD related resources
    38  type FluentdManager interface {
    39  	Apply(logInfo *LogInfo, resource vzapi.QualifiedResourceRelation, fluentdPod *FluentdPod) error
    40  	Remove(logInfo *LogInfo, resource vzapi.QualifiedResourceRelation, fluentdPod *FluentdPod) bool
    41  }
    42  
    43  // Fluentd is an implementation of FluentdManager.
    44  type Fluentd struct {
    45  	k8sclient.Client
    46  	Log                    *zap.SugaredLogger
    47  	Context                context.Context
    48  	ParseRules             string
    49  	StorageVolumeName      string
    50  	StorageVolumeMountPath string
    51  	WorkloadType           string
    52  }
    53  
    54  // FluentdPod contains pod information for pods which require FLUENTD integration
    55  type FluentdPod struct {
    56  	Containers   []corev1.Container
    57  	Volumes      []corev1.Volume
    58  	VolumeMounts []corev1.VolumeMount
    59  	HandlerEnv   []corev1.EnvVar
    60  	LogPath      string
    61  }
    62  
    63  // Apply applies FLUENTD configuration to create/update FLUENTD container, configmap, volumes and volume mounts.
    64  // Returns true if any changes are made; false otherwise.
    65  func (f *Fluentd) Apply(logInfo *LogInfo, resource vzapi.QualifiedResourceRelation, fluentdPod *FluentdPod) error {
    66  	if err := f.ensureFluentdConfigMapExists(resource.Namespace); err != nil {
    67  		return err
    68  	}
    69  
    70  	f.ensureFluentdVolumes(fluentdPod)
    71  	f.ensureFluentdVolumeMountExists(fluentdPod)
    72  	f.ensureFluentdContainer(fluentdPod, logInfo, resource.Namespace)
    73  
    74  	return nil
    75  }
    76  
    77  // Remove removes FLUENTD container, configmap, volumes and volume mounts.
    78  // Returns whether the remove action has been verified so that the caller knows when it is safe to forget the association.
    79  func (f *Fluentd) Remove(logInfo *LogInfo, resource vzapi.QualifiedResourceRelation, fluentdPod *FluentdPod) bool {
    80  	configMapVerified := f.removeFluentdConfigMap(resource.Namespace)
    81  	volumesVerified := f.removeFluentdVolumes(fluentdPod)
    82  	mountsVerified := f.removeFluentdVolumeMounts(fluentdPod)
    83  	containersVerified := f.removeFluentdContainer(fluentdPod)
    84  
    85  	return configMapVerified && volumesVerified && mountsVerified && containersVerified
    86  }
    87  
    88  // ensureFluentdContainer ensures that the FLUENTD container is in the expected state. If a FLUENTD container already
    89  // exists, replace it with a container created with the current logInfo information. If no FLUENTD container already
    90  // exists, create one and add it to the FluentdPod.
    91  func (f *Fluentd) ensureFluentdContainer(fluentdPod *FluentdPod, logInfo *LogInfo, namespace string) {
    92  	containers := fluentdPod.Containers
    93  	fluentdContainerIndex := -1
    94  	// iterate over existing containers looking for FLUENTD container
    95  	for i, container := range containers {
    96  		if container.Name == FluentdStdoutSidecarName {
    97  			// FLUENTD container found, save the index
    98  			fluentdContainerIndex = i
    99  			break
   100  		}
   101  	}
   102  	fluentdContainer := f.createFluentdContainer(fluentdPod, logInfo, namespace)
   103  	if fluentdContainerIndex != -1 {
   104  		// the index is still the initial -1 so we didn't find an existing FLUENTD container so we replace it
   105  		containers[fluentdContainerIndex] = fluentdContainer
   106  	} else {
   107  		// no existing FLUENTD container was found so add it to the list
   108  		containers = append(containers, fluentdContainer)
   109  	}
   110  	fluentdPod.Containers = containers
   111  }
   112  
   113  // ensureFluentdVolumes ensures that the FLUENTD volumes exist. We expect 2 volumes, a FLUENTD volume and a
   114  // FLUENTD config map volume. If these already exist, nothing needs to be done. If they don't already exist,
   115  // create them and add to the FluentdPod.
   116  func (f *Fluentd) ensureFluentdVolumes(fluentdPod *FluentdPod) {
   117  	volumes := fluentdPod.Volumes
   118  	configMapVolumeExists := false
   119  	fluentdVolumeExists := false
   120  	for _, volume := range volumes {
   121  		if volume.Name == f.StorageVolumeName {
   122  			fluentdVolumeExists = true
   123  		} else if volume.Name == fmt.Sprintf("%s-volume", configMapName) {
   124  			configMapVolumeExists = true
   125  		}
   126  	}
   127  	if !configMapVolumeExists {
   128  		volumes = append(volumes, f.createFluentdConfigMapVolume(configMapName))
   129  	}
   130  	if !fluentdVolumeExists {
   131  		volumes = append(volumes, f.createFluentdEmptyDirVolume())
   132  	}
   133  	fluentdPod.Volumes = volumes
   134  }
   135  
   136  // ensureFluentdVolumeMountExists ensures that the FLUENTD volume mount exists. If one already exists, nothing
   137  // needs to be done. If it doesn't already exist create one and add it to the FluentdPod.
   138  func (f *Fluentd) ensureFluentdVolumeMountExists(fluentdPod *FluentdPod) {
   139  	volumeMounts := fluentdPod.VolumeMounts
   140  	storageVolumeMountExists := false
   141  	for _, volumeMount := range volumeMounts {
   142  		if volumeMount.Name == f.StorageVolumeName {
   143  			storageVolumeMountExists = true
   144  		}
   145  	}
   146  
   147  	// If no storage volume mount exists create one and add it to the list.
   148  	if !storageVolumeMountExists {
   149  		volumeMounts = append(volumeMounts, f.createStorageVolumeMount())
   150  	}
   151  
   152  	fluentdPod.VolumeMounts = volumeMounts
   153  }
   154  
   155  // ensureFluentdConfigMapExists ensures that the FLUENTD configmap exists. If it already exists, there is nothing
   156  // to do. If it doesn't exist, create it.
   157  func (f *Fluentd) ensureFluentdConfigMapExists(namespace string) error {
   158  	// check if configmap exists
   159  	configMapExists, err := resourceExists(f.Context, f, configMapAPIVersion, configMapKind, configMapName+"-"+f.WorkloadType, namespace)
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	if configMapExists {
   165  		return f.Update(f.Context, f.createFluentdConfigMap(namespace), &k8sclient.UpdateOptions{})
   166  	}
   167  	return f.Create(f.Context, f.createFluentdConfigMap(namespace), &k8sclient.CreateOptions{})
   168  }
   169  
   170  // createFluentdConfigMap creates the FLUENTD configmap per given namespace.
   171  func (f *Fluentd) createFluentdConfigMap(namespace string) *corev1.ConfigMap {
   172  	return &corev1.ConfigMap{
   173  		ObjectMeta: metav1.ObjectMeta{
   174  			Name:      configMapName + "-" + f.WorkloadType,
   175  			Namespace: namespace,
   176  		},
   177  		Data: func() map[string]string {
   178  			var data = make(map[string]string)
   179  			data[fluentdConfKey] = f.ParseRules
   180  			return data
   181  		}(),
   182  	}
   183  }
   184  
   185  // removeFluentdContainer removes FLUENTD container
   186  func (f *Fluentd) removeFluentdContainer(fluentdPod *FluentdPod) bool {
   187  	containers := fluentdPod.Containers
   188  	fluentdContainerIndex := -1
   189  	for i, container := range containers {
   190  		if container.Name == FluentdStdoutSidecarName {
   191  			fluentdContainerIndex = i
   192  			break
   193  		}
   194  	}
   195  
   196  	if fluentdContainerIndex >= 0 {
   197  		length := len(containers)
   198  		containers[fluentdContainerIndex] = containers[length-1]
   199  		containers = containers[:length-1]
   200  	}
   201  
   202  	fluentdPod.Containers = containers
   203  	// return true when we confirm that the fluentd container has been removed
   204  	return fluentdContainerIndex == -1
   205  }
   206  
   207  // removeFluentdVolumeMounts removes FLUENTD volume mounts
   208  func (f *Fluentd) removeFluentdVolumeMounts(fluentPod *FluentdPod) bool {
   209  	// For now we can't remove the FLUENTD volume mount because we need to keep the logs in scratch
   210  	// since we can't set 'logHomeEnabled' to false for the wls domain.
   211  	return true
   212  }
   213  
   214  // removeFluentdVolumes removes FLUENTD volumes. There are currently 2 volumes, a FLUENTD volume and a
   215  // FLUENTD configmap volume.
   216  // Returns true if we have validated that we have already deleted the volumes; false otherwise. This ensures
   217  // that we don't remove knowledge of the workload until we have validated that it has been fully cleaned up
   218  // in the system.
   219  func (f *Fluentd) removeFluentdVolumes(fluentdPod *FluentdPod) bool {
   220  	// If the FLUENTD configmap volume exists, delete it.
   221  	// For now we can't remove the FLUENTD volume because we need to keep the logs in scratch
   222  	// since we can't set 'logHomeEnabled' to false for the wls domain.
   223  	volumes := fluentdPod.Volumes
   224  	configMapVolumeName := fmt.Sprintf("%s-volume", configMapName)
   225  	configMapVolumeIndex := -1
   226  	for i, volume := range volumes {
   227  		if volume.Name == configMapVolumeName {
   228  			configMapVolumeIndex = i
   229  			break
   230  		}
   231  	}
   232  
   233  	if configMapVolumeIndex >= 0 {
   234  		length := len(volumes)
   235  		volumes[configMapVolumeIndex] = volumes[length-1]
   236  		volumes = volumes[:length-1]
   237  	}
   238  
   239  	fluentdPod.Volumes = volumes
   240  	// return true when we verify that volumes have been removed
   241  	return configMapVolumeIndex == -1
   242  }
   243  
   244  // removeFluentdConfigMap removes the FLUENTD configmap
   245  func (f *Fluentd) removeFluentdConfigMap(namespace string) bool {
   246  	configMapExists, err := resourceExists(f.Context, f, configMapAPIVersion, configMapKind, configMapName+"-"+f.WorkloadType, namespace)
   247  
   248  	if configMapExists {
   249  		_ = f.Delete(f.Context, f.createFluentdConfigMap(namespace), &k8sclient.DeleteOptions{})
   250  	}
   251  	// return true when we confirm that the configmap has been successfully deleted
   252  	return !(configMapExists) && err == nil
   253  }
   254  
   255  // createFluentdContainer creates the FLUENTD stdout sidecar container
   256  func (f *Fluentd) createFluentdContainer(fluentdPod *FluentdPod, logInfo *LogInfo, namespace string) corev1.Container {
   257  	container := corev1.Container{
   258  		Name:            FluentdStdoutSidecarName,
   259  		Args:            []string{"-c", "/etc/fluent.conf"},
   260  		Image:           logInfo.FluentdImage,
   261  		ImagePullPolicy: corev1.PullIfNotPresent,
   262  		Env: []corev1.EnvVar{
   263  			{
   264  				Name:  "LOG_PATH",
   265  				Value: fluentdPod.LogPath,
   266  			},
   267  			{
   268  				Name:  "FLUENTD_CONF",
   269  				Value: fluentdConfKey,
   270  			},
   271  			{
   272  				Name:  "NAMESPACE",
   273  				Value: namespace,
   274  			},
   275  			{
   276  				Name: "APP_CONF_NAME",
   277  				ValueFrom: &corev1.EnvVarSource{
   278  					FieldRef: &corev1.ObjectFieldSelector{
   279  						FieldPath: "metadata.labels['" + oam.LabelAppName + "']",
   280  					},
   281  				},
   282  			},
   283  			{
   284  				Name: "COMPONENT_NAME",
   285  				ValueFrom: &corev1.EnvVarSource{
   286  					FieldRef: &corev1.ObjectFieldSelector{
   287  						FieldPath: "metadata.labels['" + oam.LabelAppComponent + "']",
   288  					},
   289  				},
   290  			},
   291  		},
   292  		VolumeMounts: []corev1.VolumeMount{
   293  			{
   294  				MountPath: fluentdConfMountPath,
   295  				Name:      confVolume,
   296  				SubPath:   fluentdConfKey,
   297  				ReadOnly:  true,
   298  			},
   299  			{
   300  				MountPath: f.StorageVolumeMountPath,
   301  				Name:      f.StorageVolumeName,
   302  				ReadOnly:  true,
   303  			},
   304  		},
   305  	}
   306  
   307  	// add handler specific env vars
   308  	container.Env = append(container.Env, fluentdPod.HandlerEnv...)
   309  
   310  	return container
   311  }
   312  
   313  // createFluentdEmptyDirVolume creates an empty FLUENTD directory volume
   314  func (f *Fluentd) createFluentdEmptyDirVolume() corev1.Volume {
   315  	return corev1.Volume{
   316  		Name: f.StorageVolumeName,
   317  		VolumeSource: corev1.VolumeSource{
   318  			EmptyDir: &corev1.EmptyDirVolumeSource{},
   319  		},
   320  	}
   321  }
   322  
   323  // createFluentdConfigMapVolume creates a FLUENTD configmap volume
   324  func (f *Fluentd) createFluentdConfigMapVolume(name string) corev1.Volume {
   325  	return corev1.Volume{
   326  		Name: fmt.Sprintf("%s-volume", name),
   327  		VolumeSource: corev1.VolumeSource{
   328  			ConfigMap: &corev1.ConfigMapVolumeSource{
   329  				LocalObjectReference: corev1.LocalObjectReference{
   330  					Name: name + "-" + f.WorkloadType,
   331  				},
   332  				DefaultMode: func(mode int32) *int32 {
   333  					return &mode
   334  				}(420),
   335  			},
   336  		},
   337  	}
   338  }
   339  
   340  // createStorageVolumeMount creates a storage volume mount
   341  func (f *Fluentd) createStorageVolumeMount() corev1.VolumeMount {
   342  	return corev1.VolumeMount{
   343  		Name:      f.StorageVolumeName,
   344  		MountPath: f.StorageVolumeMountPath,
   345  	}
   346  }
   347  
   348  // resourceExists determines whether or not a resource of the given kind identified by the given name and namespace exists
   349  func resourceExists(ctx context.Context, r k8sclient.Reader, apiVersion, kind, name, namespace string) (bool, error) {
   350  	resources := unstructured.UnstructuredList{}
   351  	resources.SetAPIVersion(apiVersion)
   352  	resources.SetKind(kind)
   353  	err := r.List(ctx, &resources, k8sclient.InNamespace(namespace), k8sclient.MatchingFields{"metadata.name": name})
   354  	return len(resources.Items) != 0, err
   355  }