pkg/controller/elasticsearch/nodespec/podspec.go (205 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 nodespec
import (
"context"
"fmt"
"hash/fnv"
"sort"
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
esv1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/elasticsearch/v1"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/annotation"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/container"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/defaults"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/hash"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/keystore"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/version"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/volume"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/elasticsearch/initcontainer"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/elasticsearch/label"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/elasticsearch/network"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/elasticsearch/securitycontext"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/elasticsearch/settings"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/elasticsearch/stackmon"
esvolume "github.com/elastic/cloud-on-k8s/v3/pkg/controller/elasticsearch/volume"
"github.com/elastic/cloud-on-k8s/v3/pkg/utils/k8s"
"github.com/elastic/cloud-on-k8s/v3/pkg/utils/maps"
)
const (
defaultFsGroup = 1000
log4j2FormatMsgNoLookupsParamName = "-Dlog4j2.formatMsgNoLookups"
// ConfigHashAnnotationName is an annotation used to store a hash of the Elasticsearch configuration.
configHashAnnotationName = "elasticsearch.k8s.elastic.co/config-hash"
)
// Starting 8.0.0, the Elasticsearch container does not run with the root user anymore. As a result,
// we cannot chown the mounted volumes to the right user (id 1000) in an init container.
// Instead, we can rely on Kubernetes `securityContext.fsGroup` feature: by setting it to 1000
// mounted volumes can correctly be accessed by the default container user.
// On some restricted environments (custom PSPs or Openshift), setting the Pod security context
// is forbidden: the user can either set `--set-default-security-context=false`, or override the
// podTemplate securityContext to an empty value.
var minDefaultSecurityContextVersion = version.MinFor(8, 0, 0)
// BuildPodTemplateSpec builds a new PodTemplateSpec for an Elasticsearch node.
func BuildPodTemplateSpec(
ctx context.Context,
client k8s.Client,
es esv1.Elasticsearch,
nodeSet esv1.NodeSet,
cfg settings.CanonicalConfig,
keystoreResources *keystore.Resources,
setDefaultSecurityContext bool,
policyConfig PolicyConfig,
) (corev1.PodTemplateSpec, error) {
ver, err := version.Parse(es.Spec.Version)
if err != nil {
return corev1.PodTemplateSpec{}, err
}
downwardAPIVolume := volume.DownwardAPI{}.WithAnnotations(es.HasDownwardNodeLabels())
volumes, volumeMounts := buildVolumes(es.Name, ver, nodeSet, keystoreResources, downwardAPIVolume, policyConfig.AdditionalVolumes)
labels, err := buildLabels(es, cfg, nodeSet)
if err != nil {
return corev1.PodTemplateSpec{}, err
}
defaultContainerPorts := getDefaultContainerPorts(es)
// now build the initContainers using the effective main container resources as an input
initContainers, err := initcontainer.NewInitContainers(
transportCertificatesVolume(esv1.StatefulSet(es.Name, nodeSet.Name)),
keystoreResources,
es.DownwardNodeLabels(),
)
if err != nil {
return corev1.PodTemplateSpec{}, err
}
builder := defaults.NewPodTemplateBuilder(nodeSet.PodTemplate, esv1.ElasticsearchContainerName)
if ver.GTE(minDefaultSecurityContextVersion) && setDefaultSecurityContext {
builder = builder.WithPodSecurityContext(corev1.PodSecurityContext{
FSGroup: ptr.To[int64](defaultFsGroup),
})
}
headlessServiceName := HeadlessServiceName(esv1.StatefulSet(es.Name, nodeSet.Name))
// We retrieve the ConfigMap that holds the scripts to trigger a Pod restart if it is updated.
esScripts := &corev1.ConfigMap{}
if err := client.Get(context.Background(), types.NamespacedName{Namespace: es.Namespace, Name: esv1.ScriptsConfigMap(es.Name)}, esScripts); err != nil {
return corev1.PodTemplateSpec{}, err
}
annotations := buildAnnotations(es, cfg, keystoreResources, getScriptsConfigMapContent(esScripts), policyConfig.PolicyAnnotations)
// Attempt to detect if the default data directory is mounted in a volume.
// If not, it could be a bug, a misconfiguration, or a custom storage configuration that requires the user to
// explicitly set ReadOnlyRootFilesystem to true.
enableReadOnlyRootFilesystem := false
for _, volumeMount := range volumeMounts {
if volumeMount.Name == esvolume.ElasticsearchDataVolumeName {
enableReadOnlyRootFilesystem = true
break
}
}
// build the podTemplate until we have the effective resources configured
builder = builder.
WithLabels(labels).
WithAnnotations(annotations).
WithDockerImage(es.Spec.Image, container.ImageRepository(container.ElasticsearchImage, ver)).
WithResources(DefaultResources).
WithTerminationGracePeriod(DefaultTerminationGracePeriodSeconds).
WithPorts(defaultContainerPorts).
WithReadinessProbe(*NewReadinessProbe(ver)).
WithAffinity(DefaultAffinity(es.Name)).
WithEnv(DefaultEnvVars(ver, es.Spec.HTTP, headlessServiceName)...).
WithVolumes(volumes...).
WithVolumeMounts(volumeMounts...).
WithInitContainers(initContainers...).
// inherit all env vars from main containers to allow Elasticsearch tools that read ES config to work in initContainers
WithInitContainerDefaults(builder.MainContainer().Env...).
// set a default security context for both the Containers and the InitContainers
WithContainersSecurityContext(securitycontext.For(ver, enableReadOnlyRootFilesystem)).
WithPreStopHook(*NewPreStopHook())
builder, err = stackmon.WithMonitoring(ctx, client, builder, es)
if err != nil {
return corev1.PodTemplateSpec{}, err
}
if ver.LT(version.From(7, 2, 0)) {
// mitigate CVE-2021-44228
enableLog4JFormatMsgNoLookups(builder)
}
return builder.PodTemplate, nil
}
func getDefaultContainerPorts(es esv1.Elasticsearch) []corev1.ContainerPort {
return []corev1.ContainerPort{
{Name: es.Spec.HTTP.Protocol(), ContainerPort: network.HTTPPort, Protocol: corev1.ProtocolTCP},
{Name: "transport", ContainerPort: network.TransportPort, Protocol: corev1.ProtocolTCP},
}
}
func transportCertificatesVolume(ssetName string) volume.SecretVolume {
return volume.NewSecretVolumeWithMountPath(
esv1.StatefulSetTransportCertificatesSecret(ssetName),
esvolume.TransportCertificatesSecretVolumeName,
esvolume.TransportCertificatesSecretVolumeMountPath,
)
}
func buildLabels(
es esv1.Elasticsearch,
cfg settings.CanonicalConfig,
nodeSet esv1.NodeSet,
) (map[string]string, error) {
// label with version
ver, err := version.Parse(es.Spec.Version)
if err != nil {
return nil, err
}
unpackedCfg, err := cfg.Unpack(ver)
if err != nil {
return nil, err
}
node := unpackedCfg.Node
podLabels := label.NewPodLabels(
k8s.ExtractNamespacedName(&es),
esv1.StatefulSet(es.Name, nodeSet.Name),
ver, node, es.Spec.HTTP.Protocol(),
)
return podLabels, nil
}
func buildAnnotations(
es esv1.Elasticsearch,
cfg settings.CanonicalConfig,
keystoreResources *keystore.Resources,
scriptsContent string,
policyAnnotations map[string]string,
) map[string]string {
// start from our defaults
annotations := map[string]string{
annotation.FilebeatModuleAnnotation: "elasticsearch",
}
configHash := fnv.New32a()
// hash of the ES config to rotate the pod on config changes
hash.WriteHashObject(configHash, cfg)
// hash of the scripts' content to rotate the pod if the scripts have changed
_, _ = configHash.Write([]byte(scriptsContent))
if es.HasDownwardNodeLabels() {
// list of node labels expected on the pod to rotate the pod when the list is updated
_, _ = configHash.Write([]byte(es.Annotations[esv1.DownwardNodeLabelsAnnotation]))
}
if keystoreResources != nil {
// resource version of the secure settings secret to rotate the pod on secure settings change
_, _ = configHash.Write([]byte(keystoreResources.Hash))
}
if !es.Spec.Transport.TLS.SelfSignedEnabled() {
annotations[esv1.TransportCertDisabledAnnotationName] = "true"
}
// set the annotation in place
annotations[configHashAnnotationName] = fmt.Sprint(configHash.Sum32())
// set policy annotations
maps.Merge(annotations, policyAnnotations)
return annotations
}
// enableLog4JFormatMsgNoLookups prepends the JVM parameter `-Dlog4j2.formatMsgNoLookups=true` to the environment variable `ES_JAVA_OPTS`
// in order to mitigate the Log4Shell vulnerability CVE-2021-44228, if it is not yet defined by the user, for
// versions of Elasticsearch before 7.2.0.
func enableLog4JFormatMsgNoLookups(builder *defaults.PodTemplateBuilder) {
log4j2Param := fmt.Sprintf("%s=true", log4j2FormatMsgNoLookupsParamName)
for c, esContainer := range builder.PodTemplate.Spec.Containers {
if esContainer.Name != esv1.ElasticsearchContainerName {
continue
}
currentJvmOpts := ""
for e, envVar := range esContainer.Env {
if envVar.Name != settings.EnvEsJavaOpts {
continue
}
currentJvmOpts = envVar.Value
if !strings.Contains(currentJvmOpts, log4j2FormatMsgNoLookupsParamName) {
builder.PodTemplate.Spec.Containers[c].Env[e].Value = log4j2Param + " " + currentJvmOpts
}
}
if currentJvmOpts == "" {
builder.PodTemplate.Spec.Containers[c].Env = append(
builder.PodTemplate.Spec.Containers[c].Env,
corev1.EnvVar{Name: settings.EnvEsJavaOpts, Value: log4j2Param},
)
}
}
}
// Get contents of the script ConfigMap to generate config hash, excluding the suspended_pods.txt, as it may change over time.
func getScriptsConfigMapContent(cm *corev1.ConfigMap) string {
var builder strings.Builder
var keys []string
for k := range cm.Data {
if k != initcontainer.SuspendedHostsFile {
keys = append(keys, k)
}
}
sort.Strings(keys)
for _, k := range keys {
builder.WriteString(cm.Data[k])
}
return builder.String()
}