pkg/controller/common/keystore/user_secret.go (156 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 keystore
import (
"context"
"fmt"
pkgerrors "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
commonv1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/common/v1"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/driver"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/events"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/hash"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/name"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/reconciler"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/volume"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/watches"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/stackconfigpolicy"
"github.com/elastic/cloud-on-k8s/v3/pkg/utils/k8s"
ulog "github.com/elastic/cloud-on-k8s/v3/pkg/utils/log"
)
const secureSettingsSecretSuffix = "secure-settings"
// secureSettingsVolume creates a volume from the optional user-provided secure settings secrets.
//
// Secure settings are provided by the user in the resource Spec through secret references.
// The user provided secrets are then aggregated into a single secret.
// This secret is mounted into the pods for secure settings to be injected into a keystore.
// The user-provided secrets are watched to reconcile on any change.
// The user secret resource version is returned along with the volume, so that
// any change in the user secret leads to pod rotation.
func secureSettingsVolume(
ctx context.Context,
r driver.Interface,
hasKeystore HasKeystore,
labels map[string]string,
namer name.Namer,
additionalSources ...commonv1.NamespacedSecretSource,
) (*volume.SecretVolume, string, error) {
// setup (or remove) watches for the user-provided secret to reconcile on any change
watcher := k8s.ExtractNamespacedName(hasKeystore)
// user-provided Secrets referenced in the resource
secretSources := WatchedSecretNames(hasKeystore)
// Additional sources, introduced to load remote cluster keys.
secretSources = append(secretSources, additionalSources...)
// user-provided Secrets referenced in a StackConfigPolicy that configures the resource
policySecretSources, err := stackconfigpolicy.GetSecureSettingsSecretSourcesForResources(ctx, r.K8sClient(), hasKeystore, hasKeystore.GetObjectKind().GroupVersionKind().Kind)
if err != nil {
return nil, "", pkgerrors.Wrap(err, "fail to get secure settings secret sources")
}
secretSources = append(secretSources, policySecretSources...)
if err := watches.WatchUserProvidedNamespacedSecrets(
watcher,
r.DynamicWatches(),
SecureSettingsWatchName(watcher),
secretSources,
); err != nil {
return nil, "", err
}
userSecrets, err := retrieveUserSecrets(ctx, r.K8sClient(), r.Recorder(), hasKeystore, secretSources)
if err != nil {
return nil, "", err
}
secureSettingsSecret, err := reconcileSecureSettings(ctx, r.K8sClient(), hasKeystore, userSecrets, namer, labels)
if err != nil {
return nil, "", err
}
if secureSettingsSecret == nil {
return nil, "", nil
}
// build a volume from that secret
secureSettingsVolume := volume.NewSecretVolumeWithMountPath(
secureSettingsSecret.Name,
SecureSettingsVolumeName,
SecureSettingsVolumeMountPath,
)
// secret data hash will be included in pod labels to recreate pods on any secret change
secureSettingsSecretHash := hash.HashObject(secureSettingsSecret.Data)
return &secureSettingsVolume, secureSettingsSecretHash, nil
}
func reconcileSecureSettings(
ctx context.Context,
c k8s.Client,
hasKeystore HasKeystore,
userSecrets []corev1.Secret,
namer name.Namer,
labels map[string]string) (*corev1.Secret, error) {
aggregatedData := map[string][]byte{}
for _, s := range userSecrets {
for k, v := range s.Data {
aggregatedData[k] = v
}
}
// reconcile our managed secret with the user-provided secret content
expected := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secureSettingsSecretName(namer, hasKeystore),
Namespace: hasKeystore.GetNamespace(),
Labels: labels,
},
Data: aggregatedData,
}
if len(aggregatedData) == 0 {
// no secure settings specified, delete any existing operator-managed settings secret
err := k8s.DeleteSecretIfExists(ctx, c, k8s.ExtractNamespacedName(&expected))
return nil, err
}
secret, err := reconciler.ReconcileSecret(ctx, c, expected, hasKeystore)
if err != nil {
return nil, err
}
return &secret, nil
}
func retrieveUserSecrets(ctx context.Context, c k8s.Client, recorder record.EventRecorder, hasKeystore HasKeystore, userSecretSources []commonv1.NamespacedSecretSource) ([]corev1.Secret, error) {
userSecrets := make([]corev1.Secret, 0, len(userSecretSources))
for _, userSecretsRef := range userSecretSources {
// retrieve the secret referenced by the user in the same namespace
userSecret, exists, err := retrieveUserSecret(ctx, c, recorder, hasKeystore, userSecretsRef)
if err != nil {
return nil, err
}
if !exists {
// a secret does not exist (yet)
continue
}
userSecrets = append(userSecrets, *userSecret)
}
return userSecrets, nil
}
func retrieveUserSecret(ctx context.Context, c k8s.Client, recorder record.EventRecorder, hasKeystore HasKeystore, secretSrc commonv1.NamespacedSecretSource) (*corev1.Secret, bool, error) {
secretNamespace := secretSrc.Namespace
secretName := secretSrc.SecretName
var userSecret corev1.Secret
err := c.Get(context.Background(), types.NamespacedName{Namespace: secretNamespace, Name: secretName}, &userSecret)
if err != nil && apierrors.IsNotFound(err) {
msg := "Secure settings secret not found"
ulog.FromContext(ctx).Info(msg, "namespace", secretNamespace, "secret_name", secretName)
recorder.Event(hasKeystore, corev1.EventTypeWarning, events.EventReasonUnexpected, fmt.Sprintf("%s: %s/%s", msg, secretNamespace, secretName))
return nil, false, nil
} else if err != nil {
return nil, false, err
}
// If no entries, return the whole user secret
if secretSrc.Entries == nil {
return &userSecret, true, nil
}
if len(secretSrc.Entries) == 0 {
return nil, false, pkgerrors.Errorf("set is empty in secure settings secret %s", secretName)
}
// Else if entries is defined, return only a subset of the user secret
projectionSecret := corev1.Secret{
ObjectMeta: userSecret.ObjectMeta,
Data: map[string][]byte{},
}
for _, entry := range secretSrc.Entries {
if entry.Key == "" {
return nil, false, pkgerrors.Errorf("key is empty in secure settings secret %s", secretName)
}
newKey := entry.Path
if newKey == "" {
newKey = entry.Key
}
value, ok := userSecret.Data[entry.Key]
if !ok {
return nil, false, pkgerrors.Errorf("key %s not found in secure settings secret %s", entry.Key, secretName)
}
projectionSecret.Data[newKey] = value
}
return &projectionSecret, true, nil
}
func secureSettingsSecretName(namer name.Namer, hasKeystore HasKeystore) string {
return namer.Suffix(hasKeystore.GetName(), secureSettingsSecretSuffix)
}
// SecureSettingsWatchName returns the watch name according to the deployment name.
// It is unique per APM or Kibana deployment.
func SecureSettingsWatchName(namespacedName types.NamespacedName) string {
return fmt.Sprintf("%s-%s-secure-settings", namespacedName.Namespace, namespacedName.Name)
}