diff --git a/pkg/apis/kibana/v1/name.go b/pkg/apis/kibana/v1/name.go index 42edde66ec..c148d2f3e4 100644 --- a/pkg/apis/kibana/v1/name.go +++ b/pkg/apis/kibana/v1/name.go @@ -8,7 +8,11 @@ import ( common_name "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/name" ) -const httpServiceSuffix = "http" +const ( + httpServiceSuffix = "http" + scriptsConfigMapSuffix = "scripts" + configSecretSuffix = "config" +) // KBNamer is a KBNamer that is configured with the defaults for resources related to a Kibana resource. var KBNamer = common_name.NewNamer("kb") @@ -20,3 +24,13 @@ func HTTPService(kbName string) string { func Deployment(kbName string) string { return KBNamer.Suffix(kbName) } + +// ScriptsConfigMap returns the name of the ConfigMap containing scripts for the given Kibana resource. +func ScriptsConfigMap(kbName string) string { + return KBNamer.Suffix(kbName, scriptsConfigMapSuffix) +} + +// ConfigSecret returns the name of the Secret containing the Kibana configuration for the given Kibana resource. +func ConfigSecret(kb Kibana) string { + return KBNamer.Suffix(kb.Name, configSecretSuffix) +} diff --git a/pkg/apis/kibana/v1/name_test.go b/pkg/apis/kibana/v1/name_test.go index 2386f24172..b905687254 100644 --- a/pkg/apis/kibana/v1/name_test.go +++ b/pkg/apis/kibana/v1/name_test.go @@ -6,27 +6,55 @@ package v1 import ( "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestHTTPService(t *testing.T) { - type args struct { - kbName string - } +func TestNamers(t *testing.T) { tests := []struct { - name string - args args - want string + name string + namer any + arg any + want string }{ { - name: "sample", - args: args{kbName: "sample"}, - want: "sample-kb-http", + name: "test httpService namer", + namer: HTTPService, + arg: "sample", + want: "sample-kb-http", + }, + { + name: "test deployment namer", + namer: Deployment, + arg: "sample", + want: "sample-kb", + }, + { + name: "test scripts configmap namer", + namer: ScriptsConfigMap, + arg: "sample", + want: "sample-kb-scripts", + }, + { + name: "test ConfigSecret namer", + namer: ConfigSecret, + arg: Kibana{ObjectMeta: metav1.ObjectMeta{Name: "sample"}}, + want: "sample-kb-config", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := HTTPService(tt.args.kbName); got != tt.want { - t.Errorf("HTTPService() = %v, want %v", got, tt.want) + switch f := tt.namer.(type) { + case func(string) string: + arg := tt.arg.(string) //nolint:forcetypeassert + if got := f(arg); got != tt.want { + t.Errorf("%s = %v, want %v", tt.name, got, tt.want) + } + case func(Kibana) string: + arg := tt.arg.(Kibana) //nolint:forcetypeassert + if got := f(arg); got != tt.want { + t.Errorf("%s = %v, want %v", tt.name, got, tt.want) + } } }) } diff --git a/pkg/controller/kibana/config_reconcile.go b/pkg/controller/kibana/config_reconcile.go index 8f68984fe7..9e202b5ef5 100644 --- a/pkg/controller/kibana/config_reconcile.go +++ b/pkg/controller/kibana/config_reconcile.go @@ -18,51 +18,15 @@ import ( "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/labels" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/reconciler" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/tracing" - "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/volume" kblabel "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/label" "github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s" ) // Constants to use for the config files in a Kibana pod. const ( - ConfigVolumeName = "elastic-internal-kibana-config-local" - ConfigVolumeMountPath = "/usr/share/kibana/config" - InitContainerConfigVolumeMountPath = "/mnt/elastic-internal/kibana-config-local" - - // InternalConfigVolumeName is a volume which contains the generated configuration. - InternalConfigVolumeName = "elastic-internal-kibana-config" - InternalConfigVolumeMountPath = "/mnt/elastic-internal/kibana-config" - TelemetryFilename = "telemetry.yml" ) -var ( - // ConfigSharedVolume contains the Kibana config/ directory, it's an empty volume where the required configuration - // is initialized by the elastic-internal-init-config init container. Its content is then shared by the init container - // that creates the keystore and the main Kibana container. - // This is needed in order to have in a same directory both the generated configuration and the keystore file which - // is created in /usr/share/kibana/config since Kibana 7.9 - ConfigSharedVolume = volume.SharedVolume{ - VolumeName: ConfigVolumeName, - InitContainerMountPath: InitContainerConfigVolumeMountPath, - ContainerMountPath: ConfigVolumeMountPath, - } -) - -// ConfigVolume returns a SecretVolume to hold the Kibana config of the given Kibana resource. -func ConfigVolume(kb kbv1.Kibana) volume.SecretVolume { - return volume.NewSecretVolumeWithMountPath( - SecretName(kb), - InternalConfigVolumeName, - InternalConfigVolumeMountPath, - ) -} - -// SecretName is the name of the secret that holds the Kibana config for the given Kibana resource. -func SecretName(kb kbv1.Kibana) string { - return kb.Name + "-kb-config" -} - // ReconcileConfigSecret reconciles the expected Kibana config secret for the given Kibana resource. // This managed secret is mounted into each pod of the Kibana deployment. func ReconcileConfigSecret( @@ -95,7 +59,7 @@ func ReconcileConfigSecret( expected := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: kb.Namespace, - Name: SecretName(kb), + Name: kbv1.ConfigSecret(kb), Labels: labels.AddCredentialsLabel(map[string]string{ kblabel.KibanaNameLabelName: kb.Name, }), @@ -111,7 +75,7 @@ func ReconcileConfigSecret( // if the Secret or usage key doesn't exist yet. func getTelemetryYamlBytes(client k8s.Client, kb kbv1.Kibana) ([]byte, error) { var secret corev1.Secret - if err := client.Get(context.Background(), types.NamespacedName{Namespace: kb.Namespace, Name: SecretName(kb)}, &secret); err != nil { + if err := client.Get(context.Background(), types.NamespacedName{Namespace: kb.Namespace, Name: kbv1.ConfigSecret(kb)}, &secret); err != nil { if apierrors.IsNotFound(err) { // this secret is just about to be created, we don't know usage yet return nil, nil diff --git a/pkg/controller/kibana/config_settings.go b/pkg/controller/kibana/config_settings.go index 1e7d3fb791..eb3a833c6c 100644 --- a/pkg/controller/kibana/config_settings.go +++ b/pkg/controller/kibana/config_settings.go @@ -199,7 +199,7 @@ type reusableSettings struct { 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: SecretName(kb), Namespace: kb.Namespace}, &secret) + err := client.Get(context.Background(), types.NamespacedName{Name: kbv1.ConfigSecret(kb), 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 diff --git a/pkg/controller/kibana/config_settings_test.go b/pkg/controller/kibana/config_settings_test.go index 70236ee82c..461d945a12 100644 --- a/pkg/controller/kibana/config_settings_test.go +++ b/pkg/controller/kibana/config_settings_test.go @@ -104,7 +104,7 @@ func Test_reuseOrGenerateSecrets(t *testing.T) { args: args{ c: k8s.NewFakeClient( &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Namespace: defaultKb.Namespace, Name: SecretName(defaultKb)}, + ObjectMeta: metav1.ObjectMeta{Namespace: defaultKb.Namespace, Name: kbv1.ConfigSecret(defaultKb)}, Data: map[string][]byte{ SettingsFilename: defaultConfig, }, @@ -127,7 +127,7 @@ func Test_reuseOrGenerateSecrets(t *testing.T) { args: args{ c: k8s.NewFakeClient( &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Namespace: defaultKb.Namespace, Name: SecretName(defaultKb)}, + ObjectMeta: metav1.ObjectMeta{Namespace: defaultKb.Namespace, Name: kbv1.ConfigSecret(defaultKb)}, Data: map[string][]byte{ SettingsFilename: esAssociationConfig, }, @@ -151,7 +151,7 @@ func Test_reuseOrGenerateSecrets(t *testing.T) { args: args{ c: k8s.NewFakeClient( &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Namespace: defaultKb.Namespace, Name: SecretName(defaultKb)}, + ObjectMeta: metav1.ObjectMeta{Namespace: defaultKb.Namespace, Name: kbv1.ConfigSecret(defaultKb)}, Data: map[string][]byte{ SettingsFilename: esAssociationConfig, }, @@ -186,7 +186,7 @@ func TestNewConfigSettings(t *testing.T) { defaultKb := mkKibana() existingSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: SecretName(defaultKb), + Name: kbv1.ConfigSecret(defaultKb), Namespace: defaultKb.Namespace, }, Data: map[string][]byte{ @@ -519,7 +519,7 @@ func TestNewConfigSettings(t *testing.T) { args: args{ client: k8s.NewFakeClient(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: SecretName(defaultKb), + Name: kbv1.ConfigSecret(defaultKb), Namespace: defaultKb.Namespace, }, Data: map[string][]byte{ @@ -544,7 +544,7 @@ func TestNewConfigSettings(t *testing.T) { args: args{ client: k8s.NewFakeClient(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: SecretName(defaultKb), + Name: kbv1.ConfigSecret(defaultKb), Namespace: defaultKb.Namespace, }, Data: map[string][]byte{ @@ -607,7 +607,7 @@ func TestNewConfigSettingsExistingEncryptionKey(t *testing.T) { savedObjsKey := "savedObjsKey" existingSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: SecretName(kb), + Name: kbv1.ConfigSecret(kb), Namespace: kb.Namespace, }, Data: map[string][]byte{ @@ -686,7 +686,7 @@ func Test_getExistingConfig(t *testing.T) { } testValidSecret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: SecretName(testKb), + Name: kbv1.ConfigSecret(testKb), Namespace: testKb.Namespace, }, Data: map[string][]byte{ @@ -695,7 +695,7 @@ func Test_getExistingConfig(t *testing.T) { } testNoYaml := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: SecretName(testKb), + Name: kbv1.ConfigSecret(testKb), Namespace: testKb.Namespace, }, Data: map[string][]byte{ @@ -704,7 +704,7 @@ func Test_getExistingConfig(t *testing.T) { } testInvalidYaml := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: SecretName(testKb), + Name: kbv1.ConfigSecret(testKb), Namespace: testKb.Namespace, }, Data: map[string][]byte{ diff --git a/pkg/controller/kibana/controller.go b/pkg/controller/kibana/controller.go index f9cea6239c..5e50edfa15 100644 --- a/pkg/controller/kibana/controller.go +++ b/pkg/controller/kibana/controller.go @@ -101,6 +101,14 @@ func addWatches(mgr manager.Manager, c controller.Controller, r *ReconcileKibana return err } + // Watch configmaps + if err := c.Watch(source.Kind(mgr.GetCache(), &corev1.ConfigMap{}, handler.TypedEnqueueRequestForOwner[*corev1.ConfigMap]( + mgr.GetScheme(), mgr.GetRESTMapper(), + &kbv1.Kibana{}, handler.OnlyControllerOwner(), + ))); err != nil { + return err + } + // dynamically watch referenced secrets to connect to Elasticsearch return c.Watch(source.Kind(mgr.GetCache(), &corev1.Secret{}, r.dynamicWatches.Secrets)) } diff --git a/pkg/controller/kibana/driver.go b/pkg/controller/kibana/driver.go index cd53c3f7e8..646e795d2d 100644 --- a/pkg/controller/kibana/driver.go +++ b/pkg/controller/kibana/driver.go @@ -33,6 +33,7 @@ import ( "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/version" commonvolume "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/volume" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/watches" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/initcontainer" kblabel "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/label" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/network" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/stackmon" @@ -160,8 +161,7 @@ func (d *driver) Reconcile( return results.WithError(err) } - err = ReconcileConfigSecret(ctx, d.client, *kb, kbSettings) - if err != nil { + if err = ReconcileConfigSecret(ctx, d.client, *kb, kbSettings); err != nil { return results.WithError(err) } @@ -170,8 +170,11 @@ func (d *driver) Reconcile( return results.WithError(err) } - err = stackmon.ReconcileConfigSecrets(ctx, d.client, *kb, basePath) - if err != nil { + if err = stackmon.ReconcileConfigSecrets(ctx, d.client, *kb, basePath); err != nil { + return results.WithError(err) + } + + if err = initcontainer.ReconcileScriptsConfigMap(ctx, d.client, *kb); err != nil { return results.WithError(err) } @@ -226,7 +229,7 @@ func (d *driver) getStrategyType(kb *kbv1.Kibana) (appsv1.DeploymentStrategyType } func (d *driver) deploymentParams(ctx context.Context, kb *kbv1.Kibana, policyAnnotations map[string]string, basePath string, setDefaultSecurityContext bool) (deployment.Params, error) { - initContainersParameters, err := newInitContainersParameters(kb) + initContainersParameters, err := initcontainer.NewInitContainersParameters(kb) if err != nil { return deployment.Params{}, err } @@ -282,7 +285,7 @@ func (d *driver) deploymentParams(ctx context.Context, kb *kbv1.Kibana, policyAn // get config secret to add its content to the config checksum configSecret := corev1.Secret{} - err = d.client.Get(ctx, types.NamespacedName{Name: SecretName(*kb), Namespace: kb.Namespace}, &configSecret) + err = d.client.Get(ctx, types.NamespacedName{Name: kbv1.ConfigSecret(*kb), Namespace: kb.Namespace}, &configSecret) if err != nil { return deployment.Params{}, err } @@ -314,7 +317,7 @@ func (d *driver) deploymentParams(ctx context.Context, kb *kbv1.Kibana, policyAn } func (d *driver) buildVolumes(kb *kbv1.Kibana) ([]commonvolume.VolumeLike, error) { - volumes := []commonvolume.VolumeLike{DataVolume, ConfigSharedVolume, ConfigVolume(*kb)} + volumes := []commonvolume.VolumeLike{DataVolume, initcontainer.ConfigSharedVolume, initcontainer.ConfigVolume(*kb)} esAssocConf, err := kb.EsAssociation().AssociationConf() if err != nil { diff --git a/pkg/controller/kibana/driver_test.go b/pkg/controller/kibana/driver_test.go index 816f3b6e7f..4fea28075c 100644 --- a/pkg/controller/kibana/driver_test.go +++ b/pkg/controller/kibana/driver_test.go @@ -28,8 +28,10 @@ import ( "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/deployment" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/watches" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/settings" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/initcontainer" kblabel "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/label" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/network" + kbsettings "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/settings" "github.com/elastic/cloud-on-k8s/v2/pkg/utils/compare" "github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s" ) @@ -511,7 +513,7 @@ func expectedDeploymentParams() deployment.Params { }, }, { - Name: ConfigSharedVolume.VolumeName, + Name: initcontainer.ConfigSharedVolume.VolumeName, VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{}, }, @@ -526,7 +528,7 @@ func expectedDeploymentParams() deployment.Params { }, }, { - Name: DataVolumeName, + Name: kbsettings.DataVolumeName, VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{}, }, @@ -543,6 +545,18 @@ func expectedDeploymentParams() deployment.Params { EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, + { + Name: "kibana-scripts", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test-kb-scripts", + }, + DefaultMode: ptr.To(int32(0755)), + Optional: ptr.To(false), + }, + }, + }, { Name: "temp-volume", VolumeSource: corev1.VolumeSource{ @@ -550,76 +564,87 @@ func expectedDeploymentParams() deployment.Params { }, }, }, - InitContainers: []corev1.Container{{ - Name: "elastic-internal-init-config", - ImagePullPolicy: corev1.PullIfNotPresent, - Image: "my-image", - Command: []string{"/usr/bin/env", "bash", "-c", InitConfigScript}, - SecurityContext: &defaultSecurityContext, - Env: []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"}, - }}, - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: certificates.HTTPCertificatesSecretVolumeName, - ReadOnly: true, - MountPath: certificates.HTTPCertificatesSecretVolumeMountPath, - }, - { - Name: "elastic-internal-kibana-config", - ReadOnly: true, - MountPath: InternalConfigVolumeMountPath, - }, - ConfigSharedVolume.InitContainerVolumeMount(), - { - Name: "elasticsearch-certs", - ReadOnly: true, - MountPath: "/usr/share/kibana/config/elasticsearch-certs", - }, - { - Name: DataVolumeName, - ReadOnly: falseVal, - MountPath: DataVolumeMountPath, - }, - { - Name: "kibana-logs", - ReadOnly: falseVal, - MountPath: "/usr/share/kibana/logs", - }, - { - Name: "kibana-plugins", - ReadOnly: falseVal, - MountPath: "/usr/share/kibana/plugins", - }, - { - Name: "temp-volume", - ReadOnly: falseVal, - MountPath: "/tmp", + InitContainers: []corev1.Container{ + { + Name: "elastic-internal-init", + ImagePullPolicy: corev1.PullIfNotPresent, + Image: "my-image", + Command: []string{"/usr/bin/env", "bash", "-c", "/mnt/elastic-internal/scripts/init.sh"}, + SecurityContext: nil, + Env: []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"}, + }}, }, - }, - Resources: corev1.ResourceRequirements{ - Requests: map[corev1.ResourceName]resource.Quantity{ - corev1.ResourceMemory: resource.MustParse("50Mi"), - corev1.ResourceCPU: resource.MustParse("0.1"), + VolumeMounts: []corev1.VolumeMount{ + { + Name: certificates.HTTPCertificatesSecretVolumeName, + ReadOnly: true, + MountPath: certificates.HTTPCertificatesSecretVolumeMountPath, + }, + { + Name: "elastic-internal-kibana-config", + ReadOnly: true, + MountPath: "/mnt/elastic-internal/kibana-config", + }, + { + Name: "elastic-internal-kibana-config-local", + ReadOnly: false, + MountPath: "/mnt/elastic-internal/kibana-config-local", + }, + { + Name: "elasticsearch-certs", + ReadOnly: true, + MountPath: "/usr/share/kibana/config/elasticsearch-certs", + }, + { + Name: kbsettings.DataVolumeName, + ReadOnly: falseVal, + MountPath: kbsettings.DataVolumeMountPath, + }, + { + Name: "kibana-logs", + ReadOnly: falseVal, + MountPath: "/usr/share/kibana/logs", + }, + { + Name: "kibana-plugins", + ReadOnly: falseVal, + MountPath: "/mnt/elastic-internal/kibana-plugins-local", + }, + { + Name: "kibana-scripts", + ReadOnly: true, + MountPath: "/mnt/elastic-internal/scripts", + }, + { + Name: "temp-volume", + ReadOnly: falseVal, + MountPath: "/tmp", + }, }, - Limits: map[corev1.ResourceName]resource.Quantity{ - // Memory limit should be at least 12582912 when running with CRI-O - corev1.ResourceMemory: resource.MustParse("50Mi"), - corev1.ResourceCPU: resource.MustParse("0.1"), + Resources: corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceMemory: resource.MustParse("50Mi"), + corev1.ResourceCPU: resource.MustParse("0.1"), + }, + Limits: map[corev1.ResourceName]resource.Quantity{ + // Memory limit should be at least 12582912 when running with CRI-O + corev1.ResourceMemory: resource.MustParse("50Mi"), + corev1.ResourceCPU: resource.MustParse("0.1"), + }, }, }, - }}, + }, Containers: []corev1.Container{{ VolumeMounts: []corev1.VolumeMount{ { @@ -630,18 +655,18 @@ func expectedDeploymentParams() deployment.Params { { Name: "elastic-internal-kibana-config", ReadOnly: true, - MountPath: InternalConfigVolumeMountPath, + MountPath: kbsettings.InternalConfigVolumeMountPath, }, - ConfigSharedVolume.VolumeMount(), + initcontainer.ConfigSharedVolume.VolumeMount(), { Name: "elasticsearch-certs", ReadOnly: true, MountPath: "/usr/share/kibana/config/elasticsearch-certs", }, { - Name: DataVolumeName, + Name: kbsettings.DataVolumeName, ReadOnly: falseVal, - MountPath: DataVolumeMountPath, + MountPath: kbsettings.DataVolumeMountPath, }, { Name: "kibana-logs", @@ -653,6 +678,11 @@ func expectedDeploymentParams() deployment.Params { ReadOnly: falseVal, MountPath: "/usr/share/kibana/plugins", }, + { + Name: "kibana-scripts", + ReadOnly: true, + MountPath: "/mnt/elastic-internal/scripts", + }, { Name: "temp-volume", ReadOnly: falseVal, @@ -700,9 +730,9 @@ func pre710(params deployment.Params) deployment.Params { params.PodTemplateSpec.Spec.Containers[0].SecurityContext = nil params.PodTemplateSpec.Spec.InitContainers[0].SecurityContext = nil params.PodTemplateSpec.Spec.SecurityContext = nil - params.PodTemplateSpec.Spec.Volumes = params.PodTemplateSpec.Spec.Volumes[:5] - params.PodTemplateSpec.Spec.InitContainers[0].VolumeMounts = params.PodTemplateSpec.Spec.InitContainers[0].VolumeMounts[:5] - params.PodTemplateSpec.Spec.Containers[0].VolumeMounts = params.PodTemplateSpec.Spec.Containers[0].VolumeMounts[:5] + params.PodTemplateSpec.Spec.Volumes = append(params.PodTemplateSpec.Spec.Volumes[:5], params.PodTemplateSpec.Spec.Volumes[6], params.PodTemplateSpec.Spec.Volumes[7]) + params.PodTemplateSpec.Spec.InitContainers[0].VolumeMounts = append(params.PodTemplateSpec.Spec.InitContainers[0].VolumeMounts[:5], params.PodTemplateSpec.Spec.InitContainers[0].VolumeMounts[6], params.PodTemplateSpec.Spec.InitContainers[0].VolumeMounts[7]) + params.PodTemplateSpec.Spec.Containers[0].VolumeMounts = append(params.PodTemplateSpec.Spec.Containers[0].VolumeMounts[:5], params.PodTemplateSpec.Spec.Containers[0].VolumeMounts[6], params.PodTemplateSpec.Spec.Containers[0].VolumeMounts[7]) return params } diff --git a/pkg/controller/kibana/init_configuration.go b/pkg/controller/kibana/init_configuration.go deleted file mode 100644 index ed2481753a..0000000000 --- a/pkg/controller/kibana/init_configuration.go +++ /dev/null @@ -1,62 +0,0 @@ -// 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 ( - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - - kbv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/kibana/v1" -) - -const ( - InitConfigContainerName = "elastic-internal-init-config" - - // InitConfigScript is a small bash script to prepare the Kibana configuration directory - InitConfigScript = `#!/usr/bin/env bash -set -eux - -init_config_initialized_flag=` + InitContainerConfigVolumeMountPath + `/elastic-internal-init-config.ok - -if [[ -f "${init_config_initialized_flag}" ]]; then - echo "Kibana configuration already initialized." - exit 0 -fi - -echo "Setup Kibana configuration" - -ln -sf ` + InternalConfigVolumeMountPath + `/* ` + InitContainerConfigVolumeMountPath + `/ - -touch "${init_config_initialized_flag}" -echo "Kibana configuration successfully prepared." -` -) - -// initConfigContainer returns an init container that executes a bash script to prepare the Kibana config directory. -// The script creates symbolic links from the generated configuration files in /mnt/elastic-internal/kibana-config/ to -// an empty directory later mounted in /use/share/kibana/config -func initConfigContainer(kb kbv1.Kibana) corev1.Container { - return corev1.Container{ - // Image will be inherited from pod template defaults - ImagePullPolicy: corev1.PullIfNotPresent, - Name: InitConfigContainerName, - Command: []string{"/usr/bin/env", "bash", "-c", InitConfigScript}, - VolumeMounts: []corev1.VolumeMount{ - ConfigSharedVolume.InitContainerVolumeMount(), - ConfigVolume(kb).VolumeMount(), - }, - Resources: corev1.ResourceRequirements{ - Requests: map[corev1.ResourceName]resource.Quantity{ - corev1.ResourceMemory: resource.MustParse("50Mi"), - corev1.ResourceCPU: resource.MustParse("0.1"), - }, - Limits: map[corev1.ResourceName]resource.Quantity{ - // Memory limit should be at least 12582912 when running with CRI-O - corev1.ResourceMemory: resource.MustParse("50Mi"), - corev1.ResourceCPU: resource.MustParse("0.1"), - }, - }, - } -} diff --git a/pkg/controller/kibana/initcontainer/configmap.go b/pkg/controller/kibana/initcontainer/configmap.go new file mode 100644 index 0000000000..e2c2765e07 --- /dev/null +++ b/pkg/controller/kibana/initcontainer/configmap.go @@ -0,0 +1,81 @@ +// 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 initcontainer + +import ( + "context" + "reflect" + + "go.elastic.co/apm/v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + kbv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/kibana/v1" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/reconciler" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/tracing" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/version" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/volume" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/label" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/settings" + "github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s" + "github.com/elastic/cloud-on-k8s/v2/pkg/utils/maps" +) + +// HardenedSecurityContextSupportedVersion is the version in which a hardened security context is supported. +var HardenedSecurityContextSupportedVersion = version.From(7, 9, 0) + +// NewScriptsConfigMapVolume creates a new volume for the ConfigMap containing scripts used by the Kibana init container. +func NewScriptsConfigMapVolume(kbName string) volume.ConfigMapVolume { + return volume.NewConfigMapVolumeWithMode( + kbv1.ScriptsConfigMap(kbName), + settings.ScriptsVolumeName, + settings.ScriptsVolumeMountPath, + 0755) +} + +// ReconcileScriptsConfigMap reconciles the ConfigMap containing scripts used by the Kibana elastic-internal-init container. +func ReconcileScriptsConfigMap(ctx context.Context, c k8s.Client, kb kbv1.Kibana) error { + span, ctx := apm.StartSpan(ctx, "reconcile_scripts", tracing.SpanTypeApp) + defer span.End() + + initScript, err := renderInitScript() + if err != nil { + return err + } + + nsn := types.NamespacedName{Namespace: kb.Namespace, Name: kbv1.ScriptsConfigMap(kb.Name)} + scriptsConfigMap := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsn.Name, + Namespace: kb.Namespace, + Labels: label.NewLabels(nsn), + }, + Data: map[string]string{ + KibanaInitScriptConfigKey: initScript, + }, + } + + reconciled := &corev1.ConfigMap{} + return reconciler.ReconcileResource( + reconciler.Params{ + Context: ctx, + Client: c, + Owner: &kb, + Expected: &scriptsConfigMap, + Reconciled: reconciled, + NeedsUpdate: func() bool { + return !reflect.DeepEqual(scriptsConfigMap.Data, reconciled.Data) || + !maps.IsSubset(scriptsConfigMap.Labels, reconciled.Labels) || + !maps.IsSubset(scriptsConfigMap.Annotations, reconciled.Annotations) + }, + UpdateReconciled: func() { + reconciled.Data = scriptsConfigMap.Data + reconciled.Labels = maps.Merge(reconciled.Labels, scriptsConfigMap.Labels) + reconciled.Annotations = maps.Merge(reconciled.Annotations, scriptsConfigMap.Annotations) + }, + }, + ) +} diff --git a/pkg/controller/kibana/initcontainer/configmap_test.go b/pkg/controller/kibana/initcontainer/configmap_test.go new file mode 100644 index 0000000000..1f1d7d46d1 --- /dev/null +++ b/pkg/controller/kibana/initcontainer/configmap_test.go @@ -0,0 +1,44 @@ +// 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 initcontainer + +import ( + "fmt" + "testing" + + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/volume" +) + +func TestNewScriptsConfigMapVolume(t *testing.T) { + tests := []struct { + name string + kbName string + verify func(volume.ConfigMapVolume) error + }{ + { + name: "returns expected volume name, mount path, and mode", + kbName: "test-kb", + verify: func(v volume.ConfigMapVolume) error { + if v.Name() != "kibana-scripts" { + return fmt.Errorf("unexpected name: %s", v.Name()) + } + if v.VolumeMount().MountPath != "/mnt/elastic-internal/scripts" { + return fmt.Errorf("unexpected mount path: %s", v.VolumeMount().MountPath) + } + if *v.Volume().ConfigMap.DefaultMode != 0755 { + return fmt.Errorf("unexpected default mode: %d", *v.Volume().ConfigMap.DefaultMode) + } + return nil + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewScriptsConfigMapVolume(tt.kbName); tt.verify(got) != nil { + t.Errorf("NewScriptsConfigMapVolume() = %s", tt.verify(got)) + } + }) + } +} diff --git a/pkg/controller/kibana/initcontainer/fs_scripts.go b/pkg/controller/kibana/initcontainer/fs_scripts.go new file mode 100644 index 0000000000..3de0cd0364 --- /dev/null +++ b/pkg/controller/kibana/initcontainer/fs_scripts.go @@ -0,0 +1,82 @@ +// 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 initcontainer + +import ( + "bytes" + "text/template" + + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/settings" +) + +const ( + KibanaInitScriptConfigKey = "init.sh" +) + +// templateParams are the parameters used in the initFSScriptTemplate template. +type templateParams struct { + // ContainerPluginsMountPath is the mount path for plugins + // within the Kibana container. + ContainerPluginsMountPath string + // InitContainerPluginsMountPath is the mount path for plugins + // within the init container. + InitContainerPluginsMountPath string +} + +var initFsScriptTemplate = template.Must(template.New("").Parse( + `#!/usr/bin/env bash +set -eux + +# compute time in seconds since the given start time +function duration() { + local start=$1 + end=$(date +%s) + echo $((end-start)) +} + +####################### +# Plugins persistence # +####################### + +init_plugins_copied_flag={{.InitContainerPluginsMountPath}}/elastic-internal-init-plugins.ok + +# Persist the content of plugins/ to a volume, so installed +# plugins files can to be used by the Kibana container. +mv_start=$(date +%s) +if [[ ! -f "${init_plugins_copied_flag}" ]]; then + if [[ -z "$(ls -A {{.ContainerPluginsMountPath}})" ]]; then + echo "Empty dir {{.ContainerPluginsMountPath}}" + else + echo "Copying {{.ContainerPluginsMountPath}}/* to {{.InitContainerPluginsMountPath}}/" + # Use "yes" and "-f" as we want the init container to be idempotent and not to fail when executed more than once. + yes | cp -avf {{.ContainerPluginsMountPath}}/* {{.InitContainerPluginsMountPath}}/ + fi +fi +touch "${init_plugins_copied_flag}" +echo "Files copy duration: $(duration $mv_start) sec." + +init_config_initialized_flag=` + settings.InitContainerConfigVolumeMountPath + `/elastic-internal-init-config.ok + +if [[ -f "${init_config_initialized_flag}" ]]; then + echo "Kibana configuration already initialized." + exit 0 +fi + +echo "Setup Kibana configuration" + +ln -sf ` + settings.InternalConfigVolumeMountPath + `/* ` + settings.InitContainerConfigVolumeMountPath + `/ + +touch "${init_config_initialized_flag}" +echo "Kibana configuration successfully prepared." +`)) + +// renderScriptTemplate renders initFsScriptTemplate using the given TemplateParams +func renderScriptTemplate(params templateParams) (string, error) { + tplBuffer := bytes.Buffer{} + if err := initFsScriptTemplate.Execute(&tplBuffer, params); err != nil { + return "", err + } + return tplBuffer.String(), nil +} diff --git a/pkg/controller/kibana/initcontainer/fs_scripts_test.go b/pkg/controller/kibana/initcontainer/fs_scripts_test.go new file mode 100644 index 0000000000..45e3ec707b --- /dev/null +++ b/pkg/controller/kibana/initcontainer/fs_scripts_test.go @@ -0,0 +1,89 @@ +// 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 initcontainer + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestRenderScriptTemplate(t *testing.T) { + type args struct { + params templateParams + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "template renders with plugin section", + args: args{params: templateParams{ + ContainerPluginsMountPath: "/usr/share/kibana/plugins", + InitContainerPluginsMountPath: "/mnt/elastic-internal/kibana-plugins-local", + }}, + want: `#!/usr/bin/env bash +set -eux + +# compute time in seconds since the given start time +function duration() { + local start=$1 + end=$(date +%s) + echo $((end-start)) +} + +####################### +# Plugins persistence # +####################### + +init_plugins_copied_flag=/mnt/elastic-internal/kibana-plugins-local/elastic-internal-init-plugins.ok + +# Persist the content of plugins/ to a volume, so installed +# plugins files can to be used by the Kibana container. +mv_start=$(date +%s) +if [[ ! -f "${init_plugins_copied_flag}" ]]; then + if [[ -z "$(ls -A /usr/share/kibana/plugins)" ]]; then + echo "Empty dir /usr/share/kibana/plugins" + else + echo "Copying /usr/share/kibana/plugins/* to /mnt/elastic-internal/kibana-plugins-local/" + # Use "yes" and "-f" as we want the init container to be idempotent and not to fail when executed more than once. + yes | cp -avf /usr/share/kibana/plugins/* /mnt/elastic-internal/kibana-plugins-local/ + fi +fi +touch "${init_plugins_copied_flag}" +echo "Files copy duration: $(duration $mv_start) sec." + +init_config_initialized_flag=/mnt/elastic-internal/kibana-config-local/elastic-internal-init-config.ok + +if [[ -f "${init_config_initialized_flag}" ]]; then + echo "Kibana configuration already initialized." + exit 0 +fi + +echo "Setup Kibana configuration" + +ln -sf /mnt/elastic-internal/kibana-config/* /mnt/elastic-internal/kibana-config-local/ + +touch "${init_config_initialized_flag}" +echo "Kibana configuration successfully prepared." +`, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := renderScriptTemplate(tt.args.params) + if (err != nil) != tt.wantErr { + t.Errorf("RenderScriptTemplate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("RenderScriptTemplate() diff = %s", cmp.Diff(got, tt.want)) + } + }) + } +} diff --git a/pkg/controller/kibana/keystore.go b/pkg/controller/kibana/initcontainer/keystore.go similarity index 86% rename from pkg/controller/kibana/keystore.go rename to pkg/controller/kibana/initcontainer/keystore.go index a69429090b..517393a157 100644 --- a/pkg/controller/kibana/keystore.go +++ b/pkg/controller/kibana/initcontainer/keystore.go @@ -2,7 +2,7 @@ // 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 +package initcontainer import ( corev1 "k8s.io/api/core/v1" @@ -11,18 +11,19 @@ import ( kbv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/kibana/v1" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/keystore" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/version" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/settings" ) // keystoreInConfigDirVersion is the version in which the keystore is no longer stored in the data directory but in the config one. var keystoreInConfigDirVersion = version.From(7, 9, 0) -// newInitContainersParameters is used to generate the init container that will load the secure settings into a keystore -func newInitContainersParameters(kb *kbv1.Kibana) (keystore.InitContainerParameters, error) { +// NewInitContainersParameters is used to generate the init container that will load the secure settings into a keystore +func NewInitContainersParameters(kb *kbv1.Kibana) (keystore.InitContainerParameters, error) { parameters := keystore.InitContainerParameters{ KeystoreCreateCommand: "/usr/share/kibana/bin/kibana-keystore create", KeystoreAddCommand: `/usr/share/kibana/bin/kibana-keystore add "$key" --stdin < "$filename"`, SecureSettingsVolumeMountPath: keystore.SecureSettingsVolumeMountPath, - KeystoreVolumePath: DataVolumeMountPath, + KeystoreVolumePath: settings.DataVolumeMountPath, Resources: corev1.ResourceRequirements{ Requests: map[corev1.ResourceName]resource.Quantity{ corev1.ResourceMemory: resource.MustParse("128Mi"), diff --git a/pkg/controller/kibana/initcontainer/prepare_fs.go b/pkg/controller/kibana/initcontainer/prepare_fs.go new file mode 100644 index 0000000000..933defc405 --- /dev/null +++ b/pkg/controller/kibana/initcontainer/prepare_fs.go @@ -0,0 +1,95 @@ +// 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 initcontainer + +import ( + "path" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + + kbv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/kibana/v1" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/defaults" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/volume" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/settings" +) + +var ( + // ConfigSharedVolume contains the Kibana config/ directory, it's an empty volume where the required configuration + // is initialized by the elastic-internal-init init container. Its content is then shared by the init container + // that creates the keystore and the main Kibana container. + // This is needed in order to have in a same directory both the generated configuration and the keystore file which + // is created in /usr/share/kibana/config since Kibana 7.9 + ConfigSharedVolume = volume.SharedVolume{ + VolumeName: settings.ConfigVolumeName, + InitContainerMountPath: settings.InitContainerConfigVolumeMountPath, + ContainerMountPath: settings.ConfigVolumeMountPath, + } + + // PluginsSharedVolume contains the Kibana plugins/ directory + PluginsSharedVolume = volume.SharedVolume{ + // This volume name is the same as the primary container's volume name + // so that the init container does not mount the plugins emptydir volume + // on top of /usr/share/kibana/plugins. + VolumeName: settings.PluginsVolumeName, + InitContainerMountPath: settings.PluginsVolumeInternalMountPath, + ContainerMountPath: settings.PluginsVolumeMountPath, + } + + PluginVolumes = volume.SharedVolumeArray{ + Array: []volume.SharedVolume{ + PluginsSharedVolume, + }, + } + + // defaultResources are the default request and limits for the init container. + defaultResources = corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceMemory: resource.MustParse("50Mi"), + corev1.ResourceCPU: resource.MustParse("0.1"), + }, + Limits: map[corev1.ResourceName]resource.Quantity{ + // Memory limit should be at least 12582912 when running with CRI-O + corev1.ResourceMemory: resource.MustParse("50Mi"), + corev1.ResourceCPU: resource.MustParse("0.1"), + }, + } +) + +// ConfigVolume returns a SecretVolume to hold the Kibana config of the given Kibana resource. +func ConfigVolume(kb kbv1.Kibana) volume.SecretVolume { + return volume.NewSecretVolumeWithMountPath( + kbv1.ConfigSecret(kb), + settings.InternalConfigVolumeName, + settings.InternalConfigVolumeMountPath, + ) +} + +// NewInitContainer creates an init container to handle kibana configuration and plugins persistence. +func NewInitContainer(kb kbv1.Kibana, setDefaultSecurityContext bool) (corev1.Container, error) { + container := corev1.Container{ + ImagePullPolicy: corev1.PullIfNotPresent, + Name: settings.InitContainerName, + Env: defaults.PodDownwardEnvVars(), + Command: []string{"/usr/bin/env", "bash", "-c", path.Join(settings.ScriptsVolumeMountPath, KibanaInitScriptConfigKey)}, + VolumeMounts: []corev1.VolumeMount{ + ConfigSharedVolume.InitContainerVolumeMount(), + ConfigVolume(kb).VolumeMount(), + PluginsSharedVolume.InitContainerVolumeMount(), + }, + Resources: defaultResources, + } + + return container, nil +} + +// renderInitScript renders the init script that will be run by the init container. +func renderInitScript() (string, error) { + templateParams := templateParams{ + ContainerPluginsMountPath: PluginsSharedVolume.ContainerMountPath, + InitContainerPluginsMountPath: PluginsSharedVolume.InitContainerMountPath, + } + return renderScriptTemplate(templateParams) +} diff --git a/pkg/controller/kibana/initcontainer/prepare_fs_test.go b/pkg/controller/kibana/initcontainer/prepare_fs_test.go new file mode 100644 index 0000000000..fc1ba2c325 --- /dev/null +++ b/pkg/controller/kibana/initcontainer/prepare_fs_test.go @@ -0,0 +1,178 @@ +// 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 initcontainer + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/google/go-cmp/cmp" + + kbv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/kibana/v1" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/defaults" +) + +func TestNewInitContainer(t *testing.T) { + defaultKibana := kbv1.Kibana{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test-ns", + }, + Spec: kbv1.KibanaSpec{ + Version: "7.10.0", + }, + } + olderKibana := defaultKibana + olderKibana.Spec.Version = "7.8.0" + type args struct { + kb kbv1.Kibana + setDefaultSecurityContext bool + } + tests := []struct { + name string + args args + want corev1.Container + wantErr bool + }{ + { + name: "newer Kibana without default security context includes plugins volume", + args: args{ + kb: defaultKibana, + setDefaultSecurityContext: false, + }, + want: corev1.Container{ + ImagePullPolicy: corev1.PullIfNotPresent, + Name: "elastic-internal-init", + Env: defaults.PodDownwardEnvVars(), + Command: []string{"/usr/bin/env", "bash", "-c", "/mnt/elastic-internal/scripts/init.sh"}, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "elastic-internal-kibana-config-local", + MountPath: "/mnt/elastic-internal/kibana-config-local", + }, + { + Name: "elastic-internal-kibana-config", + MountPath: "/mnt/elastic-internal/kibana-config", + ReadOnly: true, + }, + { + Name: "kibana-plugins", + MountPath: "/mnt/elastic-internal/kibana-plugins-local", + }, + }, + Resources: defaultResources, + }, + wantErr: false, + }, + { + name: "newer Kibana with default security context includes plugins volume", + args: args{ + kb: defaultKibana, + setDefaultSecurityContext: true, + }, + want: corev1.Container{ + ImagePullPolicy: corev1.PullIfNotPresent, + Name: "elastic-internal-init", + Env: defaults.PodDownwardEnvVars(), + Command: []string{"/usr/bin/env", "bash", "-c", "/mnt/elastic-internal/scripts/init.sh"}, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "elastic-internal-kibana-config-local", + MountPath: "/mnt/elastic-internal/kibana-config-local", + ReadOnly: false, + }, + { + Name: "elastic-internal-kibana-config", + MountPath: "/mnt/elastic-internal/kibana-config", + ReadOnly: true, + }, + { + Name: "kibana-plugins", + MountPath: "/mnt/elastic-internal/kibana-plugins-local", + ReadOnly: false, + }, + }, + Resources: defaultResources, + }, + wantErr: false, + }, + { + name: "older Kibana without default security context includes plugins volume", + args: args{ + kb: olderKibana, + setDefaultSecurityContext: false, + }, + want: corev1.Container{ + ImagePullPolicy: corev1.PullIfNotPresent, + Name: "elastic-internal-init", + Env: defaults.PodDownwardEnvVars(), + Command: []string{"/usr/bin/env", "bash", "-c", "/mnt/elastic-internal/scripts/init.sh"}, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "elastic-internal-kibana-config-local", + MountPath: "/mnt/elastic-internal/kibana-config-local", + ReadOnly: false, + }, + { + Name: "elastic-internal-kibana-config", + MountPath: "/mnt/elastic-internal/kibana-config", + ReadOnly: true, + }, + { + Name: "kibana-plugins", + MountPath: "/mnt/elastic-internal/kibana-plugins-local", + }, + }, + Resources: defaultResources, + }, + wantErr: false, + }, + { + name: "older Kibana with default security context does not include plugins volume", + args: args{ + kb: olderKibana, + setDefaultSecurityContext: true, + }, + want: corev1.Container{ + ImagePullPolicy: corev1.PullIfNotPresent, + Name: "elastic-internal-init", + Env: defaults.PodDownwardEnvVars(), + Command: []string{"/usr/bin/env", "bash", "-c", "/mnt/elastic-internal/scripts/init.sh"}, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "elastic-internal-kibana-config-local", + MountPath: "/mnt/elastic-internal/kibana-config-local", + ReadOnly: false, + }, + { + Name: "elastic-internal-kibana-config", + MountPath: "/mnt/elastic-internal/kibana-config", + ReadOnly: true, + }, + { + Name: "kibana-plugins", + MountPath: "/mnt/elastic-internal/kibana-plugins-local", + }, + }, + Resources: defaultResources, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewInitContainer(tt.args.kb, tt.args.setDefaultSecurityContext) + if (err != nil) != tt.wantErr { + t.Errorf("NewInitContainer() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !cmp.Equal(got, tt.want) { + t.Errorf("NewInitContainer() diff = %s", cmp.Diff(got, tt.want)) + } + }) + } +} diff --git a/pkg/controller/kibana/pod.go b/pkg/controller/kibana/pod.go index 8b9b66bab0..065341c833 100644 --- a/pkg/controller/kibana/pod.go +++ b/pkg/controller/kibana/pod.go @@ -25,43 +25,35 @@ import ( "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/settings" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/version" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/volume" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/initcontainer" kblabel "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/label" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/network" + kbsettings "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/settings" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana/stackmon" ) const ( - DataVolumeName = "kibana-data" - DataVolumeMountPath = "/usr/share/kibana/data" - PluginsVolumeName = "kibana-plugins" - PluginsVolumeMountPath = "/usr/share/kibana/plugins" - LogsVolumeName = "kibana-logs" - LogsVolumeMountPath = "/usr/share/kibana/logs" - TempVolumeName = "temp-volume" - TempVolumeMountPath = "/tmp" - KibanaBasePathEnvName = "SERVER_BASEPATH" - KibanaRewriteBasePathEnvName = "SERVER_REWRITEBASEPATH" - defaultFSGroup = 1000 - defaultFSUser = 1000 + defaultFSGroup = 1000 + defaultFSUser = 1000 ) var ( // DataVolume is used to propagate the keystore file from the init container to // Kibana running in the main container. // Since Kibana is stateless and the keystore is created on pod start, an EmptyDir is fine here. - DataVolume = volume.NewEmptyDirVolume(DataVolumeName, DataVolumeMountPath) + DataVolume = volume.NewEmptyDirVolume(kbsettings.DataVolumeName, kbsettings.DataVolumeMountPath) // PluginsVolume can be used to persist plugins after installation via an init container when // the Kibana pod has readOnlyRootFilesystem set to true. - PluginsVolume = volume.NewEmptyDirVolume(PluginsVolumeName, PluginsVolumeMountPath) + PluginsVolume = volume.NewEmptyDirVolume(kbsettings.PluginsVolumeName, kbsettings.PluginsVolumeMountPath) // LogsVolume can be used to persist logs even when // the Kibana pod has readOnlyRootFilesystem set to true. - LogsVolume = volume.NewEmptyDirVolume(LogsVolumeName, LogsVolumeMountPath) + LogsVolume = volume.NewEmptyDirVolume(kbsettings.LogsVolumeName, kbsettings.LogsVolumeMountPath) // TempVolume can be used for some reporting features when the Kibana pod has // readOnlyRootFilesystem set to true. - TempVolume = volume.NewEmptyDirVolume(TempVolumeName, TempVolumeMountPath) + TempVolume = volume.NewEmptyDirVolume(kbsettings.TempVolumeName, kbsettings.TempVolumeMountPath) DefaultMemoryLimits = resource.MustParse("1Gi") DefaultResources = corev1.ResourceRequirements{ @@ -127,14 +119,16 @@ func NewPodTemplateSpec( return corev1.PodTemplateSpec{}, err // error unlikely and should have been caught during validation } + scriptsConfigMapVolume := initcontainer.NewScriptsConfigMapVolume(kb.Name) builder := defaults.NewPodTemplateBuilder(kb.Spec.PodTemplate, kbv1.KibanaContainerName). WithResources(DefaultResources). WithLabels(labels). WithAnnotations(DefaultAnnotations). WithDockerImage(kb.Spec.Image, container.ImageRepository(container.KibanaImage, v)). WithReadinessProbe(readinessProbe(kb.Spec.HTTP.TLS.Enabled(), basePath)). - WithPorts(ports). - WithInitContainers(initConfigContainer(kb)) + WithVolumes(scriptsConfigMapVolume.Volume()).WithVolumeMounts(scriptsConfigMapVolume.VolumeMount()). + WithVolumes(PluginsVolume.Volume()).WithVolumeMounts(PluginsVolume.VolumeMount()). + WithPorts(ports) for _, volume := range volumes { builder.WithVolumes(volume.Volume()).WithVolumeMounts(volume.VolumeMount()) @@ -142,18 +136,27 @@ func NewPodTemplateSpec( // Kibana 7.5.0 and above support running with a read-only root filesystem, // but require a temporary volume to be mounted at /tmp for some reporting features - // and a plugin volume mounted at /usr/share/kibana/plugins. + // and a plugin volume mounted at /usr/share/kibana/plugins. Also needed is an + // init container to copy any existing plugins in /usr/share/kibana/plugins to the + // temporary volume. // Limiting to 7.10.0 here as there was a bug in previous versions causing rebuilding // of browser bundles to happen on plugin install, which would attempt a write to the // root filesystem on restart. - if v.GTE(version.From(7, 10, 0)) && setDefaultSecurityContext { + var canEnableSecurityContext = v.GTE(initcontainer.HardenedSecurityContextSupportedVersion) && setDefaultSecurityContext + if canEnableSecurityContext { builder.WithContainersSecurityContext(defaultSecurityContext). WithPodSecurityContext(defaultPodSecurityContext). WithVolumes(LogsVolume.Volume()).WithVolumeMounts(LogsVolume.VolumeMount()). - WithVolumes(PluginsVolume.Volume()).WithVolumeMounts(PluginsVolume.VolumeMount()). WithVolumes(TempVolume.Volume()).WithVolumeMounts(TempVolume.VolumeMount()) } + initContainer, err := initcontainer.NewInitContainer(kb, setDefaultSecurityContext) + if err != nil { + return corev1.PodTemplateSpec{}, err + } + + builder.WithInitContainers(initContainer) + if keystore != nil { builder.WithVolumes(keystore.Volume). WithInitContainers(keystore.InitContainer) @@ -180,19 +183,19 @@ func GetKibanaBasePathFromSpecEnv(podSpec corev1.PodSpec) (string, error) { envMap := make(map[string]string) for _, envVar := range kbContainer.Env { - if envVar.Name == KibanaBasePathEnvName || envVar.Name == KibanaRewriteBasePathEnvName { + if envVar.Name == kbsettings.BasePathEnvName || envVar.Name == kbsettings.RewriteBasePathEnvName { envMap[envVar.Name] = envVar.Value } } // If SERVER_REWRITEBASEPATH is set to true, we should use the value of SERVER_BASEPATH - if rewriteBasePath, ok := envMap[KibanaRewriteBasePathEnvName]; ok { + if rewriteBasePath, ok := envMap[kbsettings.RewriteBasePathEnvName]; ok { rewriteBasePathBool, err := strconv.ParseBool(rewriteBasePath) if err != nil { return "", fmt.Errorf("failed to parse SERVER_REWRITEBASEPATH value %s: %w", rewriteBasePath, err) } if rewriteBasePathBool { - return envMap[KibanaBasePathEnvName], nil + return envMap[kbsettings.BasePathEnvName], nil } } diff --git a/pkg/controller/kibana/pod_test.go b/pkg/controller/kibana/pod_test.go index 3139d489a2..1fbe2886bc 100644 --- a/pkg/controller/kibana/pod_test.go +++ b/pkg/controller/kibana/pod_test.go @@ -44,10 +44,10 @@ func TestNewPodTemplateSpec(t *testing.T) { assert.Equal(t, false, *pod.Spec.AutomountServiceAccountToken) assert.Len(t, pod.Spec.Containers, 1) assert.Len(t, pod.Spec.InitContainers, 1) - assert.Len(t, pod.Spec.Volumes, 0) + assert.Len(t, pod.Spec.Volumes, 2) kibanaContainer := GetKibanaContainer(pod.Spec) require.NotNil(t, kibanaContainer) - assert.Equal(t, 0, len(kibanaContainer.VolumeMounts)) + assert.Equal(t, 2, len(kibanaContainer.VolumeMounts)) assert.Equal(t, container.ImageRepository(container.KibanaImage, version.MustParse("7.1.0")), kibanaContainer.Image) assert.NotNil(t, kibanaContainer.ReadinessProbe) assert.NotEmpty(t, kibanaContainer.Ports) @@ -66,7 +66,7 @@ func TestNewPodTemplateSpec(t *testing.T) { }, assertions: func(pod corev1.PodTemplateSpec) { assert.Len(t, pod.Spec.InitContainers, 2) - assert.Len(t, pod.Spec.Volumes, 1) + assert.Len(t, pod.Spec.Volumes, 3) }, }, { @@ -219,9 +219,9 @@ func TestNewPodTemplateSpec(t *testing.T) { }}, assertions: func(pod corev1.PodTemplateSpec) { assert.Len(t, pod.Spec.InitContainers, 1) - assert.Len(t, pod.Spec.InitContainers[0].VolumeMounts, 6) - assert.Len(t, pod.Spec.Volumes, 4) - assert.Len(t, GetKibanaContainer(pod.Spec).VolumeMounts, 4) + assert.Len(t, pod.Spec.InitContainers[0].VolumeMounts, 7) + assert.Len(t, pod.Spec.Volumes, 5) + assert.Len(t, GetKibanaContainer(pod.Spec).VolumeMounts, 5) assert.Equal(t, GetKibanaContainer(pod.Spec).SecurityContext, &defaultSecurityContext) }, }, diff --git a/pkg/controller/kibana/settings/settings.go b/pkg/controller/kibana/settings/settings.go new file mode 100644 index 0000000000..d54b0d870b --- /dev/null +++ b/pkg/controller/kibana/settings/settings.go @@ -0,0 +1,46 @@ +// 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 settings + +const ( + // DataVolumeName is the name of the volume that holds the Kibana data + DataVolumeName = "kibana-data" + // DataVolumeMountPath is the path where the Kibana data is mounted in the Kibana container + DataVolumeMountPath = "/usr/share/kibana/data" + // PluginsVolumeName is the name of the volume that holds the Kibana plugins + PluginsVolumeName = "kibana-plugins" + // PluginsVolumeMountPath is the path where the Kibana plugins are mounted in the Kibana container + PluginsVolumeMountPath = "/usr/share/kibana/plugins" + // PluginsVolumeInternalMountPath is the path where the Kibana plugins are mounted in the init container + PluginsVolumeInternalMountPath = "/mnt/elastic-internal/kibana-plugins-local" + // LogsVolumeName is the name of the volume that holds the Kibana logs + LogsVolumeName = "kibana-logs" + // LogsVolumeMountPath is the path where the Kibana logs are mounted in the Kibana container + LogsVolumeMountPath = "/usr/share/kibana/logs" + // TempVolumeName is the name of the volume that holds the temporary files + TempVolumeName = "temp-volume" + // TempVolumeMountPath is the path where the temporary files are mounted in the Kibana container + TempVolumeMountPath = "/tmp" + // BasePathEnvName is the environment variable name that allows ibe to specify a path to mount Kibana at if you are running behind a proxy + BasePathEnvName = "SERVER_BASEPATH" + // RewriteBasePathEnvName is the environment variable name that specifies whether Kibana should rewrite requests that are prefixed with server.basePath + RewriteBasePathEnvName = "SERVER_REWRITEBASEPATH" + // ScriptsVolumeName is the name of the volume that holds the Kibana scripts for the init container + ScriptsVolumeName = "kibana-scripts" + // ScriptsVolumeMountPath is the path where the Kibana scripts are mounted in the init container + ScriptsVolumeMountPath = "/mnt/elastic-internal/scripts" + // InitConfigContainerName is the name of the container that initializes the configuration + InitContainerName = "elastic-internal-init" + // ConfigVolumeName is the name of the volume that holds the Kibana configuration + ConfigVolumeName = "elastic-internal-kibana-config-local" + // ConfigVolumeMountPath is the path where the Kibana configuration is mounted in the Kibana container + ConfigVolumeMountPath = "/usr/share/kibana/config" + // InitContainerConfigVolumeMountPath is the path where the Kibana configuration is mounted in the init container + InitContainerConfigVolumeMountPath = "/mnt/elastic-internal/kibana-config-local" + // InternalConfigVolumeName is a volume which contains the generated configuration. + InternalConfigVolumeName = "elastic-internal-kibana-config" + // InternalConfigVolumeMountPath is the path where the generated configuration is mounted in the Kibana init container + InternalConfigVolumeMountPath = "/mnt/elastic-internal/kibana-config" +) diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index 660c1e91f2..c69332414d 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -178,7 +178,7 @@ func (r *Reporter) reconcileKibanaSecret(ctx context.Context, kb kbv1.Kibana, te log := ulog.FromContext(ctx) var secret corev1.Secret - nsName := types.NamespacedName{Namespace: kb.Namespace, Name: kibana.SecretName(kb)} + nsName := types.NamespacedName{Namespace: kb.Namespace, Name: kbv1.ConfigSecret(kb)} if err := r.client.Get(ctx, nsName, &secret); err != nil { log.Error(err, "failed to get Kibana secret") return diff --git a/pkg/telemetry/telemetry_test.go b/pkg/telemetry/telemetry_test.go index 08a29651fa..e3aec03fce 100644 --- a/pkg/telemetry/telemetry_test.go +++ b/pkg/telemetry/telemetry_test.go @@ -27,7 +27,6 @@ import ( logstashv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/logstash/v1alpha1" mapsv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/maps/v1alpha1" policyv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/stackconfigpolicy/v1alpha1" - "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana" "github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s" ) @@ -137,7 +136,7 @@ func createKbAndSecret(name, namespace string, count int32) (kbv1.Kibana, corev1 } return kb, corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: kibana.SecretName(kb), + Name: kbv1.ConfigSecret(kb), Namespace: namespace, }, } diff --git a/test/e2e/kb/telemetry_test.go b/test/e2e/kb/telemetry_test.go index 15bff839f8..dd982edc18 100644 --- a/test/e2e/kb/telemetry_test.go +++ b/test/e2e/kb/telemetry_test.go @@ -16,7 +16,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - kibana2 "github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana" + kbv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/kibana/v1" "github.com/elastic/cloud-on-k8s/v2/test/e2e/test" "github.com/elastic/cloud-on-k8s/v2/test/e2e/test/elasticsearch" "github.com/elastic/cloud-on-k8s/v2/test/e2e/test/kibana" @@ -42,7 +42,7 @@ func TestTelemetry(t *testing.T) { var secret corev1.Secret err := k.Client.Get(context.Background(), types.NamespacedName{ Namespace: kbBuilder.Kibana.Namespace, - Name: kibana2.SecretName(kbBuilder.Kibana), + Name: kbv1.ConfigSecret(kbBuilder.Kibana), }, &secret) if err != nil { return err