Skip to content

Commit f3d85c7

Browse files
committed
MC-1296 Make k8ssandra-operator deploy Reaper capable of interaction with encrypted mgmt-api
1 parent 992633e commit f3d85c7

File tree

13 files changed

+561
-60
lines changed

13 files changed

+561
-60
lines changed

CHANGELOG/CHANGELOG-1.21.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ When cutting a new release, update the `unreleased` heading to the tag being gen
1515

1616
## unreleased
1717

18-
18+
* [FEATURE] [#1508](https://github.com/riptano/mission-control/issues/1508) Make k8ssandra-operator deploy Reaper capable of interaction with encrypted mgmt-api

controllers/k8ssandra/reaper.go

+90
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
k8ssandralabels "github.com/k8ssandra/k8ssandra-operator/pkg/labels"
3030
"github.com/k8ssandra/k8ssandra-operator/pkg/reaper"
3131
"github.com/k8ssandra/k8ssandra-operator/pkg/result"
32+
corev1 "k8s.io/api/core/v1"
3233
"k8s.io/apimachinery/pkg/api/errors"
3334
"k8s.io/apimachinery/pkg/labels"
3435
"k8s.io/apimachinery/pkg/types"
@@ -100,6 +101,13 @@ func (r *K8ssandraClusterReconciler) reconcileReaper(
100101
// we might have nil-ed the template because a DC got stopped, so we need to re-check
101102
if reaperTemplate != nil {
102103
if reaperTemplate.HasReaperRef() {
104+
105+
if uses, conf := usesHttpAuth(kc, actualDc); uses {
106+
if err := addManagementApiSecretsToReaper(ctx, remoteClient, kc, actualDc, logger, conf); err != nil {
107+
return result.Error(err)
108+
}
109+
}
110+
103111
logger.Info("ReaperRef present, registering with referenced Reaper instead of creating a new one")
104112
return r.addClusterToExternalReaper(ctx, kc, actualDc, logger)
105113
}
@@ -267,6 +275,22 @@ func getSingleReaperDcName(kc *api.K8ssandraCluster) string {
267275
return ""
268276
}
269277

278+
func usesHttpAuth(kc *api.K8ssandraCluster, actualDc *cassdcapi.CassandraDatacenter) (bool, *cassdcapi.ManagementApiAuthManualConfig) {
279+
// check for the mgmt api auth config in the cass-dc object of the DC were in
280+
for _, dc := range kc.Spec.Cassandra.Datacenters {
281+
if !dc.Stopped && dc.DatacenterName == actualDc.DatacenterName() {
282+
return true, dc.ManagementApiAuth.Manual
283+
}
284+
}
285+
// if that wasn't found, then check for the mgmt api auth config in the cluster object
286+
if kc.Spec.Cassandra.ManagementApiAuth != nil {
287+
if kc.Spec.Cassandra.ManagementApiAuth.Manual != nil {
288+
return true, kc.Spec.Cassandra.ManagementApiAuth.Manual
289+
}
290+
}
291+
return false, nil
292+
}
293+
270294
func (r *K8ssandraClusterReconciler) addClusterToExternalReaper(
271295
ctx context.Context,
272296
kc *api.K8ssandraCluster,
@@ -290,3 +314,69 @@ func (r *K8ssandraClusterReconciler) addClusterToExternalReaper(
290314
}
291315
return result.Continue()
292316
}
317+
318+
func addManagementApiSecretsToReaper(
319+
ctx context.Context,
320+
remoteClient client.Client,
321+
kc *api.K8ssandraCluster,
322+
actualDc *cassdcapi.CassandraDatacenter,
323+
logger logr.Logger,
324+
authConfig *cassdcapi.ManagementApiAuthManualConfig,
325+
) error {
326+
327+
reaperName := kc.Spec.Reaper.ReaperRef.Name
328+
329+
reaperNamespace := kc.Spec.Reaper.ReaperRef.Namespace
330+
if reaperNamespace == "" {
331+
reaperNamespace = kc.Namespace
332+
}
333+
334+
tssName := reaper.GetTruststoresSecretName(reaperName)
335+
tssKey := client.ObjectKey{Namespace: reaperNamespace, Name: tssName}
336+
337+
tss := &corev1.Secret{}
338+
if err := remoteClient.Get(ctx, tssKey, tss); err != nil {
339+
logger.Error(err, "failed to get Reaper's truststore secret")
340+
return err
341+
}
342+
343+
cs := &corev1.Secret{}
344+
csName := authConfig.ClientSecretName + "-ks"
345+
csKey := client.ObjectKey{Namespace: kc.Namespace, Name: csName}
346+
if err := remoteClient.Get(ctx, csKey, cs); err != nil {
347+
logger.Error(err, "failed to get k8ssandra cluster client secret", "secretName", csName)
348+
return err
349+
}
350+
351+
clusterName := cassdcapi.CleanupForKubernetes(actualDc.Spec.ClusterName)
352+
clustersTruststore := clusterName + "-truststore.jks"
353+
clustersKeystore := clusterName + "-keystore.jks"
354+
355+
// check if the reaper's big secret already has entry for this cluster
356+
if tss.Data == nil {
357+
tss.Data = make(map[string][]byte)
358+
}
359+
_, hasTruststore := tss.Data[clustersTruststore]
360+
_, hasKeystore := tss.Data[clustersKeystore]
361+
362+
if hasTruststore && hasKeystore {
363+
logger.Info("Cluster secrets already present in Reapers secret", "reaperName", reaperName, "clusterName", kc.Name)
364+
return nil
365+
}
366+
367+
logger.Info("Patching Reaper's truststores with new secrets", "reaperName", reaperName, "clusterName", kc.Name)
368+
369+
patch := client.MergeFrom(tss.DeepCopy())
370+
if !hasTruststore {
371+
tss.Data[clustersTruststore] = cs.Data["truststore.jks"]
372+
}
373+
if !hasKeystore {
374+
tss.Data[clustersKeystore] = cs.Data["keystore.jks"]
375+
}
376+
if err := remoteClient.Patch(ctx, tss, patch); err != nil {
377+
logger.Error(err, "failed to patch reaper's config map")
378+
return err
379+
}
380+
381+
return nil
382+
}

controllers/k8ssandra/reaper_test.go

+11-8
Original file line numberDiff line numberDiff line change
@@ -235,18 +235,28 @@ func createMultiDcClusterWithReaper(t *testing.T, ctx context.Context, f *framew
235235

236236
func createMultiDcClusterWithControlPlaneReaper(t *testing.T, ctx context.Context, f *framework.Framework, namespace string) {
237237
require := require.New(t)
238+
reaperName := "reaper"
238239

239240
cpr := &reaperapi.Reaper{
240241
ObjectMeta: metav1.ObjectMeta{
241242
Namespace: namespace,
242-
Name: "reaper",
243+
Name: reaperName,
243244
},
244245
Spec: newControlPlaneReaper(),
245246
}
246247

247248
err := f.Client.Create(ctx, cpr)
248249
require.NoError(err, "failed to create control plane reaper")
249250

251+
rts := &corev1.Secret{
252+
ObjectMeta: metav1.ObjectMeta{
253+
Namespace: namespace,
254+
Name: reaper.GetTruststoresSecretName(reaperName),
255+
},
256+
}
257+
err = f.Client.Create(ctx, rts)
258+
require.NoError(err, "failed to create reaper's truststore secret")
259+
250260
cpReaperKey := framework.ClusterKey{
251261
K8sContext: f.ControlPlaneContext,
252262
NamespacedName: types.NamespacedName{
@@ -308,9 +318,6 @@ func createMultiDcClusterWithControlPlaneReaper(t *testing.T, ctx context.Contex
308318
verifyReaperAbsent(t, f, ctx, kc, f.DataPlaneContexts[0], dc1Key, namespace)
309319
verifyReaperAbsent(t, f, ctx, kc, f.DataPlaneContexts[1], dc2Key, namespace)
310320

311-
// check the kc is added to reaper
312-
verifyClusterRegistered(t, f, ctx, kc, namespace)
313-
314321
err = f.DeleteK8ssandraCluster(ctx, utils.GetKey(kc), timeout, interval)
315322
require.NoError(err, "failed to delete K8ssandraCluster")
316323
}
@@ -401,7 +408,3 @@ func verifyReaperAbsent(t *testing.T, f *framework.Framework, ctx context.Contex
401408
err := f.Get(ctx, reaperKey, reaper)
402409
require.True(t, err != nil && errors.IsNotFound(err), fmt.Sprintf("reaper %s should not be created in dc %s", reaperKey, dcKey))
403410
}
404-
405-
func verifyClusterRegistered(t *testing.T, f *framework.Framework, ctx context.Context, kc *api.K8ssandraCluster, namespace string) {
406-
407-
}

controllers/reaper/reaper_controller.go

+36
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
appsv1 "k8s.io/api/apps/v1"
3131
corev1 "k8s.io/api/core/v1"
3232
"k8s.io/apimachinery/pkg/api/errors"
33+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3334
"k8s.io/apimachinery/pkg/runtime"
3435
"k8s.io/apimachinery/pkg/types"
3536
"k8s.io/utils/ptr"
@@ -206,6 +207,13 @@ func (r *ReaperReconciler) reconcileDeployment(
206207
}
207208
}
208209

210+
if actualReaper.Spec.HttpManagement.Enabled {
211+
err := r.reconcileTrustStoresSecret(ctx, actualReaper, logger)
212+
if err != nil {
213+
return ctrl.Result{}, err
214+
}
215+
}
216+
209217
logger.Info("Reconciling reaper deployment", "actualReaper", actualReaper)
210218

211219
// work out how to deploy Reaper
@@ -484,3 +492,31 @@ func (r *ReaperReconciler) SetupWithManager(mgr ctrl.Manager) error {
484492
Owns(&corev1.Service{}).
485493
Complete(r)
486494
}
495+
496+
func (r *ReaperReconciler) reconcileTrustStoresSecret(ctx context.Context, actualReaper *reaperapi.Reaper, logger logr.Logger) error {
497+
sName := reaper.GetTruststoresSecretName(actualReaper.Name)
498+
sKey := types.NamespacedName{Namespace: actualReaper.Namespace, Name: sName}
499+
s := &corev1.Secret{
500+
ObjectMeta: metav1.ObjectMeta{
501+
Name: sName,
502+
Namespace: actualReaper.Namespace,
503+
},
504+
}
505+
if err := r.Client.Get(ctx, sKey, s); err != nil {
506+
if errors.IsNotFound(err) {
507+
logger.Info("Creating Reaper's truststore ConfigMap", "ConfigMap", sKey)
508+
if err = controllerutil.SetControllerReference(actualReaper, s, r.Scheme); err != nil {
509+
logger.Error(err, "Failed to set owner on truststore ConfigMap")
510+
return err
511+
}
512+
if err = r.Client.Create(ctx, s); err != nil {
513+
logger.Error(err, "Failed to create Reaper's truststore ConfigMap")
514+
return err
515+
}
516+
return nil
517+
}
518+
logger.Error(err, "Failed to get Reaper's truststores ConfigMap")
519+
return err
520+
}
521+
return nil
522+
}

controllers/reaper/reaper_controller_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package reaper
22

33
import (
44
"context"
5+
"k8s.io/apimachinery/pkg/api/errors"
56
"k8s.io/apimachinery/pkg/api/resource"
67
"k8s.io/utils/ptr"
78
"testing"
@@ -64,6 +65,7 @@ func TestReaper(t *testing.T) {
6465
t.Run("CreateReaperWithAuthEnabled", reaperControllerTest(ctx, testEnv, testCreateReaperWithAuthEnabled))
6566
t.Run("CreateReaperWithAuthEnabledExternalSecret", reaperControllerTest(ctx, testEnv, testCreateReaperWithAuthEnabledExternalSecret))
6667
t.Run("CreateReaperWithLocalStorageBackend", reaperControllerTest(ctx, testEnv, testCreateReaperWithLocalStorageType))
68+
t.Run("CreateReaperWithHttpAuthEnabled", reaperControllerTest(ctx, testEnv, testCreateReaperWithHttpAuthEnabled))
6769
}
6870

6971
func newMockManager() reaper.Manager {
@@ -570,6 +572,72 @@ func testCreateReaperWithLocalStorageType(t *testing.T, ctx context.Context, k8s
570572
assert.Equal(t, "reaper-data", dataVolumeMount.Name)
571573
}
572574

575+
func testCreateReaperWithHttpAuthEnabled(t *testing.T, ctx context.Context, k8sClient client.Client, testNamespace string) {
576+
t.Log("create the Reaper object")
577+
r := newReaper(testNamespace)
578+
r.Spec.StorageType = reaperapi.StorageTypeLocal
579+
r.Spec.StorageConfig = newStorageConfig()
580+
r.Spec.HttpManagement.Enabled = true
581+
582+
err := k8sClient.Create(ctx, r)
583+
require.NoError(t, err)
584+
585+
t.Log("check that the stateful set is created")
586+
stsKey := types.NamespacedName{Namespace: testNamespace, Name: reaperName}
587+
sts := &appsv1.StatefulSet{}
588+
589+
require.Eventually(t, func() bool {
590+
return k8sClient.Get(ctx, stsKey, sts) == nil
591+
}, timeout, interval, "stateful set creation check failed")
592+
593+
// if the http auth is enabled, reaper controller should prepare the secret where k8s controller will place per-cluster secrets
594+
secretKey := types.NamespacedName{Namespace: testNamespace, Name: reaper.GetTruststoresSecretName(r.Name)}
595+
truststoresSecret := &corev1.Secret{}
596+
require.Eventually(t, func() bool {
597+
return k8sClient.Get(ctx, secretKey, truststoresSecret) == nil
598+
}, timeout, interval, "truststore secret creation check failed")
599+
assert.True(t, truststoresSecret.Data == nil)
600+
assert.Equal(t, 0, len(truststoresSecret.Data))
601+
602+
// In this configuration, we expect Reaper to also have a mount for the http auth secrets
603+
assert.Len(t, sts.Spec.Template.Spec.Containers[0].VolumeMounts, 3)
604+
confVolumeMount := sts.Spec.Template.Spec.Containers[0].VolumeMounts[0].DeepCopy()
605+
assert.Equal(t, "conf", confVolumeMount.Name)
606+
dataVolumeMount := sts.Spec.Template.Spec.Containers[0].VolumeMounts[2].DeepCopy()
607+
assert.Equal(t, "reaper-data", dataVolumeMount.Name)
608+
truststoresVolumeMount := sts.Spec.Template.Spec.Containers[0].VolumeMounts[1].DeepCopy()
609+
assert.Equal(t, "management-api-keystores-per-cluster", truststoresVolumeMount.Name)
610+
611+
// when we delete reaper, the STS and the secret both go away due to the owner reference
612+
// however, that does not happen in env tests
613+
err = k8sClient.Delete(ctx, r)
614+
require.NoError(t, err)
615+
616+
reaperKey := types.NamespacedName{Namespace: testNamespace, Name: reaperName}
617+
assert.Eventually(t, func() bool {
618+
err = k8sClient.Get(ctx, reaperKey, r)
619+
return errors.IsNotFound(err)
620+
}, timeout, interval, "reaper stateful set deletion check failed")
621+
622+
assert.Eventually(t, func() bool {
623+
err = k8sClient.Get(ctx, stsKey, sts)
624+
// we'd expect errors.IsNotFound(err) here, except for some reason this is not happening in env tests
625+
return err == nil
626+
}, timeout, interval, "reaper stateful set deletion check failed")
627+
628+
assert.Eventually(t, func() bool {
629+
err = k8sClient.Get(ctx, secretKey, truststoresSecret)
630+
// again, we'd expect errors.IsNotFound(err) ...
631+
return err == nil
632+
}, timeout, interval, "reaper truststore secret deletion check failed")
633+
634+
// so we delete stuff manually
635+
err = k8sClient.Delete(ctx, sts)
636+
require.NoError(t, err)
637+
err = k8sClient.Delete(ctx, truststoresSecret)
638+
require.NoError(t, err)
639+
}
640+
573641
// Check if env var exists
574642
func envVarExists(envVars []corev1.EnvVar, name string) bool {
575643
for _, envVar := range envVars {

pkg/reaper/deployment.go

+36-2
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ func computeEnvVars(reaper *api.Reaper, dc *cassdcapi.CassandraDatacenter) []cor
155155
Value: "true",
156156
})
157157

158+
// we might have a general-purpose keystore and truststore
158159
if reaper.Spec.HttpManagement.Keystores != nil {
159160
envVars = append(envVars, corev1.EnvVar{
160161
Name: "REAPER_HTTP_MANAGEMENT_KEYSTORE_PATH",
@@ -165,6 +166,18 @@ func computeEnvVars(reaper *api.Reaper, dc *cassdcapi.CassandraDatacenter) []cor
165166
Value: "/etc/encryption/mgmt/truststore.jks",
166167
})
167168
}
169+
170+
// when we're a control plane, and we use http
171+
// we might need to have specific stores per cluster managed by this Reaper instance
172+
// we always deploy the secret that holds them, even if it never gets populated
173+
if reaper.Spec.StorageType == api.StorageTypeLocal && reaper.Spec.DatacenterRef.Name == "" {
174+
if reaper.Spec.HttpManagement.Enabled {
175+
envVars = append(envVars, corev1.EnvVar{
176+
Name: "REAPER_HTTP_MANAGEMENT_TRUSTSTORES_DIR",
177+
Value: "/etc/encryption/mgmt/perClusterTruststores",
178+
})
179+
}
180+
}
168181
}
169182

170183
return envVars
@@ -203,6 +216,21 @@ func computeVolumes(reaper *api.Reaper) ([]corev1.Volume, []corev1.VolumeMount)
203216
})
204217
}
205218

219+
if reaper.Spec.HttpManagement.Enabled {
220+
volumes = append(volumes, corev1.Volume{
221+
Name: "management-api-keystores-per-cluster",
222+
VolumeSource: corev1.VolumeSource{
223+
Secret: &corev1.SecretVolumeSource{
224+
SecretName: GetTruststoresSecretName(reaper.Name),
225+
},
226+
},
227+
})
228+
volumeMounts = append(volumeMounts, corev1.VolumeMount{
229+
Name: "management-api-keystores-per-cluster",
230+
MountPath: "/etc/encryption/mgmt/perClusterTruststores",
231+
})
232+
}
233+
206234
if reaper.Spec.StorageType == api.StorageTypeLocal {
207235
volumes = append(volumes, corev1.Volume{
208236
Name: "reaper-data",
@@ -286,7 +314,13 @@ func configureClientEncryption(reaper *api.Reaper, envVars []corev1.EnvVar, volu
286314
return envVars, volumes, volumeMounts
287315
}
288316

289-
func computePodSpec(reaper *api.Reaper, dc *cassdcapi.CassandraDatacenter, initContainerResources *corev1.ResourceRequirements, keystorePassword *string, truststorePassword *string) corev1.PodSpec {
317+
func computePodSpec(
318+
reaper *api.Reaper,
319+
dc *cassdcapi.CassandraDatacenter,
320+
initContainerResources *corev1.ResourceRequirements,
321+
keystorePassword *string,
322+
truststorePassword *string,
323+
) corev1.PodSpec {
290324
envVars := computeEnvVars(reaper, dc)
291325
volumes, volumeMounts := computeVolumes(reaper)
292326
mainImage := reaper.Spec.ContainerImage.ApplyDefaults(defaultImage)
@@ -365,7 +399,7 @@ func NewStatefulSet(reaper *api.Reaper, dc *cassdcapi.CassandraDatacenter, logge
365399
}
366400

367401
if reaper.Spec.ReaperTemplate.StorageConfig == nil {
368-
logger.Error(fmt.Errorf("reaper spec needs storage config when using memory sotrage type"), "missing storage config")
402+
logger.Error(fmt.Errorf("reaper spec needs storage config when using 'local' sotrage type"), "missing storage config")
369403
return nil
370404
}
371405

0 commit comments

Comments
 (0)