Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions internal/controller/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ import (
const (
etcdDataDir = "/var/lib/etcd"
volumeName = "etcd-data"

// TLS cert secret volume names and mount paths. The volume names must match
// the cert Volumes added in createOrPatchStatefulSet; the mount paths are the
// locations the etcd TLS flags will reference once those flags land.
serverCertVolumeName = "server-secret"
peerCertVolumeName = "peer-secret"
serverCertMountPath = "/etc/etcd/server-tls/"
peerCertMountPath = "/etc/etcd/peer-tls/"
)

type etcdClusterState string
Expand Down Expand Up @@ -194,18 +202,33 @@ func createOrPatchStatefulSet(ctx context.Context, logger logr.Logger, ec *ecv1a
peerCertName := getPeerCertName(ec.Name)
if ec.Spec.TLS != nil {
serverCertVolume := corev1.Volume{
Name: "server-secret",
Name: serverCertVolumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{SecretName: serverCertName},
},
}
peerCertVolume := corev1.Volume{
Name: "peer-secret",
Name: peerCertVolumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{SecretName: peerCertName},
},
}
certVolume = append(certVolume, serverCertVolume, peerCertVolume)

// Mount the cert secrets into the etcd container so etcd can read them.
// Gated on TLS (not StorageSpec): a TLS cluster without persistent
// storage must still receive the cert mounts. The flags that consume
// these paths land in a follow-up; the mounts alone are a safe no-op.
podSpec.Containers[0].VolumeMounts = append(podSpec.Containers[0].VolumeMounts,
corev1.VolumeMount{
Name: serverCertVolumeName,
MountPath: serverCertMountPath,
},
corev1.VolumeMount{
Name: peerCertVolumeName,
MountPath: peerCertMountPath,
},
)
}
if len(certVolume) != 0 {
podSpec.Volumes = certVolume
Expand Down Expand Up @@ -254,11 +277,14 @@ func createOrPatchStatefulSet(ctx context.Context, logger logr.Logger, ec *ecv1a

if ec.Spec.StorageSpec != nil {

stsSpec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{{
Name: volumeName,
MountPath: etcdDataDir,
SubPathExpr: "$(POD_NAME)",
}}
// Append (not assign) so any TLS cert mounts added above are preserved.
stsSpec.Template.Spec.Containers[0].VolumeMounts = append(
stsSpec.Template.Spec.Containers[0].VolumeMounts,
corev1.VolumeMount{
Name: volumeName,
MountPath: etcdDataDir,
SubPathExpr: "$(POD_NAME)",
})
// Create a new volume claim template
if ec.Spec.StorageSpec.VolumeSizeRequest.Cmp(resource.MustParse("1Mi")) < 0 {
return fmt.Errorf("VolumeSizeRequest must be at least 1Mi")
Expand Down
101 changes: 101 additions & 0 deletions internal/controller/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -1090,3 +1091,103 @@ func TestCreateCMCertificateConfig(t *testing.T) {
})
}
}

// findVolumeMount returns the VolumeMount with the given name, or false if absent.
func findVolumeMount(mounts []corev1.VolumeMount, name string) (corev1.VolumeMount, bool) {
for _, m := range mounts {
if m.Name == name {
return m, true
}
}
return corev1.VolumeMount{}, false
}

func TestCreateOrPatchStatefulSetTLSCertMounts(t *testing.T) {
ctx := t.Context()
logger := log.FromContext(ctx)

scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
_ = ecv1alpha1.AddToScheme(scheme)
_ = appsv1.AddToScheme(scheme)

tests := []struct {
name string
tls *ecv1alpha1.TLSCertificate
storageSpec *ecv1alpha1.StorageSpec
expectMount bool
}{
{
name: "TLS configured mounts server and peer cert secrets",
tls: &ecv1alpha1.TLSCertificate{},
expectMount: true,
},
{
name: "TLS configured without storage still mounts cert secrets",
tls: &ecv1alpha1.TLSCertificate{},
storageSpec: nil,
expectMount: true,
},
{
name: "TLS configured alongside storage keeps both data dir and cert mounts",
tls: &ecv1alpha1.TLSCertificate{},
storageSpec: &ecv1alpha1.StorageSpec{
VolumeSizeRequest: resource.MustParse("1Gi"),
},
expectMount: true,
},
{
name: "TLS nil adds no cert mounts",
tls: nil,
expectMount: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()

ec := &ecv1alpha1.EtcdCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test-etcd-tls",
Namespace: "default",
},
Spec: ecv1alpha1.EtcdClusterSpec{
Size: 3,
Version: "3.5.17",
TLS: tt.tls,
StorageSpec: tt.storageSpec,
},
}

err := createOrPatchStatefulSet(ctx, logger, ec, fakeClient, 3, scheme)
require.NoError(t, err)

sts := &appsv1.StatefulSet{}
err = fakeClient.Get(ctx, client.ObjectKey{Name: ec.Name, Namespace: "default"}, sts)
require.NoError(t, err)

require.Len(t, sts.Spec.Template.Spec.Containers, 1)
mounts := sts.Spec.Template.Spec.Containers[0].VolumeMounts

serverMount, serverOK := findVolumeMount(mounts, "server-secret")
peerMount, peerOK := findVolumeMount(mounts, "peer-secret")

if tt.expectMount {
require.True(t, serverOK, "expected server-secret VolumeMount to be present")
require.True(t, peerOK, "expected peer-secret VolumeMount to be present")
assert.Equal(t, "/etc/etcd/server-tls/", serverMount.MountPath)
assert.Equal(t, "/etc/etcd/peer-tls/", peerMount.MountPath)
} else {
assert.False(t, serverOK, "did not expect server-secret VolumeMount when TLS is nil")
assert.False(t, peerOK, "did not expect peer-secret VolumeMount when TLS is nil")
}

// When storage is requested, the data-dir mount must coexist with the cert mounts.
if tt.storageSpec != nil {
_, dataOK := findVolumeMount(mounts, volumeName)
assert.True(t, dataOK, "expected data-dir VolumeMount to be present alongside cert mounts")
}
})
}
}