pkg/controller/kibana/config_settings.go (265 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 kibana
import (
"context"
"path"
"path/filepath"
"github.com/elastic/go-ucfg"
"github.com/pkg/errors"
"go.elastic.co/apm/v2"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
commonv1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/common/v1"
kbv1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/kibana/v1"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/association"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/certificates"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/settings"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/tracing"
"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/kibana/stackmon"
"github.com/elastic/cloud-on-k8s/v3/pkg/utils/k8s"
ulog "github.com/elastic/cloud-on-k8s/v3/pkg/utils/log"
"github.com/elastic/cloud-on-k8s/v3/pkg/utils/net"
)
const (
// SettingsFilename is the Kibana configuration settings file
SettingsFilename = "kibana.yml"
// EnvNodeOptions is the environment variable name for the Node options that can be used to increase the Kibana maximum memory limit
EnvNodeOptions = "NODE_OPTIONS"
// esCertsVolumeMountPath is the directory containing Elasticsearch certificates.
esCertsVolumeMountPath = "/usr/share/kibana/config/elasticsearch-certs"
// entCertsVolumeMountPath is the directory into which trusted Enterprise Search HTTP CA certs are mounted.
entCertsVolumeMountPath = "/usr/share/kibana/config/ent-certs"
)
// Constants to use for the Kibana configuration settings.
const (
ServerName = "server.name"
ServerHost = "server.host"
XpackMonitoringUIContainerElasticsearchEnabled = "xpack.monitoring.ui.container.elasticsearch.enabled" // <= 7.15
MonitoringUIContainerElasticsearchEnabled = "monitoring.ui.container.elasticsearch.enabled" // >= 7.16
XpackLicenseManagementUIEnabled = "xpack.license_management.ui.enabled" // >= 7.6
XpackSecurityEncryptionKey = "xpack.security.encryptionKey"
XpackReportingEncryptionKey = "xpack.reporting.encryptionKey"
XpackEncryptedSavedObjects = "xpack.encryptedSavedObjects"
XpackEncryptedSavedObjectsEncryptionKey = "xpack.encryptedSavedObjects.encryptionKey"
ElasticsearchSslCertificateAuthorities = "elasticsearch.ssl.certificateAuthorities"
ElasticsearchSslVerificationMode = "elasticsearch.ssl.verificationMode"
ElasticsearchUsername = "elasticsearch.username"
ElasticsearchPassword = "elasticsearch.password"
ElasticsearchServiceAccountToken = "elasticsearch.serviceAccountToken"
ElasticsearchHosts = "elasticsearch.hosts"
EnterpriseSearchHost = "enterpriseSearch.host"
EnterpriseSearchSslCertificateAuthorities = "enterpriseSearch.ssl.certificateAuthorities"
EnterpriseSearchSslVerificationMode = "enterpriseSearch.ssl.verificationMode"
ServerSSLEnabled = "server.ssl.enabled"
ServerSSLCertificate = "server.ssl.certificate"
ServerSSLKey = "server.ssl.key"
)
// CanonicalConfig contains configuration for Kibana ("kibana.yml"),
// as a hierarchical key-value configuration.
type CanonicalConfig struct {
*settings.CanonicalConfig
}
// NewConfigSettings returns the Kibana configuration settings for the given Kibana resource.
func NewConfigSettings(ctx context.Context, client k8s.Client, kb kbv1.Kibana, v version.Version, ipFamily corev1.IPFamily, kibanaConfigFromPolicy *settings.CanonicalConfig) (CanonicalConfig, error) {
span, _ := apm.StartSpan(ctx, "new_config_settings", tracing.SpanTypeApp)
defer span.End()
reusableSettings, err := getOrCreateReusableSettings(ctx, client, kb)
if err != nil {
return CanonicalConfig{}, err
}
// hack to support pre-7.6.0 Kibana configs as it errors out with unsupported keys, ideally we would not unpack empty values and could skip this
err = filterConfigSettings(kb, reusableSettings)
if err != nil {
return CanonicalConfig{}, err
}
// parse user-provided settings
specConfig := kb.Spec.Config
if specConfig == nil {
specConfig = &commonv1.Config{}
}
userSettings, err := settings.NewCanonicalConfigFrom(specConfig.Data)
if err != nil {
return CanonicalConfig{}, err
}
baseSettingsMap, err := baseSettings(&kb, ipFamily)
if err != nil {
return CanonicalConfig{}, err
}
cfg := settings.MustCanonicalConfig(baseSettingsMap)
kibanaTLSCfg := settings.MustCanonicalConfig(kibanaTLSSettings(kb))
versionSpecificCfg := VersionDefaults(&kb, v)
entSearchCfg := settings.MustCanonicalConfig(enterpriseSearchSettings(kb))
monitoringCfg, err := settings.NewCanonicalConfigFrom(stackmon.MonitoringConfig(kb).Data)
if err != nil {
return CanonicalConfig{}, err
}
err = cfg.MergeWith(
reusableSettings,
versionSpecificCfg,
kibanaTLSCfg,
entSearchCfg,
monitoringCfg)
if err != nil {
return CanonicalConfig{}, err
}
// Elasticsearch configuration
esAssocConf, err := kb.EsAssociation().AssociationConf()
if err != nil {
return CanonicalConfig{}, err
}
if esAssocConf.IsConfigured() {
credentials, err := association.ElasticsearchAuthSettings(ctx, client, kb.EsAssociation())
if err != nil {
return CanonicalConfig{}, err
}
var esCreds map[string]interface{}
if credentials.HasServiceAccountToken() {
esCreds = map[string]interface{}{
ElasticsearchServiceAccountToken: credentials.ServiceAccountToken,
}
} else {
esCreds = map[string]interface{}{
ElasticsearchUsername: credentials.Username,
ElasticsearchPassword: credentials.Password,
}
}
credentialsCfg := settings.MustCanonicalConfig(esCreds)
esAssocCfg := settings.MustCanonicalConfig(elasticsearchTLSSettings(*esAssocConf))
if err = cfg.MergeWith(esAssocCfg, credentialsCfg); err != nil {
return CanonicalConfig{}, err
}
}
// Kibana settings from a StackConfigPolicy takes precedence over user provided settings, merge them last.
if err = cfg.MergeWith(userSettings, kibanaConfigFromPolicy); err != nil {
return CanonicalConfig{}, err
}
return CanonicalConfig{cfg}, nil
}
// Some previously-unsupported keys cause Kibana to error out even if the values are empty. ucfg cannot ignore fields easily so this is necessary to
// support older versions
func filterConfigSettings(kb kbv1.Kibana, cfg *settings.CanonicalConfig) error {
ver, err := version.Parse(kb.Spec.Version)
if err != nil {
return err
}
if !ver.GTE(version.From(7, 6, 0)) {
_, err = (*ucfg.Config)(cfg).Remove(XpackEncryptedSavedObjects, -1, settings.Options...)
}
return err
}
// VersionDefaults generates any version specific settings that should exist by default.
func VersionDefaults(_ *kbv1.Kibana, v version.Version) *settings.CanonicalConfig {
if v.GTE(version.From(7, 6, 0)) {
// setting exists only as of 7.6.0
return settings.MustCanonicalConfig(map[string]interface{}{XpackLicenseManagementUIEnabled: false})
}
return settings.NewCanonicalConfig()
}
// reusableSettings captures secrets settings in the Kibana configuration that we want to reuse.
type reusableSettings struct {
EncryptionKey string `config:"xpack.security.encryptionKey"`
ReportingKey string `config:"xpack.reporting.encryptionKey"`
SavedObjectsKey string `config:"xpack.encryptedSavedObjects.encryptionKey"`
}
// getExistingConfig retrieves the canonical config for a given Kibana, if one exists
func getExistingConfig(ctx context.Context, client k8s.Client, kb kbv1.Kibana) (*settings.CanonicalConfig, error) {
log := ulog.FromContext(ctx)
var secret corev1.Secret
err := client.Get(context.Background(), types.NamespacedName{Name: kbv1.ConfigSecret(kb.Name), Namespace: kb.Namespace}, &secret)
if err != nil && apierrors.IsNotFound(err) {
log.V(1).Info("Kibana config secret does not exist", "namespace", kb.Namespace, "kibana_name", kb.Name)
return nil, nil
} else if err != nil {
log.Error(err, "Error retrieving kibana config secret", "namespace", kb.Namespace, "kibana_name", kb.Name)
return nil, err
}
rawCfg, exists := secret.Data[SettingsFilename]
if !exists {
err = errors.New("Kibana config secret exists but missing config file key")
log.Error(err, "", "namespace", secret.Namespace, "secret_name", secret.Name, "key", SettingsFilename)
return nil, err
}
cfg, err := settings.ParseConfig(rawCfg)
if err != nil {
log.Error(err, "Error parsing existing kibana config in secret", "namespace", secret.Namespace, "secret_name", secret.Name, "key", SettingsFilename)
return nil, err
}
return cfg, nil
}
// getOrCreateReusableSettings filters an existing config for only items we want to preserve between spec changes
// because they cannot be generated deterministically, e.g. encryption keys
func getOrCreateReusableSettings(ctx context.Context, c k8s.Client, kb kbv1.Kibana) (*settings.CanonicalConfig, error) {
cfg, err := getExistingConfig(ctx, c, kb)
if err != nil {
return nil, err
}
var r reusableSettings
if cfg == nil {
r = reusableSettings{}
} else if err := cfg.Unpack(&r); err != nil {
return nil, err
}
if len(r.EncryptionKey) == 0 {
r.EncryptionKey = string(common.RandomBytes(64))
}
if len(r.ReportingKey) == 0 {
r.ReportingKey = string(common.RandomBytes(64))
}
kbVer, err := version.Parse(kb.Spec.Version)
if err != nil {
return nil, err
}
// xpack.encryptedSavedObjects.encryptionKey was only added in 7.6.0 and earlier versions error out
if len(r.SavedObjectsKey) == 0 && kbVer.GTE(version.From(7, 6, 0)) {
r.SavedObjectsKey = string(common.RandomBytes(64))
}
return settings.MustCanonicalConfig(r), nil
}
func baseSettings(kb *kbv1.Kibana, ipFamily corev1.IPFamily) (map[string]interface{}, error) {
ver, err := version.Parse(kb.Spec.Version)
if err != nil {
return nil, err
}
conf := map[string]interface{}{
ServerName: kb.Name,
ServerHost: net.InAddrAnyFor(ipFamily).String(),
}
if ver.GTE(version.MinFor(7, 16, 0)) {
conf[MonitoringUIContainerElasticsearchEnabled] = true
} else {
conf[XpackMonitoringUIContainerElasticsearchEnabled] = true
}
assocConf, _ := kb.EsAssociation().AssociationConf()
if assocConf.URLIsConfigured() {
conf[ElasticsearchHosts] = []string{assocConf.GetURL()}
}
return conf, nil
}
func kibanaTLSSettings(kb kbv1.Kibana) map[string]interface{} {
if !kb.Spec.HTTP.TLS.Enabled() {
return nil
}
return map[string]interface{}{
ServerSSLEnabled: true,
ServerSSLCertificate: path.Join(certificates.HTTPCertificatesSecretVolumeMountPath, certificates.CertFileName),
ServerSSLKey: path.Join(certificates.HTTPCertificatesSecretVolumeMountPath, certificates.KeyFileName),
}
}
func elasticsearchTLSSettings(esAssocConf commonv1.AssociationConf) map[string]interface{} {
cfg := map[string]interface{}{
ElasticsearchSslVerificationMode: "certificate",
}
if esAssocConf.GetCACertProvided() {
esCertsVolumeMountPath := esCaCertSecretVolume(esAssocConf).VolumeMount().MountPath
cfg[ElasticsearchSslCertificateAuthorities] = path.Join(esCertsVolumeMountPath, certificates.CAFileName)
}
return cfg
}
// esCaCertSecretVolume returns a SecretVolume to hold the Elasticsearch CA certs for the given Kibana resource.
func esCaCertSecretVolume(esAssocConf commonv1.AssociationConf) volume.SecretVolume {
return volume.NewSecretVolumeWithMountPath(
esAssocConf.GetCASecretName(),
"elasticsearch-certs",
esCertsVolumeMountPath,
)
}
// entCaCertSecretVolume returns a SecretVolume to hold the EnterpriseSearch CA certs for the given Kibana resource.
func entCaCertSecretVolume(entAssocConf commonv1.AssociationConf) volume.SecretVolume {
return volume.NewSecretVolumeWithMountPath(
entAssocConf.GetCASecretName(),
"ent-certs",
entCertsVolumeMountPath,
)
}
func enterpriseSearchSettings(kb kbv1.Kibana) map[string]interface{} {
cfg := map[string]interface{}{}
assocConf, _ := kb.EntAssociation().AssociationConf()
if assocConf.URLIsConfigured() {
cfg[EnterpriseSearchHost] = assocConf.GetURL()
}
if assocConf.GetCACertProvided() {
cfg[EnterpriseSearchSslCertificateAuthorities] = filepath.Join(entCertsVolumeMountPath, certificates.CAFileName)
// Rely on "certificate" verification mode rather than "full" to allow Kibana
// to connect to Enterprise Search through the k8s-internal service DNS name
// even though the user-provided certificate may only specify a public-facing DNS.
cfg[EnterpriseSearchSslVerificationMode] = "certificate"
}
return cfg
}