pkg/controller/common/defaults/pod_template.go (266 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package defaults
import (
"sort"
corev1 "k8s.io/api/core/v1"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/container"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/volume"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/elasticsearch/settings"
"github.com/elastic/cloud-on-k8s/v3/pkg/utils/maps"
)
// PodDownwardEnvVars returns default environment variables created from the downward API.
func PodDownwardEnvVars() []corev1.EnvVar {
return []corev1.EnvVar{
{Name: settings.EnvPodIP, Value: "", ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "status.podIP"},
}},
{Name: settings.EnvPodName, Value: "", ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.name"},
}},
{Name: settings.EnvNodeName, Value: "", ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "spec.nodeName"},
}},
{Name: settings.EnvNamespace, Value: "", ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.namespace"},
}},
}
}
// ExtendPodDownwardEnvVars creates a new EnvVar array with the default downward API variables prepended to given list.
func ExtendPodDownwardEnvVars(vars ...corev1.EnvVar) []corev1.EnvVar {
podDownwardEnvVars := PodDownwardEnvVars()
return append(podDownwardEnvVars, vars...)
}
// PodTemplateBuilder helps with building a pod template inheriting values
// from a user-provided pod template. It focuses on building a pod with
// one main Container.
type PodTemplateBuilder struct {
PodTemplate corev1.PodTemplateSpec
containerName string
containerDefaulter container.Defaulter
}
// NewPodTemplateBuilder returns an initialized PodTemplateBuilder with some defaults.
func NewPodTemplateBuilder(base corev1.PodTemplateSpec, containerName string) *PodTemplateBuilder {
builder := &PodTemplateBuilder{
PodTemplate: *base.DeepCopy(),
containerName: containerName,
}
return builder.setDefaults()
}
// MainContainer retrieves the main Container from the pod template or nil if not found.
func (b *PodTemplateBuilder) MainContainer() *corev1.Container {
for i, c := range b.PodTemplate.Spec.Containers {
if c.Name == b.containerName {
return &b.PodTemplate.Spec.Containers[i]
}
}
return nil
}
func (b *PodTemplateBuilder) setContainerDefaulter() {
b.containerDefaulter = container.NewDefaulter(b.MainContainer())
}
// setDefaults sets up a default Container in the pod template,
// and disables service account token auto mount.
func (b *PodTemplateBuilder) setDefaults() *PodTemplateBuilder {
userContainer := b.MainContainer()
if userContainer == nil {
// create the default Container if not provided by the user
b.PodTemplate.Spec.Containers = append(b.PodTemplate.Spec.Containers, corev1.Container{Name: b.containerName})
b.setContainerDefaulter()
} else {
b.containerDefaulter = container.NewDefaulter(userContainer)
}
// disable service account token auto mount, unless explicitly enabled by the user
varFalse := false
if b.PodTemplate.Spec.AutomountServiceAccountToken == nil {
b.PodTemplate.Spec.AutomountServiceAccountToken = &varFalse
}
return b
}
// WithLabels sets the given labels, but does not override those that already exist.
func (b *PodTemplateBuilder) WithLabels(labels map[string]string) *PodTemplateBuilder {
b.PodTemplate.Labels = maps.MergePreservingExistingKeys(b.PodTemplate.Labels, labels)
return b
}
// WithAnnotations sets the given annotations, but does not override those that already exist.
func (b *PodTemplateBuilder) WithAnnotations(annotations map[string]string) *PodTemplateBuilder {
b.PodTemplate.Annotations = maps.MergePreservingExistingKeys(b.PodTemplate.Annotations, annotations)
return b
}
// WithDockerImage sets up the Container Docker image, unless already provided.
// The default image will be used unless customImage is not empty.
func (b *PodTemplateBuilder) WithDockerImage(customImage string, defaultImage string) *PodTemplateBuilder {
if customImage != "" {
b.containerDefaulter.WithImage(customImage)
} else {
b.containerDefaulter.WithImage(defaultImage)
}
return b
}
// WithReadinessProbe sets up the given readiness probe, unless already provided in the template.
func (b *PodTemplateBuilder) WithReadinessProbe(readinessProbe corev1.Probe) *PodTemplateBuilder {
b.containerDefaulter.WithReadinessProbe(&readinessProbe)
return b
}
// WithLivenessProbe sets up the given liveness probe, unless already provided in the template.
func (b *PodTemplateBuilder) WithLivenessProbe(livenessProbe corev1.Probe) *PodTemplateBuilder {
b.containerDefaulter.WithLivenessProbe(&livenessProbe)
return b
}
// WithStartupProbe sets up the given startup probe, unless already provided in the template.
func (b *PodTemplateBuilder) WithStartupProbe(startupProbe corev1.Probe) *PodTemplateBuilder {
b.containerDefaulter.WithStartupProbe(&startupProbe)
return b
}
// WithAffinity sets a default affinity, unless already provided in the template.
// An empty affinity in the spec is not overridden.
func (b *PodTemplateBuilder) WithAffinity(affinity *corev1.Affinity) *PodTemplateBuilder {
if b.PodTemplate.Spec.Affinity == nil {
b.PodTemplate.Spec.Affinity = affinity
}
return b
}
// WithPorts appends the given ports to the Container ports, unless already provided in the template.
func (b *PodTemplateBuilder) WithPorts(ports []corev1.ContainerPort) *PodTemplateBuilder {
b.containerDefaulter.WithPorts(ports)
return b
}
// WithCommand sets the given command to the Container, unless already provided in the template.
func (b *PodTemplateBuilder) WithCommand(command []string) *PodTemplateBuilder {
b.containerDefaulter.WithCommand(command)
return b
}
// volumeExists checks if a volume with the given name already exists in the Container.
func (b *PodTemplateBuilder) volumeExists(name string) bool {
for _, v := range b.PodTemplate.Spec.Volumes {
if v.Name == name {
return true
}
}
return false
}
// WithVolumes appends the given volumes to the Container, unless already provided in the template.
func (b *PodTemplateBuilder) WithVolumes(volumes ...corev1.Volume) *PodTemplateBuilder {
for _, v := range volumes {
if !b.volumeExists(v.Name) {
b.PodTemplate.Spec.Volumes = append(b.PodTemplate.Spec.Volumes, v)
}
}
// order volumes by name to ensure stable pod spec comparison
sort.SliceStable(b.PodTemplate.Spec.Volumes, func(i, j int) bool {
return b.PodTemplate.Spec.Volumes[i].Name < b.PodTemplate.Spec.Volumes[j].Name
})
return b
}
// WithVolumeMounts appends the given volume mounts to the Container, unless already provided in the template.
func (b *PodTemplateBuilder) WithVolumeMounts(volumeMounts ...corev1.VolumeMount) *PodTemplateBuilder {
b.containerDefaulter.WithVolumeMounts(volumeMounts)
return b
}
func (b *PodTemplateBuilder) WithVolumeLikes(volumeLikes ...volume.VolumeLike) *PodTemplateBuilder {
for _, v := range volumeLikes {
b = b.WithVolumes(v.Volume()).WithVolumeMounts(v.VolumeMount())
}
return b
}
// WithEnv appends the given env vars to the Container, unless already provided in the template.
func (b *PodTemplateBuilder) WithEnv(vars ...corev1.EnvVar) *PodTemplateBuilder {
b.containerDefaulter.WithNewEnv(vars)
return b
}
// WithNewEnv appends the given env vars to the Container, unless already provided in the template. Returns true if and
// only if the all env vars were not previously set in the Container
func (b *PodTemplateBuilder) WithNewEnv(vars ...corev1.EnvVar) (*PodTemplateBuilder, bool) {
_, allNew := b.containerDefaulter.WithNewEnv(vars)
return b, allNew
}
// WithTerminationGracePeriod sets the given termination grace period if not already specified in the template.
func (b *PodTemplateBuilder) WithTerminationGracePeriod(period int64) *PodTemplateBuilder {
if b.PodTemplate.Spec.TerminationGracePeriodSeconds == nil {
b.PodTemplate.Spec.TerminationGracePeriodSeconds = &period
}
return b
}
// WithContainers appends the given containers to the list of containers belonging to the pod.
// It also ensures that the base container defaulter still points to the container in the list because append()
// creates a new slice.
func (b *PodTemplateBuilder) WithContainers(containers ...corev1.Container) *PodTemplateBuilder {
for _, c := range containers {
found := false
for i := range b.PodTemplate.Spec.Containers {
podTplContainer := b.PodTemplate.Spec.Containers[i]
if c.Name == podTplContainer.Name {
found = true
// inherits default values from container already defined on the pod template
b.PodTemplate.Spec.Containers[i] = container.NewDefaulter(&podTplContainer).From(c).Container()
}
}
if !found {
b.PodTemplate.Spec.Containers = append(b.PodTemplate.Spec.Containers, c)
b.setContainerDefaulter()
}
}
return b
}
// WithInitContainerDefaults sets default values for the current init containers.
//
// Defaults:
// - If the init container contains an empty image field, it's inherited from the main container.
// - VolumeMounts from the main container are added to the init container VolumeMounts, unless they would conflict
// with a specified VolumeMount (by having the same VolumeMount.Name or VolumeMount.MountPath)
// - default environment variables
//
// This method can also be used to set some additional environment variables.
func (b *PodTemplateBuilder) WithInitContainerDefaults(additionalEnvVars ...corev1.EnvVar) *PodTemplateBuilder {
mainContainer := b.containerDefaulter.Container()
for i := range b.PodTemplate.Spec.InitContainers {
b.PodTemplate.Spec.InitContainers[i] =
container.NewDefaulter(&b.PodTemplate.Spec.InitContainers[i]).
// Inherit image and volume mounts from main container in the Pod
WithImage(mainContainer.Image).
WithVolumeMounts(mainContainer.VolumeMounts).
WithResources(mainContainer.Resources).
WithEnv(ExtendPodDownwardEnvVars(additionalEnvVars...)).
Container()
}
return b
}
// findInitContainerByName attempts to find an init container with the given name in the template
// Returns the index of the container or -1 if no init container by that name was found.
func (b *PodTemplateBuilder) findInitContainerByName(name string) int {
for i, c := range b.PodTemplate.Spec.InitContainers {
if c.Name == name {
return i
}
}
return -1
}
// WithInitContainers includes the given init containers to the pod template.
//
// Ordering:
// - Provided init containers are prepended to the existing ones in the template.
// - If an init container by the same name already exists in the template, the two PodTemplates are merged, the values
// provided by the user take precedence.
func (b *PodTemplateBuilder) WithInitContainers(
initContainers ...corev1.Container,
) *PodTemplateBuilder {
var containers []corev1.Container
for _, c := range initContainers {
if index := b.findInitContainerByName(c.Name); index != -1 {
userContainer := b.PodTemplate.Spec.InitContainers[index]
// remove it from the podTemplate
b.PodTemplate.Spec.InitContainers = append(
b.PodTemplate.Spec.InitContainers[:index],
b.PodTemplate.Spec.InitContainers[index+1:]...,
)
// Create a container based on what the user specified but ensure that values
// are set if none are provided.
containers = append(containers,
container.
// Set the container provided by the user as the base.
NewDefaulter(userContainer.DeepCopy()).
// Inherit all other values from the container built by the controller.
From(c).
Container())
} else {
containers = append(containers, c)
}
}
b.PodTemplate.Spec.InitContainers = append(containers, b.PodTemplate.Spec.InitContainers...)
return b
}
// WithResources sets up the given resource requirements if both resources limits and requests
// are nil in the main container.
// If a zero-value (empty map) for at least one of limits or request is provided, the given resource requirements
// are not applied: the user may want to use a LimitRange.
func (b *PodTemplateBuilder) WithResources(resources corev1.ResourceRequirements) *PodTemplateBuilder {
b.containerDefaulter.WithResources(resources)
return b
}
func (b *PodTemplateBuilder) WithPreStopHook(handler corev1.LifecycleHandler) *PodTemplateBuilder {
b.containerDefaulter.WithPreStopHook(&handler)
return b
}
func (b *PodTemplateBuilder) WithArgs(args ...string) *PodTemplateBuilder {
b.containerDefaulter.WithArgs(args)
return b
}
func (b *PodTemplateBuilder) WithServiceAccount(serviceAccount string) *PodTemplateBuilder {
if b.PodTemplate.Spec.ServiceAccountName == "" {
b.PodTemplate.Spec.ServiceAccountName = serviceAccount
}
return b
}
func (b *PodTemplateBuilder) WithHostNetwork() *PodTemplateBuilder {
b.PodTemplate.Spec.HostNetwork = true
return b
}
func (b *PodTemplateBuilder) WithDNSPolicy(dnsPolicy corev1.DNSPolicy) *PodTemplateBuilder {
if b.PodTemplate.Spec.DNSPolicy == "" {
b.PodTemplate.Spec.DNSPolicy = dnsPolicy
}
return b
}
func (b *PodTemplateBuilder) WithPodSecurityContext(securityContext corev1.PodSecurityContext) *PodTemplateBuilder {
if b.PodTemplate.Spec.SecurityContext == nil {
b.PodTemplate.Spec.SecurityContext = &securityContext
}
return b
}
// WithContainersSecurityContext sets Containers and InitContainers SecurityContext.
// Must be called once all the Containers and InitContainers have been set.
func (b *PodTemplateBuilder) WithContainersSecurityContext(securityContext corev1.SecurityContext) *PodTemplateBuilder {
for i := range b.PodTemplate.Spec.Containers {
if b.PodTemplate.Spec.Containers[i].SecurityContext == nil {
b.PodTemplate.Spec.Containers[i].SecurityContext = securityContext.DeepCopy()
}
}
for i := range b.PodTemplate.Spec.InitContainers {
if b.PodTemplate.Spec.InitContainers[i].SecurityContext == nil {
b.PodTemplate.Spec.InitContainers[i].SecurityContext = securityContext.DeepCopy()
}
}
return b
}
func (b *PodTemplateBuilder) WithAutomountServiceAccountToken() *PodTemplateBuilder {
if b.PodTemplate.Spec.AutomountServiceAccountToken == nil {
t := true
b.PodTemplate.Spec.AutomountServiceAccountToken = &t
}
return b
}