internal/manifests/collector/container.go (154 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package collector
import (
"errors"
"fmt"
"sort"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1"
"github.com/aws/amazon-cloudwatch-agent-operator/internal/config"
"github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/adapters"
"github.com/aws/amazon-cloudwatch-agent-operator/internal/naming"
)
// maxPortLen allows us to truncate a port name according to what is considered valid port syntax:
// https://pkg.go.dev/k8s.io/apimachinery/pkg/util/validation#IsValidPortName
const maxPortLen = 15
// Container builds a container for the given collector.
func Container(cfg config.Config, logger logr.Logger, agent v1alpha1.AmazonCloudWatchAgent, addConfig bool) corev1.Container {
image := agent.Spec.Image
if len(image) == 0 {
image = cfg.CollectorImage()
}
ports := getContainerPorts(logger, agent.Spec.Config, agent.Spec.OtelConfig, agent.Spec.Ports)
var volumeMounts []corev1.VolumeMount
argsMap := agent.Spec.Args
if argsMap == nil {
argsMap = map[string]string{}
}
// defines the output (sorted) array for final output
var args []string
// When adding a config via v1alpha1.AmazonCloudWatchAgentSpec.Config, we ensure that it is always the
// first item in the args. At the time of writing, although multiple configs are allowed in the
// cloudwatch agent, the operator has yet to implement such functionality. When multiple configs
// are present they should be merged in a deterministic manner using the order given, and because
// v1alpha1.AmazonCloudWatchAgentSpec.Config is a required field we assume that it will always be the
// "primary" config and in the future additional configs can be appended to the container args in a simple manner.
if addConfig {
volumeMounts = append(volumeMounts, getVolumeMounts(agent.Spec.NodeSelector["kubernetes.io/os"]))
if !agent.Spec.Prometheus.IsEmpty() {
volumeMounts = append(volumeMounts, getPrometheusVolumeMounts(agent.Spec.NodeSelector["kubernetes.io/os"]))
}
}
// ensure that the v1alpha1.AmazonCloudWatchAgentSpec.Args are ordered when moved to container.Args,
// where iterating over a map does not guarantee, so that reconcile will not be fooled by different
// ordering in args.
var sortedArgs []string
for k, v := range argsMap {
sortedArgs = append(sortedArgs, fmt.Sprintf("--%s=%s", k, v))
}
sort.Strings(sortedArgs)
args = append(args, sortedArgs...)
if len(agent.Spec.VolumeMounts) > 0 {
volumeMounts = append(volumeMounts, agent.Spec.VolumeMounts...)
}
var envVars = agent.Spec.Env
if agent.Spec.Env == nil {
envVars = []corev1.EnvVar{}
}
envVars = append(envVars, corev1.EnvVar{
Name: "POD_NAME",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.name",
},
},
})
if agent.Spec.TargetAllocator.Enabled {
// We need to add a SHARD here so the collector is able to keep targets after the hashmod operation which is
// added by default by the Prometheus operator's config generator.
// All collector instances use SHARD == 0 as they only receive targets
// allocated to them and should not use the Prometheus hashmod-based
// allocation.
envVars = append(envVars, corev1.EnvVar{
Name: "SHARD",
Value: "0",
})
}
if _, err := adapters.ConfigFromJSONString(agent.Spec.Config); err != nil {
logger.Error(err, "error parsing config")
}
var livenessProbe *corev1.Probe
if configFromString, err := adapters.ConfigFromString(agent.Spec.OtelConfig); err == nil {
if probe, err := getLivenessProbe(configFromString, agent.Spec.LivenessProbe); err == nil {
livenessProbe = probe
} else if errors.Is(err, adapters.ErrNoServiceExtensions) {
logger.Info("extensions not configured, skipping liveness probe creation")
} else if errors.Is(err, adapters.ErrNoServiceExtensionHealthCheck) {
logger.Info("healthcheck extension not configured, skipping liveness probe creation")
} else {
logger.Error(err, "cannot create liveness probe.")
}
}
return corev1.Container{
Name: naming.Container(),
Image: image,
ImagePullPolicy: agent.Spec.ImagePullPolicy,
WorkingDir: agent.Spec.WorkingDir,
VolumeMounts: volumeMounts,
Args: args,
Env: envVars,
EnvFrom: agent.Spec.EnvFrom,
Resources: agent.Spec.Resources,
Ports: portMapToContainerPortList(ports),
SecurityContext: agent.Spec.SecurityContext,
LivenessProbe: livenessProbe,
Lifecycle: agent.Spec.Lifecycle,
}
}
func getVolumeMounts(os string) corev1.VolumeMount {
var volumeMount corev1.VolumeMount
if os == "windows" {
volumeMount = corev1.VolumeMount{
Name: naming.ConfigMapVolume(),
MountPath: "C:\\Program Files\\Amazon\\AmazonCloudWatchAgent\\cwagentconfig",
}
} else {
volumeMount = corev1.VolumeMount{
Name: naming.ConfigMapVolume(),
MountPath: "/etc/cwagentconfig",
}
}
return volumeMount
}
func getPrometheusVolumeMounts(os string) corev1.VolumeMount {
var volumeMount corev1.VolumeMount
if os == "windows" {
volumeMount = corev1.VolumeMount{
Name: naming.PrometheusConfigMapVolume(),
MountPath: "C:\\Program Files\\Amazon\\AmazonCloudWatchAgent\\prometheusconfig",
}
} else {
volumeMount = corev1.VolumeMount{
Name: naming.PrometheusConfigMapVolume(),
MountPath: "/etc/prometheusconfig",
}
}
return volumeMount
}
func portMapToContainerPortList(portMap map[string]corev1.ContainerPort) []corev1.ContainerPort {
ports := make([]corev1.ContainerPort, 0, len(portMap))
for _, p := range portMap {
ports = append(ports, p)
}
sort.Slice(ports, func(i, j int) bool {
return ports[i].Name < ports[j].Name
})
return ports
}
func getLivenessProbe(config map[interface{}]interface{}, probeConfig *v1alpha1.Probe) (*corev1.Probe, error) {
probe, err := adapters.ConfigToContainerProbe(config)
if err != nil {
return nil, err
}
if probeConfig != nil {
if probeConfig.InitialDelaySeconds != nil {
probe.InitialDelaySeconds = *probeConfig.InitialDelaySeconds
}
if probeConfig.PeriodSeconds != nil {
probe.PeriodSeconds = *probeConfig.PeriodSeconds
}
if probeConfig.FailureThreshold != nil {
probe.FailureThreshold = *probeConfig.FailureThreshold
}
if probeConfig.SuccessThreshold != nil {
probe.SuccessThreshold = *probeConfig.SuccessThreshold
}
if probeConfig.TimeoutSeconds != nil {
probe.TimeoutSeconds = *probeConfig.TimeoutSeconds
}
probe.TerminationGracePeriodSeconds = probeConfig.TerminationGracePeriodSeconds
}
return probe, nil
}