Files
my-rook-config/pkg/operator/ceph/cluster/mon/mon_test.go
T
Blaine Gardner a952b7511a core: allow cephx rotation for ceph 19.2.3 except mons
Allow Rook to rotate CephX keys for Ceph v19.2.3 and up with the
exception of Ceph mons. While v19.2.3 has the `ceph auth rotate` command
that Rook needs to perform key rotation, there is still ongoing work in
Ceph to change how the `mon.` key is tracked internally. Rook's mon key
rotation requires those currently-unreleased Ceph changes.

Signed-off-by: Blaine Gardner <blaine.gardner@ibm.com>
2025-08-18 16:50:11 -06:00

1265 lines
44 KiB
Go

/*
Copyright 2016 The Rook Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mon
import (
"context"
"fmt"
"os"
"path"
"reflect"
"strconv"
"strings"
"testing"
"time"
"github.com/coreos/pkg/capnslog"
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/pkg/client/clientset/versioned/scheme"
"github.com/rook/rook/pkg/clusterd"
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
clienttest "github.com/rook/rook/pkg/daemon/ceph/client/test"
"github.com/rook/rook/pkg/operator/ceph/config"
"github.com/rook/rook/pkg/operator/ceph/config/keyring"
opcontroller "github.com/rook/rook/pkg/operator/ceph/controller"
cephver "github.com/rook/rook/pkg/operator/ceph/version"
"github.com/rook/rook/pkg/operator/k8sutil"
"github.com/rook/rook/pkg/operator/test"
exectest "github.com/rook/rook/pkg/util/exec/test"
"github.com/stretchr/testify/assert"
apps "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
discoveryv1 "k8s.io/api/discovery/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
// generate a standard mon config from a mon id w/ default port and IP 2.4.6.{1,2,3,...}
// support mon ID as new ["a", "b", etc.] form or as legacy ["mon0", "mon1", etc.] form
func testGenMonConfig(monID string) *monConfig {
var moniker string
var index int
var err error
if strings.HasPrefix(monID, "mon") { // is legacy mon name
moniker = monID // keep legacy "mon#" name
index, err = strconv.Atoi(strings.Replace(monID, "mon", "", 1)) // get # off end of mon#
} else {
moniker = "mon-" + monID
index, err = k8sutil.NameToIndex(monID)
}
if err != nil {
panic(err)
}
return &monConfig{
ResourceName: "rook-ceph-" + moniker, // rook-ceph-mon-A or rook-ceph-mon#
DaemonName: monID, // A or mon#
Port: DefaultMsgr1Port,
PublicIP: fmt.Sprintf("2.4.6.%d", index+1),
// dataDirHostPath assumed to be /var/lib/rook
DataPathMap: config.NewStatefulDaemonDataPathMap(
"/var/lib/rook", dataDirRelativeHostPath(monID), config.MonType, monID, "rook-ceph"),
}
}
func newTestStartCluster(t *testing.T, namespace string) (*clusterd.Context, error) {
monResponse := func() (string, error) {
return clienttest.MonInQuorumResponseMany(3), nil
}
return newTestStartClusterWithQuorumResponse(t, namespace, monResponse)
}
func newTestStartClusterWithQuorumResponse(t *testing.T, namespace string, monResponse func() (string, error)) (*clusterd.Context, error) {
clientset := test.New(t, 3)
configDir := t.TempDir()
executor := &exectest.MockExecutor{
MockExecuteCommandWithOutput: func(command string, args ...string) (string, error) {
if strings.Contains(command, "ceph-authtool") {
err := clienttest.CreateConfigDir(path.Join(configDir, namespace))
return "", errors.Wrap(err, "failed testing of start cluster without quorum response")
} else {
return monResponse()
}
},
}
return &clusterd.Context{
Clientset: clientset,
Executor: executor,
ConfigDir: configDir,
}, nil
}
func newCluster(context *clusterd.Context, namespace string, allowMultiplePerNode bool, resources v1.ResourceRequirements) *Cluster {
ownerInfo := cephclient.NewMinimumOwnerInfoWithOwnerRef()
return &Cluster{
ClusterInfo: nil,
context: context,
Namespace: namespace,
rookImage: "myversion",
spec: cephv1.ClusterSpec{
Mon: cephv1.MonSpec{
Count: 3,
AllowMultiplePerNode: allowMultiplePerNode,
},
Resources: map[string]v1.ResourceRequirements{"mon": resources},
},
maxMonID: -1,
waitForStart: false,
monTimeoutList: map[string]time.Time{},
mapping: &opcontroller.Mapping{
Schedule: map[string]*opcontroller.MonScheduleInfo{},
},
ownerInfo: ownerInfo,
monsToFailover: map[string]*monConfig{},
}
}
// setCommonMonProperties is a convenience helper for setting common test properties
func setCommonMonProperties(c *Cluster, currentMons int, mon cephv1.MonSpec, rookImage string) {
c.ClusterInfo = clienttest.CreateTestClusterInfo(currentMons)
c.spec.Mon.Count = mon.Count
c.spec.Mon.AllowMultiplePerNode = mon.AllowMultiplePerNode
c.rookImage = rookImage
}
func TestResourceName(t *testing.T) {
assert.Equal(t, "rook-ceph-mon-a", resourceName("rook-ceph-mon-a"))
assert.Equal(t, "rook-ceph-mon123", resourceName("rook-ceph-mon123"))
assert.Equal(t, "rook-ceph-mon-b", resourceName("b"))
}
func TestStartMonDeployment(t *testing.T) {
namespace := "ns"
context, err := newTestStartCluster(t, namespace)
assert.NoError(t, err)
c := newCluster(context, namespace, true, v1.ResourceRequirements{})
c.ClusterInfo = clienttest.CreateTestClusterInfo(1)
cm := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: EndpointConfigMapName},
Data: map[string]string{"maxMonId": "1"},
}
_, err = c.context.Clientset.CoreV1().ConfigMaps(c.Namespace).Create(c.ClusterInfo.Context, cm, metav1.CreateOptions{})
assert.NoError(t, err)
// Start mon a on a specific node since there is no volumeClaimTemplate
m := &monConfig{ResourceName: "rook-ceph-mon-a", DaemonName: "a", Port: 3300, PublicIP: "1.2.3.4", DataPathMap: &config.DataPathMap{}}
schedule := &opcontroller.MonScheduleInfo{Hostname: "host-a", Zone: "zonea"}
err = c.startMon(m, schedule)
assert.NoError(t, err)
deployment, err := c.context.Clientset.AppsV1().Deployments(c.Namespace).Get(c.ClusterInfo.Context, m.ResourceName, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, schedule.Hostname, deployment.Spec.Template.Spec.NodeSelector["kubernetes.io/hostname"])
// Start mon b on any node in a zone since there is a volumeClaimTemplate
m = &monConfig{ResourceName: "rook-ceph-mon-b", DaemonName: "b", Port: 3300, PublicIP: "1.2.3.5", DataPathMap: &config.DataPathMap{}}
schedule = &opcontroller.MonScheduleInfo{Hostname: "host-b", Zone: "zoneb"}
c.spec.Mon.VolumeClaimTemplate = &cephv1.VolumeClaimTemplate{}
err = c.startMon(m, schedule)
assert.NoError(t, err)
deployment, err = c.context.Clientset.AppsV1().Deployments(c.Namespace).Get(c.ClusterInfo.Context, m.ResourceName, metav1.GetOptions{})
assert.NoError(t, err)
// no node selector when there is a volumeClaimTemplate and the mon is assigned to a zone
assert.Equal(t, 0, len(deployment.Spec.Template.Spec.NodeSelector))
}
func TestStartMonPods(t *testing.T) {
capnslog.SetGlobalLogLevel(capnslog.DEBUG)
os.Setenv("ROOK_LOG_LEVEL", "DEBUG")
namespace := "ns"
context, err := newTestStartCluster(t, namespace)
assert.NoError(t, err)
c := newCluster(context, namespace, true, v1.ResourceRequirements{})
c.ClusterInfo = clienttest.CreateTestClusterInfo(1)
c.spec.Annotations = cephv1.AnnotationsSpec{
cephv1.KeyClusterMetadata: cephv1.Annotations{
"key": "value",
},
}
// start a basic cluster
_, err = c.Start(c.ClusterInfo, c.rookImage, cephver.Squid, c.spec)
assert.NoError(t, err)
// test annotations
secret, err := c.context.Clientset.CoreV1().Secrets(c.Namespace).Get(c.ClusterInfo.Context, AppName, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, map[string]string{"key": "value"}, secret.Annotations)
validateStart(t, c)
// starting again should be a no-op
_, err = c.Start(c.ClusterInfo, c.rookImage, cephver.Squid, c.spec)
assert.NoError(t, err)
validateStart(t, c)
}
func TestOperatorRestart(t *testing.T) {
namespace := "ns"
context, err := newTestStartCluster(t, namespace)
assert.NoError(t, err)
c := newCluster(context, namespace, true, v1.ResourceRequirements{})
c.ClusterInfo = clienttest.CreateTestClusterInfo(1)
// start a basic cluster
info, err := c.Start(c.ClusterInfo, c.rookImage, cephver.Squid, c.spec)
assert.NoError(t, err)
assert.NoError(t, info.IsInitialized())
validateStart(t, c)
c = newCluster(context, namespace, true, v1.ResourceRequirements{})
c.ClusterInfo = clienttest.CreateTestClusterInfo(1)
// starting again should be a no-op, but will not result in an error
info, err = c.Start(c.ClusterInfo, c.rookImage, cephver.Squid, c.spec)
assert.NoError(t, err)
assert.NoError(t, info.IsInitialized())
validateStart(t, c)
}
// safety check that if hostNetwork is used no changes occur on an operator restart
func TestOperatorRestartHostNetwork(t *testing.T) {
namespace := "ns"
context, err := newTestStartCluster(t, namespace)
assert.NoError(t, err)
// cluster without host networking
c := newCluster(context, namespace, false, v1.ResourceRequirements{})
c.ClusterInfo = clienttest.CreateTestClusterInfo(1)
// start a basic cluster
info, err := c.Start(c.ClusterInfo, c.rookImage, cephver.Squid, c.spec)
assert.NoError(t, err)
assert.NoError(t, info.IsInitialized())
validateStart(t, c)
// cluster with host networking
c = newCluster(context, namespace, false, v1.ResourceRequirements{})
c.spec.Network.HostNetwork = true
c.ClusterInfo = clienttest.CreateTestClusterInfo(1)
// starting again should be a no-op, but still results in an error
info, err = c.Start(c.ClusterInfo, c.rookImage, cephver.Squid, c.spec)
assert.NoError(t, err)
assert.NoError(t, info.IsInitialized(), info)
validateStart(t, c)
}
func validateStart(t *testing.T, c *Cluster) {
s, err := c.context.Clientset.CoreV1().Secrets(c.Namespace).Get(c.ClusterInfo.Context, AppName, metav1.GetOptions{})
assert.NoError(t, err) // there shouldn't be an error due the secret existing
assert.Equal(t, 4, len(s.Data))
// there is only one pod created. the other two won't be created since the first one doesn't start
_, err = c.context.Clientset.AppsV1().Deployments(c.Namespace).Get(c.ClusterInfo.Context, "rook-ceph-mon-a", metav1.GetOptions{})
assert.NoError(t, err)
}
func TestPersistMons(t *testing.T) {
clientset := test.New(t, 1)
ownerInfo := cephclient.NewMinimumOwnerInfoWithOwnerRef()
c := New(context.TODO(), &clusterd.Context{Clientset: clientset}, "ns", cephv1.ClusterSpec{Annotations: cephv1.AnnotationsSpec{cephv1.KeyClusterMetadata: cephv1.Annotations{"key": "value"}}}, ownerInfo)
setCommonMonProperties(c, 1, cephv1.MonSpec{Count: 3, AllowMultiplePerNode: true}, "myversion")
expectedPorts := []discoveryv1.EndpointPort{
{Name: ptr.To(DefaultMsgr2PortName), Protocol: ptr.To(v1.ProtocolTCP), Port: ptr.To(DefaultMsgr2Port)},
{Name: ptr.To(DefaultMsgr1PortName), Protocol: ptr.To(v1.ProtocolTCP), Port: ptr.To(DefaultMsgr1Port)},
}
// Persist mon a
err := c.persistExpectedMonDaemonsInConfigMap()
assert.NoError(t, err)
err = c.persistExpectedMonDaemonsAsEndpointSlice()
assert.NoError(t, err)
cm, err := c.context.Clientset.CoreV1().ConfigMaps(c.Namespace).Get(context.TODO(), EndpointConfigMapName, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, "a=1.2.3.1:3300", cm.Data[EndpointDataKey])
assert.Equal(t, map[string]string{"key": "value"}, cm.Annotations)
ep, err := c.context.Clientset.DiscoveryV1().EndpointSlices(c.Namespace).Get(context.TODO(), endpointSliceNameIPv4, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, "1.2.3.1", ep.Endpoints[0].Addresses[0])
assert.ElementsMatch(t, expectedPorts, ep.Ports)
assert.Equal(t, map[string]string{"key": "value"}, cm.Annotations)
// Persist mon b, and remove mon a for simply testing the configmap is updated
c.ClusterInfo.InternalMonitors["b"] = &cephclient.MonInfo{Name: "b", Endpoint: "4.5.6.7:3300"}
delete(c.ClusterInfo.InternalMonitors, "a")
err = c.persistExpectedMonDaemonsInConfigMap()
assert.NoError(t, err)
err = c.persistExpectedMonDaemonsAsEndpointSlice()
assert.NoError(t, err)
cm, err = c.context.Clientset.CoreV1().ConfigMaps(c.Namespace).Get(context.TODO(), EndpointConfigMapName, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, "b=4.5.6.7:3300", cm.Data[EndpointDataKey])
ep, err = c.context.Clientset.DiscoveryV1().EndpointSlices(c.Namespace).Get(context.TODO(), endpointSliceNameIPv4, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, "4.5.6.7", ep.Endpoints[0].Addresses[0])
assert.ElementsMatch(t, expectedPorts, ep.Ports)
assert.Equal(t, map[string]string{"key": "value"}, cm.Annotations)
}
func TestCreateEndpointSlices(t *testing.T) {
clientset := test.New(t, 1)
ownerInfo := cephclient.NewMinimumOwnerInfoWithOwnerRef()
// RequireMsgr2=false
c := New(context.TODO(), &clusterd.Context{Clientset: clientset}, "ns", cephv1.ClusterSpec{}, ownerInfo)
expectedPorts := []discoveryv1.EndpointPort{
{Name: ptr.To(DefaultMsgr2PortName), Protocol: ptr.To(v1.ProtocolTCP), Port: ptr.To(DefaultMsgr2Port)},
{Name: ptr.To(DefaultMsgr1PortName), Protocol: ptr.To(v1.ProtocolTCP), Port: ptr.To(DefaultMsgr1Port)},
}
testCreateEndpointSlicesForCluster(t, c, expectedPorts)
// RequireMsgr2=true
c = New(
context.TODO(),
&clusterd.Context{Clientset: clientset},
"ns",
cephv1.ClusterSpec{
Network: cephv1.NetworkSpec{
Connections: &cephv1.ConnectionsSpec{
RequireMsgr2: true,
},
},
}, ownerInfo)
expectedPorts = []discoveryv1.EndpointPort{
{Name: ptr.To(DefaultMsgr2PortName), Protocol: ptr.To(v1.ProtocolTCP), Port: ptr.To(DefaultMsgr2Port)},
}
testCreateEndpointSlicesForCluster(t, c, expectedPorts)
}
func testCreateEndpointSlicesForCluster(t *testing.T, c *Cluster, expectedPorts []discoveryv1.EndpointPort) {
ipv4Addresses := []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"}
ipv4Mons := []cephclient.MonInfo{
{Name: "a", Endpoint: fmt.Sprintf("%s:6789", ipv4Addresses[0])},
{Name: "b", Endpoint: fmt.Sprintf("%s:6789", ipv4Addresses[1])},
{Name: "c", Endpoint: fmt.Sprintf("%s:6789", ipv4Addresses[2])},
}
ipv6Addresses := []string{"2001:db8::1", "2001:db8::2", "2001:db8::3"}
ipv6Mons := []cephclient.MonInfo{
{Name: "d", Endpoint: fmt.Sprintf("[%s]:6789", ipv6Addresses[0])},
{Name: "e", Endpoint: fmt.Sprintf("[%s]:6789", ipv6Addresses[1])},
{Name: "f", Endpoint: fmt.Sprintf("[%s]:6789", ipv6Addresses[2])},
}
// IPv4 test
c.ClusterInfo.InternalMonitors = map[string]*cephclient.MonInfo{}
for _, mon := range ipv4Mons {
c.ClusterInfo.InternalMonitors[mon.Name] = &mon
}
err := c.persistExpectedMonDaemonsAsEndpointSlice()
assert.NoError(t, err)
epSliceIPv4, err := c.context.Clientset.DiscoveryV1().EndpointSlices(c.Namespace).Get(context.TODO(), endpointSliceNameIPv4, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, discoveryv1.AddressTypeIPv4, epSliceIPv4.AddressType)
assert.Len(t, epSliceIPv4.Endpoints, 1)
assert.ElementsMatch(t, ipv4Addresses, epSliceIPv4.Endpoints[0].Addresses)
assert.ElementsMatch(t, expectedPorts, epSliceIPv4.Ports)
_, err = c.context.Clientset.DiscoveryV1().EndpointSlices(c.Namespace).Get(context.TODO(), endpointSliceNameIPv6, metav1.GetOptions{})
assert.True(t, kerrors.IsNotFound(err))
// IPv6 test
c.ClusterInfo.InternalMonitors = map[string]*cephclient.MonInfo{}
for _, mon := range ipv6Mons {
c.ClusterInfo.InternalMonitors[mon.Name] = &mon
}
err = c.persistExpectedMonDaemonsAsEndpointSlice()
assert.NoError(t, err)
_, err = c.context.Clientset.DiscoveryV1().EndpointSlices(c.Namespace).Get(context.TODO(), endpointSliceNameIPv4, metav1.GetOptions{})
assert.True(t, kerrors.IsNotFound(err))
epSliceIPv6, err := c.context.Clientset.DiscoveryV1().EndpointSlices(c.Namespace).Get(context.TODO(), endpointSliceNameIPv6, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, discoveryv1.AddressTypeIPv6, epSliceIPv6.AddressType)
assert.Len(t, epSliceIPv6.Endpoints, 1)
assert.ElementsMatch(t, ipv6Addresses, epSliceIPv6.Endpoints[0].Addresses)
assert.ElementsMatch(t, expectedPorts, epSliceIPv6.Ports)
// Mixed IPv4 and IPv6 test.
// Note that this normally doesn't happen, because rook uses only IPv4 or
// only IPv6 mons. But in the future, migration between IPv4 and IPv6 might
// be supported, and then, during migration, the old mons would use the
// old IP family and the new mons would use the new IP family.
c.ClusterInfo.InternalMonitors = map[string]*cephclient.MonInfo{}
for _, mon := range ipv4Mons {
c.ClusterInfo.InternalMonitors[mon.Name] = &mon
}
for _, mon := range ipv6Mons {
c.ClusterInfo.InternalMonitors[mon.Name] = &mon
}
err = c.persistExpectedMonDaemonsAsEndpointSlice()
assert.NoError(t, err)
epSliceIPv4, err = c.context.Clientset.DiscoveryV1().EndpointSlices(c.Namespace).Get(context.TODO(), endpointSliceNameIPv4, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, discoveryv1.AddressTypeIPv4, epSliceIPv4.AddressType)
assert.Len(t, epSliceIPv4.Endpoints, 1)
assert.ElementsMatch(t, ipv4Addresses, epSliceIPv4.Endpoints[0].Addresses)
assert.ElementsMatch(t, expectedPorts, epSliceIPv4.Ports)
epSliceIPv6, err = c.context.Clientset.DiscoveryV1().EndpointSlices(c.Namespace).Get(context.TODO(), endpointSliceNameIPv6, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, discoveryv1.AddressTypeIPv6, epSliceIPv6.AddressType)
assert.Len(t, epSliceIPv6.Endpoints, 1)
assert.ElementsMatch(t, ipv6Addresses, epSliceIPv6.Endpoints[0].Addresses)
assert.ElementsMatch(t, expectedPorts, epSliceIPv6.Ports)
}
func TestSaveMonEndpoints(t *testing.T) {
ctx := context.TODO()
clientset := test.New(t, 1)
configDir := t.TempDir()
ownerInfo := cephclient.NewMinimumOwnerInfoWithOwnerRef()
c := New(ctx, &clusterd.Context{Clientset: clientset, ConfigDir: configDir}, "ns", cephv1.ClusterSpec{}, ownerInfo)
setCommonMonProperties(c, 1, cephv1.MonSpec{Count: 3, AllowMultiplePerNode: true}, "myversion")
// create the initial config map
err := c.saveMonConfig()
assert.NoError(t, err)
cm, err := c.context.Clientset.CoreV1().ConfigMaps(c.Namespace).Get(ctx, EndpointConfigMapName, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, "a=1.2.3.1:3300", cm.Data[EndpointDataKey])
assert.Equal(t, `{"node":{}}`, cm.Data[opcontroller.MappingKey])
assert.Equal(t, "-1", cm.Data[opcontroller.MaxMonIDKey])
// update the config map
c.ClusterInfo.InternalMonitors["a"].Endpoint = "2.3.4.5:6789"
c.maxMonID = 2
c.mapping.Schedule["a"] = &opcontroller.MonScheduleInfo{
Name: "node0",
Address: "1.1.1.1",
Hostname: "myhost",
}
err = c.saveMonConfig()
assert.NoError(t, err)
cm, err = c.context.Clientset.CoreV1().ConfigMaps(c.Namespace).Get(ctx, EndpointConfigMapName, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, "a=2.3.4.5:6789", cm.Data[EndpointDataKey])
assert.Equal(t, `{"node":{"a":{"Name":"node0","Hostname":"myhost","Address":"1.1.1.1"}}}`, cm.Data[opcontroller.MappingKey])
assert.Equal(t, "-1", cm.Data[opcontroller.MaxMonIDKey])
// Update the maxMonID to some random value
cm.Data[opcontroller.MaxMonIDKey] = "23"
_, err = c.context.Clientset.CoreV1().ConfigMaps(c.Namespace).Update(ctx, cm, metav1.UpdateOptions{})
assert.NoError(t, err)
// Confirm the maxMonId will be persisted and not updated to anything else.
// The value is only expected to be set directly to the configmap when a mon deployment is started.
err = c.saveMonConfig()
assert.NoError(t, err)
cm, err = c.context.Clientset.CoreV1().ConfigMaps(c.Namespace).Get(ctx, EndpointConfigMapName, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, "23", cm.Data[opcontroller.MaxMonIDKey])
}
func TestMaxMonID(t *testing.T) {
clientset := test.New(t, 1)
configDir := t.TempDir()
ownerInfo := cephclient.NewMinimumOwnerInfoWithOwnerRef()
c := New(context.TODO(), &clusterd.Context{Clientset: clientset, ConfigDir: configDir}, "ns", cephv1.ClusterSpec{}, ownerInfo)
c.ClusterInfo = clienttest.CreateTestClusterInfo(1)
// when the configmap is not found, the maxMonID is -1
maxMonID, err := c.getStoredMaxMonID()
assert.NoError(t, err)
assert.Equal(t, "-1", maxMonID)
// initialize the configmap
setCommonMonProperties(c, 1, cephv1.MonSpec{Count: 3, AllowMultiplePerNode: true}, "myversion")
err = c.saveMonConfig()
assert.NoError(t, err)
// invalid mon names won't update the maxMonID
err = c.commitMaxMonID("bad-id")
assert.Error(t, err)
// starting a mon deployment will set the maxMonID
err = c.commitMaxMonID("a")
assert.NoError(t, err)
maxMonID, err = c.getStoredMaxMonID()
assert.NoError(t, err)
assert.Equal(t, "0", maxMonID)
// set to a higher id
err = c.commitMaxMonID("d")
assert.NoError(t, err)
maxMonID, err = c.getStoredMaxMonID()
assert.NoError(t, err)
assert.Equal(t, "3", maxMonID)
// setting to an id lower than the max will not update it
err = c.commitMaxMonID("c")
assert.NoError(t, err)
maxMonID, err = c.getStoredMaxMonID()
assert.NoError(t, err)
assert.Equal(t, "3", maxMonID)
}
func TestMonInQuorum(t *testing.T) {
entry := cephclient.MonMapEntry{Name: "foo", Rank: 23}
quorum := []int{}
// Nothing in quorum
assert.False(t, monInQuorum(entry, quorum))
// One or more members in quorum
quorum = []int{23}
assert.True(t, monInQuorum(entry, quorum))
quorum = []int{5, 6, 7, 23, 8}
assert.True(t, monInQuorum(entry, quorum))
// Not in quorum
entry.Rank = 1
assert.False(t, monInQuorum(entry, quorum))
}
func TestNameToIndex(t *testing.T) {
// invalid
id, err := fullNameToIndex("rook-ceph-monitor0")
assert.NotNil(t, err)
assert.Equal(t, -1, id)
id, err = fullNameToIndex("rook-ceph-mon123")
assert.NotNil(t, err)
assert.Equal(t, -1, id)
// valid
id, err = fullNameToIndex("b")
assert.NoError(t, err)
assert.Equal(t, 1, id)
id, err = fullNameToIndex("m")
assert.NoError(t, err)
assert.Equal(t, 12, id)
id, err = fullNameToIndex("rook-ceph-mon-a")
assert.NoError(t, err)
assert.Equal(t, 0, id)
}
func TestWaitForQuorum(t *testing.T) {
namespace := "ns"
quorumChecks := 0
quorumResponse := func() (string, error) {
mons := map[string]*cephclient.MonInfo{
"a": {},
}
quorumChecks++
if quorumChecks == 1 {
// return an error the first time while we're waiting for the mon to join quorum
return "", errors.New("test error")
}
// a successful response indicates that we have quorum, even if we didn't check which specific mons were in quorum
return clienttest.MonInQuorumResponseFromMons(mons), nil
}
context, err := newTestStartClusterWithQuorumResponse(t, namespace, quorumResponse)
assert.NoError(t, err)
requireAllInQuorum := false
expectedMons := []string{"a"}
clusterInfo := cephclient.AdminTestClusterInfo("mycluster")
err = waitForQuorumWithMons(context, clusterInfo, expectedMons, 0, requireAllInQuorum)
assert.NoError(t, err)
}
func TestMonFoundInQuorum(t *testing.T) {
response := cephclient.MonStatusResponse{}
// "a" is in quorum
response.Quorum = []int{0}
response.MonMap.Mons = []cephclient.MonMapEntry{
{Name: "a", Rank: 0},
{Name: "b", Rank: 1},
{Name: "c", Rank: 2},
}
assert.True(t, monFoundInQuorum("a", response))
assert.False(t, monFoundInQuorum("b", response))
assert.False(t, monFoundInQuorum("c", response))
// b and c also in quorum, but not d
response.Quorum = []int{0, 1, 2}
assert.True(t, monFoundInQuorum("a", response))
assert.True(t, monFoundInQuorum("b", response))
assert.True(t, monFoundInQuorum("c", response))
assert.False(t, monFoundInQuorum("d", response))
}
func TestConfigureArbiter(t *testing.T) {
c := &Cluster{spec: cephv1.ClusterSpec{
Mon: cephv1.MonSpec{
StretchCluster: &cephv1.StretchClusterSpec{
Zones: []cephv1.MonZoneSpec{
{Name: "a", Arbiter: true},
{Name: "b"},
{Name: "c"},
},
},
},
}}
c.arbiterMon = "arb"
currentArbiter := c.arbiterMon
setNewTiebreaker := false
executor := &exectest.MockExecutor{
MockExecuteCommandWithOutput: func(command string, args ...string) (string, error) {
logger.Infof("%s %v", command, args)
if args[0] == "mon" {
if args[1] == "dump" {
return fmt.Sprintf(`{"tiebreaker_mon": "%s", "stretch_mode": true}`, currentArbiter), nil
}
if args[1] == "set_new_tiebreaker" {
assert.Equal(t, c.arbiterMon, args[2])
setNewTiebreaker = true
return "", nil
}
}
return "", fmt.Errorf("unrecognized output file command: %s %v", command, args)
},
}
c.context = &clusterd.Context{Clientset: test.New(t, 5), Executor: executor}
c.ClusterInfo = clienttest.CreateTestClusterInfo(5)
t.Run("stretch mode already configured - new", func(t *testing.T) {
c.arbiterMon = currentArbiter
err := c.ConfigureArbiter()
assert.NoError(t, err)
assert.False(t, setNewTiebreaker)
})
t.Run("tiebreaker changed", func(t *testing.T) {
c.arbiterMon = "changed"
err := c.ConfigureArbiter()
assert.NoError(t, err)
assert.True(t, setNewTiebreaker)
})
}
func TestFindAvailableZoneMon(t *testing.T) {
c := &Cluster{spec: cephv1.ClusterSpec{
Mon: cephv1.MonSpec{
Zones: []cephv1.MonZoneSpec{
{Name: "a"},
{Name: "b"},
{Name: "c"},
},
},
}}
// No mons are assigned to a zone yet
existingMons := []*monConfig{}
availableZone, err := c.findAvailableZone(existingMons)
assert.NoError(t, err)
assert.NotEqual(t, "", availableZone)
// With 3 mons, we have one available zone
existingMons = []*monConfig{
{ResourceName: "x", Zone: "a"},
{ResourceName: "y", Zone: "b"},
}
c.spec.Mon.Count = 3
availableZone, err = c.findAvailableZone(existingMons)
assert.NoError(t, err)
assert.Equal(t, "c", availableZone)
// With 3 mons and no available zones
existingMons = []*monConfig{
{ResourceName: "x", Zone: "a"},
{ResourceName: "y", Zone: "b"},
{ResourceName: "z", Zone: "c"},
}
c.spec.Mon.Count = 3
availableZone, err = c.findAvailableZone(existingMons)
assert.Error(t, err)
assert.Equal(t, "", availableZone)
}
func TestFindAvailableZoneForStretchedMon(t *testing.T) {
c := &Cluster{spec: cephv1.ClusterSpec{
Mon: cephv1.MonSpec{
StretchCluster: &cephv1.StretchClusterSpec{
Zones: []cephv1.MonZoneSpec{
{Name: "a", Arbiter: true},
{Name: "b"},
{Name: "c"},
},
},
},
}}
// No mons are assigned to a zone yet
existingMons := []*monConfig{}
availableZone, err := c.findAvailableZone(existingMons)
assert.NoError(t, err)
assert.NotEqual(t, "", availableZone)
// With 3 mons, we have one available zone
existingMons = []*monConfig{
{ResourceName: "x", Zone: "a"},
{ResourceName: "y", Zone: "b"},
}
c.spec.Mon.Count = 3
availableZone, err = c.findAvailableZone(existingMons)
assert.NoError(t, err)
assert.Equal(t, "c", availableZone)
// With 3 mons and no available zones
existingMons = []*monConfig{
{ResourceName: "x", Zone: "a"},
{ResourceName: "y", Zone: "b"},
{ResourceName: "z", Zone: "c"},
}
c.spec.Mon.Count = 3
availableZone, err = c.findAvailableZone(existingMons)
assert.Error(t, err)
assert.Equal(t, "", availableZone)
// With 5 mons and no available zones
existingMons = []*monConfig{
{ResourceName: "w", Zone: "a"},
{ResourceName: "x", Zone: "b"},
{ResourceName: "y", Zone: "b"},
{ResourceName: "z", Zone: "c"},
{ResourceName: "q", Zone: "c"},
}
c.spec.Mon.Count = 5
availableZone, err = c.findAvailableZone(existingMons)
assert.Error(t, err)
assert.Equal(t, "", availableZone)
// With 5 mons and one available zone
existingMons = []*monConfig{
{ResourceName: "w", Zone: "a"},
{ResourceName: "x", Zone: "b"},
{ResourceName: "y", Zone: "b"},
{ResourceName: "z", Zone: "c"},
}
availableZone, err = c.findAvailableZone(existingMons)
assert.NoError(t, err)
assert.Equal(t, "c", availableZone)
// With 5 mons and arbiter zone is available zone
existingMons = []*monConfig{
{ResourceName: "w", Zone: "b"},
{ResourceName: "x", Zone: "b"},
{ResourceName: "y", Zone: "c"},
{ResourceName: "z", Zone: "c"},
}
availableZone, err = c.findAvailableZone(existingMons)
assert.NoError(t, err)
assert.Equal(t, "a", availableZone)
}
func TestMonVolumeClaimTemplate(t *testing.T) {
generalSC := "generalSC"
zoneSC := "zoneSC"
defaultTemplate := &cephv1.VolumeClaimTemplate{Spec: v1.PersistentVolumeClaimSpec{StorageClassName: &generalSC}}
zoneTemplate := &cephv1.VolumeClaimTemplate{Spec: v1.PersistentVolumeClaimSpec{StorageClassName: &zoneSC}}
type fields struct {
spec cephv1.ClusterSpec
}
type args struct {
mon *monConfig
}
tests := []struct {
name string
fields fields
args args
want *v1.PersistentVolumeClaim
}{
{"no template", fields{cephv1.ClusterSpec{}}, args{&monConfig{Zone: "z1"}}, nil},
{"default template", fields{cephv1.ClusterSpec{Mon: cephv1.MonSpec{VolumeClaimTemplate: defaultTemplate}}}, args{&monConfig{Zone: "z1"}}, defaultTemplate.ToPVC()},
{
"default template with 3 zones",
fields{cephv1.ClusterSpec{Mon: cephv1.MonSpec{
VolumeClaimTemplate: defaultTemplate,
Zones: []cephv1.MonZoneSpec{{Name: "z1"}, {Name: "z2"}, {Name: "z3"}},
}}},
args{&monConfig{Zone: "z1"}},
defaultTemplate.ToPVC(),
},
{
"overridden template",
fields{cephv1.ClusterSpec{Mon: cephv1.MonSpec{
VolumeClaimTemplate: defaultTemplate,
Zones: []cephv1.MonZoneSpec{{Name: "z1", VolumeClaimTemplate: zoneTemplate}, {Name: "z2"}, {Name: "z3"}},
}}},
args{&monConfig{Zone: "z1"}},
zoneTemplate.ToPVC(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Cluster{
spec: tt.fields.spec,
}
if got := c.monVolumeClaimTemplate(tt.args.mon); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Cluster.monVolumeClaimTemplate() = %v, want %v", got, tt.want)
}
})
}
}
func TestRemoveExtraMonDeployments(t *testing.T) {
namespace := "ns"
context, err := newTestStartCluster(t, namespace)
assert.NoError(t, err)
c := newCluster(context, namespace, true, v1.ResourceRequirements{})
c.ClusterInfo = clienttest.CreateTestClusterInfo(1)
// Nothing to remove when the mons match the deployment
mons := []*monConfig{
{ResourceName: "rook-ceph-mon-a", DaemonName: "a"},
}
deployments := []apps.Deployment{
{
ObjectMeta: metav1.ObjectMeta{
Name: "rook-ceph-mon-a",
Labels: map[string]string{"ceph_daemon_id": "a"},
},
},
}
c.spec.Mon.Count = 1
removed := c.checkForExtraMonResources(mons, deployments)
assert.Equal(t, "", removed)
// Remove an extra mon deployment
deployments = append(deployments, apps.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "rook-ceph-mon-b",
Labels: map[string]string{"ceph_daemon_id": "b"},
},
})
removed = c.checkForExtraMonResources(mons, deployments)
assert.Equal(t, "b", removed)
// Nothing to remove when there are not enough deployments for the expected mons
mons = []*monConfig{
{ResourceName: "rook-ceph-mon-a", DaemonName: "a"},
{ResourceName: "rook-ceph-mon-b", DaemonName: "b"},
{ResourceName: "rook-ceph-mon-c", DaemonName: "c"},
}
c.spec.Mon.Count = 3
removed = c.checkForExtraMonResources(mons, deployments)
assert.Equal(t, "", removed)
// Do not remove a mon when it was during failover and only a single mon is in the list, even if extra deployments exist
mons = []*monConfig{
{ResourceName: "rook-ceph-mon-d", DaemonName: "d"},
}
deployments = append(deployments, apps.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "rook-ceph-mon-c",
Labels: map[string]string{"ceph_daemon_id": "c"},
},
})
deployments = append(deployments, apps.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "rook-ceph-mon-d",
Labels: map[string]string{"ceph_daemon_id": "d"},
},
})
c.spec.Mon.Count = 3
removed = c.checkForExtraMonResources(mons, deployments)
assert.Equal(t, "", removed)
}
func TestStretchMonVolumeClaimTemplate(t *testing.T) {
generalSC := "generalSC"
zoneSC := "zoneSC"
defaultTemplate := &cephv1.VolumeClaimTemplate{Spec: v1.PersistentVolumeClaimSpec{StorageClassName: &generalSC}}
zoneTemplate := &cephv1.VolumeClaimTemplate{Spec: v1.PersistentVolumeClaimSpec{StorageClassName: &zoneSC}}
type fields struct {
spec cephv1.ClusterSpec
}
type args struct {
mon *monConfig
}
tests := []struct {
name string
fields fields
args args
want *v1.PersistentVolumeClaim
}{
{"no template", fields{cephv1.ClusterSpec{}}, args{&monConfig{Zone: "z1"}}, nil},
{"default template", fields{cephv1.ClusterSpec{Mon: cephv1.MonSpec{VolumeClaimTemplate: defaultTemplate}}}, args{&monConfig{Zone: "z1"}}, defaultTemplate.ToPVC()},
{
"default template with 3 zones",
fields{cephv1.ClusterSpec{Mon: cephv1.MonSpec{
VolumeClaimTemplate: defaultTemplate,
StretchCluster: &cephv1.StretchClusterSpec{Zones: []cephv1.MonZoneSpec{{Name: "z1"}, {Name: "z2"}, {Name: "z3"}}},
}}},
args{&monConfig{Zone: "z1"}},
defaultTemplate.ToPVC(),
},
{
"overridden template",
fields{cephv1.ClusterSpec{Mon: cephv1.MonSpec{
VolumeClaimTemplate: defaultTemplate,
StretchCluster: &cephv1.StretchClusterSpec{Zones: []cephv1.MonZoneSpec{{Name: "z1", VolumeClaimTemplate: zoneTemplate}, {Name: "z2"}, {Name: "z3"}}},
}}},
args{&monConfig{Zone: "z1"}},
zoneTemplate.ToPVC(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Cluster{
spec: tt.fields.spec,
}
if got := c.monVolumeClaimTemplate(tt.args.mon); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Cluster.monVolumeClaimTemplate() = %v, want %v", got, tt.want)
}
})
}
}
func TestArbiterPlacement(t *testing.T) {
placement := cephv1.Placement{
NodeAffinity: &v1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{
{
Key: "foo",
Operator: v1.NodeSelectorOpExists,
Values: []string{"bar"},
},
},
},
},
},
},
}
c := &Cluster{spec: cephv1.ClusterSpec{
Mon: cephv1.MonSpec{
StretchCluster: &cephv1.StretchClusterSpec{
Zones: []cephv1.MonZoneSpec{
{Name: "a", Arbiter: true},
{Name: "b"},
{Name: "c"},
},
},
},
}}
c.spec.Placement = cephv1.PlacementSpec{}
c.spec.Placement[cephv1.KeyMonArbiter] = placement
// No placement is found if not requesting the arbiter placement
result := c.getMonPlacement("c")
assert.Equal(t, cephv1.Placement{}, result)
// Placement is found if requesting the arbiter
result = c.getMonPlacement("a")
assert.Equal(t, placement, result)
// Arbiter and all mons have the same placement if no arbiter placement is specified
c.spec.Placement = cephv1.PlacementSpec{}
c.spec.Placement[cephv1.KeyMon] = placement
result = c.getMonPlacement("a")
assert.Equal(t, placement, result)
result = c.getMonPlacement("c")
assert.Equal(t, placement, result)
}
func TestCheckIfArbiterReady(t *testing.T) {
c := &Cluster{
Namespace: "ns",
spec: cephv1.ClusterSpec{
Mon: cephv1.MonSpec{
StretchCluster: &cephv1.StretchClusterSpec{
Zones: []cephv1.MonZoneSpec{
{Name: "a", Arbiter: true},
{Name: "b"},
{Name: "c"},
},
},
},
},
}
crushZoneCount := 0
balanced := true
executor := &exectest.MockExecutor{
MockExecuteCommandWithOutput: func(command string, args ...string) (string, error) {
switch {
case args[0] == "osd" && args[1] == "crush" && args[2] == "dump":
crushBuckets := `
{"id": -1,"name": "default","type_id": 10,"type_name": "root","weight": 1028},
{"id": -2,"name": "default~hdd","type_id": 10,"type_name": "root","weight": 1028},
{"id": -3,"name": "mynode","type_id": 1,"type_name": "host","weight": 1028},
{"id": -4,"name": "mynode~hdd","type_id": 1,"type_name": "host","weight": 1028}`
for i := 0; i < crushZoneCount; i++ {
weight := 2056
if !balanced && i%2 == 1 {
// simulate unbalanced with every other zone having half the weight
weight = 1028
}
crushBuckets = crushBuckets +
fmt.Sprintf(`,{"id": -%d,"name": "zone%d","type_id": 1,"type_name": "zone","weight": %d}
,{"id": -%d,"name": "zone%d~ssd","type_id": 1,"type_name": "zone","weight": 2056}`, i+5, i, weight, i+6, i)
}
return fmt.Sprintf(`{"buckets": [%s]}`, crushBuckets), nil
}
return "", fmt.Errorf("unrecognized output file command: %s %v", command, args)
},
}
c.context = &clusterd.Context{Clientset: test.New(t, 5), Executor: executor}
c.ClusterInfo = clienttest.CreateTestClusterInfo(5)
// Not ready if no pods running
ready, err := c.readyToConfigureArbiter(true)
assert.False(t, ready)
assert.NoError(t, err)
// For the remainder of tests, skip checking OSD pods
// Now there are not enough zones
ready, err = c.readyToConfigureArbiter(false)
assert.False(t, ready)
assert.NoError(t, err)
// Valid
crushZoneCount = 2
ready, err = c.readyToConfigureArbiter(false)
assert.True(t, ready)
assert.NoError(t, err)
// Valid, except the CRUSH map is not balanced
balanced = false
ready, err = c.readyToConfigureArbiter(false)
assert.False(t, ready)
assert.NoError(t, err)
// Too many zones in the CRUSH map
crushZoneCount = 3
balanced = true
ready, err = c.readyToConfigureArbiter(false)
assert.False(t, ready)
assert.Error(t, err)
}
func TestSkipReconcile(t *testing.T) {
c := New(context.TODO(), &clusterd.Context{Clientset: test.New(t, 1), ConfigDir: t.TempDir()}, "ns", cephv1.ClusterSpec{}, cephclient.NewMinimumOwnerInfoWithOwnerRef())
c.ClusterInfo = clienttest.CreateTestClusterInfo(1)
c.ClusterInfo.Namespace = "ns"
monDeployment := &apps.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "rook-ceph-mon-a",
Namespace: c.ClusterInfo.Namespace,
Labels: map[string]string{
k8sutil.AppAttr: AppName,
config.MonType: "a",
},
},
}
deployment, err := c.context.Clientset.AppsV1().Deployments(c.ClusterInfo.Namespace).Create(c.ClusterInfo.Context, monDeployment, metav1.CreateOptions{})
assert.NoError(t, err)
result, err := opcontroller.GetDaemonsToSkipReconcile(c.ClusterInfo.Context, c.context, c.ClusterInfo.Namespace, config.MonType, AppName)
assert.NoError(t, err)
assert.Equal(t, 0, result.Len())
deployment, err = c.context.Clientset.AppsV1().Deployments(c.ClusterInfo.Namespace).Get(c.ClusterInfo.Context, deployment.Name, metav1.GetOptions{})
assert.NoError(t, err)
deployment.Labels[cephv1.SkipReconcileLabelKey] = ""
_, err = c.context.Clientset.AppsV1().Deployments(c.ClusterInfo.Namespace).Update(c.ClusterInfo.Context, deployment, metav1.UpdateOptions{})
assert.NoError(t, err)
result, err = opcontroller.GetDaemonsToSkipReconcile(c.ClusterInfo.Context, c.context, c.ClusterInfo.Namespace, config.MonType, AppName)
assert.NoError(t, err)
assert.Equal(t, 1, result.Len())
}
func TestHasMonPathChanged(t *testing.T) {
monDeployment := &apps.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "rook-ceph-mon-a",
Namespace: "test",
Labels: map[string]string{
"pvc_name": "test-pvc",
},
},
}
pvcTemplate := &v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "test-claim",
},
}
t.Run("mon path changed from pv to hostpath", func(t *testing.T) {
assert.True(t, hasMonPathChanged(monDeployment, nil))
})
t.Run("mon path not changed from pv to hostpath", func(t *testing.T) {
assert.False(t, hasMonPathChanged(monDeployment, pvcTemplate))
})
t.Run("mon path changed from hostPath to pvc", func(t *testing.T) {
delete(monDeployment.Labels, "pvc_name")
assert.True(t, hasMonPathChanged(monDeployment, pvcTemplate))
})
t.Run("mon path not changed from hostPath to pvc", func(t *testing.T) {
delete(monDeployment.Labels, "pvc_name")
assert.False(t, hasMonPathChanged(monDeployment, nil))
})
}
func TestIsMonIPUpdateRequiredForHostNetwork(t *testing.T) {
t.Run("both cluster and mon are set to use host network", func(t *testing.T) {
hostNetwork := &cephv1.NetworkSpec{HostNetwork: true}
monUsingHostNetwork := true
assert.False(t, isMonIPUpdateRequiredForHostNetwork("a", monUsingHostNetwork, hostNetwork))
})
t.Run("both cluster and mon are not set for host network", func(t *testing.T) {
hostNetwork := &cephv1.NetworkSpec{}
monUsingHostNetwork := false
assert.False(t, isMonIPUpdateRequiredForHostNetwork("a", monUsingHostNetwork, hostNetwork))
})
t.Run("cluster is set for host networking but mon pod is not", func(t *testing.T) {
hostNetwork := &cephv1.NetworkSpec{HostNetwork: true}
monUsingHostNetwork := false
assert.True(t, isMonIPUpdateRequiredForHostNetwork("a", monUsingHostNetwork, hostNetwork))
})
t.Run("mon is using host networking but cluster is updated to not use host network ", func(t *testing.T) {
hostNetwork := &cephv1.NetworkSpec{}
monUsingHostNetwork := true
assert.True(t, isMonIPUpdateRequiredForHostNetwork("a", monUsingHostNetwork, hostNetwork))
})
t.Run("mon is using host networking and cluster is set host network via NetworkProviderHost ", func(t *testing.T) {
hostNetwork := &cephv1.NetworkSpec{Provider: cephv1.NetworkProviderHost}
monUsingHostNetwork := true
assert.False(t, isMonIPUpdateRequiredForHostNetwork("a", monUsingHostNetwork, hostNetwork))
})
t.Run("mon is not using host networking but cluster is updated to use host network via NetworkProviderHost ", func(t *testing.T) {
hostNetwork := &cephv1.NetworkSpec{Provider: cephv1.NetworkProviderHost}
monUsingHostNetwork := false
assert.True(t, isMonIPUpdateRequiredForHostNetwork("a", monUsingHostNetwork, hostNetwork))
})
}
func TestRotateMonCephxKeys(t *testing.T) {
ctx := context.TODO()
namespace := "default"
context, err := newTestStartCluster(t, namespace)
assert.NoError(t, err)
c := newCluster(context, namespace, true, v1.ResourceRequirements{})
uninitializedStatus := keyring.UninitializedCephxStatus()
cluster := &cephv1.CephCluster{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
Namespace: namespace,
},
Spec: cephv1.ClusterSpec{
Security: cephv1.ClusterSecuritySpec{
CephX: cephv1.ClusterCephxConfig{
Daemon: cephv1.CephxConfig{},
},
},
},
Status: cephv1.ClusterStatus{
Cephx: cephv1.ClusterCephxStatus{
Mon: uninitializedStatus,
},
},
}
s := scheme.Scheme
s.AddKnownTypes(cephv1.SchemeGroupVersion, &cephv1.CephCluster{})
s.AddKnownTypes(v1.SchemeGroupVersion, &v1.Secret{})
object := []runtime.Object{
cluster,
}
cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(object...).Build()
executor := &exectest.MockExecutor{
MockExecuteCommandWithOutput: func(command string, args ...string) (string, error) {
logger.Infof("%s %v", command, args)
if args[0] == "auth" && args[1] == "rotate" {
return `[{"key":"myrotatedkey"}]`, nil
}
return "", errors.New("unknown command")
},
}
c.context = &clusterd.Context{Clientset: test.New(t, 5), Executor: executor, Client: cl}
c.ClusterInfo = clienttest.CreateTestClusterInfo(1)
c.ClusterInfo.CephVersion = keyring.CephAuthMonRotateSupportedVersion
// create rook-ceph-mon secret
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: AppName,
Namespace: namespace,
},
Data: map[string][]byte{opcontroller.MonSecretNameKey: []byte("bar")},
}
_, err = c.context.Clientset.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{})
assert.NoError(t, err)
// No key rotation required
shouldRotate, err := c.RotateMonCephxKeys(cluster)
assert.NoError(t, err)
assert.False(t, shouldRotate)
// verify key is rotated in the secret
cluster.Spec.Security.CephX.Daemon.KeyRotationPolicy = cephv1.KeyGenerationCephxKeyRotationPolicy
cluster.Spec.Security.CephX.Daemon.KeyGeneration = 2
cluster.Status.Cephx.Mon.KeyGeneration = 1
cluster.Status.Cephx.Mon.KeyCephVersion = "19.2.3-0"
err = c.context.Client.Update(ctx, cluster)
assert.NoError(t, err)
shouldRotate, err = c.RotateMonCephxKeys(cluster)
assert.NoError(t, err)
assert.True(t, shouldRotate)
secret, err = c.context.Clientset.CoreV1().Secrets(namespace).Get(c.ClusterInfo.Context, AppName, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, "myrotatedkey", string(secret.Data[opcontroller.MonSecretNameKey]))
// verify mon key does not rotate when other daemons rotate
cluster.Spec.Security.CephX.Daemon.KeyRotationPolicy = cephv1.KeyGenerationCephxKeyRotationPolicy
cluster.Spec.Security.CephX.Daemon.KeyGeneration = 2
cluster.Status.Cephx.Mon.KeyGeneration = 1
cluster.Status.Cephx.Mon.KeyCephVersion = "19.2.3-0"
err = c.context.Client.Update(ctx, cluster)
assert.NoError(t, err)
c.ClusterInfo.CephVersion = keyring.CephAuthRotateSupportedVersion // but not CephAuthMonRotateSupportedVersion
shouldRotate, err = c.RotateMonCephxKeys(cluster)
assert.NoError(t, err)
assert.False(t, shouldRotate)
}