/* 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/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/controller" 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" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" ) // 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: sets.New[string](), } } // 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.Quincy, 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.Quincy, 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.Quincy, 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.Quincy, 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.Quincy, 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.Quincy, 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") // Persist mon a err := c.persistExpectedMonDaemons() 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) // Persist mon b, and remove mon a for simply testing the configmap is updated c.ClusterInfo.Monitors["b"] = &cephclient.MonInfo{Name: "b", Endpoint: "4.5.6.7:3300"} delete(c.ClusterInfo.Monitors, "a") err = c.persistExpectedMonDaemons() 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]) } 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.Monitors["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 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 := controller.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 = controller.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)) }) }