Merge pull request #7386 from BlaineEXE/update-osds-in-parallel

ceph: Update osds in parallel
This commit is contained in:
Blaine Gardner
2021-03-29 16:32:07 -06:00
committed by GitHub
40 changed files with 4769 additions and 1559 deletions
+4 -1
View File
@@ -27,5 +27,8 @@ v1.6...
* Enable disruption budgets (PDBs) by default for Mon, RGW, MDS, and OSD daemons
* Add CephFilesystemMirror CRD to deploy cephfs-mirror daemon
* Multiple Ceph mgr daemons are supported for stretch clusters and other clusters where HA of the mgr is more critical
* Ceph OSD: as of Nautilus 14.2.14 and Octopus 15.2.9 if the OSD scenario is simple (one OSD per disk) we won't use LVM to prepare the disk anymore
* OSDs:
* as of Nautilus 14.2.14 and Octopus 15.2.9 if the OSD scenario is simple (one OSD per disk) we won't use LVM to prepare the disk anymore
* for Pacific (16.2.x), Rook is able to update multiple OSD Deployments at the same time to speed
up updates and upgrades for larger Ceph clusters
* Disable CSI GRPC metrics by default
@@ -4,7 +4,7 @@ metadata:
name: replicapool
namespace: rook-ceph # namespace:cluster
spec:
failureDomain: host
failureDomain: osd
replicated:
size: 1
# Disallow setting pool with replica 1, this could lead to data loss without recovery.
+11 -8
View File
@@ -97,18 +97,21 @@ List all OSD Deployments belonging to the Rook cluster. Build a list of OSD IDs
Deployments. Record this in a data structure that allows O(1) lookup.
### How to build the update queue
List all OSD Deployments belonging to the Rook cluster to use as the update queue. Ignore OSD
Deployments which are already updated. OSD Deployments which need updated will have a
`rook-version` label that does not match the current version of the Rook operator and a
`ceph-version` label that does not match the current Ceph version being deployed.
Go channels are likely not the best option for the update queue since it may be necessary to select
individual items from the queue at any position and not merely grab the top/bottom item from the
queue.
List all OSD Deployments belonging to the Rook cluster to use as the update queue. All OSDs should
be updated in case there are changes to the CephCluster resource that result in OSD deployments
being updated.
The minimal information each item in the queue needs is only the OSD ID. The OSD Deployment managed
by Rook can easily be inferred from the OSD ID.
Note: A previous version of this design planned to ignore OSD Deployments which are already updated.
The plan was to identify OSD Deployments which need updated by looking at the OSD Deployments for:
(1) a `rook-version` label that does not match the current version of the Rook operator AND/OR
(2) a `ceph-version` label that does not match the current Ceph version being deployed. This is an
invalid optimization that does not account for OSD Deployments changing due to CephCluster resource
updates. Instead of trying to optimize, it is better to always update OSD Deplyments and rely on the
lower level update calls to finish quickly when there is no update to apply.
### User configuration
#### Modifications to `CephCluster` CRD
+10
View File
@@ -39,6 +39,16 @@ func (s *StorageScopeSpec) ClearUseAllDevices() {
}
}
// NodeExists returns true if the node exists in the storage spec. False otherwise.
func (s *StorageScopeSpec) NodeExists(nodeName string) bool {
for i := range s.Nodes {
if s.Nodes[i].Name == nodeName {
return true
}
}
return false
}
// Fully resolves the config of the given node name, taking into account cluster level and node level specified config.
// In general, the more fine grained the configuration is specified, the more precedence it takes. Fully resolved
// configuration for the node has the following order of precedence.
+32
View File
@@ -21,6 +21,38 @@ import (
"github.com/stretchr/testify/assert"
)
func TestNodeExists(t *testing.T) {
t.Run("does not exist - no nodes specified", func(t *testing.T) {
spec := StorageScopeSpec{}
assert.False(t, spec.NodeExists("does-not-exist"))
})
t.Run("exists - single node specified", func(t *testing.T) {
spec := StorageScopeSpec{
Nodes: []Node{
{Name: "node1"}, // node gets nothing but its name set
},
}
assert.True(t, spec.NodeExists("node1"))
})
t.Run("exists and not exists - multiple nodes specified", func(t *testing.T) {
spec := StorageScopeSpec{
Nodes: []Node{
{Name: "node1"}, // node gets nothing but its name set
{Name: "node3"},
{Name: "node4"},
},
}
assert.True(t, spec.NodeExists("node1"))
assert.False(t, spec.NodeExists("node2"))
assert.True(t, spec.NodeExists("node3"))
assert.True(t, spec.NodeExists("node4"))
assert.False(t, spec.NodeExists("node5"))
assert.False(t, spec.NodeExists("does-not-exist"))
})
}
func TestResolveNodeNotExist(t *testing.T) {
// a non existing node should return nil
storageSpec := StorageScopeSpec{}
+130
View File
@@ -0,0 +1,130 @@
/*
Copyright 2021 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 fake
import (
"fmt"
"strconv"
"strings"
)
// OsdLsOutput returns JSON output from 'ceph osd ls' that can be used for unit tests. It
// returns output for a Ceph cluster with the number of OSDs given as input starting with ID 0.
// example: numOSDs = 5 => return: "[0,1,2,3,4]"
func OsdLsOutput(numOSDs int) string {
stringIDs := make([]string, 0, numOSDs)
for id := 0; id < numOSDs; id++ {
stringIDs = append(stringIDs, strconv.Itoa(id))
}
return fmt.Sprintf("[%s]", strings.Join(stringIDs, ","))
}
// OsdTreeOutput returns JSON output from 'ceph osd tree' that can be used for unit tests.
// It returns output for a Ceph cluster with the given number of nodes and the given number of OSDs
// per node with no complex configuration. This should work even for 0 nodes.
// example: OsdTreeOutput(3, 3) // returns JSON output for the Ceph cluster below
// node0: node1: node2:
// - osd0 - osd1 - osd2
// - osd3 - osd4 - osd5
// - osd6 - osd7 - osd8
func OsdTreeOutput(numNodes, numOSDsPerNode int) string {
// JSON output taken from Ceph Pacific
rootFormat := ` {
"id": -1,
"name": "default",
"type": "root",
"type_id": 11,
"children": [%s]
}` // format: negative node IDs as comma-delimited string (e.g., "-3,-4,-5")
nodeFormat := ` {
"id": %d,
"name": "%s",
"type": "host",
"type_id": 1,
"pool_weights": {},
"children": [%s]
}` // format: negative node ID, node name, OSD IDs as comma-delimited string (e.g., "0,3,6")
osdFormat := ` {
"id": %d,
"device_class": "hdd",
"name": "osd.%d",
"type": "osd",
"type_id": 0,
"crush_weight": 0.009796142578125,
"depth": 2,
"pool_weights": {},
"exists": 1,
"status": "up",
"reweight": 1,
"primary_affinity": 1
}` // format: OSD ID, OSD ID
wrapperFormat := `{
"nodes": [
%s
],
"stray": []
}` // format: <rendered root JSON, rendered nodes, rendered osds - with commas in between>
nodesJSON := []string{}
osdsJSON := []string{}
nodes := []string{}
for n := 0; n < numNodes; n++ {
osds := []string{}
nodeName := fmt.Sprintf("node%d", n)
nodeID := -3 - n
nodes = append(nodes, strconv.Itoa(nodeID))
for i := 0; i < numOSDsPerNode; i++ {
osdID := n + 3*i
osds = append(osds, strconv.Itoa(osdID))
osdsJSON = append(osdsJSON, fmt.Sprintf(osdFormat, osdID, osdID))
}
nodesJSON = append(nodesJSON, fmt.Sprintf(nodeFormat, nodeID, nodeName, strings.Join(osds, ",")))
}
rootJSON := fmt.Sprintf(rootFormat, strings.Join(nodes, ","))
fullJSON := append(append([]string{rootJSON}, nodesJSON...), osdsJSON...)
rendered := fmt.Sprintf(wrapperFormat, strings.Join(fullJSON, ",\n"))
return rendered
}
// OsdOkToStopOutput returns JSON output from 'ceph osd ok-to-stop' that can be used for unit tests.
// queriedID should be given as the ID sent to the 'osd ok-to-stop <id> [--max=N]' command. It will
// be returned with relevant NOT ok-to-stop results.
// If returnOsdIds is empty, this returns a NOT ok-to-stop result. Otherwise, it returns an
// ok-to-stop result. returnOsdIds should include queriedID if the result should be successful.
// usePacificPlusOutput instructs the function to render output for Ceph Pacific (v16) and above or
// to render output for Ceph Octopus (v15) and below.
func OsdOkToStopOutput(queriedID int, returnOsdIds []int, useCephPacificPlusOutput bool) string {
// For Pacific and up (Pacific+)
okTemplate := `{"ok_to_stop":true,"osds":[%s],"num_ok_pgs":132,"num_not_ok_pgs":0,"ok_become_degraded":["1.0","1.2","1.3"]}`
notOkTemplate := `{"ok_to_stop":false,"osds":[%d],"num_ok_pgs":161,"num_not_ok_pgs":50,"bad_become_inactive":["1.0","1.3","1.a"],"ok_become_degraded":["1.2","1.4","1.5"]}`
// Ceph Octopus and below don't return anything on stdout, only success/failure via retcode
if !useCephPacificPlusOutput {
return ""
}
// Pacific+, NOT ok-to-stop
if len(returnOsdIds) == 0 {
return fmt.Sprintf(notOkTemplate, queriedID)
}
// Pacific+, ok-to-stop
osdIdsStr := make([]string, len(returnOsdIds))
for i := 0; i < len(returnOsdIds); i++ {
osdIdsStr[i] = strconv.Itoa(returnOsdIds[i])
}
return fmt.Sprintf(okTemplate, strings.Join(osdIdsStr, ","))
}
+50
View File
@@ -17,6 +17,7 @@ package client
import (
"encoding/json"
"fmt"
"strconv"
"strings"
@@ -305,3 +306,52 @@ func OsdListNum(context *clusterd.Context, clusterInfo *ClusterInfo) (OsdList, e
return output, nil
}
// OSDOkToStopStats report detailed information about which OSDs are okay to stop
type OSDOkToStopStats struct {
OkToStop bool `json:"ok_to_stop"`
OSDs []int `json:"osds"`
NumOkPGs int `json:"num_ok_pgs"`
NumNotOkPGs int `json:"num_not_ok_pgs"`
BadBecomeInactive []string `json:"bad_become_inactive"`
OkBecomeDegraded []string `json:"ok_become_degraded"`
}
// OSDOkToStop returns a list of OSDs that can be stopped that includes the OSD ID given.
// This is relevant, for example, when checking which OSDs can be updated.
// The number of OSDs returned is limited by the value set in maxReturned.
// maxReturned=0 is the same as maxReturned=1.
func OSDOkToStop(context *clusterd.Context, clusterInfo *ClusterInfo, osdID, maxReturned int) ([]int, error) {
args := []string{"osd", "ok-to-stop", strconv.Itoa(osdID)}
returnsList := false // does the ceph call return a list of OSD IDs?
if clusterInfo.CephVersion.IsAtLeastPacific() {
returnsList = true
// NOTE: if the number of OSD IDs given in the CLI arg query is Q and --max=N is given, if
// N < Q, Ceph treats the query as though max=Q instead, always returning at least Q OSDs.
args = append(args, fmt.Sprintf("--max=%d", maxReturned))
}
buf, err := NewCephCommand(context, clusterInfo, args).Run()
if err != nil {
// is not ok to stop (or command error)
return []int{}, errors.Wrapf(err, "OSD %d is not ok to stop", osdID)
}
if !returnsList {
// If does not return list, just return a slice including only the OSD ID queried
return []int{osdID}, nil
}
var stats OSDOkToStopStats
err = json.Unmarshal(buf, &stats)
if err != nil {
// Since the command succeeded we still know that at least the given OSD ID is ok to
// stop, so we do not *have* to return an error. However, it is good to do it anyway so
// that we can catch breaking changes to JSON output in CI testing. As a middle ground
// here, return error but also return the given OSD ID in the output in case the calling
// function wants to recover from this case.
return []int{osdID}, errors.Wrapf(err, "failed to unmarshal 'osd ok-to-stop %d' response", osdID)
}
return stats.OSDs, nil
}
+89
View File
@@ -17,10 +17,13 @@ limitations under the License.
package client
import (
"fmt"
"testing"
"github.com/pkg/errors"
"github.com/rook/rook/pkg/clusterd"
"github.com/rook/rook/pkg/daemon/ceph/client/fake"
"github.com/rook/rook/pkg/operator/ceph/version"
exectest "github.com/rook/rook/pkg/util/exec/test"
"github.com/stretchr/testify/assert"
)
@@ -110,3 +113,89 @@ func TestOsdListNum(t *testing.T) {
assert.Error(t, err)
assert.Equal(t, 0, len(list))
}
func TestOSDOkToStop(t *testing.T) {
returnString := ""
returnOkResult := true
seenArgs := []string{}
executor := &exectest.MockExecutor{}
executor.MockExecuteCommandWithOutputFile = func(command, outputFile string, args ...string) (string, error) {
logger.Infof("Command: %s %v", command, args)
switch {
case args[0] == "osd" && args[1] == "ok-to-stop":
seenArgs = args
if returnOkResult {
return returnString, nil
}
return returnString, errors.Errorf("Error EBUSY: unsafe to stop osd(s) at this time (50 PGs are or would become offline)")
}
panic(fmt.Sprintf("unexpected ceph command %q", args))
}
context := &clusterd.Context{Executor: executor}
clusterInfo := AdminClusterInfo("mycluster")
doSetup := func() {
seenArgs = []string{}
}
t.Run("pacific output ok to stop", func(t *testing.T) {
doSetup()
clusterInfo.CephVersion = version.Pacific
returnString = fake.OsdOkToStopOutput(1, []int{1, 2}, true)
returnOkResult = true
osds, err := OSDOkToStop(context, clusterInfo, 1, 2)
assert.NoError(t, err)
assert.ElementsMatch(t, osds, []int{1, 2})
assert.Equal(t, "1", seenArgs[2])
assert.Equal(t, "--max=2", seenArgs[3])
})
t.Run("pacific output not ok to stop", func(t *testing.T) {
doSetup()
clusterInfo.CephVersion = version.Pacific
returnString = fake.OsdOkToStopOutput(3, []int{}, true)
returnOkResult = false
_, err := OSDOkToStop(context, clusterInfo, 3, 5)
assert.Error(t, err)
assert.Equal(t, "3", seenArgs[2])
assert.Equal(t, "--max=5", seenArgs[3])
})
t.Run("pacific handles maxReturned=0", func(t *testing.T) {
doSetup()
clusterInfo.CephVersion = version.Pacific
returnString = fake.OsdOkToStopOutput(4, []int{4, 8}, true)
returnOkResult = true
osds, err := OSDOkToStop(context, clusterInfo, 4, 0)
assert.NoError(t, err)
assert.ElementsMatch(t, osds, []int{4, 8})
assert.Equal(t, "4", seenArgs[2])
// should just pass through as --max=0; don't do any special processing
assert.Equal(t, "--max=0", seenArgs[3])
})
t.Run("octopus output not ok to stop", func(t *testing.T) {
doSetup()
clusterInfo.CephVersion = version.Octopus
returnString = fake.OsdOkToStopOutput(3, []int{}, false)
returnOkResult = false
_, err := OSDOkToStop(context, clusterInfo, 3, 5)
assert.Error(t, err)
assert.Equal(t, "3", seenArgs[2])
assert.NotContains(t, seenArgs[3], "--max") // do not issue the "--max" flag below pacific
})
t.Run("octopus output ok to stop", func(t *testing.T) {
doSetup()
clusterInfo.CephVersion = version.Octopus
returnString = fake.OsdOkToStopOutput(50, []int{50}, false)
returnOkResult = true
osds, err := OSDOkToStop(context, clusterInfo, 50, 2)
assert.NoError(t, err)
assert.ElementsMatch(t, osds, []int{50})
assert.Equal(t, "50", seenArgs[2])
assert.NotContains(t, seenArgs[3], "--max") // do not issue the "--max" flag below pacific
})
}
+34 -1
View File
@@ -356,6 +356,40 @@ func buildHostListFromTree(tree OsdTree) (OsdTree, error) {
return osdList, nil
}
// OSDUpdateShouldCheckOkToStop returns true if Rook should check ok-to-stop for OSDs when doing
// OSD daemon updates. It will return false if it should not perform ok-to-stop checks, for example,
// when there are fewer than 3 OSDs
func OSDUpdateShouldCheckOkToStop(context *clusterd.Context, clusterInfo *ClusterInfo) bool {
userIntervention := "the user will likely need to set continueUpgradeAfterChecksEvenIfNotHealthy to allow OSD updates to proceed"
osds, err := OsdListNum(context, clusterInfo)
if err != nil {
// If calling osd list fails, we assume there are more than 3 OSDs and we check if ok-to-stop
// If there are less than 3 OSDs, the ok-to-stop call will fail
// this can still be controlled by setting continueUpgradeAfterChecksEvenIfNotHealthy
// At least this will happen for a single OSD only, which means 2 OSDs will restart in a small interval
logger.Warningf("failed to determine the total number of osds. will check if OSDs are ok-to-stop. if there are fewer than 3 OSDs %s. %v", userIntervention, err)
return true
}
if len(osds) < 3 {
logger.Warningf("the cluster has fewer than 3 osds. not performing upgrade check. running in best-effort")
return false
}
// aio means all in one
aio, err := allOSDsSameHost(context, clusterInfo)
if err != nil {
logger.Warningf("failed to determine if all osds are running on the same host. will check if OSDs are ok-to-stop. if all OSDs are running on one host %s. %v", userIntervention, err)
return true
}
if aio {
logger.Warningf("all OSDs are running on the same host. not performing upgrade check. running in best-effort")
return false
}
return true
}
// osdDoNothing determines whether we should perform upgrade pre-check and post-checks for the OSD daemon
// it checks for various cluster info like number of OSD and their placement
// it returns 'true' if we need to do nothing and false and we should pre-check/post-check
@@ -377,7 +411,6 @@ func osdDoNothing(context *clusterd.Context, clusterInfo *ClusterInfo) bool {
// aio means all in one
aio, err := allOSDsSameHost(context, clusterInfo)
if err != nil {
// If calling osd list fails, we assume there are more than 3 OSDs and we check if ok-to-stop
logger.Warningf("failed to determine if all osds are running on the same host, performing upgrade check anyways. %v", err)
return false
}
+61
View File
@@ -23,6 +23,7 @@ import (
"github.com/pkg/errors"
"github.com/rook/rook/pkg/clusterd"
"github.com/rook/rook/pkg/daemon/ceph/client/fake"
exectest "github.com/rook/rook/pkg/util/exec/test"
"github.com/stretchr/testify/assert"
)
@@ -297,3 +298,63 @@ func TestGetRetryConfig(t *testing.T) {
assert.Equalf(t, tc.expectedDelay, actualDelay, "[%s] failed to get correct delays between retries", tc.label)
}
}
func TestOSDUpdateShouldCheckOkToStop(t *testing.T) {
clusterInfo := &ClusterInfo{}
lsOutput := ""
treeOutput := ""
context := &clusterd.Context{
Executor: &exectest.MockExecutor{
MockExecuteCommandWithOutputFile: func(command string, outFileArg string, args ...string) (string, error) {
t.Logf("command: %s %v", command, args)
if command != "ceph" || args[0] != "osd" {
panic("not a 'ceph osd' call")
}
if args[1] == "tree" {
if treeOutput == "" {
return "", errors.Errorf("induced error")
}
return treeOutput, nil
}
if args[1] == "ls" {
if lsOutput == "" {
return "", errors.Errorf("induced error")
}
return lsOutput, nil
}
panic("do not understand command")
},
},
}
t.Run("3 nodes with 1 OSD each", func(t *testing.T) {
lsOutput = fake.OsdLsOutput(3)
treeOutput = fake.OsdTreeOutput(3, 1)
assert.True(t, OSDUpdateShouldCheckOkToStop(context, clusterInfo))
})
t.Run("1 node with 3 OSDs", func(t *testing.T) {
lsOutput = fake.OsdLsOutput(3)
treeOutput = fake.OsdTreeOutput(1, 3)
assert.False(t, OSDUpdateShouldCheckOkToStop(context, clusterInfo))
})
t.Run("2 nodes with 1 OSD each", func(t *testing.T) {
lsOutput = fake.OsdLsOutput(2)
treeOutput = fake.OsdTreeOutput(2, 1)
assert.False(t, OSDUpdateShouldCheckOkToStop(context, clusterInfo))
})
t.Run("3 nodes with 3 OSDs each", func(t *testing.T) {
lsOutput = fake.OsdLsOutput(9)
treeOutput = fake.OsdTreeOutput(3, 3)
assert.True(t, OSDUpdateShouldCheckOkToStop(context, clusterInfo))
})
// degraded case but good to test just in case
t.Run("0 nodes", func(t *testing.T) {
lsOutput = fake.OsdLsOutput(0)
treeOutput = fake.OsdTreeOutput(0, 0)
assert.False(t, OSDUpdateShouldCheckOkToStop(context, clusterInfo))
})
}
+2 -2
View File
@@ -778,12 +778,12 @@ func (a *OsdAgent) initializeDevicesLVMMode(context *clusterd.Context, devices *
}
if len(strings.Split(conf["devices"], " ")) != len(cvReports) {
return fmt.Errorf("failed to create enough required devices, required: %s, actual: %v", cvOut, cvReports)
return errors.Errorf("failed to create enough required devices, required: %s, actual: %v", cvOut, cvReports)
}
for _, report := range cvReports {
if report.BlockDB != mdPath {
return fmt.Errorf("wrong db device for %s, required: %s, actual: %s", report.Data, mdPath, report.BlockDB)
return errors.Errorf("wrong db device for %s, required: %s, actual: %s", report.Data, mdPath, report.BlockDB)
}
}
}
+413
View File
@@ -0,0 +1,413 @@
/*
Copyright 2021 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 osd
import (
"fmt"
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
rookv1 "github.com/rook/rook/pkg/apis/rook.io/v1"
kms "github.com/rook/rook/pkg/daemon/ceph/osd/kms"
osdconfig "github.com/rook/rook/pkg/operator/ceph/cluster/osd/config"
"github.com/rook/rook/pkg/operator/ceph/controller"
opcontroller "github.com/rook/rook/pkg/operator/ceph/controller"
"github.com/rook/rook/pkg/operator/k8sutil"
"github.com/rook/rook/pkg/util"
v1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/util/version"
)
type createConfig struct {
cluster *Cluster
provisionConfig *provisionConfig
awaitingStatusConfigMaps *util.Set // These status configmaps were created for OSD prepare jobs
finishedStatusConfigMaps *util.Set // Status configmaps are added here as provisioning is completed for them
deployments *existenceList // these OSDs have existing deployments
}
// allow overriding these functions for unit tests
var (
createDaemonOnNodeFunc = createDaemonOnNode
createDaemonOnPVCFunc = createDaemonOnPVC
updateConditionFunc = opcontroller.UpdateCondition
)
func (c *Cluster) newCreateConfig(
provisionConfig *provisionConfig,
awaitingStatusConfigMaps *util.Set,
deployments *existenceList,
) *createConfig {
if awaitingStatusConfigMaps == nil {
awaitingStatusConfigMaps = util.NewSet()
}
return &createConfig{
c,
provisionConfig,
awaitingStatusConfigMaps,
util.NewSet(),
deployments,
}
}
func (c *createConfig) progress() (completed, initial int) {
return c.finishedStatusConfigMaps.Count(), c.awaitingStatusConfigMaps.Count()
}
func (c *createConfig) doneCreating() bool {
return c.awaitingStatusConfigMaps.Count() == c.finishedStatusConfigMaps.Count()
}
func (c *createConfig) createNewOSDsFromStatus(
status *OrchestrationStatus,
nodeOrPVCName string,
errs *provisionErrors,
) {
if !c.awaitingStatusConfigMaps.Contains(statusConfigMapName(nodeOrPVCName)) {
// If there is a dangling OSD prepare configmap from another reconcile, don't process it
logger.Infof("not creating deployments for OSD prepare results found in ConfigMap %q which was not created for the latest storage spec", statusConfigMapName(nodeOrPVCName))
return
}
if c.finishedStatusConfigMaps.Contains(statusConfigMapName(nodeOrPVCName)) {
// If we have already processed this configmap, don't process it again
logger.Infof("not creating deployments for OSD prepare results found in ConfigMap %q which was already processed", statusConfigMapName(nodeOrPVCName))
return
}
for _, osd := range status.OSDs {
if c.deployments.Exists(osd.ID) {
// This OSD will be handled by the updater
logger.Debugf("not creating deployment for OSD %d which already exists", osd.ID)
continue
}
if status.PvcBackedOSD {
logger.Infof("creating OSD %d on PVC %q", osd.ID, nodeOrPVCName)
err := createDaemonOnPVCFunc(c.cluster, osd, nodeOrPVCName, c.provisionConfig)
if err != nil {
errs.addError("%v", errors.Wrapf(err, "failed to create OSD %d on PVC %q", osd.ID, nodeOrPVCName))
}
} else {
logger.Infof("creating OSD %d on node %q", osd.ID, nodeOrPVCName)
err := createDaemonOnNodeFunc(c.cluster, osd, nodeOrPVCName, c.provisionConfig)
if err != nil {
errs.addError("%v", errors.Wrapf(err, "failed to create OSD %d on node %q", osd.ID, nodeOrPVCName))
}
}
}
c.doneWithStatus(nodeOrPVCName)
}
// Call this if createNewOSDsFromStatus() isn't going to be called (like for a failed status)
func (c *createConfig) doneWithStatus(nodeOrPVCName string) {
c.finishedStatusConfigMaps.Add(statusConfigMapName(nodeOrPVCName))
}
// Returns a set of all the awaitingStatusConfigMaps that will be updated by provisioning jobs.
// Returns error only if the calling function should halt all OSD provisioning. Non-halting errors
// are added to provisionErrors.
//
// Creation of prepare jobs is most directly related to creating new OSDs. And we want to keep all
// usage of awaitingStatusConfigMaps in this file.
func (c *Cluster) startProvisioningOverPVCs(config *provisionConfig, errs *provisionErrors) (*util.Set, error) {
// Parsing storageClassDeviceSets and parsing it to volume sources
c.spec.Storage.VolumeSources = append(c.spec.Storage.VolumeSources, c.prepareStorageClassDeviceSets(errs)...)
c.ValidStorage.VolumeSources = c.spec.Storage.VolumeSources
// no valid VolumeSource is ready to run an osd
if !c.shouldProvisionOverPVCs() {
logger.Info("no storageClassDeviceSets or volumeSources are defined to configure OSDs on PVCs")
return util.NewSet(), nil
}
// Check k8s version
k8sVersion, err := k8sutil.GetK8SVersion(c.context.Clientset)
if err != nil {
errs.addError("failed to provision OSDs on PVCs. user has specified storageClassDeviceSets, but the Kubernetes version could not be determined. minimum Kubernetes version required: 1.13.0. %v", err)
return util.NewSet(), nil
}
if !k8sVersion.AtLeast(version.MustParseSemantic("v1.13.0")) {
errs.addError("failed to provision OSDs on PVCs. user has specified storageClassDeviceSets, but the Kubernetes version is not supported. user must update Kubernetes version. minimum Kubernetes version required: 1.13.0. version detected: %s", k8sVersion.String())
return util.NewSet(), nil
}
existingDeployments, err := c.getExistingOSDDeploymentsOnPVCs()
if err != nil {
errs.addError("failed to provision OSDs on PVCs. failed to query existing OSD deployments on PVCs. %v", err)
return util.NewSet(), nil
}
awaitingStatusConfigMaps := util.NewSet()
for _, volume := range c.ValidStorage.VolumeSources {
// Check whether we need to cancel the orchestration
if err := controller.CheckForCancelledOrchestration(c.context); err != nil {
return awaitingStatusConfigMaps, err
}
dataSource, dataOK := volume.PVCSources[bluestorePVCData]
// The data PVC template is required.
if !dataOK {
errs.addError("failed to create OSD provisioner for storageClassDeviceSet %q. missing the data template", volume.Name)
continue
}
metadataSource, metadataOK := volume.PVCSources[bluestorePVCMetadata]
if metadataOK {
logger.Infof("OSD will have its main bluestore block on %q and its metadata device on %q", dataSource.ClaimName, metadataSource.ClaimName)
} else {
logger.Infof("OSD will have its main bluestore block on %q", dataSource.ClaimName)
}
walSource, walOK := volume.PVCSources[bluestorePVCWal]
if walOK {
logger.Infof("OSD will have its wal device on %q", walSource.ClaimName)
}
osdProps := osdProperties{
crushHostname: dataSource.ClaimName,
pvc: dataSource,
metadataPVC: metadataSource,
walPVC: walSource,
resources: volume.Resources,
placement: volume.Placement,
preparePlacement: volume.PreparePlacement,
portable: volume.Portable,
schedulerName: volume.SchedulerName,
encrypted: volume.Encrypted,
deviceSetName: volume.Name,
}
osdProps.storeConfig.DeviceClass = volume.CrushDeviceClass
if osdProps.encrypted {
// If the deviceSet template has "encrypted" but the Ceph version is not compatible
if !c.isCephVolumeRawModeSupported() {
errMsg := fmt.Sprintf("failed to validate storageClassDeviceSet %q. min required ceph version to support encryption is %q or %q", volume.Name, cephVolumeRawEncryptionModeMinNautilusCephVersion.String(), cephVolumeRawEncryptionModeMinOctopusCephVersion.String())
errs.addError(errMsg)
continue
}
// create encryption Kubernetes Secret if the PVC is encrypted
key, err := generateDmCryptKey()
if err != nil {
errMsg := fmt.Sprintf("failed to generate dmcrypt key for osd claim %q. %v", osdProps.pvc.ClaimName, err)
errs.addError(errMsg)
continue
}
// Initialize the KMS code
kmsConfig := kms.NewConfig(c.context, &c.spec, c.clusterInfo)
// We could set an env var in the Operator or a global var instead of the API call?
// Hopefully, the API is cheap and we can always retrieve the token if it has changed...
if c.spec.Security.KeyManagementService.IsTokenAuthEnabled() {
err := kms.SetTokenToEnvVar(c.context, c.spec.Security.KeyManagementService.TokenSecretName, kmsConfig.Provider, c.clusterInfo.Namespace)
if err != nil {
errMsg := fmt.Sprintf("failed to fetch kms token secret %q. %v", c.spec.Security.KeyManagementService.TokenSecretName, err)
errs.addError(errMsg)
continue
}
}
// Generate and store the encrypted key in whatever KMS is configured
err = kmsConfig.PutSecret(osdProps.pvc.ClaimName, key)
if err != nil {
errMsg := fmt.Sprintf("failed to store secret. %v", err)
errs.addError(errMsg)
continue
}
}
// Skip OSD prepare if deployment already exists for the PVC
if existingDeployments.Contains(dataSource.ClaimName) {
logger.Debugf("skipping OSD prepare job creation for PVC %q because OSD daemon using the PVC already exists", osdProps.crushHostname)
continue
}
// Update the orchestration status of this pvc to the starting state
status := OrchestrationStatus{Status: OrchestrationStatusStarting, PvcBackedOSD: true}
cmName := c.updateOSDStatus(osdProps.crushHostname, status)
if err := c.runPrepareJob(&osdProps, config); err != nil {
c.handleOrchestrationFailure(errs, osdProps.crushHostname, "%v", err)
c.deleteStatusConfigMap(osdProps.crushHostname)
continue // do not record the status CM's name
}
// record the name of the status configmap that will eventually receive results from the
// OSD provisioning job we just created. This will help us determine when we are done
// processing the results of provisioning jobs.
awaitingStatusConfigMaps.Add(cmName)
}
return awaitingStatusConfigMaps, nil
}
// Returns a set of all the awaitingStatusConfigMaps that will be updated by provisioning jobs.
// Returns error only if the calling function should halt all OSD provisioning. Non-halting errors
// are added to provisionErrors.
//
// Creation of prepare jobs is most directly related to creating new OSDs. And we want to keep all
// usage of awaitingStatusConfigMaps in this file.
func (c *Cluster) startProvisioningOverNodes(config *provisionConfig, errs *provisionErrors) (*util.Set, error) {
if !c.spec.Storage.UseAllNodes && len(c.spec.Storage.Nodes) == 0 {
logger.Info("no nodes are defined for configuring OSDs on raw devices")
return util.NewSet(), nil
}
if c.spec.Storage.UseAllNodes {
if len(c.spec.Storage.Nodes) > 0 {
logger.Warningf("useAllNodes is TRUE, but nodes are specified. NODES in the cluster CR will be IGNORED unless useAllNodes is FALSE.")
}
// Get the list of all nodes in the cluster. The placement settings will be applied below.
hostnameMap, err := k8sutil.GetNodeHostNames(c.context.Clientset)
if err != nil {
errs.addError("failed to provision OSDs on nodes. failed to get node hostnames. %v", err)
return util.NewSet(), nil
}
c.spec.Storage.Nodes = nil
for _, hostname := range hostnameMap {
storageNode := rookv1.Node{
Name: hostname,
}
c.spec.Storage.Nodes = append(c.spec.Storage.Nodes, storageNode)
}
logger.Debugf("storage nodes: %+v", c.spec.Storage.Nodes)
}
// generally speaking, this finds nodes which are capable of running new osds
validNodes := k8sutil.GetValidNodes(c.spec.Storage, c.context.Clientset, cephv1.GetOSDPlacement(c.spec.Placement))
logger.Infof("%d of the %d storage nodes are valid", len(validNodes), len(c.spec.Storage.Nodes))
c.ValidStorage = *c.spec.Storage.DeepCopy()
c.ValidStorage.Nodes = validNodes
// no valid node is ready to run an osd
if len(validNodes) == 0 {
logger.Warningf("no valid nodes available to run osds on nodes in namespace %q", c.clusterInfo.Namespace)
return util.NewSet(), nil
}
if len(c.spec.DataDirHostPath) == 0 {
errs.addError("failed to provision OSDs on nodes. user has specified valid nodes for storage, but dataDirHostPath is empty. user must set CephCluster dataDirHostPath")
return util.NewSet(), nil
}
awaitingStatusConfigMaps := util.NewSet()
for _, node := range c.ValidStorage.Nodes {
// Check whether we need to cancel the orchestration
if err := controller.CheckForCancelledOrchestration(c.context); err != nil {
return awaitingStatusConfigMaps, err
}
// fully resolve the storage config and resources for this node
n := c.resolveNode(node.Name)
if n == nil {
logger.Warningf("node %q did not resolve", node.Name)
continue
}
if n.Name == "" {
logger.Warningf("skipping node with a blank name! %+v", n)
continue
}
// create the job that prepares osds on the node
storeConfig := osdconfig.ToStoreConfig(n.Config)
metadataDevice := osdconfig.MetadataDevice(n.Config)
osdProps := osdProperties{
crushHostname: n.Name,
devices: n.Devices,
selection: n.Selection,
resources: n.Resources,
storeConfig: storeConfig,
metadataDevice: metadataDevice,
}
// update the orchestration status of this node to the starting state
status := OrchestrationStatus{Status: OrchestrationStatusStarting}
cmName := c.updateOSDStatus(n.Name, status)
if err := c.runPrepareJob(&osdProps, config); err != nil {
c.handleOrchestrationFailure(errs, n.Name, "%v", err)
c.deleteStatusConfigMap(n.Name)
continue // do not record the status CM's name
}
// record the name of the status configmap that will eventually receive results from the
// OSD provisioning job we just created. This will help us determine when we are done
// processing the results of provisioning jobs.
awaitingStatusConfigMaps.Add(cmName)
}
return awaitingStatusConfigMaps, nil
}
func (c *Cluster) runPrepareJob(osdProps *osdProperties, config *provisionConfig) error {
nodeOrPVC := "node"
if osdProps.onPVC() {
nodeOrPVC = "PVC"
}
nodeOrPVCName := osdProps.crushHostname
job, err := c.makeJob(*osdProps, config)
if err != nil {
return errors.Wrapf(err, "failed to generate osd provisioning job template for %s %q", nodeOrPVC, nodeOrPVCName)
}
if err := k8sutil.RunReplaceableJob(c.context.Clientset, job, false); err != nil {
if !kerrors.IsAlreadyExists(err) {
return errors.Wrapf(err, "failed to run provisioning job for %s %q", nodeOrPVC, nodeOrPVCName)
}
logger.Infof("letting preexisting OSD provisioning job run to completion for %s %q", nodeOrPVC, nodeOrPVCName)
return nil
}
logger.Infof("started OSD provisioning job for %s %q", nodeOrPVC, nodeOrPVCName)
return nil
}
func createDaemonOnPVC(c *Cluster, osd OSDInfo, pvcName string, config *provisionConfig) error {
d, err := deploymentOnPVC(c, osd, pvcName, config)
if err != nil {
return err
}
message := fmt.Sprintf("Processing OSD %d on PVC %q", osd.ID, pvcName)
updateConditionFunc(c.context, c.clusterInfo.NamespacedName(), cephv1.ConditionProgressing, v1.ConditionTrue, cephv1.ClusterProgressingReason, message)
_, err = k8sutil.CreateDeployment(c.context.Clientset, d)
return errors.Wrapf(err, "failed to create deployment for OSD %d on PVC %q", osd.ID, pvcName)
}
func createDaemonOnNode(c *Cluster, osd OSDInfo, nodeName string, config *provisionConfig) error {
d, err := deploymentOnNode(c, osd, nodeName, config)
if err != nil {
return err
}
message := fmt.Sprintf("Processing OSD %d on node %q", osd.ID, nodeName)
updateConditionFunc(c.context, c.clusterInfo.NamespacedName(), cephv1.ConditionProgressing, v1.ConditionTrue, cephv1.ClusterProgressingReason, message)
_, err = k8sutil.CreateDeployment(c.context.Clientset, d)
return errors.Wrapf(err, "failed to create deployment for OSD %d on node %q", osd.ID, nodeName)
}
@@ -0,0 +1,705 @@
/*
Copyright 2021 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 osd
import (
"context"
"strings"
"testing"
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
rookv1 "github.com/rook/rook/pkg/apis/rook.io/v1"
"github.com/rook/rook/pkg/clusterd"
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
cephver "github.com/rook/rook/pkg/operator/ceph/version"
"github.com/rook/rook/pkg/operator/test"
"github.com/rook/rook/pkg/util"
"github.com/stretchr/testify/assert"
"github.com/tevino/abool"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
apiresource "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
)
func Test_createNewOSDsFromStatus(t *testing.T) {
namespace := "my-namespace"
clientset := fake.NewSimpleClientset()
// clusterd.Context is created in doSetup()
// names of status configmaps for nodes
statusNameNode0 := statusConfigMapName("node0")
statusNameNode2 := statusConfigMapName("node2")
statusNamePVC1 := statusConfigMapName("pvc1")
statusNamePVC2 := statusConfigMapName("pvc2")
clusterInfo := &cephclient.ClusterInfo{
Namespace: namespace,
}
clusterInfo.SetName("mycluster")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
var oldCreateDaemonOnNodeFunc = createDaemonOnNodeFunc
defer func() {
createDaemonOnNodeFunc = oldCreateDaemonOnNodeFunc
}()
createCallsOnNode := []int{}
induceFailureCreatingOSD := -1 // allow causing the create call to fail for a given OSD ID
createDaemonOnNodeFunc = func(c *Cluster, osd OSDInfo, nodeName string, config *provisionConfig) error {
createCallsOnNode = append(createCallsOnNode, osd.ID)
if induceFailureCreatingOSD == osd.ID {
return errors.Errorf("createOSDDaemonOnNode: induced failure on OSD %d", osd.ID)
}
return nil
}
var oldCreateDaemonOnPVCFunc = createDaemonOnPVCFunc
defer func() {
createDaemonOnPVCFunc = oldCreateDaemonOnPVCFunc
}()
createCallsOnPVC := []int{}
// reuse induceFailureCreatingOSD from above
createDaemonOnPVCFunc = func(c *Cluster, osd OSDInfo, pvcName string, config *provisionConfig) error {
createCallsOnPVC = append(createCallsOnPVC, osd.ID)
if induceFailureCreatingOSD == osd.ID {
return errors.Errorf("createOSDDaemonOnNode: induced failure on OSD %d", osd.ID)
}
return nil
}
// Simulate an environment where deployments exist for OSDs 3, 4, and 6
deployments := newExistenceListWithCapacity(5)
deployments.Add(3)
deployments.Add(4)
deployments.Add(6)
spec := cephv1.ClusterSpec{}
var status *OrchestrationStatus
awaitingStatusConfigMaps := util.NewSet()
var c *Cluster
var createConfig *createConfig
var errs *provisionErrors
doSetup := func() {
// none of this code should ever add or remove deployments from the existence list
assert.Equal(t, 3, deployments.Len())
// Simulate environment where provision jobs were created for node0, node2, pvc1, and pvc2
awaitingStatusConfigMaps = util.NewSet()
awaitingStatusConfigMaps.AddMultiple([]string{
statusNameNode0, statusNameNode2,
statusNamePVC1, statusNamePVC2})
createCallsOnNode = createCallsOnNode[:0]
createCallsOnPVC = createCallsOnPVC[:0]
errs = newProvisionErrors()
ctx := &clusterd.Context{
Clientset: clientset,
}
c = New(ctx, clusterInfo, spec, "rook/rook:master")
config := c.newProvisionConfig()
createConfig = c.newCreateConfig(config, awaitingStatusConfigMaps, deployments)
}
t.Run("node: create no OSDs when none are returned from node", func(t *testing.T) {
doSetup()
status = &OrchestrationStatus{
OSDs: []OSDInfo{},
PvcBackedOSD: false,
}
createConfig.createNewOSDsFromStatus(status, "node0", errs)
assert.Zero(t, errs.len())
assert.Len(t, createCallsOnNode, 0)
assert.Len(t, createCallsOnPVC, 0)
// status map should have been marked completed
assert.Equal(t, 4, awaitingStatusConfigMaps.Count())
assert.Equal(t, 1, createConfig.finishedStatusConfigMaps.Count())
assert.True(t, createConfig.finishedStatusConfigMaps.Contains(statusNameNode0))
})
t.Run("test: node: create all OSDs on node when all do not exist", func(t *testing.T) {
doSetup()
status = &OrchestrationStatus{
OSDs: []OSDInfo{
{ID: 0}, {ID: 1}, {ID: 2},
},
PvcBackedOSD: false,
}
createConfig.createNewOSDsFromStatus(status, "node2", errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, createCallsOnNode, []int{0, 1, 2})
assert.Len(t, createCallsOnPVC, 0)
// status map should have been marked completed
assert.Equal(t, 4, awaitingStatusConfigMaps.Count())
assert.Equal(t, 1, createConfig.finishedStatusConfigMaps.Count())
assert.True(t, createConfig.finishedStatusConfigMaps.Contains(statusNameNode2))
})
t.Run("node: create only nonexistent OSDs on node when some already exist", func(t *testing.T) {
doSetup()
status = &OrchestrationStatus{
OSDs: []OSDInfo{
{ID: 3}, {ID: 4}, // already exist
{ID: 5}, // does not exist
{ID: 6}, // already exists
{ID: 7}, // does not exist
},
PvcBackedOSD: false,
}
createConfig.createNewOSDsFromStatus(status, "node0", errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, createCallsOnNode, []int{5, 7})
assert.Len(t, createCallsOnPVC, 0)
// status map should have been marked completed
assert.Equal(t, 4, awaitingStatusConfigMaps.Count())
assert.Equal(t, 1, createConfig.finishedStatusConfigMaps.Count())
assert.True(t, createConfig.finishedStatusConfigMaps.Contains(statusNameNode0))
})
t.Run("node: skip creating OSDs for status configmaps that weren't created for this reconcile", func(t *testing.T) {
doSetup()
status = &OrchestrationStatus{
OSDs: []OSDInfo{
{ID: 0}, {ID: 1}, {ID: 2},
},
PvcBackedOSD: false,
}
createConfig.createNewOSDsFromStatus(status, "node1", errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, createCallsOnNode, []int{})
assert.Len(t, createCallsOnPVC, 0)
// status map should NOT have been marked completed
assert.Equal(t, 4, awaitingStatusConfigMaps.Count())
assert.Equal(t, 0, createConfig.finishedStatusConfigMaps.Count())
})
t.Run("node: errors reported if OSDs fail to create", func(t *testing.T) {
induceFailureCreatingOSD = 1 // fail when creating OSD 1
doSetup()
status = &OrchestrationStatus{
OSDs: []OSDInfo{
{ID: 0}, {ID: 1}, {ID: 2},
},
PvcBackedOSD: false,
}
createConfig.createNewOSDsFromStatus(status, "node0", errs)
assert.Equal(t, 1, errs.len())
assert.ElementsMatch(t, createCallsOnNode, []int{0, 1, 2})
assert.Len(t, createCallsOnPVC, 0)
// status map should have been marked completed
assert.Equal(t, 4, awaitingStatusConfigMaps.Count())
assert.Equal(t, 1, createConfig.finishedStatusConfigMaps.Count())
assert.True(t, createConfig.finishedStatusConfigMaps.Contains(statusNameNode0))
induceFailureCreatingOSD = -1 // off
})
t.Run("pvc: create no OSDs when none are returned from PVC", func(t *testing.T) {
doSetup()
status = &OrchestrationStatus{
OSDs: []OSDInfo{},
PvcBackedOSD: true,
}
createConfig.createNewOSDsFromStatus(status, "pvc1", errs)
assert.Zero(t, errs.len())
assert.Len(t, createCallsOnNode, 0)
assert.Len(t, createCallsOnPVC, 0)
// status map should have been marked completed
assert.Equal(t, 4, awaitingStatusConfigMaps.Count())
assert.Equal(t, 1, createConfig.finishedStatusConfigMaps.Count())
assert.True(t, createConfig.finishedStatusConfigMaps.Contains(statusNamePVC1))
})
t.Run("pvc: create all OSDs on pvc when all do not exist", func(t *testing.T) {
doSetup()
status = &OrchestrationStatus{
OSDs: []OSDInfo{
{ID: 0}, {ID: 1}, {ID: 2},
},
PvcBackedOSD: true,
}
createConfig.createNewOSDsFromStatus(status, "pvc2", errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, createCallsOnPVC, []int{0, 1, 2})
assert.Len(t, createCallsOnNode, 0)
// status map should have been marked completed
assert.Equal(t, 4, awaitingStatusConfigMaps.Count())
assert.Equal(t, 1, createConfig.finishedStatusConfigMaps.Count())
assert.True(t, createConfig.finishedStatusConfigMaps.Contains(statusNamePVC2))
})
t.Run("pvc: create only nonexistent OSDs on pvc when some already exist", func(t *testing.T) {
doSetup()
status = &OrchestrationStatus{
OSDs: []OSDInfo{
{ID: 3}, {ID: 4}, // already exist
{ID: 5}, // does not exist
{ID: 6}, // already exists
{ID: 7}, // does not exist
},
PvcBackedOSD: true,
}
createConfig.createNewOSDsFromStatus(status, "pvc1", errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, createCallsOnPVC, []int{5, 7})
assert.Len(t, createCallsOnNode, 0)
// status map should have been marked completed
assert.Equal(t, 4, awaitingStatusConfigMaps.Count())
assert.Equal(t, 1, createConfig.finishedStatusConfigMaps.Count())
assert.True(t, createConfig.finishedStatusConfigMaps.Contains(statusNamePVC1))
})
t.Run("pvc: skip creating OSDs for status configmaps that weren't created for this reconcile", func(t *testing.T) {
doSetup()
status = &OrchestrationStatus{
OSDs: []OSDInfo{
{ID: 0}, {ID: 1}, {ID: 2},
},
PvcBackedOSD: true,
}
createConfig.createNewOSDsFromStatus(status, "pvc0", errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, createCallsOnPVC, []int{})
assert.Len(t, createCallsOnNode, 0)
// no status maps should have been marked completed
assert.Equal(t, 4, awaitingStatusConfigMaps.Count())
assert.Equal(t, 0, createConfig.finishedStatusConfigMaps.Count())
})
t.Run("pvc: errors reported if OSDs fail to create", func(t *testing.T) {
induceFailureCreatingOSD = 1 // fail when creating OSD 1
doSetup()
status = &OrchestrationStatus{
OSDs: []OSDInfo{
{ID: 0}, {ID: 1}, {ID: 2},
},
PvcBackedOSD: true,
}
createConfig.createNewOSDsFromStatus(status, "pvc1", errs)
assert.Equal(t, 1, errs.len())
assert.ElementsMatch(t, createCallsOnPVC, []int{0, 1, 2})
assert.Len(t, createCallsOnNode, 0)
// status map should have been marked completed
assert.Equal(t, 4, awaitingStatusConfigMaps.Count())
assert.Equal(t, 1, createConfig.finishedStatusConfigMaps.Count())
assert.True(t, createConfig.finishedStatusConfigMaps.Contains(statusNamePVC1))
induceFailureCreatingOSD = -1 // off
})
}
func Test_startProvisioningOverPVCs(t *testing.T) {
namespace := "rook-ceph"
clientset := test.NewComplexClientset(t) // fake clientset with generate name functionality
requestCancelOrchestration := *abool.New()
// clusterd.Context is created in doSetup()
clusterInfo := &cephclient.ClusterInfo{
Namespace: namespace,
CephVersion: cephver.Nautilus,
}
clusterInfo.SetName("mycluster")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
spec := cephv1.ClusterSpec{}
fakeK8sVersion := "v1.13.0"
var errs *provisionErrors
var c *Cluster
var config *provisionConfig
var awaitingStatusConfigMaps *util.Set
var err error
doSetup := func() {
test.SetFakeKubernetesVersion(clientset, fakeK8sVersion) // PVCs require k8s version v1.13+
errs = newProvisionErrors()
ctx := &clusterd.Context{
Clientset: clientset,
RequestCancelOrchestration: &requestCancelOrchestration,
}
c = New(ctx, clusterInfo, spec, "rook/rook:master")
config = c.newProvisionConfig()
}
t.Run("do nothing if no storage spec is given", func(t *testing.T) {
spec = cephv1.ClusterSpec{}
doSetup()
awaitingStatusConfigMaps, err = c.startProvisioningOverPVCs(config, errs)
assert.NoError(t, err)
assert.Zero(t, awaitingStatusConfigMaps.Count())
assert.Zero(t, errs.len())
// no result configmaps should have been created
cms, err := clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
assert.Len(t, cms.Items, 0)
})
t.Run("do nothing if device set count is zero", func(t *testing.T) {
spec = cephv1.ClusterSpec{
Storage: rookv1.StorageScopeSpec{
StorageClassDeviceSets: []rookv1.StorageClassDeviceSet{
{
Name: "set1",
Count: 0,
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
newDummyPVC("data", namespace, "10Gi", "gp2"),
},
},
},
},
}
doSetup()
awaitingStatusConfigMaps, err = c.startProvisioningOverPVCs(config, errs)
assert.NoError(t, err)
assert.Zero(t, awaitingStatusConfigMaps.Count())
assert.Zero(t, errs.len()) // this was not a problem with a single job but with ALL jobs
// no result configmaps should have been created
cms, err := clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
assert.Len(t, cms.Items, 0)
})
t.Run("one device set with 2 PVCs", func(t *testing.T) {
spec = cephv1.ClusterSpec{
Storage: rookv1.StorageScopeSpec{
StorageClassDeviceSets: []rookv1.StorageClassDeviceSet{
{
Name: "set1",
Count: 2,
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
newDummyPVC("data", namespace, "10Gi", "gp2"),
},
},
},
},
}
doSetup()
awaitingStatusConfigMaps, err = c.startProvisioningOverPVCs(config, errs)
assert.NoError(t, err)
assert.Equal(t, 2, awaitingStatusConfigMaps.Count())
assert.Zero(t, errs.len())
cms, err := clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
assert.Len(t, cms.Items, 2)
})
t.Run("repeat same device set with 2 PVCs (before provisioning jobs are done and before OSD deployments are created)", func(t *testing.T) {
// spec = <working spec from prior test>
doSetup()
awaitingStatusConfigMaps, err = c.startProvisioningOverPVCs(config, errs)
assert.NoError(t, err)
assert.Equal(t, 2, awaitingStatusConfigMaps.Count())
assert.Zero(t, errs.len())
cms, err := clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
assert.Len(t, cms.Items, 2) // still just 2 configmaps should exist (the same 2 from before)
})
t.Run("error if k8s version not high enough", func(t *testing.T) {
// spec = <working spec from prior test>
clientset = test.NewComplexClientset(t) // reset to empty fake k8s environment
fakeK8sVersion = "v1.12.7"
doSetup()
awaitingStatusConfigMaps, err = c.startProvisioningOverPVCs(config, errs)
assert.NoError(t, err)
assert.Equal(t, 0, awaitingStatusConfigMaps.Count())
assert.Equal(t, 1, errs.len())
cms, err := clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
assert.Len(t, cms.Items, 0)
fakeK8sVersion = "v1.13.0"
})
t.Run("request cancel orchestration", func(t *testing.T) {
// spec = <working spec from prior test>
clientset = test.NewComplexClientset(t) // reset to empty fake k8s environment
requestCancelOrchestration.Set()
doSetup()
awaitingStatusConfigMaps, err = c.startProvisioningOverPVCs(config, errs)
assert.Error(t, err)
assert.Zero(t, errs.len())
assert.Zero(t, awaitingStatusConfigMaps.Count())
requestCancelOrchestration.UnSet()
})
t.Run("error if no volume claim template", func(t *testing.T) {
spec = cephv1.ClusterSpec{
Storage: rookv1.StorageScopeSpec{
StorageClassDeviceSets: []rookv1.StorageClassDeviceSet{
{
Name: "set1",
Count: 2,
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{},
},
},
},
}
clientset = test.NewComplexClientset(t) // reset to empty fake k8s environment
doSetup()
awaitingStatusConfigMaps, err = c.startProvisioningOverPVCs(config, errs)
assert.NoError(t, err)
assert.Equal(t, 0, awaitingStatusConfigMaps.Count())
assert.Equal(t, 1, errs.len())
cms, err := clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
assert.Len(t, cms.Items, 0)
})
// TODO: should we verify the osdProps set on the job?
}
func Test_startProvisioningOverNodes(t *testing.T) {
namespace := "rook-ceph"
dataDirHostPath := "/var/lib/mycluster"
clientset := test.New(t, 3) // fake clientset with 3 nodes
requestCancelOrchestration := *abool.New()
// clusterd.Context is created in doSetup()
// names of status configmaps for nodes
statusNameNode0 := statusConfigMapName("node0")
statusNameNode1 := statusConfigMapName("node1")
statusNameNode2 := statusConfigMapName("node2")
// statusNameNode3 := statusConfigMapName("node3")
clusterInfo := &cephclient.ClusterInfo{
Namespace: namespace,
CephVersion: cephver.Nautilus,
}
clusterInfo.SetName("mycluster")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
var useAllDevices bool
spec := cephv1.ClusterSpec{}
var errs *provisionErrors
var c *Cluster
var config *provisionConfig
var prepareJobsRun *util.Set
var err error
var cms *corev1.ConfigMapList
doSetup := func() {
errs = newProvisionErrors()
ctx := &clusterd.Context{
Clientset: clientset,
RequestCancelOrchestration: &requestCancelOrchestration,
}
c = New(ctx, clusterInfo, spec, "rook/rook:master")
config = c.newProvisionConfig()
}
t.Run("do nothing if no storage spec is given", func(t *testing.T) {
spec = cephv1.ClusterSpec{}
doSetup()
prepareJobsRun, err = c.startProvisioningOverNodes(config, errs)
assert.NoError(t, err)
assert.Zero(t, prepareJobsRun.Count())
assert.Zero(t, errs.len())
// no result configmaps should have been created
cms, err = clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
assert.Len(t, cms.Items, 0)
})
t.Run("error on empty dataDirHostPath", func(t *testing.T) {
useAllDevices = true
spec = cephv1.ClusterSpec{
// this storage spec should cause prepare jobs to run on all (3) nodes
Storage: rookv1.StorageScopeSpec{
UseAllNodes: true,
Selection: rookv1.Selection{
UseAllDevices: &useAllDevices,
},
},
// BUT empty should not allow any jobs to be created
DataDirHostPath: "",
}
doSetup()
prepareJobsRun, err = c.startProvisioningOverNodes(config, errs)
assert.NoError(t, err)
assert.Zero(t, prepareJobsRun.Count())
assert.Equal(t, 1, errs.len()) // this was not a problem with a single job but with ALL jobs
// no result configmaps should have been created
cms, err = clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
assert.Len(t, cms.Items, 0)
})
t.Run("use all nodes and devices", func(t *testing.T) {
// Setting dataDirHostPath non-empty on the previous config should have jobs run for all nodes
spec.DataDirHostPath = dataDirHostPath
doSetup()
prepareJobsRun, err = c.startProvisioningOverNodes(config, errs)
assert.NoError(t, err)
assert.Zero(t, errs.len())
assert.ElementsMatch(t,
[]string{statusNameNode0, statusNameNode1, statusNameNode2},
prepareJobsRun.ToSlice(),
)
// all result configmaps should have been created
cms, err = clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
assert.Len(t, cms.Items, 3)
})
t.Run("use all nodes and devices when useAllNodes and individual nodes are both set", func(t *testing.T) {
// this also tests that jobs that currently exist (created in previous test) are handled
spec.Storage.Nodes = []rookv1.Node{
{Name: "node0"}, {Name: "node2"},
}
doSetup()
prepareJobsRun, err = c.startProvisioningOverNodes(config, errs)
assert.NoError(t, err)
assert.Zero(t, errs.len())
assert.ElementsMatch(t,
[]string{statusNameNode0, statusNameNode1, statusNameNode2},
prepareJobsRun.ToSlice(),
)
})
t.Run("use individual nodes", func(t *testing.T) {
// this also tests that existing status configmaps (from the previous tests) don't affect the
// reported status configmaps from this run
spec = cephv1.ClusterSpec{
Storage: rookv1.StorageScopeSpec{
UseAllNodes: false,
Nodes: []rookv1.Node{
// run on only node0 and node2
{Name: "node0"},
{Name: "node2"},
},
Selection: rookv1.Selection{
UseAllDevices: &useAllDevices,
},
},
DataDirHostPath: dataDirHostPath,
}
doSetup()
prepareJobsRun, err = c.startProvisioningOverNodes(config, errs)
assert.NoError(t, err)
assert.Zero(t, errs.len())
assert.ElementsMatch(t,
[]string{statusNameNode0, statusNameNode2},
prepareJobsRun.ToSlice(),
)
})
t.Run("request cancel orchestration", func(t *testing.T) {
requestCancelOrchestration.Set()
doSetup()
prepareJobsRun, err = c.startProvisioningOverNodes(config, errs)
assert.Error(t, err)
assert.Zero(t, errs.len())
assert.Zero(t, prepareJobsRun.Count())
requestCancelOrchestration.UnSet()
})
t.Run("use no nodes", func(t *testing.T) {
spec = cephv1.ClusterSpec{
Storage: rookv1.StorageScopeSpec{
UseAllNodes: false,
Nodes: []rookv1.Node{
// empty
},
Selection: rookv1.Selection{
UseAllDevices: &useAllDevices,
},
},
DataDirHostPath: dataDirHostPath,
}
doSetup()
prepareJobsRun, err = c.startProvisioningOverNodes(config, errs)
assert.NoError(t, err)
assert.Zero(t, errs.len())
assert.Zero(t, prepareJobsRun.Count())
})
t.Run("failures running prepare jobs", func(t *testing.T) {
spec = cephv1.ClusterSpec{
Storage: rookv1.StorageScopeSpec{
UseAllNodes: false,
Nodes: []rookv1.Node{
// run on only node0 and node2
{Name: "node0"},
{Name: "node2"},
},
Selection: rookv1.Selection{
UseAllDevices: &useAllDevices,
},
},
DataDirHostPath: dataDirHostPath,
}
// re-initialize an empty test clientset with 3 nodes
clientset = test.New(t, 3)
// add a job reactor that will cause the node2 job to fail
var jobReactor k8stesting.ReactionFunc = func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
switch action := action.(type) {
case k8stesting.CreateActionImpl:
obj := action.GetObject()
objMeta, err := meta.Accessor(obj)
if err != nil {
panic(err)
}
objName := objMeta.GetName()
if strings.Contains(objName, "node2") {
return true, nil, errors.Errorf("induced error")
}
default:
panic("this should not happen")
}
return false, nil, nil
}
clientset.PrependReactor("create", "jobs", jobReactor)
doSetup()
prepareJobsRun, err = c.startProvisioningOverNodes(config, errs)
assert.NoError(t, err)
assert.Equal(t, 1, errs.len())
assert.ElementsMatch(t,
[]string{statusNameNode0},
prepareJobsRun.ToSlice(),
)
// with a fresh clientset, only the one results ConfigMap should exist
cms, err = clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
assert.Len(t, cms.Items, 1)
assert.Equal(t, prepareJobsRun.ToSlice()[0], cms.Items[0].Name)
})
}
func newDummyPVC(name, namespace string, capacity string, storageClassName string) corev1.PersistentVolumeClaim {
volMode := corev1.PersistentVolumeBlock
return corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: corev1.PersistentVolumeClaimSpec{
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceName(corev1.ResourceStorage): apiresource.MustParse(capacity),
},
},
StorageClassName: &storageClassName,
VolumeMode: &volMode,
AccessModes: []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteOnce,
},
},
}
}
+17 -17
View File
@@ -32,24 +32,24 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (c *Cluster) prepareStorageClassDeviceSets(config *provisionConfig) []rookv1.VolumeSource {
func (c *Cluster) prepareStorageClassDeviceSets(errs *provisionErrors) []rookv1.VolumeSource {
volumeSources := []rookv1.VolumeSource{}
existingPVCs, uniqueOSDsPerDeviceSet, err := GetExistingPVCs(c.context, c.clusterInfo.Namespace)
if err != nil {
config.addError("failed to detect existing OSD PVCs. %v", err)
errs.addError("failed to detect existing OSD PVCs. %v", err)
return volumeSources
}
// Iterate over deviceSet
for _, deviceSet := range c.spec.Storage.StorageClassDeviceSets {
if err := controller.CheckPodMemory(cephv1.ResourcesKeyPrepareOSD, deviceSet.Resources, cephOsdPodMinimumMemory); err != nil {
config.addError("cannot use device set %q for creating osds %v", deviceSet.Name, err)
errs.addError("failed to provision OSDs on PVC for storageClassDeviceSet %q. %v", deviceSet.Name, err)
continue
}
// Check if the volume claim template is specified
if len(deviceSet.VolumeClaimTemplates) == 0 {
logger.Warningf("no volumeClaimTemplate specified for device set %q", deviceSet.Name)
errs.addError("failed to provision OSDs on PVC for storageClassDeviceSet %q. no volumeClaimTemplate is specified. user must specify a volumeClaimTemplate", deviceSet.Name)
continue
}
@@ -57,18 +57,18 @@ func (c *Cluster) prepareStorageClassDeviceSets(config *provisionConfig) []rookv
highestExistingID := -1
countInDeviceSet := 0
if existingIDs, ok := uniqueOSDsPerDeviceSet[deviceSet.Name]; ok {
logger.Infof("verifying pvcs exist for %d osds in device set %q", existingIDs.Count(), deviceSet.Name)
logger.Infof("verifying PVCs exist for %d OSDs in device set %q", existingIDs.Count(), deviceSet.Name)
for existingID := range existingIDs.Iter() {
pvcID, err := strconv.Atoi(existingID)
if err != nil {
config.addError("invalid PVC index %q found for device set %q", existingID, deviceSet.Name)
errs.addError("invalid PVC index %q found for device set %q", existingID, deviceSet.Name)
continue
}
// keep track of the max PVC index found so we know what index to start with for new OSDs
if pvcID > highestExistingID {
highestExistingID = pvcID
}
volumeSource := c.createDeviceSetPVCsForIndex(config, deviceSet, existingPVCs, pvcID)
volumeSource := c.createDeviceSetPVCsForIndex(deviceSet, existingPVCs, pvcID, errs)
volumeSources = append(volumeSources, volumeSource)
}
countInDeviceSet = existingIDs.Count()
@@ -81,7 +81,7 @@ func (c *Cluster) prepareStorageClassDeviceSets(config *provisionConfig) []rookv
}
for i := 0; i < pvcsToCreate; i++ {
pvcID := highestExistingID + i + 1
volumeSource := c.createDeviceSetPVCsForIndex(config, deviceSet, existingPVCs, pvcID)
volumeSource := c.createDeviceSetPVCsForIndex(deviceSet, existingPVCs, pvcID, errs)
volumeSources = append(volumeSources, volumeSource)
countInDeviceSet++
}
@@ -90,7 +90,7 @@ func (c *Cluster) prepareStorageClassDeviceSets(config *provisionConfig) []rookv
return volumeSources
}
func (c *Cluster) createDeviceSetPVCsForIndex(config *provisionConfig, deviceSet rookv1.StorageClassDeviceSet, existingPVCs map[string]*v1.PersistentVolumeClaim, setIndex int) rookv1.VolumeSource {
func (c *Cluster) createDeviceSetPVCsForIndex(deviceSet rookv1.StorageClassDeviceSet, existingPVCs map[string]*v1.PersistentVolumeClaim, setIndex int, errs *provisionErrors) rookv1.VolumeSource {
// Create the PVC source for each of the data, metadata, and other types of templates if defined.
pvcSources := map[string]v1.PersistentVolumeClaimVolumeSource{}
@@ -104,7 +104,7 @@ func (c *Cluster) createDeviceSetPVCsForIndex(config *provisionConfig, deviceSet
pvc, err := c.createDeviceSetPVC(existingPVCs, deviceSet.Name, pvcTemplate, setIndex)
if err != nil {
config.addError("failed to create osd for device set %q for count %d. %v", deviceSet.Name, setIndex, err)
errs.addError("failed to provision PVC for device set %q index %d. %v", deviceSet.Name, setIndex, err)
continue
}
@@ -172,7 +172,7 @@ func (c *Cluster) createDeviceSetPVC(existingPVCs map[string]*v1.PersistentVolum
// No PVC found, creating a new one
deployedPVC, err := c.context.Clientset.CoreV1().PersistentVolumeClaims(c.clusterInfo.Namespace).Create(ctx, pvc, metav1.CreateOptions{})
if err != nil {
return nil, errors.Wrapf(err, "failed to create pvc %q for device set %q", pvc.Name, deviceSetName)
return nil, errors.Wrapf(err, "failed to create PVC %q for device set %q", pvc.Name, deviceSetName)
}
logger.Infof("successfully provisioned PVC %q", deployedPVC.Name)
@@ -184,20 +184,20 @@ func (c *Cluster) updatePVCIfChanged(desiredPVC *v1.PersistentVolumeClaim, curre
desiredSize, desiredOK := desiredPVC.Spec.Resources.Requests[v1.ResourceStorage]
currentSize, currentOK := currentPVC.Spec.Resources.Requests[v1.ResourceStorage]
if !desiredOK || !currentOK {
logger.Debugf("desired or current size are not specified for pvc %q", currentPVC.Name)
logger.Debugf("desired or current size are not specified for PVC %q", currentPVC.Name)
return
}
if desiredSize.Value() > currentSize.Value() {
currentPVC.Spec.Resources.Requests[v1.ResourceStorage] = desiredSize
logger.Infof("updating pvc %q size from %s to %s", currentPVC.Name, currentSize.String(), desiredSize.String())
logger.Infof("updating PVC %q size from %s to %s", currentPVC.Name, currentSize.String(), desiredSize.String())
if _, err := c.context.Clientset.CoreV1().PersistentVolumeClaims(c.clusterInfo.Namespace).Update(ctx, currentPVC, metav1.UpdateOptions{}); err != nil {
// log the error, but don't fail the reconcile
logger.Errorf("failed to update pvc size. %v", err)
logger.Errorf("failed to update PVC size. %v", err)
return
}
logger.Infof("successfully updated pvc %q size", currentPVC.Name)
logger.Infof("successfully updated PVC %q size", currentPVC.Name)
} else if desiredSize.Value() < currentSize.Value() {
logger.Warningf("ignoring request to shrink osd pvc %q size from %s to %s, only expansion is allowed", currentPVC.Name, currentSize.String(), desiredSize.String())
logger.Warningf("ignoring request to shrink osd PVC %q size from %s to %s, only expansion is allowed", currentPVC.Name, currentSize.String(), desiredSize.String())
}
}
@@ -229,7 +229,7 @@ func GetExistingPVCs(clusterdContext *clusterd.Context, namespace string) (map[s
selector := metav1.ListOptions{LabelSelector: CephDeviceSetPVCIDLabelKey}
pvcs, err := clusterdContext.Clientset.CoreV1().PersistentVolumeClaims(namespace).List(ctx, selector)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to detect pvcs")
return nil, nil, errors.Wrap(err, "failed to detect PVCs")
}
result := map[string]*v1.PersistentVolumeClaim{}
uniqueOSDsPerDeviceSet := map[string]*util.Set{}
@@ -66,10 +66,10 @@ func testPrepareDeviceSets(t *testing.T, setTemplateName bool) {
spec: spec,
}
config := &provisionConfig{}
volumeSources := cluster.prepareStorageClassDeviceSets(config)
errs := newProvisionErrors()
volumeSources := cluster.prepareStorageClassDeviceSets(errs)
assert.Equal(t, 1, len(volumeSources))
assert.Equal(t, 0, len(config.errorMessages))
assert.Equal(t, 0, errs.len())
assert.Equal(t, "mydata", volumeSources[0].Name)
assert.True(t, volumeSources[0].Portable)
_, dataOK := volumeSources[0].PVCSources["data"]
@@ -141,10 +141,10 @@ func TestPrepareDeviceSetWithHolesInPVCs(t *testing.T) {
clientset.PrependReactor("create", "persistentvolumeclaims", pvcReactor)
// Create 3 PVCs for two OSDs in the device set
config := &provisionConfig{}
config := newProvisionErrors()
volumeSources := cluster.prepareStorageClassDeviceSets(config)
assert.Equal(t, 1, len(volumeSources))
assert.Equal(t, 0, len(config.errorMessages))
assert.Equal(t, 0, config.len())
assert.Equal(t, "mydata", volumeSources[0].Name)
assert.True(t, volumeSources[0].Portable)
_, dataOK := volumeSources[0].PVCSources["data"]
@@ -162,7 +162,7 @@ func TestPrepareDeviceSetWithHolesInPVCs(t *testing.T) {
cluster.spec.Storage.StorageClassDeviceSets[0].Count = 2
volumeSources = cluster.prepareStorageClassDeviceSets(config)
assert.Equal(t, 2, len(volumeSources))
assert.Equal(t, 0, len(config.errorMessages))
assert.Equal(t, 0, config.len())
// Verify the PVCs all exist
pvcs, err = clientset.CoreV1().PersistentVolumeClaims(cluster.clusterInfo.Namespace).List(ctx, metav1.ListOptions{})
+4
View File
@@ -127,6 +127,10 @@ func metadataDeviceEnvVar(metadataDevice string) v1.EnvVar {
return v1.EnvVar{Name: osdMetadataDeviceEnvVarName, Value: metadataDevice}
}
func walDeviceEnvVar(walDevice string) v1.EnvVar {
return v1.EnvVar{Name: osdWalDeviceEnvVarName, Value: walDevice}
}
func pvcBackedOSDEnvVar(pvcBacked string) v1.EnvVar {
return v1.EnvVar{Name: PVCBackedOSDVarName, Value: pvcBacked}
}
+1 -1
View File
@@ -80,7 +80,7 @@ func (m *OSDHealthMonitor) Start(stopCh chan struct{}) {
m.checkOSDHealth()
case <-stopCh:
logger.Infof("Stopping monitoring of OSDs in namespace %s", m.clusterInfo.Namespace)
logger.Infof("Stopping monitoring of OSDs in namespace %q", m.clusterInfo.Namespace)
return
}
}
@@ -0,0 +1,679 @@
/*
Copyright 2021 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 osd
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"testing"
"time"
"github.com/coreos/pkg/capnslog"
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
rookv1 "github.com/rook/rook/pkg/apis/rook.io/v1"
"github.com/rook/rook/pkg/clusterd"
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
cephclientfake "github.com/rook/rook/pkg/daemon/ceph/client/fake"
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"
"github.com/tevino/abool"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
)
var (
// global to allow creating helper functions that are not inline with test functions
testIDGenerator osdIDGenerator
)
// TODO: look into: failed to calculate diff between current deployment and newly generated one.
// Failed to generate strategic merge patch: map: map[] does not contain declared merge key: uid
// appears in unit tests, but does it appear in real-world?
// Test integration between OSD creates and updates including on both nodes and PVCs.
// The definition for this test is a wrapper for the test function that adds a timeout.
func TestOSDIntegration(t *testing.T) {
oldLogger := *logger
defer func() { logger = &oldLogger }() // reset logger to default after this test
logger.SetLevel(capnslog.TRACE) // want more log info for this test if it fails
oldOpportunisticDuration := osdOpportunisticUpdateDuration
oldMinuteDuration := minuteTickerDuration
defer func() {
osdOpportunisticUpdateDuration = oldOpportunisticDuration
minuteTickerDuration = oldMinuteDuration
}()
// lower the check durations for unit tests to speed them up
osdOpportunisticUpdateDuration = 1 * time.Millisecond
minuteTickerDuration = 3 * time.Millisecond
done := make(chan bool)
// runs in less than 650ms on 6-core, 16MB RAM system, but github CI can be much slower
timeout := time.After(20 * 750 * time.Millisecond)
go func() {
// use defer because t.Fatal will kill this goroutine, and we always want done set if the
// test func stops running
defer func() { done <- true }()
// run the actual test
testOSDIntegration(t)
}()
select {
case <-timeout:
t.Fatal("Test timed out. This is a test failure.")
case <-done:
}
}
// This is the actual test. If it hangs, we should consider that an error.
func testOSDIntegration(t *testing.T) {
ctx := context.TODO()
namespace := "osd-integration"
clusterName := "my-cluster"
testIDGenerator = newOSDIDGenerator()
// mock/stub functions as needed
oldConditionExportFunc := updateConditionFunc
defer func() {
updateConditionFunc = oldConditionExportFunc
}()
// stub out the conditionExportFunc to do nothing. we do not have a fake Rook interface that
// allows us to interact with a CephCluster resource like the fake K8s clientset.
updateConditionFunc = func(c *clusterd.Context, namespaceName types.NamespacedName, conditionType cephv1.ConditionType, status corev1.ConditionStatus, reason cephv1.ClusterReasonType, message string) {
// do nothing
}
// set up a fake k8s client set and watcher to generate events that the operator will listen to
clientset := test.NewComplexClientset(t)
test.AddSomeReadyNodes(t, clientset, 3)
assignPodToNode := true
test.PrependComplexJobReactor(t, clientset, assignPodToNode)
test.SetFakeKubernetesVersion(clientset, "v1.13.2") // v1.13 or higher is required for OSDs on PVC
os.Setenv(k8sutil.PodNamespaceEnvVar, namespace)
defer os.Unsetenv(k8sutil.PodNamespaceEnvVar)
statusMapWatcher := watch.NewRaceFreeFake()
clientset.PrependWatchReactor("configmaps", k8stesting.DefaultWatchReactor(statusMapWatcher, nil))
failCreatingDeployments := []string{}
failUpdatingDeployments := []string{}
deploymentGeneration := int64(1) // mock deployment generation constantly increasing
deploymentsCreated := []string{}
deploymentsUpdated := []string{}
var deploymentReactor k8stesting.ReactionFunc = func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
switch action := action.(type) {
case k8stesting.CreateActionImpl:
obj := action.GetObject()
d, ok := obj.(*appsv1.Deployment)
if !ok {
panic(fmt.Sprintf("object not a deployment: %+v", obj))
}
t.Logf("deployment reactor: create event for deployment %q", d.Name)
// (1) keep track of deployments which have been created
deploymentsCreated = append(deploymentsCreated, d.Name)
// (2) set deployments ready immediately. Don't have to test waiting for deployments to
// be ready b/c that is tested thoroughly in update_test.go
d.Status.ObservedGeneration = deploymentGeneration
d.Status.UpdatedReplicas = 1
d.Status.ReadyReplicas = 1
deploymentGeneration++
// (3) return a failure if asked
for _, match := range failCreatingDeployments {
if strings.Contains(d.Name, match) {
return true, nil, errors.Errorf("induced error creating deployment %q", d.Name)
}
}
case k8stesting.UpdateActionImpl:
obj := action.GetObject()
d, ok := obj.(*appsv1.Deployment)
if !ok {
panic(fmt.Sprintf("object not a deployment: %+v", obj))
}
t.Logf("deployment reactor: update event for deployment %q", d.Name)
// (1) keep track of deployments which have been created
deploymentsUpdated = append(deploymentsUpdated, d.Name)
// (2) set deployments ready immediately. Don't have to test waiting for deployments to
// be ready b/c that is tested thoroughly in update_test.go
d.Status.ObservedGeneration = deploymentGeneration
d.Status.UpdatedReplicas = 1
d.Status.ReadyReplicas = 1
deploymentGeneration++
// (3) return a failure if asked
for _, match := range failUpdatingDeployments {
if strings.Contains(d.Name, match) {
return true, nil, errors.Errorf("induced error creating deployment %q", d.Name)
}
}
case k8stesting.DeleteActionImpl:
panic(fmt.Sprintf("deployments should not be deleted: %+v", action))
}
// modify the object in-place so that the default reactor will create it with our
// modifications, if we have made any
return false, nil, nil
}
clientset.PrependReactor("*", "deployments", deploymentReactor)
clusterInfo := cephclient.NewClusterInfo(namespace, clusterName)
clusterInfo.CephVersion = cephver.Pacific
clusterInfo.SetName("mycluster")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
executor := osdIntegrationTestExecutor(t, clientset, namespace)
context := &clusterd.Context{
Clientset: clientset,
ConfigDir: "/var/lib/rook",
Executor: executor,
RequestCancelOrchestration: abool.New(),
}
spec := cephv1.ClusterSpec{
CephVersion: cephv1.CephVersionSpec{
Image: "ceph/ceph:v16.2.0",
},
DataDirHostPath: context.ConfigDir,
// This storage spec should... (see inline)
Storage: rookv1.StorageScopeSpec{
UseAllNodes: true,
Selection: rookv1.Selection{
// ... create 2 osd on each of the 3 nodes (6 total)
DeviceFilter: "vd[ab]",
},
StorageClassDeviceSets: []rookv1.StorageClassDeviceSet{
// ... create 6 portable osds
newDummyStorageClassDeviceSet("portable-set", namespace, 6, true, "ec2"),
// ... create 3 local, non-portable osds, one on each node
newDummyStorageClassDeviceSet("local-set", namespace, 3, false, "local"),
},
},
}
osdsPerNode := 2 // vda and vdb
c := New(context, clusterInfo, spec, "myversion")
var startErr error
var done bool
runReconcile := func() {
// reset environment
c = New(context, clusterInfo, spec, "myversion")
statusMapWatcher.Reset()
// reset counters
deploymentsCreated = []string{}
deploymentsUpdated = []string{}
done = false
startErr = c.Start()
done = true
}
waitForDone := func() {
for {
if done == true {
return
}
time.Sleep(1 * time.Millisecond)
}
}
// NOTE: these tests all use the same environment
t.Run("initial creation", func(t *testing.T) {
go runReconcile()
cms := waitForNumConfigMaps(clientset, namespace, 12) // 3 nodes + 9 new PVCs
for _, cm := range cms {
cpy := cm.DeepCopy()
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
}
waitForDone()
assert.NoError(t, startErr)
assert.Len(t, deploymentsCreated, 15)
assert.Len(t, deploymentsUpdated, 0)
})
t.Run("reconcile again with no changes", func(t *testing.T) {
go runReconcile()
cms := waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes
for _, cm := range cms {
cpy := cm.DeepCopy()
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
}
waitForDone()
assert.NoError(t, startErr)
assert.Len(t, deploymentsCreated, 0)
assert.Len(t, deploymentsUpdated, 15)
})
t.Run("increase number of OSDs", func(t *testing.T) {
spec.Storage.Selection.DeviceFilter = "/dev/vd[abc]" // 3 more (1 more per node)
spec.Storage.StorageClassDeviceSets[0].Count = 8 // 2 more portable
spec.Storage.StorageClassDeviceSets[1].Count = 6 // 3 more (1 more per node)
osdsPerNode = 3 // vda, vdb, vdc
go runReconcile()
cms := waitForNumConfigMaps(clientset, namespace, 8) // 3 nodes + 5 new PVCs
for _, cm := range cms {
cpy := cm.DeepCopy()
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
}
waitForDone()
assert.NoError(t, startErr)
assert.Len(t, deploymentsCreated, 8)
assert.Len(t, deploymentsUpdated, 15)
})
t.Run("mixed create and update, cancel reconcile, and continue reconcile", func(t *testing.T) {
spec.Storage.Selection.DeviceFilter = "/dev/vd[abcd]" // 3 more (1 more per node)
spec.Storage.StorageClassDeviceSets[0].Count = 10 // 2 more portable
osdsPerNode = 4 // vd[a-d]
go runReconcile()
cms := waitForNumConfigMaps(clientset, namespace, 5) // 3 nodes + 2 new PVCs
i := 1
for _, cm := range cms {
if !strings.Contains(cm.Name, "node") {
// only do node configmaps right now since those are always created and we want
// a deterministic number of configmaps in the next step
continue
}
cpy := cm.DeepCopy()
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
if i == 2 {
t.Log("canceling orchestration")
time.Sleep(10 * time.Millisecond)
// after the second status map is made ready, cancel the orchestration. wait a short
// while to make sure the watcher picks up the updated change
c.context.RequestCancelOrchestration.Set()
break
}
i++
}
waitForDone()
assert.Error(t, startErr)
t.Logf("c.Start() error: %+v", startErr)
// should have created 2 more OSDs for the configmaps we updated
assert.Len(t, deploymentsCreated, 2)
// we don't know exactly how many updates might have happened by this point
numUpdates := len(deploymentsUpdated)
t.Logf("deployments updated: %d", numUpdates)
go runReconcile()
cms = waitForNumConfigMaps(clientset, namespace, 5) // 3 nodes + 2 new PVCs
for _, cm := range cms {
cpy := cm.DeepCopy()
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
}
waitForDone()
assert.NoError(t, startErr)
assert.Len(t, deploymentsCreated, 3) // 5 less the 2 created in the previous step
assert.Len(t, deploymentsUpdated, 25) // 23 + 2 created in previous step
})
t.Run("failures reported in status configmaps", func(t *testing.T) {
spec.Storage.Selection.DeviceFilter = "/dev/vd[abcde]" // 3 more (1 more per node)
osdsPerNode = 5 // vd[a-e]
go runReconcile()
cms := waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes
for _, cm := range cms {
cpy := cm.DeepCopy()
if strings.Contains(cm.Name, "node1") {
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
} else {
setStatusConfigMapToFailed(t, cpy) // fail on node0 and node2
}
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
}
waitForDone()
assert.Error(t, startErr)
t.Logf("c.Start() error: %+v", startErr)
assert.Len(t, deploymentsCreated, 1)
assert.Len(t, deploymentsUpdated, 28)
// should get back to healthy after
go runReconcile()
cms = waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes
for _, cm := range cms {
cpy := cm.DeepCopy()
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
}
waitForDone()
assert.NoError(t, startErr)
assert.Len(t, deploymentsCreated, 2)
assert.Len(t, deploymentsUpdated, 29) // 28 + 1 created in previous step
})
t.Run("failures during deployment updates", func(t *testing.T) {
failUpdatingDeployments = []string{"osd-15", "osd-22"}
go runReconcile()
cms := waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes
for _, cm := range cms {
cpy := cm.DeepCopy()
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
}
waitForDone()
assert.Error(t, startErr)
t.Logf("c.Start() error: %+v", startErr)
assert.Len(t, deploymentsCreated, 0)
assert.Len(t, deploymentsUpdated, 31) // should attempt to update all deployments
failUpdatingDeployments = []string{}
go runReconcile()
cms = waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes
for _, cm := range cms {
cpy := cm.DeepCopy()
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
}
waitForDone()
assert.NoError(t, startErr)
assert.Len(t, deploymentsCreated, 0)
assert.Len(t, deploymentsUpdated, 31) // all deployments should be updated again
})
t.Run("failures during deployment creation", func(t *testing.T) {
spec.Storage.Selection.DeviceFilter = "/dev/vd[abcdef]" // 3 more (1 more per node)
osdsPerNode = 6 // vd[a-f]
failCreatingDeployments = []string{"osd-31", "osd-33"}
go runReconcile()
cms := waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes
for _, cm := range cms {
cpy := cm.DeepCopy()
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
}
waitForDone()
assert.Error(t, startErr)
t.Logf("c.Start() error: %+v", startErr)
assert.Len(t, deploymentsCreated, 3)
assert.Len(t, deploymentsUpdated, 31)
failCreatingDeployments = []string{}
go runReconcile()
cms = waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes
for _, cm := range cms {
cpy := cm.DeepCopy()
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
}
waitForDone()
assert.NoError(t, startErr)
assert.Len(t, deploymentsCreated, 2) // the 2 deployments NOT created before should be created now
assert.Len(t, deploymentsUpdated, 32) // 31 + 1 from previous step
})
t.Run("failures from improperly formatted StorageClassDeviceSet", func(t *testing.T) {
newSCDS := rookv1.StorageClassDeviceSet{
Name: "new",
Count: 3,
Portable: true,
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{},
}
spec.Storage.StorageClassDeviceSets = append(spec.Storage.StorageClassDeviceSets, newSCDS)
go runReconcile()
cms := waitForNumConfigMaps(clientset, namespace, 3) // 3 nodes
for _, cm := range cms {
cpy := cm.DeepCopy()
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
}
waitForDone()
assert.Error(t, startErr)
t.Logf("c.Start() error: %+v", startErr)
assert.Len(t, deploymentsCreated, 0)
assert.Len(t, deploymentsUpdated, 34)
spec.Storage.StorageClassDeviceSets[2].VolumeClaimTemplates = []corev1.PersistentVolumeClaim{
newDummyPVC("data", namespace, "100Gi", "ec2"),
newDummyPVC("metadata", namespace, "10Gi", "uncle-rogers-secret-stuff"),
}
go runReconcile()
cms = waitForNumConfigMaps(clientset, namespace, 6) // 3 nodes + 3 new PVCs
for _, cm := range cms {
cpy := cm.DeepCopy()
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
}
waitForDone()
assert.NoError(t, startErr)
assert.Len(t, deploymentsCreated, 3)
assert.Len(t, deploymentsUpdated, 34)
})
t.Run("clean up dangling configmaps", func(t *testing.T) {
danglingCM := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "dangling-status-configmap",
Namespace: namespace,
Labels: statusConfigMapLabels("node0"),
},
}
_, err := clientset.CoreV1().ConfigMaps(namespace).Create(ctx, danglingCM, metav1.CreateOptions{})
assert.NoError(t, err)
go runReconcile()
cms := waitForNumConfigMaps(clientset, namespace, 4) // 3 nodes + dangling
for _, cm := range cms {
cpy := cm.DeepCopy()
if cpy.Name == "dangling-status-configmap" {
continue
}
setStatusConfigMapToCompleted(t, cpy, osdsPerNode)
updateStatusConfigmap(clientset, statusMapWatcher, cpy)
}
waitForDone()
assert.NoError(t, err)
assert.Len(t, deploymentsCreated, 0)
assert.Len(t, deploymentsUpdated, 37)
cmList, err := clientset.CoreV1().ConfigMaps(namespace).List(ctx, metav1.ListOptions{})
assert.NoError(t, err)
assert.Len(t, cmList.Items, 0)
})
}
/*
* mock executor to handle ceph commands
*/
func osdIntegrationTestExecutor(t *testing.T, clientset *fake.Clientset, namespace string) *exectest.MockExecutor {
return &exectest.MockExecutor{
MockExecuteCommandWithOutputFile: func(command string, outFileArg string, args ...string) (string, error) {
t.Logf("command: %s %v", command, args)
if command != "ceph" {
return "", errors.Errorf("unexpected command %q with args %v", command, args)
}
if args[0] == "auth" {
if args[1] == "get-or-create-key" {
return "{\"key\": \"does-not-matter\"}", nil
}
}
if args[0] == "osd" {
if args[1] == "ok-to-stop" {
osdID := args[2]
id, err := strconv.Atoi(osdID)
if err != nil {
panic(err)
}
t.Logf("returning ok for OSD %d", id)
return cephclientfake.OsdOkToStopOutput(id, []int{id}, true), nil
}
if args[1] == "ls" {
// ceph osd ls returns an array of osd IDs like [0,1,2]
// build this based on the number of deployments since they should be equal
// for this test
l, err := clientset.AppsV1().Deployments(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
panic(fmt.Sprintf("failed to build 'ceph osd ls' output. %v", err))
}
return cephclientfake.OsdLsOutput(len(l.Items)), nil
}
if args[1] == "tree" {
return cephclientfake.OsdTreeOutput(3, 3), nil // fake output for cluster with 3 nodes having 3 OSDs
}
}
if args[0] == "versions" {
// the update deploy code only cares about the mons from the ceph version command results
v := `{"mon":{"ceph version 16.2.2 (somehash) nautilus (stable)":3}}`
return v, nil
}
return "", errors.Errorf("unexpected ceph command %q", args)
},
}
}
/*
* Unique and consistent OSD ID generator
*/
type osdIDGenerator struct {
nextOSDID int
osdIDMap map[string]int
}
func newOSDIDGenerator() osdIDGenerator {
return osdIDGenerator{
nextOSDID: 0,
osdIDMap: map[string]int{},
}
}
func (g *osdIDGenerator) osdID(t *testing.T, namedResource string) int {
if id, ok := g.osdIDMap[namedResource]; ok {
t.Logf("resource %q has existing OSD ID %d", namedResource, id)
return id
}
id := g.nextOSDID
g.osdIDMap[namedResource] = id
g.nextOSDID++
t.Logf("generated new OSD ID %d for resource %q", id, namedResource)
return id
}
func newDummyStorageClassDeviceSet(
name string, namespace string, count int, portable bool, storageClassName string,
) rookv1.StorageClassDeviceSet {
return rookv1.StorageClassDeviceSet{
Name: name,
Count: count,
Portable: portable,
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
newDummyPVC("data", namespace, "10Gi", storageClassName),
},
}
}
func waitForNumConfigMaps(clientset kubernetes.Interface, namespace string, count int) []corev1.ConfigMap {
for {
cms, err := clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
panic(err)
}
if len(cms.Items) >= count {
return cms.Items
}
time.Sleep(1 * time.Microsecond)
}
}
// Helper method to set "completed" status on "starting" ConfigMaps.
// If the configmap is a result for a provision on nodes, create numOSDsIfOnNode OSDs in the return
func setStatusConfigMapToCompleted(t *testing.T, cm *corev1.ConfigMap, numOSDsIfOnNode int) {
status := parseOrchestrationStatus(cm.Data)
t.Logf("updating configmap %q status to completed", cm.Name)
// configmap names are deterministic can be mapped indirectly to an OSD ID, and since the
// configmaps are used to report completion status of OSD provisioning, we use this property in
// these unit tests
status.Status = OrchestrationStatusCompleted
if status.PvcBackedOSD {
// only one OSD per PVC
osdID := testIDGenerator.osdID(t, cm.Name)
status.OSDs = []OSDInfo{
{
ID: osdID,
UUID: fmt.Sprintf("%032d", osdID),
BlockPath: "/dev/path/to/block",
CVMode: "raw",
},
}
} else {
status.OSDs = []OSDInfo{}
for i := 0; i < numOSDsIfOnNode; i++ {
// in order to generate multiple OSDs on a node, pretend they have different configmap
// names (simply append the index). this is still deterministic.
osdID := testIDGenerator.osdID(t, fmt.Sprintf("%s-%d", cm.Name, i))
disk := k8sutil.IndexToName(i)
status.OSDs = append(status.OSDs, OSDInfo{
ID: osdID,
UUID: fmt.Sprintf("%032d", osdID),
BlockPath: fmt.Sprintf("/dev/vd%s", disk),
CVMode: "raw",
})
}
}
s, _ := json.Marshal(status)
cm.Data[orchestrationStatusKey] = string(s)
}
func setStatusConfigMapToFailed(t *testing.T, cm *corev1.ConfigMap) {
status := parseOrchestrationStatus(cm.Data)
t.Logf("updating configmap %q status to failed", cm.Name)
status.Status = OrchestrationStatusFailed
s, _ := json.Marshal(status)
cm.Data[orchestrationStatusKey] = string(s)
}
func updateStatusConfigmap(clientset kubernetes.Interface, statusMapWatcher *watch.RaceFreeFakeWatcher, cm *corev1.ConfigMap) {
_, err := clientset.CoreV1().ConfigMaps(cm.Namespace).Update(context.TODO(), cm, metav1.UpdateOptions{})
if err != nil {
panic(err)
}
statusMapWatcher.Modify(cm)
}
+175 -406
View File
@@ -18,6 +18,7 @@ limitations under the License.
package osd
import (
"bufio"
"context"
"fmt"
"sort"
@@ -27,34 +28,25 @@ import (
"k8s.io/client-go/kubernetes"
"github.com/banzaicloud/k8s-objectmatcher/patch"
"github.com/coreos/pkg/capnslog"
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
rookv1 "github.com/rook/rook/pkg/apis/rook.io/v1"
"github.com/rook/rook/pkg/clusterd"
"github.com/rook/rook/pkg/daemon/ceph/client"
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
kms "github.com/rook/rook/pkg/daemon/ceph/osd/kms"
"github.com/rook/rook/pkg/operator/ceph/cluster/mon"
osdconfig "github.com/rook/rook/pkg/operator/ceph/cluster/osd/config"
opconfig "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"
apps "k8s.io/api/apps/v1"
batch "k8s.io/api/batch/v1"
"github.com/rook/rook/pkg/util"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/version"
)
var (
logger = capnslog.NewPackageLogger("github.com/rook/rook", "op-osd")
updateDeploymentAndWait = mon.UpdateCephDeploymentAndWait
cephVolumeRawEncryptionModeMinNautilusCephVersion = cephver.CephVersion{Major: 14, Minor: 2, Extra: 11}
cephVolumeRawEncryptionModeMinOctopusCephVersion = cephver.CephVersion{Major: 15, Minor: 2, Extra: 5}
)
@@ -130,12 +122,12 @@ type osdProperties struct {
//crushHostname refers to the hostname or PVC name when the OSD is provisioned on Nodes or PVC block device, respectively.
crushHostname string
devices []rookv1.Device
pvc v1.PersistentVolumeClaimVolumeSource
metadataPVC v1.PersistentVolumeClaimVolumeSource
walPVC v1.PersistentVolumeClaimVolumeSource
pvc corev1.PersistentVolumeClaimVolumeSource
metadataPVC corev1.PersistentVolumeClaimVolumeSource
walPVC corev1.PersistentVolumeClaimVolumeSource
pvcSize string
selection rookv1.Selection
resources v1.ResourceRequirements
resources corev1.ResourceRequirements
storeConfig osdconfig.StoreConfig
placement rookv1.Placement
preparePlacement *rookv1.Placement
@@ -171,14 +163,16 @@ func (osdProps osdProperties) getPreparePlacement() rookv1.Placement {
// Start the osd management
func (c *Cluster) Start() error {
namespace := c.clusterInfo.Namespace
config := c.newProvisionConfig()
errs := newProvisionErrors()
// Validate pod's memory if specified
err := controller.CheckPodMemory(cephv1.ResourcesKeyOSD, cephv1.GetOSDResources(c.spec.Resources), cephOsdPodMinimumMemory)
if err != nil {
return errors.Wrap(err, "failed to check pod memory")
}
logger.Infof("start running osds in namespace %s", c.clusterInfo.Namespace)
logger.Infof("start running osds in namespace %q", namespace)
if !c.spec.Storage.UseAllNodes && len(c.spec.Storage.Nodes) == 0 && len(c.spec.Storage.VolumeSources) == 0 && len(c.spec.Storage.StorageClassDeviceSets) == 0 {
logger.Warningf("useAllNodes is set to false and no nodes, storageClassDevicesets or volumeSources are specified, no OSD pods are going to be created")
@@ -191,29 +185,52 @@ func (c *Cluster) Start() error {
}
logger.Infof("wait timeout for healthy OSDs during upgrade or restart is %q", c.clusterInfo.OsdUpgradeTimeout)
// start the jobs to provision the OSD devices
logger.Info("start provisioning the osds on PVCs, if needed")
c.startProvisioningOverPVCs(config)
// prepare for updating existing OSDs
updateQueue, deployments, err := c.getOSDUpdateInfo(errs)
if err != nil {
return errors.Wrapf(err, "failed to get information about currently-running OSD Deployments in namespace %q", namespace)
}
logger.Debugf("%d of %d OSD Deployments need updated", updateQueue.Len(), deployments.Len())
updateConfig := c.newUpdateConfig(config, updateQueue, deployments)
if len(config.errorMessages) > 0 {
return errors.Errorf("%d failures encountered while running osds on PVCs in namespace %q. %v",
len(config.errorMessages), c.clusterInfo.Namespace, strings.Join(config.errorMessages, "\n"))
// prepare for creating new OSDs
statusConfigMaps := util.NewSet()
logger.Info("start provisioning the OSDs on PVCs, if needed")
pvcConfigMaps, err := c.startProvisioningOverPVCs(config, errs)
if err != nil {
return err
}
statusConfigMaps.AddSet(pvcConfigMaps)
logger.Info("start provisioning the OSDs on nodes, if needed")
nodeConfigMaps, err := c.startProvisioningOverNodes(config, errs)
if err != nil {
return err
}
statusConfigMaps.AddSet(nodeConfigMaps.Copy())
createConfig := c.newCreateConfig(config, statusConfigMaps, deployments)
// do the update and create operations
err = c.updateAndCreateOSDs(createConfig, updateConfig, errs)
if err != nil {
return errors.Wrapf(err, "failed to update/create OSDs")
}
logger.Info("start provisioning the osds on nodes, if needed")
c.startProvisioningOverNodes(config)
if len(config.errorMessages) > 0 {
return errors.Errorf("%d failures encountered while running osds on nodes in namespace %q. %v",
len(config.errorMessages), c.clusterInfo.Namespace, strings.Join(config.errorMessages, "\n"))
if errs.len() > 0 {
return errors.Errorf("%d failures encountered while running osds on nodes in namespace %q. %s",
errs.len(), namespace, errs.asMessages())
}
// clean up status configmaps that might be dangling from previous reconciles
// for example, if the storage spec changed from or a node failed in a previous failed reconcile
c.deleteAllStatusConfigMaps()
// The following block is used to apply any command(s) required by an upgrade
// The block below handles the upgrade from Mimic to Nautilus.
// This should only run before Octopus
c.applyUpgradeOSDFunctionality()
logger.Infof("completed running osds in namespace %s", c.clusterInfo.Namespace)
logger.Infof("finished running OSDs in namespace %q", namespace)
return nil
}
@@ -221,158 +238,7 @@ func (c *Cluster) shouldProvisionOverPVCs() bool {
return len(c.spec.Storage.VolumeSources) > 0 || len(c.spec.Storage.StorageClassDeviceSets) > 0
}
func (c *Cluster) startProvisioningOverPVCs(config *provisionConfig) {
// Parsing storageClassDeviceSets and parsing it to volume sources
c.spec.Storage.VolumeSources = append(c.spec.Storage.VolumeSources, c.prepareStorageClassDeviceSets(config)...)
c.ValidStorage.VolumeSources = c.spec.Storage.VolumeSources
// no validVolumeSource is ready to run an osd
if !c.shouldProvisionOverPVCs() {
logger.Info("no volume sources defined to configure OSDs on PVCs.")
return
}
// Check k8s version
k8sVersion, err := k8sutil.GetK8SVersion(c.context.Clientset)
if err != nil {
config.addError("error finding Kubernetes version. %v", err)
return
}
if !k8sVersion.AtLeast(version.MustParseSemantic("v1.13.0")) {
logger.Warningf("skipping OSD on PVC provisioning. Minimum Kubernetes version required: 1.13.0. Actual version: %s", k8sVersion.String())
return
}
existingDeployments, err := c.getExistingOSDDeploymentsOnPVCs()
if err != nil {
config.addError("failed to query existing OSD deployments on PVCs. %v", err)
return
}
for _, volume := range c.ValidStorage.VolumeSources {
// Check whether we need to cancel the orchestration
if err := controller.CheckForCancelledOrchestration(c.context); err != nil {
config.addError("%s", err.Error())
return
}
dataSource, dataOK := volume.PVCSources[bluestorePVCData]
// The data PVC template is required.
if !dataOK {
config.addError("failed to create osd for storageClassDeviceSet %q, missing the data template", volume.Name)
continue
}
metadataSource, metadataOK := volume.PVCSources[bluestorePVCMetadata]
if metadataOK {
logger.Infof("OSD will have its main bluestore block on %q and its metadata device on %q", dataSource.ClaimName, metadataSource.ClaimName)
} else {
logger.Infof("OSD will have its main bluestore block on %q", dataSource.ClaimName)
}
walSource, walOK := volume.PVCSources[bluestorePVCWal]
if walOK {
logger.Infof("OSD will have its wal device on %q", walSource.ClaimName)
}
osdProps := osdProperties{
crushHostname: dataSource.ClaimName,
pvc: dataSource,
metadataPVC: metadataSource,
walPVC: walSource,
resources: volume.Resources,
placement: volume.Placement,
preparePlacement: volume.PreparePlacement,
portable: volume.Portable,
schedulerName: volume.SchedulerName,
encrypted: volume.Encrypted,
deviceSetName: volume.Name,
}
osdProps.storeConfig.DeviceClass = volume.CrushDeviceClass
logger.Debugf("osdProps are %+v", osdProps)
if osdProps.encrypted {
// If the deviceSet template has "encrypted" but the Ceph version is not compatible
if !c.isCephVolumeRawModeSupported() {
errMsg := fmt.Sprintf("failed to validate storageClassDeviceSet %q. min required ceph version to support encryption is %q or %q", volume.Name, cephVolumeRawEncryptionModeMinNautilusCephVersion.String(), cephVolumeRawEncryptionModeMinOctopusCephVersion.String())
config.addError(errMsg)
continue
}
// create encryption Kubernetes Secret if the PVC is encrypted
key, err := generateDmCryptKey()
if err != nil {
errMsg := fmt.Sprintf("failed to generate dmcrypt key for osd claim %q. %v", osdProps.pvc.ClaimName, err)
config.addError(errMsg)
continue
}
// Initialize the KMS code
kmsConfig := kms.NewConfig(c.context, &c.spec, c.clusterInfo)
// We could set an env var in the Operator or a global var instead of the API call?
// Hopefully, the API is cheap and we can always retrieve the token if it has changed...
if c.spec.Security.KeyManagementService.IsTokenAuthEnabled() {
err := kms.SetTokenToEnvVar(c.context, c.spec.Security.KeyManagementService.TokenSecretName, kmsConfig.Provider, c.clusterInfo.Namespace)
if err != nil {
errMsg := fmt.Sprintf("failed to fetch kms token secret %q. %v", c.spec.Security.KeyManagementService.TokenSecretName, err)
config.addError(errMsg)
continue
}
}
// Generate and store the encrypted key in whatever KMS is configured
err = kmsConfig.PutSecret(osdProps.pvc.ClaimName, key)
if err != nil {
errMsg := fmt.Sprintf("failed to store secret. %v", err)
config.addError(errMsg)
continue
}
}
// Skip OSD prepare if deployment already exists for the PVC
if osdDeployment, ok := existingDeployments[dataSource.ClaimName]; ok {
logger.Infof("skip OSD prepare pod creation as OSD daemon already exists for %q", osdProps.crushHostname)
osds, err := c.getOSDInfo(osdDeployment)
if err != nil {
config.addError("failed to get osdInfo for pvc %q. %v", osdProps.crushHostname, err)
continue
}
// Update the orchestration status of this pvc to the completed state
status := OrchestrationStatus{OSDs: osds, Status: OrchestrationStatusAlreadyExists, PvcBackedOSD: true}
c.updateOSDStatus(osdProps.crushHostname, status)
continue
}
// Update the orchestration status of this pvc to the starting state
status := OrchestrationStatus{Status: OrchestrationStatusStarting, PvcBackedOSD: true}
c.updateOSDStatus(osdProps.crushHostname, status)
job, err := c.makeJob(osdProps, config)
if err != nil {
message := fmt.Sprintf("failed to create prepare job for pvc %s: %v", osdProps.crushHostname, err)
config.addError(message)
status := OrchestrationStatus{Status: OrchestrationStatusCompleted, Message: message, PvcBackedOSD: true}
c.updateOSDStatus(osdProps.crushHostname, status)
}
if !c.runJob(job, osdProps.crushHostname, config, "provision") {
status := OrchestrationStatus{
Status: OrchestrationStatusCompleted,
Message: fmt.Sprintf("failed to start osd provisioning on pvc %s", osdProps.crushHostname),
PvcBackedOSD: true,
}
c.updateOSDStatus(osdProps.crushHostname, status)
}
}
logger.Infof("start osds after provisioning is completed, if needed")
c.completeProvision(config)
}
func (c *Cluster) getExistingOSDDeploymentsOnPVCs() (map[string]*apps.Deployment, error) {
func (c *Cluster) getExistingOSDDeploymentsOnPVCs() (*util.Set, error) {
ctx := context.TODO()
listOpts := metav1.ListOptions{LabelSelector: fmt.Sprintf("%s=%s,%s", k8sutil.AppAttr, AppName, OSDOverPVCLabelKey)}
@@ -381,232 +247,46 @@ func (c *Cluster) getExistingOSDDeploymentsOnPVCs() (map[string]*apps.Deployment
return nil, errors.Wrap(err, "failed to query existing OSD deployments")
}
result := map[string]*apps.Deployment{}
for i, deployment := range deployments.Items {
result := util.NewSet()
for _, deployment := range deployments.Items {
if pvcID, ok := deployment.Labels[OSDOverPVCLabelKey]; ok {
result[pvcID] = &deployments.Items[i]
result.Add(pvcID)
}
}
return result, nil
}
func (c *Cluster) startProvisioningOverNodes(config *provisionConfig) {
if len(c.spec.DataDirHostPath) == 0 {
logger.Warningf("skipping osd provisioning where no dataDirHostPath is set")
return
func deploymentOnNode(c *Cluster, osd OSDInfo, nodeName string, config *provisionConfig) (*appsv1.Deployment, error) {
osdLongName := fmt.Sprintf("OSD %d on node %q", osd.ID, nodeName)
osdProps, err := c.getOSDPropsForNode(nodeName)
if err != nil {
return nil, errors.Wrapf(err, "failed to generate config for %s", osdLongName)
}
if c.spec.Storage.UseAllNodes {
if len(c.spec.Storage.Nodes) > 0 {
logger.Warningf("useAllNodes is TRUE, but nodes are specified. NODES in the cluster CR will be IGNORED unless useAllNodes is FALSE.")
}
// Get the list of all nodes in the cluster. The placement settings will be applied below.
hostnameMap, err := k8sutil.GetNodeHostNames(c.context.Clientset)
if err != nil {
config.addError("failed to get node hostnames: %v", err)
return
}
c.spec.Storage.Nodes = nil
for _, hostname := range hostnameMap {
storageNode := rookv1.Node{
Name: hostname,
}
c.spec.Storage.Nodes = append(c.spec.Storage.Nodes, storageNode)
}
logger.Debugf("storage nodes: %+v", c.spec.Storage.Nodes)
}
// generally speaking, this finds nodes which are capable of running new osds
validNodes := k8sutil.GetValidNodes(c.spec.Storage, c.context.Clientset, cephv1.GetOSDPlacement(c.spec.Placement))
logger.Infof("%d of the %d storage nodes are valid", len(validNodes), len(c.spec.Storage.Nodes))
c.ValidStorage = *c.spec.Storage.DeepCopy()
c.ValidStorage.Nodes = validNodes
// no valid node is ready to run an osd
if len(validNodes) == 0 {
logger.Warningf("no valid nodes available to run osds on nodes in namespace %s", c.clusterInfo.Namespace)
return
d, err := c.makeDeployment(osdProps, osd, config)
if err != nil {
return nil, errors.Wrapf(err, "failed to generate deployment for %s", osdLongName)
}
// start with nodes currently in the storage spec
for _, node := range c.ValidStorage.Nodes {
// Check whether we need to cancel the orchestration
if err := controller.CheckForCancelledOrchestration(c.context); err != nil {
config.addError("%s", err.Error())
return
}
// fully resolve the storage config and resources for this node
n := c.resolveNode(node.Name)
if n == nil {
logger.Warningf("node %s did not resolve", node.Name)
continue
}
if n.Name == "" {
logger.Warningf("skipping node with a blank name! %+v", n)
continue
}
// update the orchestration status of this node to the starting state
status := OrchestrationStatus{Status: OrchestrationStatusStarting}
c.updateOSDStatus(n.Name, status)
// create the job that prepares osds on the node
storeConfig := osdconfig.ToStoreConfig(n.Config)
metadataDevice := osdconfig.MetadataDevice(n.Config)
osdProps := osdProperties{
crushHostname: n.Name,
devices: n.Devices,
selection: n.Selection,
resources: n.Resources,
storeConfig: storeConfig,
metadataDevice: metadataDevice,
}
job, err := c.makeJob(osdProps, config)
if err != nil {
message := fmt.Sprintf("failed to create prepare job node %q. %v", n.Name, err)
config.addError(message)
status := OrchestrationStatus{Status: OrchestrationStatusCompleted, Message: message}
c.updateOSDStatus(n.Name, status)
}
if !c.runJob(job, n.Name, config, "provision") {
status := OrchestrationStatus{Status: OrchestrationStatusCompleted, Message: fmt.Sprintf("failed to start osd provisioning job on node %s", n.Name)}
c.updateOSDStatus(n.Name, status)
}
}
logger.Infof("start osds after provisioning is completed, if needed")
c.completeProvision(config)
return d, nil
}
func (c *Cluster) runJob(job *batch.Job, nodeName string, config *provisionConfig, action string) bool {
if err := k8sutil.RunReplaceableJob(c.context.Clientset, job, false); err != nil {
if !kerrors.IsAlreadyExists(err) {
// we failed to create job, update the orchestration status for this node
message := fmt.Sprintf("failed to create %q job for node %q. %v", action, nodeName, err)
c.handleOrchestrationFailure(config, nodeName, message)
return false
}
func deploymentOnPVC(c *Cluster, osd OSDInfo, pvcName string, config *provisionConfig) (*appsv1.Deployment, error) {
osdLongName := fmt.Sprintf("OSD %d on PVC %q", osd.ID, pvcName)
// the job is already in progress so we will let it run to completion
}
logger.Infof("osd %s job started for node %s", action, nodeName)
return true
}
func (c *Cluster) startOSDDaemonsOnPVC(pvcName string, config *provisionConfig, configMap *v1.ConfigMap, status *OrchestrationStatus) {
ctx := context.TODO()
osds := status.OSDs
logger.Infof("starting %d osd daemons on pvc %s", len(osds), pvcName)
osdProps, err := c.getOSDPropsForPVC(pvcName)
if err != nil {
config.addError(fmt.Sprintf("%v", err))
return
return nil, errors.Wrapf(err, "failed to generate config for %s", osdLongName)
}
// start osds
for _, osd := range osds {
logger.Debugf("start osd %v", osd)
dp, err := c.makeDeployment(osdProps, osd, config)
if err != nil {
errMsg := fmt.Sprintf("failed to create deployment for pvc %q. %v", osdProps.crushHostname, err)
config.addError(errMsg)
continue
}
// Set the deployment hash as an annotation
err = patch.DefaultAnnotator.SetLastAppliedAnnotation(dp)
if err != nil {
errMsg := fmt.Sprintf("failed to set annotation for deployment %q. %v", dp.Name, err)
config.addError(errMsg)
continue
}
_, createErr := c.context.Clientset.AppsV1().Deployments(c.clusterInfo.Namespace).Create(ctx, dp, metav1.CreateOptions{})
if createErr != nil {
if kerrors.IsAlreadyExists(createErr) {
logger.Infof("deployment for osd %d already exists. updating if needed", osd.ID)
if err = updateDeploymentAndWait(c.context, c.clusterInfo, dp, opconfig.OsdType, strconv.Itoa(osd.ID), c.spec.SkipUpgradeChecks, c.spec.ContinueUpgradeAfterChecksEvenIfNotHealthy); err != nil {
logger.Errorf("failed to update osd deployment %d. %v", osd.ID, err)
}
} else {
// we failed to create job, update the orchestration status for this pvc
logger.Warningf("failed to create osd deployment for pvc %q, osd %v. %v", osdProps.pvc.ClaimName, osd, createErr)
continue
}
}
logger.Infof("started deployment for osd %d on pvc", osd.ID)
}
}
func (c *Cluster) startOSDDaemonsOnNode(nodeName string, config *provisionConfig, configMap *v1.ConfigMap, status *OrchestrationStatus) {
ctx := context.TODO()
osds := status.OSDs
logger.Infof("starting %d osd daemons on node %s", len(osds), nodeName)
// fully resolve the storage config and resources for this node
n := c.resolveNode(nodeName)
if n == nil {
logger.Errorf("node %q did not resolve to start osds", nodeName)
return
}
storeConfig := osdconfig.ToStoreConfig(n.Config)
metadataDevice := osdconfig.MetadataDevice(n.Config)
osdProps := osdProperties{
crushHostname: n.Name,
devices: n.Devices,
selection: n.Selection,
resources: n.Resources,
storeConfig: storeConfig,
metadataDevice: metadataDevice,
d, err := c.makeDeployment(osdProps, osd, config)
if err != nil {
return nil, errors.Wrapf(err, "failed to generate deployment for %s", osdLongName)
}
// start osds
for _, osd := range osds {
logger.Debugf("start osd %v", osd)
message := fmt.Sprintf("Processing OSD %d on node %q", osd.ID, nodeName)
opcontroller.UpdateCondition(c.context, c.clusterInfo.NamespacedName(), cephv1.ConditionProgressing, v1.ConditionTrue, cephv1.ClusterProgressingReason, message)
dp, err := c.makeDeployment(osdProps, osd, config)
if err != nil {
errMsg := fmt.Sprintf("failed to create deployment for node %s: %v", n.Name, err)
config.addError(errMsg)
continue
}
// Set the deployment hash as an annotation
err = patch.DefaultAnnotator.SetLastAppliedAnnotation(dp)
if err != nil {
errMsg := fmt.Sprintf("failed to set annotation for deployment %q. %v", dp.Name, err)
config.addError(errMsg)
continue
}
_, createErr := c.context.Clientset.AppsV1().Deployments(c.clusterInfo.Namespace).Create(ctx, dp, metav1.CreateOptions{})
if createErr != nil {
if kerrors.IsAlreadyExists(createErr) {
logger.Debugf("deployment for osd %d already exists. updating if needed", osd.ID)
if err = updateDeploymentAndWait(c.context, c.clusterInfo, dp, opconfig.OsdType, strconv.Itoa(osd.ID), c.spec.SkipUpgradeChecks, c.spec.ContinueUpgradeAfterChecksEvenIfNotHealthy); err != nil {
logger.Errorf("failed to update osd deployment %d. %v", osd.ID, err)
}
} else {
// we failed to create job, update the orchestration status for this pvc
logger.Warningf("failed to create osd deployment for node %q, osd %+v. %v", n.Name, osd, createErr)
continue
}
} else {
logger.Infof("created deployment for osd %d", osd.ID)
}
}
return d, nil
}
func (c *Cluster) resolveNode(nodeName string) *rookv1.Node {
@@ -620,6 +300,27 @@ func (c *Cluster) resolveNode(nodeName string) *rookv1.Node {
return rookNode
}
func (c *Cluster) getOSDPropsForNode(nodeName string) (osdProperties, error) {
// fully resolve the storage config and resources for this node
n := c.resolveNode(nodeName)
if n == nil {
return osdProperties{}, errors.Errorf("failed to resolve node %q", nodeName)
}
storeConfig := osdconfig.ToStoreConfig(n.Config)
metadataDevice := osdconfig.MetadataDevice(n.Config)
osdProps := osdProperties{
crushHostname: n.Name,
devices: n.Devices,
selection: n.Selection,
resources: n.Resources,
storeConfig: storeConfig,
metadataDevice: metadataDevice,
}
return osdProps, nil
}
func (c *Cluster) getOSDPropsForPVC(pvcName string) (osdProperties, error) {
for _, volumeSource := range c.ValidStorage.VolumeSources {
// The data PVC template is required.
@@ -668,13 +369,13 @@ func (c *Cluster) getOSDPropsForPVC(pvcName string) (osdProperties, error) {
var err error
osdProps.crushHostname, err = c.getPVCHostName(pvcName)
if err != nil {
return osdProperties{}, errors.Wrapf(err, "Unable to get crushHostname of non portable pvc %s", pvcName)
return osdProperties{}, errors.Wrapf(err, "failed to get crushHostname of non-portable PVC %q", pvcName)
}
}
return osdProps, nil
}
}
return osdProperties{}, errors.Errorf("no valid VolumeSource found for pvc %s", pvcName)
return osdProperties{}, errors.Errorf("failed to find valid VolumeSource for PVC %q", pvcName)
}
// getPVCHostName finds the node where an OSD pod should be assigned with a node selector.
@@ -693,7 +394,7 @@ func (c *Cluster) getPVCHostName(pvcName string) (string, error) {
for _, d := range deployments.Items {
selectors := d.Spec.Template.Spec.NodeSelector
for label, value := range selectors {
if label == v1.LabelHostname {
if label == corev1.LabelHostname {
return value, nil
}
}
@@ -720,20 +421,34 @@ func (c *Cluster) getPVCHostName(pvcName string) (string, error) {
return "", errors.Errorf("node selector not found on deployment for osd with pvc %q", pvcName)
}
func (c *Cluster) getOSDInfo(d *apps.Deployment) ([]OSDInfo, error) {
func getOSDID(d *appsv1.Deployment) (int, error) {
osdID, err := strconv.Atoi(d.Labels[OsdIdLabelKey])
if err != nil {
// add a question to the user AFTER the error text to help them recover from user error
return -1, errors.Wrapf(err, "failed to parse label \"ceph-osd-id\" on deployment %q. did a user modify the deployment and remove the label?", d.Name)
}
return osdID, nil
}
func (c *Cluster) getOSDInfo(d *appsv1.Deployment) (OSDInfo, error) {
container := d.Spec.Template.Spec.Containers[0]
var osd OSDInfo
osdID, err := strconv.Atoi(d.Labels[OsdIdLabelKey])
osdID, err := getOSDID(d)
if err != nil {
return []OSDInfo{}, errors.Wrap(err, "error parsing ceph-osd-id")
return OSDInfo{}, err
}
osd.ID = osdID
isPVC := false
for _, envVar := range d.Spec.Template.Spec.Containers[0].Env {
if envVar.Name == "ROOK_OSD_UUID" {
osd.UUID = envVar.Value
}
if envVar.Name == "ROOK_PVC_BACKED_OSD" {
isPVC = true
}
if envVar.Name == "ROOK_BLOCK_PATH" || envVar.Name == "ROOK_LV_PATH" {
osd.BlockPath = envVar.Value
}
@@ -746,7 +461,7 @@ func (c *Cluster) getOSDInfo(d *apps.Deployment) ([]OSDInfo, error) {
if envVar.Name == "ROOK_LV_BACKED_PV" {
lvBackedPV, err := strconv.ParseBool(envVar.Value)
if err != nil {
return []OSDInfo{}, errors.Wrap(err, "error parsing ROOK_LV_BACKED_PV")
return OSDInfo{}, errors.Wrap(err, "failed to parse ROOK_LV_BACKED_PV")
}
osd.LVBackedPV = lvBackedPV
}
@@ -758,9 +473,19 @@ func (c *Cluster) getOSDInfo(d *apps.Deployment) ([]OSDInfo, error) {
}
}
// Needed for upgrade from v1.5 to v1.6. Rook v1.5 did not set ROOK_BLOCK_PATH for OSDs on nodes
// where the 'activate' init container was needed.
if !isPVC && osd.BlockPath == "" {
osd.BlockPath, err = getBlockPathFromActivateInitContainer(d)
if err != nil {
return OSDInfo{}, errors.Wrapf(err, "failed to extract legacy OSD block path from deployment %q", d.Name)
}
}
// If CVMode is empty, this likely means we upgraded Rook
// This property did not exist before so we need to initialize it
if osd.CVMode == "" {
logger.Infof("required CVMode for OSD %d was not found. assuming this is an LVM OSD", osd.ID)
osd.CVMode = "lvm"
}
@@ -776,7 +501,7 @@ func (c *Cluster) getOSDInfo(d *apps.Deployment) ([]OSDInfo, error) {
}
if !locationFound {
location, _, err := getLocationFromPod(c.context.Clientset, d, client.GetCrushRootFromSpec(&c.spec))
location, _, err := getLocationFromPod(c.context.Clientset, d, cephclient.GetCrushRootFromSpec(&c.spec))
if err != nil {
logger.Errorf("failed to get location. %v", err)
} else {
@@ -785,13 +510,59 @@ func (c *Cluster) getOSDInfo(d *apps.Deployment) ([]OSDInfo, error) {
}
if osd.UUID == "" || osd.BlockPath == "" {
return []OSDInfo{}, errors.Errorf("failed to get required osdInfo. %+v", osd)
return OSDInfo{}, errors.Errorf("failed to get required osdInfo. %+v", osd)
}
return []OSDInfo{osd}, nil
return osd, nil
}
func getLocationFromPod(clientset kubernetes.Interface, d *apps.Deployment, crushRoot string) (string, string, error) {
func osdIsOnPVC(d *appsv1.Deployment) bool {
if _, ok := d.Labels[OSDOverPVCLabelKey]; ok {
return true
}
return false
}
func getNodeOrPVCName(d *appsv1.Deployment) (string, error) {
if v, ok := d.Labels[OSDOverPVCLabelKey]; ok {
return v, nil // OSD is on PVC
}
for k, v := range d.Spec.Template.Spec.NodeSelector {
if k == corev1.LabelHostname {
return v, nil
}
}
return "", errors.Errorf("failed to find node/PVC name for OSD deployment %q: %+v", d.Name, d)
}
// Needed for upgrades from v1.5 to v1.6
func getBlockPathFromActivateInitContainer(d *appsv1.Deployment) (string, error) {
initContainers := d.Spec.Template.Spec.InitContainers
for _, c := range initContainers {
if c.Name != activatePVCOSDInitContainer {
continue
}
if len(c.Command) != 3 {
return "", errors.Errorf("activate init container has fewer command arguments (%d) than expected (3)", len(c.Command))
}
script := c.Command[2]
varAssignment := "DEVICE=" // this variable assignment is followed by the block path
scanner := bufio.NewScanner(strings.NewReader(script))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, varAssignment) {
device := strings.TrimPrefix(line, varAssignment)
return device, nil
}
}
if scanner.Err() != nil {
return "", errors.Wrapf(scanner.Err(), "failed to scan through activate init script for variable assignment %q", varAssignment)
}
}
return "", errors.Errorf("failed to find activate init container")
}
func getLocationFromPod(clientset kubernetes.Interface, d *appsv1.Deployment, crushRoot string) (string, string, error) {
ctx := context.TODO()
pods, err := clientset.CoreV1().Pods(d.Namespace).List(ctx, metav1.ListOptions{LabelSelector: fmt.Sprintf("%s=%s", OsdIdLabelKey, d.Labels[OsdIdLabelKey])})
if err != nil || len(pods.Items) == 0 {
@@ -817,7 +588,6 @@ func getLocationFromPod(clientset kubernetes.Interface, d *apps.Deployment, crus
// topologyAffinity: The label to be applied to the OSD daemon to guarantee it will start in the same
// topology as the OSD prepare job.
func GetLocationWithNode(clientset kubernetes.Interface, nodeName string, crushRoot, crushHostname string) (string, string, error) {
node, err := getNode(clientset, nodeName)
if err != nil {
return "", "", errors.Wrapf(err, "could not get the node for topology labels")
@@ -834,7 +604,7 @@ func GetLocationWithNode(clientset kubernetes.Interface, nodeName string, crushR
// Start with the host name in the CRUSH map
// Keep the fully qualified host name in the crush map, but replace the dots with dashes to satisfy ceph
hostName := client.NormalizeCrushName(crushHostname)
hostName := cephclient.NormalizeCrushName(crushHostname)
locArgs := []string{fmt.Sprintf("root=%s", crushRoot), fmt.Sprintf("host=%s", hostName)}
nodeLabels := node.GetLabels()
@@ -868,7 +638,6 @@ func getNode(clientset kubernetes.Interface, nodeName string) (*corev1.Node, err
}
func updateLocationWithNodeLabels(location *[]string, nodeLabels map[string]string) string {
topology, topologyAffinity := ExtractOSDTopologyFromLabels(nodeLabels)
keys := make([]string, 0, len(topology))
@@ -879,7 +648,7 @@ func updateLocationWithNodeLabels(location *[]string, nodeLabels map[string]stri
for _, topologyType := range keys {
if topologyType != "host" {
client.UpdateCrushMapValue(location, topologyType, topology[topologyType])
cephclient.UpdateCrushMapValue(location, topologyType, topology[topologyType])
}
}
return topologyAffinity
@@ -889,7 +658,7 @@ func (c *Cluster) applyUpgradeOSDFunctionality() {
var osdVersion *cephver.CephVersion
// Get all the daemons versions
versions, err := client.GetAllCephDaemonVersions(c.context, c.clusterInfo)
versions, err := cephclient.GetAllCephDaemonVersions(c.context, c.clusterInfo)
if err != nil {
logger.Warningf("failed to get ceph daemons versions; this likely means there are no osds yet. %v", err)
return
@@ -909,7 +678,7 @@ func (c *Cluster) applyUpgradeOSDFunctionality() {
}
// if the version of these OSDs is Octopus then we run the command
if osdVersion.IsOctopus() {
err = client.EnableReleaseOSDFunctionality(c.context, c.clusterInfo, "octopus")
err = cephclient.EnableReleaseOSDFunctionality(c.context, c.clusterInfo, "octopus")
if err != nil {
logger.Warningf("failed to enable new osd functionality. %v", err)
return
@@ -1,680 +0,0 @@
/*
Copyright 2021 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 osd
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/coreos/pkg/capnslog"
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
rookv1 "github.com/rook/rook/pkg/apis/rook.io/v1"
"github.com/rook/rook/pkg/clusterd"
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
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"
"github.com/tevino/abool"
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"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
)
// The definition for this test is a wrapper for the test function that adds a timeout
func TestOSDsOnPVC(t *testing.T) {
oldLogger := *logger
defer func() { logger = &oldLogger }() // reset logger to default after this test
logger.SetLevel(capnslog.TRACE) // want more log info for this test if it fails
oldOpportunisticDuration := osdOpportunisticUpdateDuration
defer func() { osdOpportunisticUpdateDuration = oldOpportunisticDuration }()
// lower the opportunistic update check duration for unit tests to speed them up
osdOpportunisticUpdateDuration = 1 * time.Millisecond
// This test runs in less than 150 milliseconds on a 6-core CPU w/ 16GB RAM
// GitHub CI runner can be a lot slower (~2.5 seconds)
done := make(chan bool)
timeout := time.After(50 * 150 * time.Millisecond)
go func() {
// use defer because t.Fatal will kill this goroutine, and we always want done set if the
// func stops running
defer func() { done <- true }()
// run the actual test
testOSDsOnPVC(t)
}()
select {
case <-timeout:
t.Fatal("Test timed out. This is a test failure.")
case <-done:
}
}
// This is the actual test. If it hangs, we should consider that an error. Writing a timeout in
// tests requires running the test in a goroutine, so there is a timeout wrapper above
func testOSDsOnPVC(t *testing.T) {
namespace := "osd-on-pvc"
ctx := context.TODO()
// we don't need to create actual nodes for this test, but this is the set of nodes which should
// we will use to create fake placements for OSD prepare job pods
osdIDGenerator := newOSDIDGenerator()
// set up a fake k8s client set and watcher to generate events that the operator will listen to
clientset := test.NewComplexClientset(t)
test.AddSomeReadyNodes(t, clientset, 3)
assignPodToNode := true
test.PrependComplexJobReactor(t, clientset, assignPodToNode)
test.SetFakeKubernetesVersion(clientset, "v1.13.2") // v1.13 or higher is required for OSDs on PVC
os.Setenv(k8sutil.PodNamespaceEnvVar, namespace)
defer os.Unsetenv(k8sutil.PodNamespaceEnvVar)
statusMapWatcher := watch.NewRaceFreeFake()
clientset.PrependWatchReactor("configmaps", k8stesting.DefaultWatchReactor(statusMapWatcher, nil))
// Helper methods to set "completed" status on "starting" ConfigMaps.
setStatusConfigMapToCompleted := func(cm *corev1.ConfigMap) {
// can't use mockNodeOrchestrationCompleted here b/c it uses a context.CoreV1() method that
// is mutex locked when a reactor is processing
status := parseOrchestrationStatus(cm.Data)
t.Logf("configmap reactor: updating configmap %q status to completed", cm.Name)
// configmap names are deterministic can be mapped indirectly to an OSD ID, and since the
// configmaps are used to report completion status of OSD provisioning, we use this property in
// thse unit tests
osdID := osdIDGenerator.osdID(t, cm.Name)
status.Status = OrchestrationStatusCompleted
status.PvcBackedOSD = true
status.OSDs = []OSDInfo{
{
ID: osdID,
UUID: fmt.Sprintf("%032d", osdID),
BlockPath: "/dev/path/to/block",
},
}
s, _ := json.Marshal(status)
cm.Data[orchestrationStatusKey] = string(s)
}
createConfigMapWithStatusStartingCallback := func(cm *corev1.ConfigMap) {
// placeholder to be defined later in tests
}
deleteConfigMapWithStatusAlreadyExistsCallback := func(cm *corev1.ConfigMap, action k8stesting.DeleteActionImpl) {
// placeholder to be defined later in tests
}
var cmReactor k8stesting.ReactionFunc = func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
switch action := action.(type) {
case k8stesting.CreateActionImpl:
obj := action.GetObject()
cm, ok := obj.(*corev1.ConfigMap)
if !ok {
t.Fatal("err! action not a configmap")
}
status := parseOrchestrationStatus(cm.Data)
if status.Status == OrchestrationStatusStarting {
// allow tests to specify a custom callback for this case
createConfigMapWithStatusStartingCallback(cm)
}
case k8stesting.DeleteActionImpl:
// get the CM being deleted to figure out some info about it
obj, err := clientset.Tracker().Get(action.GetResource(), action.GetNamespace(), action.Name)
if err != nil {
t.Fatalf("err! could not get configmap %q", action.Name)
}
cm, ok := obj.(*corev1.ConfigMap)
if !ok {
t.Fatal("err! action not a configmap")
}
status := parseOrchestrationStatus(cm.Data)
if status.Status == OrchestrationStatusAlreadyExists {
t.Logf("configmap reactor: delete: OSD for configmap %q was updated", cm.Name)
// allow tests to specify a custom callback for this case
deleteConfigMapWithStatusAlreadyExistsCallback(cm, action)
} else if status.Status == OrchestrationStatusCompleted {
t.Logf("configmap reactor: delete: OSD for configmap %q was created", cm.Name)
}
}
// modify it in-place and allow it to be created later with these changes
return false, nil, nil
}
clientset.PrependReactor("*", "configmaps", cmReactor)
deploymentOps := newResourceOperationList()
// make a very simple reactor to record when deployments were created
var deploymentReactor k8stesting.ReactionFunc = func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
createAction, ok := action.(k8stesting.CreateAction)
if !ok {
t.Fatal("err! action is not a create action")
return false, nil, nil
}
obj := createAction.GetObject()
d, ok := obj.(*appsv1.Deployment)
if !ok {
t.Fatal("err! action not a deployment")
return false, nil, nil
}
if o, _ := clientset.Tracker().Get(action.GetResource(), d.Namespace, d.Name); o != nil {
// deployment already exists, so this isn't be a valid create
return false, nil, nil
}
t.Logf("creating deployment %q", d.Name)
deploymentOps.Add(d.Name, "create")
return false, nil, nil
}
clientset.PrependReactor("create", "deployments", deploymentReactor)
// patch the updateDeploymentAndWait function to always report success and record when
// deployments are updated
oldUDAW := updateDeploymentAndWait
defer func() {
updateDeploymentAndWait = oldUDAW
}()
updateDeploymentAndWait = func(context *clusterd.Context, clusterInfo *cephclient.ClusterInfo, deployment *appsv1.Deployment, daemonType string, daemonName string, skipUpgradeChecks bool, continueUpgradeAfterChecksEvenIfNotHealthy bool) error {
t.Logf("updating deployment %q", deployment.Name)
deploymentOps.Add(deployment.Name, "update")
return nil
}
// wait for a number of deployments to be updated
waitForDeploymentOps := func(count int) {
for {
if deploymentOps.Len() >= count {
return
}
<-time.After(1 * time.Millisecond)
}
}
clusterInfo := &cephclient.ClusterInfo{
Namespace: namespace,
CephVersion: cephver.Nautilus,
}
clusterInfo.SetName("mycluster")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
executor := osdPVCTestExecutor(t, clientset, namespace)
context := &clusterd.Context{Clientset: clientset, ConfigDir: "/var/lib/rook", Executor: executor, RequestCancelOrchestration: abool.New()}
storageClassName := "test-storage-class"
volumeMode := corev1.PersistentVolumeBlock
spec := cephv1.ClusterSpec{
CephVersion: cephv1.CephVersionSpec{
Image: "ceph/ceph:v14.2.2",
},
DataDirHostPath: context.ConfigDir,
Storage: rookv1.StorageScopeSpec{
StorageClassDeviceSets: []rookv1.StorageClassDeviceSet{
{
Name: "set1",
Count: 5,
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: "data",
},
Spec: corev1.PersistentVolumeClaimSpec{
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: resource.MustParse("10Gi"),
},
},
StorageClassName: &storageClassName,
VolumeMode: &volumeMode,
AccessModes: []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteOnce,
},
},
},
},
},
},
},
}
// =============================================================================================
t.Log("Step 1: create new PVCs")
// when creating new configmaps with status "starting", simulate them becoming "completed"
// before any opportunistic updates can happen by changing the status to "completed" before
// the config map gets created by the fake k8s clientset.
createConfigMapWithStatusStartingCallback = func(cm *corev1.ConfigMap) {
setStatusConfigMapToCompleted(cm)
}
deleteConfigMapWithStatusAlreadyExistsCallback = func(cm *corev1.ConfigMap, action k8stesting.DeleteActionImpl) {
// do nothing on CM deletes
}
var c *Cluster
var provisionDone bool = false
waitForDone := func() {
for {
if provisionDone {
t.Log("provisioning done")
break
}
t.Log("provisioning not done. waiting...")
time.Sleep(time.Millisecond)
}
}
run := func() {
// reset
deploymentOps = newResourceOperationList()
statusMapWatcher.Reset()
// kick off the start of the orchestration in a goroutine so we can watch the results
// and manipulate confimaps in the test if needed
c = New(context, clusterInfo, spec, "myversion")
provisionDone = false
go func() {
provisionConfig := c.newProvisionConfig()
c.startProvisioningOverPVCs(provisionConfig)
provisionDone = true
}()
}
run()
numExpectedPVCs := 5
waitForNumPVCs(t, clientset, namespace, numExpectedPVCs)
waitForNumDeployments(t, clientset, namespace, numExpectedPVCs)
// 5 deployments should have been created
assert.Equal(t, 5, deploymentOps.Len())
// all 5 should be create operations
assert.Len(t, deploymentOps.ResourcesWithOperation("create"), 5)
t.Log("deployments successfully created for new PVCs")
waitForDone()
// =============================================================================================
t.Log("Step 2: verify deployments are updated when run again")
// clean the create times maps
reset := func() {
deploymentOps = newResourceOperationList()
// fake 'watcher' can close the channel for long tests, so reset when we can
statusMapWatcher.Reset()
}
reset()
run()
waitForNumPVCs(t, clientset, namespace, numExpectedPVCs)
// 5 deployments should have been operated on
waitForDeploymentOps(numExpectedPVCs)
// all 5 should be update operations
updatedDeployments := deploymentOps.ResourcesWithOperation("update")
assert.Len(t, updatedDeployments, 5)
// use later to ensure existing deployments are updated
existingDeployments := updatedDeployments
waitForDone()
// =============================================================================================
t.Log("Step 3: verify new deployments are created before existing ones are updated")
reset()
spec.Storage.StorageClassDeviceSets[0].Count = 8
numExpectedPVCs = 8
run()
waitForNumPVCs(t, clientset, namespace, numExpectedPVCs)
waitForNumDeployments(t, clientset, namespace, numExpectedPVCs)
waitForDeploymentOps(numExpectedPVCs)
// the same deployments from before should be updated here also
updatedDeployments = deploymentOps.ResourcesWithOperation("update")
assert.Len(t, updatedDeployments, 5)
assert.ElementsMatch(t, existingDeployments, updatedDeployments)
createdDeployments := deploymentOps.ResourcesWithOperation("create")
assert.Len(t, createdDeployments, 3)
for i, do := range deploymentOps.List() {
if i < 3 {
// first 3 ops should be create ops
assert.Equal(t, "create", do.operation)
} else {
// final 5 ops should be update ops
assert.Equal(t, "update", do.operation)
}
}
existingDeployments = append(createdDeployments, updatedDeployments...)
waitForDone()
// =============================================================================================
t.Log("Step 4: verify updates can happen opportunistically")
reset()
spec.Storage.StorageClassDeviceSets[0].Count = 10
numExpectedPVCs = 10
// In this test we carefully control the configmaps. When a configmap with status
// "alreadyExisting" is deleted, we know an OSD deployment just finished updating. We then
// immediately set one of the configmaps in "starting" state to "completed" so that it should
// be the next status configmap to be processed; a new OSD should be created for it. We
// therefore know that the first operation should be an update and the second a create. Then on
// in update-then-create fashion until all creates are done, followed by all updates.
configMapsThatNeedUpdatedToCompleted := []string{}
createConfigMapWithStatusStartingCallback = func(cm *corev1.ConfigMap) {
t.Logf("configmap reactor: create: marking that configmap %q needs to be completed later", cm.Name)
configMapsThatNeedUpdatedToCompleted = append(configMapsThatNeedUpdatedToCompleted, cm.Name)
}
deleteConfigMapWithStatusAlreadyExistsCallback = func(cm *corev1.ConfigMap, action k8stesting.DeleteActionImpl) {
if len(configMapsThatNeedUpdatedToCompleted) > 0 {
cmName := configMapsThatNeedUpdatedToCompleted[0]
obj, err := clientset.Tracker().Get(action.GetResource(), action.GetNamespace(), cmName)
if err != nil {
t.Fatalf("err! could not get configmap %q", cmName)
}
cm, ok := obj.(*corev1.ConfigMap)
if !ok {
t.Fatal("err! action not a configmap")
}
setStatusConfigMapToCompleted(cm)
err = clientset.Tracker().Update(action.GetResource(), cm, action.GetNamespace())
if err != nil {
t.Fatalf("err! failed to update configmap to completed. %v", err)
}
statusMapWatcher.Modify(cm) // MUST inform the fake watcher we made a change
configMapsThatNeedUpdatedToCompleted = configMapsThatNeedUpdatedToCompleted[1:]
}
}
run()
waitForNumPVCs(t, clientset, namespace, numExpectedPVCs)
waitForNumDeployments(t, clientset, namespace, numExpectedPVCs)
waitForDeploymentOps(numExpectedPVCs)
updatedDeployments = deploymentOps.ResourcesWithOperation("update")
assert.Len(t, updatedDeployments, 8)
assert.ElementsMatch(t, existingDeployments, updatedDeployments)
createdDeployments = deploymentOps.ResourcesWithOperation("create")
assert.Len(t, createdDeployments, 2)
assert.Equal(t,
[]string{
"update",
"create",
"update",
"create",
"update",
"update",
"update",
"update",
"update",
"update",
}, deploymentOps.OperationsInOrder())
existingDeployments = append(createdDeployments, updatedDeployments...)
waitForDone()
// =============================================================================================
t.Log("Step 5: verify opportunistic updates can all happen before creates")
reset()
spec.Storage.StorageClassDeviceSets[0].Count = 12
numExpectedPVCs = 12
// In this test, we stop all configmaps from being moved from "starting" to "completed" status
// in the configmap reactor so that all opportunistic updates should happen before new OSDs
// get created.
configMapsThatNeedUpdatedToCompleted = []string{}
createConfigMapWithStatusStartingCallback = func(cm *corev1.ConfigMap) {
// re-define this behavior as a reminder for readers of the test
t.Logf("configmap reactor: create: marking that configmap %q needs to be completed later", cm.Name)
configMapsThatNeedUpdatedToCompleted = append(configMapsThatNeedUpdatedToCompleted, cm.Name)
}
deleteConfigMapWithStatusAlreadyExistsCallback = func(cm *corev1.ConfigMap, action k8stesting.DeleteActionImpl) {
// do NOT automatically move configmaps from "starting" to "completed"
}
run()
waitForDeploymentOps(10) // wait for 10 updates
updatedDeployments = deploymentOps.ResourcesWithOperation("update")
assert.Len(t, updatedDeployments, 10)
assert.ElementsMatch(t, existingDeployments, updatedDeployments)
// update configmaps from "starting" to "completed"
for _, cmName := range configMapsThatNeedUpdatedToCompleted {
cm, err := clientset.CoreV1().ConfigMaps(namespace).Get(ctx, cmName, metav1.GetOptions{})
assert.NoError(t, err)
setStatusConfigMapToCompleted(cm)
cm, err = clientset.CoreV1().ConfigMaps(namespace).Update(ctx, cm, metav1.UpdateOptions{})
assert.NoError(t, err)
statusMapWatcher.Modify(cm) // MUST inform the fake watcher we made a change
}
waitForNumPVCs(t, clientset, namespace, numExpectedPVCs)
waitForNumDeployments(t, clientset, namespace, numExpectedPVCs)
waitForDeploymentOps(numExpectedPVCs)
// should be 2 more create operations
createdDeployments = deploymentOps.ResourcesWithOperation("create")
assert.Len(t, createdDeployments, 2)
for i, do := range deploymentOps.List() {
if i < 10 {
// first 10 ops should be update ops
assert.Equal(t, "update", do.operation)
} else {
// final 2 ops should be update ops
assert.Equal(t, "create", do.operation)
}
}
waitForDone()
t.Log("success")
}
/*
* mock executor to handle ceph commands
*/
func osdPVCTestExecutor(t *testing.T, clientset *fake.Clientset, namespace string) *exectest.MockExecutor {
return &exectest.MockExecutor{
MockExecuteCommandWithOutputFile: func(command string, outFileArg string, args ...string) (string, error) {
t.Logf("command: %s %v", command, args)
if command != "ceph" {
return "", errors.Errorf("unexpected command %q with args %v", command, args)
}
if args[0] == "auth" {
if args[1] == "get-or-create-key" {
return "{\"key\": \"does-not-matter\"}", nil
}
}
if args[0] == "osd" {
if args[1] == "ok-to-stop" {
return "", nil // no need to return text, only output status is used
}
if args[1] == "ls" {
// ceph osd ls returns an array of osd IDs like [0,1,2]
// build this based on the number of deployments since they should be equal
// for this test
l, err := clientset.AppsV1().Deployments(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
t.Fatalf("failed to build 'ceph osd ls' output. %v", err)
}
num := len(l.Items)
a := []string{}
for i := 0; i < num; i++ {
a = append(a, strconv.Itoa(i))
}
return fmt.Sprintf("[%s]", strings.Join(a, ",")), nil
}
if args[1] == "tree" {
return `{"nodes":[{"id":-1,"name":"default","type":"root","type_id":11,"children":[-3]},{"id":-3,"name":"master","type":"host","type_id":1,"pool_weights":{},"children":[2,1,0]},{"id":0,"device_class":"hdd","name":"osd.0","type":"osd","type_id":0,"crush_weight":0.009796142578125,"depth":2,"pool_weights":{},"exists":1,"status":"up","reweight":1,"primary_affinity":1},{"id":1,"device_class":"hdd","name":"osd.1","type":"osd","type_id":0,"crush_weight":0.009796142578125,"depth":2,"pool_weights":{},"exists":1,"status":"up","reweight":1,"primary_affinity":1},{"id":2,"device_class":"hdd","name":"osd.2","type":"osd","type_id":0,"crush_weight":0.009796142578125,"depth":2,"pool_weights":{},"exists":1,"status":"up","reweight":1,"primary_affinity":1}],"stray":[]}`, nil
}
}
if args[0] == "versions" {
// the update deploy code only cares about the mons from the ceph version command results
v := `{"mon":{"ceph version 14.2.2 (somehash) nautilus (stable)":3}}`
return v, nil
}
return "", errors.Errorf("unexpected ceph command %q", args)
},
}
}
/*
* basic helper functions
*/
// node names for OSDs on PVC end up being the name of the PVC
func waitForNumPVCs(t *testing.T, clientset *fake.Clientset, namespace string, count int) {
for {
l, err := clientset.CoreV1().PersistentVolumeClaims(namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
if len(l.Items) >= count {
t.Log("PVCs for OSDs on PVC all exist")
break
}
<-time.After(1 * time.Millisecond)
}
}
func waitForNumDeployments(t *testing.T, clientset *fake.Clientset, namespace string, count int) {
for {
l, err := clientset.AppsV1().Deployments(namespace).List(context.TODO(), metav1.ListOptions{})
assert.NoError(t, err)
if len(l.Items) >= count {
t.Log("Deployments for OSDs on PVC all exist")
break
}
<-time.After(1 * time.Millisecond)
}
}
/*
* Unique and consistent OSD ID generator
*/
type osdIDGenerator struct {
nextOSDID int
osdIDMap map[string]int
}
func newOSDIDGenerator() osdIDGenerator {
return osdIDGenerator{
nextOSDID: 0,
osdIDMap: map[string]int{},
}
}
func (g *osdIDGenerator) osdID(t *testing.T, namedResource string) int {
if id, ok := g.osdIDMap[namedResource]; ok {
t.Logf("resource %q has existing OSD ID %d", namedResource, id)
return id
}
id := g.nextOSDID
g.osdIDMap[namedResource] = id
g.nextOSDID++
t.Logf("generated new OSD ID %d for resource %q", id, namedResource)
return id
}
/*
* resourceOperationList
* We want to keep track of the order in which some resources (notably deployments) are created and
* updated, and the tracker should be thread safe since there could be operations occurring in
* parallel.
*/
type resourceOperation struct {
resourceName string
operation string // e.g., "create", "update"
}
func newResourceOperation(resourceName, operation string) resourceOperation {
return resourceOperation{resourceName, operation}
}
type resourceOperationList struct {
sync.Mutex
resourceOps []resourceOperation
}
func newResourceOperationList() *resourceOperationList {
return &resourceOperationList{
sync.Mutex{},
[]resourceOperation{},
}
}
func (r *resourceOperationList) Add(resourceName, operation string) {
r.Lock()
defer r.Unlock()
r.resourceOps = append(r.resourceOps, newResourceOperation(resourceName, operation))
}
func (r *resourceOperationList) Len() int {
r.Lock()
defer r.Unlock()
return len(r.resourceOps)
}
func (r *resourceOperationList) List() []resourceOperation {
return r.resourceOps
}
// Return only the resources which have a given operation
func (r *resourceOperationList) ResourcesWithOperation(operation string) []string {
resources := []string{}
for _, ro := range r.List() {
if ro.operation == operation {
resources = append(resources, ro.resourceName)
}
}
return resources
}
// Return all operations in order without resource names
func (r *resourceOperationList) OperationsInOrder() []string {
ops := []string{}
for _, ro := range r.List() {
ops = append(ops, ro.operation)
}
return ops
}
+137 -79
View File
@@ -28,19 +28,22 @@ import (
"github.com/rook/rook/pkg/clusterd"
"github.com/rook/rook/pkg/daemon/ceph/client"
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
cephclientfake "github.com/rook/rook/pkg/daemon/ceph/client/fake"
discoverDaemon "github.com/rook/rook/pkg/daemon/discover"
"github.com/rook/rook/pkg/operator/ceph/cluster/osd/config"
opconfig "github.com/rook/rook/pkg/operator/ceph/config"
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"
"github.com/tevino/abool"
apps "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
@@ -48,10 +51,10 @@ import (
func TestOSDProperties(t *testing.T) {
osdProps := []osdProperties{
{pvc: v1.PersistentVolumeClaimVolumeSource{ClaimName: "claim"},
metadataPVC: v1.PersistentVolumeClaimVolumeSource{ClaimName: "claim"}},
{pvc: v1.PersistentVolumeClaimVolumeSource{ClaimName: ""},
metadataPVC: v1.PersistentVolumeClaimVolumeSource{ClaimName: ""}},
{pvc: corev1.PersistentVolumeClaimVolumeSource{ClaimName: "claim"},
metadataPVC: corev1.PersistentVolumeClaimVolumeSource{ClaimName: "claim"}},
{pvc: corev1.PersistentVolumeClaimVolumeSource{ClaimName: ""},
metadataPVC: corev1.PersistentVolumeClaimVolumeSource{ClaimName: ""}},
}
expected := [][2]bool{
{true, true},
@@ -86,7 +89,7 @@ func createDiscoverConfigmap(nodeName, ns string, clientset *fake.Clientset) err
ctx := context.TODO()
data := make(map[string]string, 1)
data[discoverDaemon.LocalDiskCMData] = `[{"name":"sdx","parent":"","hasChildren":false,"devLinks":"/dev/disk/by-id/scsi-36001405f826bd553d8c4dbf9f41c18be /dev/disk/by-id/wwn-0x6001405f826bd553d8c4dbf9f41c18be /dev/disk/by-path/ip-127.0.0.1:3260-iscsi-iqn.2016-06.world.srv:storage.target01-lun-1","size":10737418240,"uuid":"","serial":"36001405f826bd553d8c4dbf9f41c18be","type":"disk","rotational":true,"readOnly":false,"ownPartition":true,"filesystem":"","vendor":"LIO-ORG","model":"disk02","wwn":"0x6001405f826bd553","wwnVendorExtension":"0x6001405f826bd553d8c4dbf9f41c18be","empty":true}]`
cm := &v1.ConfigMap{
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "local-device-" + nodeName,
Namespace: ns,
@@ -101,16 +104,16 @@ func createDiscoverConfigmap(nodeName, ns string, clientset *fake.Clientset) err
return err
}
func createNode(nodeName string, condition v1.NodeConditionType, clientset *fake.Clientset) error {
func createNode(nodeName string, condition corev1.NodeConditionType, clientset *fake.Clientset) error {
ctx := context.TODO()
node := &v1.Node{
node := &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: nodeName,
},
Status: v1.NodeStatus{
Conditions: []v1.NodeCondition{
Status: corev1.NodeStatus{
Conditions: []corev1.NodeCondition{
{
Type: condition, Status: v1.ConditionTrue,
Type: condition, Status: corev1.ConditionTrue,
},
},
},
@@ -121,16 +124,26 @@ func createNode(nodeName string, condition v1.NodeConditionType, clientset *fake
func TestAddRemoveNode(t *testing.T) {
ctx := context.TODO()
namespace := "ns-add-remove"
// create a storage spec with the given nodes/devices/dirs
nodeName := "node23"
oldConditionExportFunc := updateConditionFunc
defer func() {
updateConditionFunc = oldConditionExportFunc
}()
// stub out the conditionExportFunc to do nothing. we do not have a fake Rook interface that
// allows us to interact with a CephCluster resource like the fake K8s clientset.
updateConditionFunc = func(c *clusterd.Context, namespaceName types.NamespacedName, conditionType cephv1.ConditionType, status corev1.ConditionStatus, reason cephv1.ClusterReasonType, message string) {
// do nothing
}
// set up a fake k8s client set and watcher to generate events that the operator will listen to
clientset := fake.NewSimpleClientset()
os.Setenv(k8sutil.PodNamespaceEnvVar, "rook-system")
defer os.Unsetenv(k8sutil.PodNamespaceEnvVar)
nodeErr := createNode(nodeName, v1.NodeReady, clientset)
assert.Nil(t, nodeErr)
test.AddReadyNode(t, clientset, nodeName, "23.23.23.23")
cmErr := createDiscoverConfigmap(nodeName, "rook-system", clientset)
assert.Nil(t, cmErr)
@@ -138,7 +151,7 @@ func TestAddRemoveNode(t *testing.T) {
clientset.PrependWatchReactor("configmaps", k8stesting.DefaultWatchReactor(statusMapWatcher, nil))
clusterInfo := &cephclient.ClusterInfo{
Namespace: "ns-add-remove",
Namespace: namespace,
CephVersion: cephver.Nautilus,
}
clusterInfo.SetName("rook-ceph-test")
@@ -180,21 +193,20 @@ func TestAddRemoveNode(t *testing.T) {
startCompleted = true
}()
// simulate the completion of the nodes orchestration
mockNodeOrchestrationCompletion(c, nodeName, statusMapWatcher)
// wait for orchestration to complete
waitForOrchestrationCompletion(c, nodeName, &startCompleted)
// verify orchestration for adding the node succeeded
assert.True(t, startCompleted)
assert.Nil(t, startErr)
assert.NoError(t, startErr)
_, err := clientset.AppsV1().Deployments(namespace).Get(ctx, deploymentName(1), metav1.GetOptions{})
assert.NoError(t, err)
// simulate the OSD pod having been created
osdPod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{
Name: "osdPod",
Labels: map[string]string{k8sutil.AppAttr: AppName}}}
_, err := c.context.Clientset.CoreV1().Pods(c.clusterInfo.Namespace).Create(ctx, osdPod, metav1.CreateOptions{})
_, err = c.context.Clientset.CoreV1().Pods(c.clusterInfo.Namespace).Create(ctx, osdPod, metav1.CreateOptions{})
assert.NoError(t, err)
// mock the ceph calls that will be called during remove node
@@ -206,7 +218,14 @@ func TestAddRemoveNode(t *testing.T) {
}
if args[0] == "osd" {
if args[1] == "df" {
return `{"nodes":[{"id":0,"name":"osd.0","kb_used":0},{"id":1,"name":"osd.1","kb_used":0}]}`, nil
return `{"nodes":[{"id":1,"name":"osd.1","kb_used":0}]}`, nil
}
if args[1] == "dump" {
// OSD 1 is down and out
return `{"OSDs": [{"OSD": 1, "Up": 0, "In": 0}]}`, nil
}
if args[1] == "safe-to-destroy" {
return `{"safe_to_destroy":[1],"active":[],"missing_stats":[],"stored_pgs":[]}`, nil
}
if args[1] == "set" {
return "", nil
@@ -232,6 +251,9 @@ func TestAddRemoveNode(t *testing.T) {
if args[1] == "find" {
return `{"crush_location":{"host":"my-host"}}`, nil
}
if args[1] == "ok-to-stop" {
return cephclientfake.OsdOkToStopOutput(1, []int{1}, true), nil
}
}
if args[0] == "df" && args[1] == "detail" {
return `{"stats":{"total_bytes":0,"total_used_bytes":0,"total_avail_bytes":3072}}`, nil
@@ -255,7 +277,6 @@ func TestAddRemoveNode(t *testing.T) {
statusMapWatcher = watch.NewFake()
clientset.PrependWatchReactor("configmaps", k8stesting.DefaultWatchReactor(statusMapWatcher, nil))
// kick off the start of the removal orchestration in a goroutine
startErr = nil
startCompleted = false
go func() {
@@ -263,15 +284,22 @@ func TestAddRemoveNode(t *testing.T) {
startCompleted = true
}()
// simulate the completion of the nodes orchestration
mockNodeOrchestrationCompletion(c, nodeName, statusMapWatcher)
// wait for orchestration to complete
waitForOrchestrationCompletion(c, nodeName, &startCompleted)
// verify orchestration for removing the node succeeded
assert.True(t, startCompleted)
assert.Nil(t, startErr)
assert.NoError(t, startErr)
// deployment should still exist; OSDs are removed by health monitor code only if they are down,
// out, and the user has set removeOSDsIfOutAndSafeToRemove
_, err = clientset.AppsV1().Deployments(namespace).Get(ctx, deploymentName(1), metav1.GetOptions{})
assert.NoError(t, err)
removeIfOutAndSafeToRemove := true
healthMon := NewOSDHealthMonitor(context, client.AdminClusterInfo(namespace), removeIfOutAndSafeToRemove, cephv1.CephClusterHealthCheckSpec{})
healthMon.checkOSDHealth()
_, err = clientset.AppsV1().Deployments(namespace).Get(ctx, deploymentName(1), metav1.GetOptions{})
assert.True(t, k8serrors.IsNotFound(err))
}
func TestAddNodeFailure(t *testing.T) {
@@ -283,7 +311,7 @@ func TestAddNodeFailure(t *testing.T) {
clientset.PrependReactor("create", "jobs", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, errors.New("mock failed to create jobs")
})
nodeErr := createNode(nodeName, v1.NodeReady, clientset)
nodeErr := createNode(nodeName, corev1.NodeReady, clientset)
assert.Nil(t, nodeErr)
os.Setenv(k8sutil.PodNamespaceEnvVar, "rook-system")
@@ -333,7 +361,9 @@ func TestAddNodeFailure(t *testing.T) {
func TestGetPVCHostName(t *testing.T) {
ctx := context.TODO()
clientset := fake.NewSimpleClientset()
clusterInfo := &client.ClusterInfo{Namespace: "ns"}
clusterInfo := &cephclient.ClusterInfo{Namespace: "ns"}
clusterInfo.SetName("mycluster")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
c := &Cluster{context: &clusterd.Context{Clientset: clientset}, clusterInfo: clusterInfo}
osdInfo := OSDInfo{ID: 23}
pvcName := "test-pvc"
@@ -352,7 +382,7 @@ func TestGetPVCHostName(t *testing.T) {
},
}
k8sutil.AddLabelToDeployment(OSDOverPVCLabelKey, pvcName, osdDeployment)
osdDeployment.Spec.Template.Spec.NodeSelector = map[string]string{v1.LabelHostname: "testnode"}
osdDeployment.Spec.Template.Spec.NodeSelector = map[string]string{corev1.LabelHostname: "testnode"}
_, err = clientset.AppsV1().Deployments(c.clusterInfo.Namespace).Create(ctx, osdDeployment, metav1.CreateOptions{})
assert.NoError(t, err)
@@ -365,7 +395,7 @@ func TestGetPVCHostName(t *testing.T) {
// delete the deployment and get the host name based on the pod
err = clientset.AppsV1().Deployments(c.clusterInfo.Namespace).Delete(ctx, osdDeployment.Name, metav1.DeleteOptions{})
assert.NoError(t, err)
osdPod := &v1.Pod{
osdPod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "osd-23",
Namespace: c.clusterInfo.Namespace,
@@ -394,38 +424,69 @@ func TestGetOSDInfo(t *testing.T) {
location := "root=default host=myhost zone=myzone"
osd1 := OSDInfo{ID: 3, UUID: "osd-uuid", BlockPath: "dev/logical-volume-path", CVMode: "raw", Location: location}
osd2 := OSDInfo{ID: 3, UUID: "osd-uuid", BlockPath: "vg1/lv1", CVMode: "lvm", LVBackedPV: true}
osd3 := OSDInfo{ID: 3, UUID: "osd-uuid", BlockPath: ""}
osd3 := OSDInfo{ID: 3, UUID: "osd-uuid", BlockPath: "", CVMode: "raw"}
osdProp := osdProperties{
crushHostname: node,
pvc: v1.PersistentVolumeClaimVolumeSource{ClaimName: "pvc"},
pvc: corev1.PersistentVolumeClaimVolumeSource{ClaimName: "pvc"},
selection: rookv1.Selection{},
resources: v1.ResourceRequirements{},
resources: corev1.ResourceRequirements{},
storeConfig: config.StoreConfig{},
}
dataPathMap := &provisionConfig{
DataPathMap: opconfig.NewDatalessDaemonDataPathMap(c.clusterInfo.Namespace, c.spec.DataDirHostPath),
}
d1, _ := c.makeDeployment(osdProp, osd1, dataPathMap)
osds1, _ := c.getOSDInfo(d1)
assert.Equal(t, 1, len(osds1))
assert.Equal(t, osd1.ID, osds1[0].ID)
assert.Equal(t, osd1.BlockPath, osds1[0].BlockPath)
assert.Equal(t, osd1.CVMode, osds1[0].CVMode)
assert.Equal(t, location, osds1[0].Location)
t.Run("get info from PVC-based OSDs", func(t *testing.T) {
d1, _ := c.makeDeployment(osdProp, osd1, dataPathMap)
osdInfo1, _ := c.getOSDInfo(d1)
assert.Equal(t, osd1.ID, osdInfo1.ID)
assert.Equal(t, osd1.BlockPath, osdInfo1.BlockPath)
assert.Equal(t, osd1.CVMode, osdInfo1.CVMode)
assert.Equal(t, location, osdInfo1.Location)
d2, _ := c.makeDeployment(osdProp, osd2, dataPathMap)
osds2, _ := c.getOSDInfo(d2)
assert.Equal(t, 1, len(osds2))
assert.Equal(t, osd2.ID, osds2[0].ID)
assert.Equal(t, osd2.BlockPath, osds2[0].BlockPath)
assert.Equal(t, osd2.CVMode, osds2[0].CVMode)
assert.Equal(t, osd2.LVBackedPV, osds2[0].LVBackedPV)
d2, _ := c.makeDeployment(osdProp, osd2, dataPathMap)
osdInfo2, _ := c.getOSDInfo(d2)
assert.Equal(t, osd2.ID, osdInfo2.ID)
assert.Equal(t, osd2.BlockPath, osdInfo2.BlockPath)
assert.Equal(t, osd2.CVMode, osdInfo2.CVMode)
assert.Equal(t, osd2.LVBackedPV, osdInfo2.LVBackedPV)
d3, _ := c.makeDeployment(osdProp, osd3, dataPathMap)
osds3, err := c.getOSDInfo(d3)
assert.Equal(t, 0, len(osds3))
assert.NotNil(t, err)
// make deployment fails if block path is empty. allow it to create a valid deployment, then
// set the deployment to have bad info
d3, err := c.makeDeployment(osdProp, osd3, dataPathMap)
assert.NoError(t, err)
d3.Spec.Template.Spec.Containers[0].Env = append(d3.Spec.Template.Spec.Containers[0].Env,
corev1.EnvVar{Name: blockPathVarName, Value: ""})
_, err = c.getOSDInfo(d3)
assert.Error(t, err)
})
t.Run("get info from node-based OSDs", func(t *testing.T) {
useAllDevices := true
osd4 := OSDInfo{ID: 3, UUID: "osd-uuid", BlockPath: "", CVMode: "lvm", Location: location}
osd5 := OSDInfo{ID: 3, UUID: "osd-uuid", BlockPath: "vg1/lv1", CVMode: "lvm"}
osdProp = osdProperties{
crushHostname: node,
devices: []rookv1.Device{},
pvc: corev1.PersistentVolumeClaimVolumeSource{},
selection: rookv1.Selection{
UseAllDevices: &useAllDevices,
},
resources: corev1.ResourceRequirements{},
storeConfig: config.StoreConfig{},
metadataDevice: "",
}
d4, _ := c.makeDeployment(osdProp, osd4, dataPathMap)
osdInfo4, _ := c.getOSDInfo(d4)
assert.Equal(t, osd4.ID, osdInfo4.ID)
assert.Equal(t, location, osdInfo4.Location)
d5, _ := c.makeDeployment(osdProp, osd5, dataPathMap)
osdInfo5, _ := c.getOSDInfo(d5)
assert.Equal(t, osd5.ID, osdInfo5.ID)
assert.Equal(t, osd5.CVMode, osdInfo5.CVMode)
})
}
func TestOSDPlacement(t *testing.T) {
@@ -435,14 +496,14 @@ func TestOSDPlacement(t *testing.T) {
assert.Nil(t, result.NodeAffinity)
// the osd daemon placement is specified
prop.placement = rookv1.Placement{NodeAffinity: &v1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{
prop.placement = rookv1.Placement{NodeAffinity: &corev1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
NodeSelectorTerms: []corev1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{
MatchExpressions: []corev1.NodeSelectorRequirement{
{
Key: "label1",
Operator: v1.NodeSelectorOpIn,
Operator: corev1.NodeSelectorOpIn,
Values: []string{"bar", "baz"},
},
},
@@ -457,14 +518,14 @@ func TestOSDPlacement(t *testing.T) {
assert.Equal(t, "label1", result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions[0].Key)
// The prepare placement is specified and takes precedence over the osd placement
prop.preparePlacement = &rookv1.Placement{NodeAffinity: &v1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{
prop.preparePlacement = &rookv1.Placement{NodeAffinity: &corev1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
NodeSelectorTerms: []corev1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{
MatchExpressions: []corev1.NodeSelectorRequirement{
{
Key: "label2",
Operator: v1.NodeSelectorOpIn,
Operator: corev1.NodeSelectorOpIn,
Values: []string{"foo", "bar"},
},
},
@@ -539,36 +600,33 @@ func TestGetOSDInfoWithCustomRoot(t *testing.T) {
location := "root=custom-root host=myhost zone=myzone"
osd1 := OSDInfo{ID: 3, UUID: "osd-uuid", BlockPath: "dev/logical-volume-path", CVMode: "raw", Location: location}
osd2 := OSDInfo{ID: 3, UUID: "osd-uuid", BlockPath: "vg1/lv1", CVMode: "lvm", LVBackedPV: true, Location: location}
osd3 := OSDInfo{ID: 3, UUID: "osd-uuid", BlockPath: "", Location: location}
osd3 := OSDInfo{ID: 3, UUID: "osd-uuid", BlockPath: "", CVMode: "lvm", Location: location}
osdProp := osdProperties{
crushHostname: node,
pvc: v1.PersistentVolumeClaimVolumeSource{ClaimName: "pvc"},
pvc: corev1.PersistentVolumeClaimVolumeSource{ClaimName: "pvc"},
selection: rookv1.Selection{},
resources: v1.ResourceRequirements{},
resources: corev1.ResourceRequirements{},
storeConfig: config.StoreConfig{},
}
dataPathMap := &provisionConfig{
DataPathMap: opconfig.NewDatalessDaemonDataPathMap(c.clusterInfo.Namespace, c.spec.DataDirHostPath),
}
d1, _ := c.makeDeployment(osdProp, osd1, dataPathMap)
osds1, _ := c.getOSDInfo(d1)
assert.Equal(t, 1, len(osds1))
assert.Equal(t, osd1.ID, osds1[0].ID)
assert.Equal(t, osd1.BlockPath, osds1[0].BlockPath)
assert.Equal(t, osd1.CVMode, osds1[0].CVMode)
assert.Equal(t, location, osds1[0].Location)
osdInfo1, _ := c.getOSDInfo(d1)
assert.Equal(t, osd1.ID, osdInfo1.ID)
assert.Equal(t, osd1.BlockPath, osdInfo1.BlockPath)
assert.Equal(t, osd1.CVMode, osdInfo1.CVMode)
assert.Equal(t, location, osdInfo1.Location)
d2, _ := c.makeDeployment(osdProp, osd2, dataPathMap)
osds2, _ := c.getOSDInfo(d2)
assert.Equal(t, 1, len(osds2))
assert.Equal(t, osd2.ID, osds2[0].ID)
assert.Equal(t, osd2.BlockPath, osds2[0].BlockPath)
assert.Equal(t, osd2.CVMode, osds2[0].CVMode)
assert.Equal(t, osd2.LVBackedPV, osds2[0].LVBackedPV)
assert.Equal(t, location, osds2[0].Location)
osdInfo2, _ := c.getOSDInfo(d2)
assert.Equal(t, osd2.ID, osdInfo2.ID)
assert.Equal(t, osd2.BlockPath, osdInfo2.BlockPath)
assert.Equal(t, osd2.CVMode, osdInfo2.CVMode)
assert.Equal(t, osd2.LVBackedPV, osdInfo2.LVBackedPV)
assert.Equal(t, location, osdInfo2.Location)
d3, _ := c.makeDeployment(osdProp, osd3, dataPathMap)
osds3, err := c.getOSDInfo(d3)
assert.Equal(t, 0, len(osds3))
assert.NotNil(t, err)
_, err := c.getOSDInfo(d3)
assert.Error(t, err)
}
+16 -11
View File
@@ -77,7 +77,7 @@ OSD_UUID=%s
OSD_STORE_FLAG="%s"
OSD_DATA_DIR=/var/lib/ceph/osd/ceph-"$OSD_ID"
CV_MODE=%s
DEVICE=%s
DEVICE="$%s"
METADATA_DEVICE="$%s"
WAL_DEVICE="$%s"
@@ -277,10 +277,14 @@ var defaultTuneSlowSettings = []string{
"--osd-delete-sleep=2", // Time in seconds to sleep before next removal transaction
}
func deploymentName(osdID int) string {
return fmt.Sprintf(osdAppNameFmt, osdID)
}
func (c *Cluster) makeDeployment(osdProps osdProperties, osd OSDInfo, provisionConfig *provisionConfig) (*apps.Deployment, error) {
// If running on Octopus, we don't need to use the host PID namespace
var hostPID = !c.clusterInfo.CephVersion.IsAtLeastOctopus()
deploymentName := fmt.Sprintf(osdAppNameFmt, osd.ID)
deploymentName := deploymentName(osd.ID)
replicaCount := int32(1)
volumeMounts := controller.CephVolumeMounts(provisionConfig.DataPathMap, false)
configVolumeMounts := controller.RookVolumeMounts(provisionConfig.DataPathMap, false)
@@ -295,11 +299,9 @@ func (c *Cluster) makeDeployment(osdProps osdProperties, osd OSDInfo, provisionC
doBinaryCopyInit := true // copy tini and rook binaries in an init container?
doActivateOSDInit := false // run an init container to activate the osd?
// If CVMode is empty, this likely means we upgraded Rook
// This property did not exist before so we need to initialize it
// This property is used for both PVC and non-PVC use case
if osd.CVMode == "" {
osd.CVMode = "lvm"
return nil, errors.Errorf("failed to generate deployment for OSD %d. required CVMode is not specified for this OSD", osd.ID)
}
dataDir := k8sutil.DataDir
@@ -346,6 +348,8 @@ func (c *Cluster) makeDeployment(osdProps osdProperties, osd OSDInfo, provisionC
Name: "rook-ceph-config"},
Key: "mon_host"}}},
{Name: "CEPH_ARGS", Value: "-m $(ROOK_CEPH_MON_HOST)"},
blockPathEnvVariable(osd.BlockPath),
cvModeEnvVariable(osd.CVMode),
}...)
configEnvVars := append(c.getConfigEnvVars(osdProps, dataDir), []v1.EnvVar{
tiniEnvVar,
@@ -458,16 +462,12 @@ func (c *Cluster) makeDeployment(osdProps osdProperties, osd OSDInfo, provisionC
// Let's use the old bridge for these lvm based pvc osds
volumeMounts = append(volumeMounts, getPvcOSDBridgeMount(osdProps.pvc.ClaimName))
envVars = append(envVars, pvcBackedOSDEnvVar("true"))
envVars = append(envVars, blockPathEnvVariable(osd.BlockPath))
envVars = append(envVars, cvModeEnvVariable(osd.CVMode))
envVars = append(envVars, lvBackedPVEnvVar(strconv.FormatBool(osd.LVBackedPV)))
}
if osdProps.onPVC() && osd.CVMode == "raw" {
volumeMounts = append(volumeMounts, getPvcOSDBridgeMountActivate(osdDataDirPath, osdProps.pvc.ClaimName))
envVars = append(envVars, pvcBackedOSDEnvVar("true"))
envVars = append(envVars, blockPathEnvVariable(osd.BlockPath))
envVars = append(envVars, cvModeEnvVariable(osd.CVMode))
}
// We cannot go un-privileged until we have a bindmount for logs and crash
@@ -745,7 +745,12 @@ func (c *Cluster) getActivateOSDInitContainer(configDir, namespace, osdID string
Name: activateOSDVolumeName,
VolumeSource: source,
}
envVars := osdActivateEnvVar()
envVars := append(
osdActivateEnvVar(),
blockPathEnvVariable(osdInfo.BlockPath),
metadataDeviceEnvVar(osdInfo.MetadataPath),
walDeviceEnvVar(osdInfo.WalPath),
)
osdStore := "--bluestore"
// Build empty dir osd path to something like "/var/lib/ceph/osd/ceph-0"
@@ -765,7 +770,7 @@ func (c *Cluster) getActivateOSDInitContainer(configDir, namespace, osdID string
Command: []string{
"/bin/bash",
"-c",
fmt.Sprintf(activateOSDCode, osdID, osdInfo.UUID, osdStore, osdInfo.CVMode, osdInfo.BlockPath, osdMetadataDeviceEnvVarName, osdWalDeviceEnvVarName),
fmt.Sprintf(activateOSDCode, osdID, osdInfo.UUID, osdStore, osdInfo.CVMode, blockPathVarName, osdMetadataDeviceEnvVarName, osdWalDeviceEnvVarName),
},
Name: "activate",
Image: c.spec.CephVersion.Image,
+46 -4
View File
@@ -23,7 +23,6 @@ import (
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
rookv1 "github.com/rook/rook/pkg/apis/rook.io/v1"
"github.com/rook/rook/pkg/clusterd"
"github.com/rook/rook/pkg/daemon/ceph/client"
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
"github.com/rook/rook/pkg/operator/ceph/cluster/osd/config"
opconfig "github.com/rook/rook/pkg/operator/ceph/config"
@@ -32,13 +31,14 @@ import (
"github.com/rook/rook/pkg/operator/k8sutil"
exectest "github.com/rook/rook/pkg/util/exec/test"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/client-go/kubernetes/fake"
)
func TestPodContainer(t *testing.T) {
cluster := &Cluster{rookVersion: "23", clusterInfo: client.AdminClusterInfo("myosd")}
cluster := &Cluster{rookVersion: "23", clusterInfo: cephclient.AdminClusterInfo("myosd")}
cluster.clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
osdProps := osdProperties{
crushHostname: "node",
@@ -117,7 +117,8 @@ func testPodDevices(t *testing.T, dataDir, deviceName string, allDevices bool) {
return
}
osd := OSDInfo{
ID: 0,
ID: 0,
CVMode: "raw",
}
osdProp := osdProperties{
@@ -567,7 +568,8 @@ func TestHostNetwork(t *testing.T) {
n := c.spec.Storage.ResolveNode(storageSpec.Nodes[0].Name)
osd := OSDInfo{
ID: 0,
ID: 0,
CVMode: "raw",
}
osdProp := osdProperties{
@@ -666,3 +668,43 @@ func TestClusterGetPVCEncryptionInitContainerActivate(t *testing.T) {
containers = c.getPVCEncryptionInitContainerActivate(mountPath, osdProperties)
assert.Equal(t, 3, len(containers))
}
// WARNING! modifies c.ValidStorage
func getDummyDeploymentOnPVC(clientset *fake.Clientset, c *Cluster, pvcName string, osdID int) *appsv1.Deployment {
osd := OSDInfo{
ID: osdID,
UUID: "some-uuid",
BlockPath: "/some/path",
CVMode: "raw",
}
c.ValidStorage.VolumeSources = append(c.ValidStorage.VolumeSources, rookv1.VolumeSource{
Name: pvcName,
PVCSources: map[string]v1.PersistentVolumeClaimVolumeSource{
bluestorePVCData: {ClaimName: pvcName},
},
Portable: true,
})
config := c.newProvisionConfig()
d, err := deploymentOnPVC(c, osd, pvcName, config)
if err != nil {
panic(err)
}
return d
}
// WARNING! modifies c.ValidStorage
func getDummyDeploymentOnNode(clientset *fake.Clientset, c *Cluster, nodeName string, osdID int) *appsv1.Deployment {
osd := OSDInfo{
ID: osdID,
UUID: "some-uuid",
BlockPath: "/dev/vda",
CVMode: "raw",
}
c.ValidStorage.Nodes = append(c.ValidStorage.Nodes, rookv1.Node{Name: nodeName})
config := c.newProvisionConfig()
d, err := deploymentOnNode(c, osd, nodeName, config)
if err != nil {
panic(err)
}
return d
}
+227 -294
View File
@@ -23,14 +23,14 @@ import (
"fmt"
"time"
"github.com/pkg/errors"
"github.com/rook/rook/pkg/operator/ceph/config"
"github.com/rook/rook/pkg/operator/ceph/controller"
"github.com/rook/rook/pkg/operator/k8sutil"
"github.com/rook/rook/pkg/util"
v1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
k8swatch "k8s.io/apimachinery/pkg/watch"
)
const (
@@ -41,9 +41,6 @@ const (
// OrchestrationStatusCompleted denotes the OSD provisioning has completed. This does not imply
// the provisioning completed successfully in whole or in part.
OrchestrationStatusCompleted = "completed"
// OrchestrationStatusAlreadyExists denotes the OSD provisioning was not started because an OSD
// has already been provisioned and started.
OrchestrationStatusAlreadyExists = "alreadyExists"
// OrchestrationStatusFailed denotes the OSD provisioning has failed.
OrchestrationStatusFailed = "failed"
@@ -51,17 +48,18 @@ const (
orchestrationStatusKey = "status"
provisioningLabelKey = "provisioning"
nodeLabelKey = "node"
completeProvisionTimeout = 20
)
var (
// time to wait before updating OSDs opportunistically while waiting for OSDs to finish provisioning
osdOpportunisticUpdateDuration = 100 * time.Millisecond
// a ticker that ticks every minute to check progress
minuteTickerDuration = time.Minute
)
type provisionConfig struct {
errorMessages []string
DataPathMap *config.DataPathMap // location to store data in container
DataPathMap *config.DataPathMap // location to store data in OSD and OSD prepare containers
}
func (c *Cluster) newProvisionConfig() *provisionConfig {
@@ -70,26 +68,58 @@ func (c *Cluster) newProvisionConfig() *provisionConfig {
}
}
func (c *provisionConfig) addError(message string, args ...interface{}) {
// The provisionErrors struct can get passed around to provisioning code which can add errors to its
// internal list of errors. The errors will be reported at the end of provisioning.
type provisionErrors struct {
errors []error
}
func newProvisionErrors() *provisionErrors {
return &provisionErrors{
errors: []error{},
}
}
func (e *provisionErrors) addError(message string, args ...interface{}) {
logger.Errorf(message, args...)
c.errorMessages = append(c.errorMessages, fmt.Sprintf(message, args...))
e.errors = append(e.errors, errors.Errorf(message, args...))
}
func (c *Cluster) updateOSDStatus(node string, status OrchestrationStatus) {
UpdateNodeStatus(c.kv, node, status)
func (e *provisionErrors) len() int {
return len(e.errors)
}
func UpdateNodeStatus(kv *k8sutil.ConfigMapKVStore, node string, status OrchestrationStatus) {
labels := map[string]string{
func (e *provisionErrors) asMessages() string {
o := ""
for _, err := range e.errors {
o = fmt.Sprintf("%s\n%v", o, err)
}
return o
}
// return name of status ConfigMap
func (c *Cluster) updateOSDStatus(node string, status OrchestrationStatus) string {
return UpdateNodeStatus(c.kv, node, status)
}
func statusConfigMapLabels(node string) map[string]string {
return map[string]string{
k8sutil.AppAttr: AppName,
orchestrationStatusKey: provisioningLabelKey,
nodeLabelKey: node,
}
}
// UpdateNodeStatus updates the status ConfigMap for the OSD on the given node. It returns the name
// the ConfigMap used.
func UpdateNodeStatus(kv *k8sutil.ConfigMapKVStore, node string, status OrchestrationStatus) string {
labels := statusConfigMapLabels(node)
// update the status map with the given status now
s, _ := json.Marshal(status)
cmName := statusConfigMapName(node)
if err := kv.SetValueWithLabels(
k8sutil.TruncateNodeName(orchestrationStatusMapName, node),
cmName,
orchestrationStatusKey,
string(s),
labels,
@@ -97,10 +127,11 @@ func UpdateNodeStatus(kv *k8sutil.ConfigMapKVStore, node string, status Orchestr
// log the error, but allow the orchestration to continue even if the status update failed
logger.Errorf("failed to set node %q status to %q for osd orchestration. %s", node, status.Status, status.Message)
}
return cmName
}
func (c *Cluster) handleOrchestrationFailure(config *provisionConfig, nodeName, message string) {
config.addError(message)
func (c *Cluster) handleOrchestrationFailure(errors *provisionErrors, nodeName, message string, args ...interface{}) {
errors.addError(message, args...)
status := OrchestrationStatus{Status: OrchestrationStatusFailed, Message: message}
UpdateNodeStatus(c.kv, nodeName, status)
}
@@ -125,313 +156,215 @@ func parseOrchestrationStatus(data map[string]string) *OrchestrationStatus {
return &status
}
func (c *Cluster) completeProvision(config *provisionConfig) bool {
return c.completeOSDsForAllNodes(config, true, completeProvisionTimeout)
// return errors from this function when OSD provisioning should stop and the reconcile be restarted
func (c *Cluster) updateAndCreateOSDs(
createConfig *createConfig,
updateConfig *updateConfig,
errs *provisionErrors, // add errors here
) error {
// tick every mintue to check-in on housekeeping stuff and report overall progress
minuteTicker := time.NewTicker(minuteTickerDuration)
defer minuteTicker.Stop()
var err error
doLoop := true
for doLoop {
doLoop, err = c.updateAndCreateOSDsLoop(createConfig, updateConfig, minuteTicker, errs)
if err != nil {
if !doLoop {
return err
}
logger.Errorf("%v", err)
}
}
return nil
}
func (c *Cluster) checkNodesCompleted(selector string, config *provisionConfig, configOSDs bool) (int, *util.Set, bool, *v1.ConfigMapList, []*v1.ConfigMap, error) {
ctx := context.TODO()
opts := metav1.ListOptions{
LabelSelector: selector,
Watch: false,
}
remainingNodes := util.NewSet()
deferredConfigMaps := []*v1.ConfigMap{}
// check the status map to see if the node is already completed before we start watching
statuses, err := c.context.Clientset.CoreV1().ConfigMaps(c.clusterInfo.Namespace).List(ctx, opts)
if err != nil {
if !kerrors.IsNotFound(err) {
config.addError("failed to get config status. %v", err)
return 0, remainingNodes, false, statuses, deferredConfigMaps, err
}
// the status map doesn't exist yet, watching below is still an OK thing to do
}
originalNodes := len(statuses.Items)
// check the nodes to see which ones are already completed
for _, configMap := range statuses.Items {
node, ok := configMap.Labels[nodeLabelKey]
if !ok {
logger.Warningf("missing node label on configmap %s", configMap.Name)
continue
}
var completed bool
localconfigMap := configMap
completed, deferredConfigMaps = c.handleStatusConfigMapStatus(node, config, &localconfigMap, deferredConfigMaps, configOSDs)
if !completed {
remainingNodes.Add(node)
}
}
if remainingNodes.Count() == 0 {
return originalNodes, remainingNodes, true, statuses, deferredConfigMaps, nil
}
return originalNodes, remainingNodes, false, statuses, deferredConfigMaps, nil
}
func (c *Cluster) completeOSDsForAllNodes(config *provisionConfig, configOSDs bool, timeoutMinutes int) bool {
ctx := context.TODO()
selector := fmt.Sprintf("%s=%s,%s=%s",
func statusConfigMapSelector() string {
return fmt.Sprintf("%s=%s,%s=%s",
k8sutil.AppAttr, AppName,
orchestrationStatusKey, provisioningLabelKey,
)
}
// For OSDs that are definitely update operations, we want to be able to defer the update until
// after we have created new OSDs in order to speed up cluster scale-out. Allow us to defer
// updating by keeping a list of deferred configmaps and processing those after creates have
// been done. If OSD provisioning doesn't report "alreadyExists" status, then no configmaps will
// be deferred, and OSD create/update ops will be intermixed in whatever order is processed.
deferredConfigMaps := []*v1.ConfigMap{}
// handleDeferredConfigMaps NEEDS to happen before the outer function returns.
// The nested for loops make it hard to break out and handle the return value cleanly,
// so remember to call this before all return statements.
handleDeferredConfigMaps := func() {
logger.Debugf("handling all deferred OSD statuses")
for _, configMap := range deferredConfigMaps {
node, ok := configMap.Labels[nodeLabelKey]
if !ok {
logger.Infof("missing node label on deferred configmap %q", configMap.Name)
continue
}
func (c *Cluster) updateAndCreateOSDsLoop(
createConfig *createConfig,
updateConfig *updateConfig,
minuteTicker *time.Ticker, // pass in the minute ticker so that we always know when a minute passes
errs *provisionErrors, // add errors here
) (shouldRestart bool, err error) {
cmClient := c.context.Clientset.CoreV1().ConfigMaps(c.clusterInfo.Namespace)
ctx := context.TODO()
selector := statusConfigMapSelector()
c.handleDeferredStatusConfigMapStatus(node, config, configMap, configOSDs)
}
listOptions := metav1.ListOptions{
LabelSelector: selector,
}
configMapList, err := cmClient.List(ctx, listOptions)
if err != nil {
return false, errors.Wrapf(err, "failed to list OSD provisioning status ConfigMaps")
}
var originalNodes int
var remainingNodes *util.Set
var completed bool
var statuses *v1.ConfigMapList
var err error
originalNodes, remainingNodes, completed, statuses, deferredConfigMaps, err = c.checkNodesCompleted(selector, config, configOSDs)
if err == nil && completed {
handleDeferredConfigMaps()
return true
// Process the configmaps initially in case any are already in a processable state
for i := range configMapList.Items {
// reference index to prevent implicit memory aliasing error
c.createOSDsForStatusMap(&configMapList.Items[i], createConfig, errs)
}
// Make a timer to help us opportunistically handle OSD updates while waiting on OSD prepare
// jobs to finish. The timer value created here doesn't practically matter for the code below.
opportunityTimer := time.NewTimer(osdOpportunisticUpdateDuration)
watchOptions := metav1.ListOptions{
LabelSelector: selector,
Watch: true,
ResourceVersion: configMapList.ResourceVersion,
}
watcher, err := cmClient.Watch(ctx, watchOptions)
defer watcher.Stop()
if err != nil {
return false, errors.Wrapf(err, "failed to start watching OSD provisioning status ConfigMaps")
}
// Make a timer that helps us log every minute that goes by without making progress and will
// be used to determine if we have gone past our timeout without making progress.
timeoutTimer := time.NewTimer(time.Minute)
shouldResetTimeoutTimer := true // set true when timeout timer should be reset
currentTimeoutMinutes := 0 // track how many minutes have gone by without progress
// tick after a short time of waiting for new OSD provision status configmaps to change state
// in order to allow opportunistic deployment updates while we wait
updateTicker := time.NewTicker(osdOpportunisticUpdateDuration)
defer updateTicker.Stop()
watchErrMsg := "failed during watch of OSD provisioning status ConfigMaps"
for {
// Check whether we need to cancel the orchestration
if err := controller.CheckForCancelledOrchestration(c.context); err != nil {
config.addError("%s", err.Error())
return false
if updateConfig.doneUpdating() && createConfig.doneCreating() {
break // loop
}
opts := metav1.ListOptions{
LabelSelector: selector,
Watch: true,
// start watching for changes on the orchestration status map
ResourceVersion: statuses.ResourceVersion,
// reset the update ticker (and drain the channel if necessary) to make sure we always
// wait a little bit for an OSD prepare result before opportunistically updating deployments
updateTicker.Reset(osdOpportunisticUpdateDuration)
if len(updateTicker.C) > 0 {
<-updateTicker.C
}
logger.Infof("%d/%d node(s) completed osd provisioning, resource version %v", (originalNodes - remainingNodes.Count()), originalNodes, opts.ResourceVersion)
w, err := c.context.Clientset.CoreV1().ConfigMaps(c.clusterInfo.Namespace).Watch(ctx, opts)
if err != nil {
logger.Warningf("failed to start watch on osd status, trying again. %v", err)
time.Sleep(5 * time.Second)
continue
}
defer w.Stop()
ResultLoop:
for {
// Reset the opportunity timer every time we start this loop. If the timer fires before
// there is a ConfigMap update (new OSD created), we opportunistically handle an OSD
// update and then return to the top of this loop. If the ConfigMap watcher has a result
// before the timeout, a new OSD will be created. The opportunity timer might fire while
// creation is happening. When the OSD is done being created, we return to the top of
// this loop and reset the timeout here to make sure a previous timer firing doesn't
// take update precedence away from any ConfigMap updates (new OSD) that need handled.
if !opportunityTimer.Stop() {
// the timer already fired
if len(opportunityTimer.C) > 0 {
// the timer fired, but the result channel needs drained before Reset below
<-opportunityTimer.C
}
select {
case event, ok := <-watcher.ResultChan():
if !ok {
logger.Infof("restarting watcher for OSD provisioning status ConfigMaps. the watcher closed the channel")
return true, nil
}
opportunityTimer.Reset(osdOpportunisticUpdateDuration)
if shouldResetTimeoutTimer {
if !timeoutTimer.Stop() {
// the timer already fired
if len(timeoutTimer.C) > 0 {
// the timer fired, but the result channel needs drained before Reset below
<-timeoutTimer.C
}
}
timeoutTimer.Reset(time.Minute)
if !isAddOrModifyEvent(event.Type) {
// We don't want to process delete events when we delete configmaps after having
// processed them. We also don't want to process BOOKMARK or ERROR events.
logger.Debugf("not processing %s event for object %q", event.Type, eventObjectName(event))
break // case
}
shouldResetTimeoutTimer = false // don't reset the timer unless a case below asks for it
select {
case e, ok := <-w.ResultChan():
if !ok {
logger.Infof("orchestration status config map result channel closed, will restart watch.")
w.Stop()
<-time.After(5 * time.Second)
var leftNodes int
var leftRemainingNodes *util.Set
var completed bool
leftNodes, leftRemainingNodes, completed, _, deferredConfigMaps, err = c.checkNodesCompleted(selector, config, configOSDs)
if err == nil {
if completed {
logger.Infof("additional %d/%d node(s) completed osd provisioning", leftNodes, originalNodes)
handleDeferredConfigMaps()
return true
}
remainingNodes = leftRemainingNodes
} else {
logger.Warningf("failed to list orchestration configmap, status: %v", err)
}
break ResultLoop
}
if e.Type == watch.Modified {
configMap, ok := e.Object.(*v1.ConfigMap)
if !ok {
logger.Errorf("expected type ConfigMap but found %T", configMap)
continue
}
node, ok := configMap.Labels[nodeLabelKey]
if !ok {
logger.Infof("missing node label on configmap %s", configMap.Name)
continue
}
if !remainingNodes.Contains(node) {
logger.Infof("skipping event from node %s status update since it is already completed", node)
continue
}
var completed bool
completed, deferredConfigMaps = c.handleStatusConfigMapStatus(node, config, configMap, deferredConfigMaps, configOSDs)
if completed {
remainingNodes.Remove(node)
if remainingNodes.Count() == 0 {
logger.Infof("%d/%d node(s) completed osd provisioning", originalNodes, originalNodes)
handleDeferredConfigMaps()
return true
}
}
// made progress on creating/updating OSDs; reset the timeout timer
shouldResetTimeoutTimer = true
}
case <-opportunityTimer.C:
// We want to make a best-effort attempt to update configmaps while provisioning is
// ongoing. While there are deferred configmaps remaining, grab one and process it.
if len(deferredConfigMaps) > 0 {
configMap := deferredConfigMaps[0]
logger.Debugf("opportunistically handling deferred OSD status %q", configMap.Name)
node, ok := configMap.Labels[nodeLabelKey]
if !ok {
logger.Infof("missing node label on deferred configmap %q", configMap.Name)
continue
}
c.handleDeferredStatusConfigMapStatus(node, config, configMap, configOSDs)
// remove the configmap we just processed from the deferred configmap list
deferredConfigMaps = deferredConfigMaps[1:]
// made progress on updating OSDs; reset the timeout timer
shouldResetTimeoutTimer = true
}
case <-timeoutTimer.C:
// Check whether we need to cancel the orchestration
if err := controller.CheckForCancelledOrchestration(c.context); err != nil {
config.addError("%s", err.Error())
return false
}
// log every so often while we are waiting
currentTimeoutMinutes++
if currentTimeoutMinutes == timeoutMinutes {
config.addError("timed out waiting for %d nodes: %+v", remainingNodes.Count(), remainingNodes)
// start to remove remainingNodes waiting timeout.
for remainingNode := range remainingNodes.Iter() {
clearNodeName := k8sutil.TruncateNodeName(orchestrationStatusMapName, remainingNode)
if err := c.kv.ClearStore(clearNodeName); err != nil {
config.addError("failed to clear node %q status with name %q. %v", remainingNode, clearNodeName, err)
}
}
handleDeferredConfigMaps()
return false
}
logger.Infof("waiting on orchestration status update from %d remaining nodes", remainingNodes.Count())
// made no progress, but the timer needs reset so we can timeout again
shouldResetTimeoutTimer = true
configMap, ok := event.Object.(*corev1.ConfigMap)
if !ok {
logger.Errorf("recovering. %s. expected type ConfigMap but found %T", watchErrMsg, configMap)
break // case
}
c.createOSDsForStatusMap(configMap, createConfig, errs)
case <-updateTicker.C:
// do an update
updateConfig.updateExistingOSDs(errs)
case <-minuteTicker.C:
// Check whether we need to cancel the orchestration
if err := controller.CheckForCancelledOrchestration(c.context); err != nil {
return false, err
}
// Log progress
c, cExp := createConfig.progress()
u, uExp := updateConfig.progress()
logger.Infof("waiting... %d of %d OSD prepare jobs have finished processing and %d of %d OSDs have been updated", c, cExp, u, uExp)
}
}
return false, nil
}
func isAddOrModifyEvent(t k8swatch.EventType) bool {
switch t {
case k8swatch.Added, k8swatch.Modified:
return true
default:
return false
}
}
func (c *Cluster) createOrUpdateOSDs(status *OrchestrationStatus, nodeName string, config *provisionConfig, configMap *v1.ConfigMap) {
if status.PvcBackedOSD {
c.startOSDDaemonsOnPVC(nodeName, config, configMap, status)
} else {
c.startOSDDaemonsOnNode(nodeName, config, configMap, status)
}
// remove the status configmap that indicated the progress
if err := c.kv.ClearStore(k8sutil.TruncateNodeName(orchestrationStatusMapName, nodeName)); err != nil {
logger.Errorf("failed to remove the status configmap %q. %v", orchestrationStatusMapName, err)
func eventObjectName(e k8swatch.Event) string {
objName := "[could not determine name]"
objMeta, _ := meta.Accessor(e.Object)
if objMeta != nil {
objName = objMeta.GetName()
}
return objName
}
func (c *Cluster) handleStatusConfigMapStatus(nodeName string, config *provisionConfig, configMap *v1.ConfigMap, deferredConfigMaps []*v1.ConfigMap, configOSDs bool) (completed bool, updatedDeferredConfigMaps []*v1.ConfigMap) {
// by default, do not append the input configmap to output to be deferred for processing later
updatedDeferredConfigMaps = deferredConfigMaps
status := parseOrchestrationStatus(configMap.Data)
if status == nil {
return false, deferredConfigMaps
}
logger.Infof("osd orchestration status for node %s is %s", nodeName, status.Status)
if status.Status == OrchestrationStatusAlreadyExists {
logger.Debugf("deferring OSD update for node %q", nodeName)
// in case where the OSD has already been provisioned, this is an update case which we want
// to defer until later
updatedDeferredConfigMaps = append(deferredConfigMaps, configMap)
// we report that the node finished provisioning here but still rely on the caller to make
// sure to process deferred configmaps at a later point
return true, updatedDeferredConfigMaps
}
if status.Status == OrchestrationStatusCompleted {
if configOSDs {
c.createOrUpdateOSDs(status, nodeName, config, configMap)
}
return true, updatedDeferredConfigMaps
}
if status.Status == OrchestrationStatusFailed {
config.addError("orchestration for node %s failed: %+v", nodeName, status)
return true, updatedDeferredConfigMaps
}
return false, updatedDeferredConfigMaps
}
// deferred configmaps are OSD update operations we want to defer until after new OSDs have been created
func (c *Cluster) handleDeferredStatusConfigMapStatus(nodeName string, config *provisionConfig, configMap *v1.ConfigMap, configOSDs bool) {
status := parseOrchestrationStatus(configMap.Data)
if status == nil {
logger.Warningf("failed to handle deferred update of OSD for node %s. osd orchestration status for node %s is %s", nodeName, nodeName, status.Status)
// Create OSD Deployments for OSDs reported by the prepare job status configmap.
// Do not create OSD deployments if a deployment already exists for a given OSD.
func (c *Cluster) createOSDsForStatusMap(
configMap *corev1.ConfigMap,
createConfig *createConfig,
errs *provisionErrors, // add errors here
) {
nodeOrPVCName, ok := configMap.Labels[nodeLabelKey]
if !ok {
logger.Warningf("missing node label on configmap %s", configMap.Name)
return
}
if configOSDs {
logger.Infof("handling deferred update of OSD for node %q", nodeName)
c.createOrUpdateOSDs(status, nodeName, config, configMap)
status := parseOrchestrationStatus(configMap.Data)
if status == nil {
return
}
nodeOrPVC := "node"
if status.PvcBackedOSD {
nodeOrPVC = "PVC"
}
logger.Infof("OSD orchestration status for %s %s is %q", nodeOrPVC, nodeOrPVCName, status.Status)
if status.Status == OrchestrationStatusCompleted {
createConfig.createNewOSDsFromStatus(status, nodeOrPVCName, errs)
c.deleteStatusConfigMap(nodeOrPVCName) // remove the provisioning status configmap
return
}
if status.Status == OrchestrationStatusFailed {
createConfig.doneWithStatus(nodeOrPVCName)
errs.addError("failed to provision OSD(s) on %s %s. %+v", nodeOrPVC, nodeOrPVCName, status)
c.deleteStatusConfigMap(nodeOrPVCName) // remove the provisioning status configmap
return
}
}
func statusConfigMapName(nodeOrPVCName string) string {
return k8sutil.TruncateNodeName(orchestrationStatusMapName, nodeOrPVCName)
}
func (c *Cluster) deleteStatusConfigMap(nodeOrPVCName string) {
if err := c.kv.ClearStore(statusConfigMapName(nodeOrPVCName)); err != nil {
logger.Errorf("failed to remove the status configmap %q. %v", statusConfigMapName(nodeOrPVCName), err)
}
}
func (c *Cluster) deleteAllStatusConfigMaps() {
ctx := context.TODO()
listOpts := metav1.ListOptions{
LabelSelector: statusConfigMapSelector(),
}
cmClientset := c.context.Clientset.CoreV1().ConfigMaps(c.clusterInfo.Namespace)
cms, err := cmClientset.List(ctx, listOpts)
if err != nil {
logger.Warningf("failed to clean up any dangling OSD prepare status configmaps. failed to list OSD prepare status configmaps. %v", err)
return
}
for _, cm := range cms.Items {
logger.Debugf("cleaning up dangling OSD prepare status configmap %q", cm.Name)
err := cmClientset.Delete(ctx, cm.Name, metav1.DeleteOptions{})
if err != nil {
logger.Warningf("failed to clean up dangling OSD prepare status configmap %q. %v", cm.Name, err)
}
}
}
+8 -4
View File
@@ -75,7 +75,7 @@ func mockNodeOrchestrationCompletion(c *Cluster, nodeName string, statusMapWatch
}
for {
// wait for the node's orchestration status to change to "starting"
cmName := fmt.Sprintf(orchestrationStatusMapName, nodeName)
cmName := statusConfigMapName(nodeName)
cm, err := c.context.Clientset.CoreV1().ConfigMaps(c.clusterInfo.Namespace).Get(ctx, cmName, metav1.GetOptions{})
if err == nil {
status := parseOrchestrationStatus(cm.Data)
@@ -85,8 +85,11 @@ func mockNodeOrchestrationCompletion(c *Cluster, nodeName string, statusMapWatch
status = &OrchestrationStatus{
OSDs: []OSDInfo{
{
ID: 1,
Cluster: "rook",
ID: 1,
UUID: "000000-0000-00000001",
Cluster: "rook",
CVMode: "raw",
BlockPath: "/dev/some/path",
},
},
Status: OrchestrationStatusCompleted,
@@ -114,7 +117,8 @@ func waitForOrchestrationCompletion(c *Cluster, nodeName string, startCompleted
if *startCompleted {
break
}
cm, err := c.context.Clientset.CoreV1().ConfigMaps(c.clusterInfo.Namespace).Get(ctx, orchestrationStatusMapName, metav1.GetOptions{})
cmName := statusConfigMapName(nodeName)
cm, err := c.context.Clientset.CoreV1().ConfigMaps(c.clusterInfo.Namespace).Get(ctx, cmName, metav1.GetOptions{})
if err == nil {
status := parseOrchestrationStatus(cm.Data)
if status != nil {
+339
View File
@@ -0,0 +1,339 @@
/*
Copyright 2021 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 osd
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
"github.com/rook/rook/pkg/operator/k8sutil"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// THE LIBRARY PROVIDED BY THIS FILE IS NOT THREAD SAFE
var (
// allow unit tests to override these values
maxUpdatesInParallel = 20
updateMultipleDeploymentsAndWaitFunc = k8sutil.UpdateMultipleDeploymentsAndWait
deploymentOnNodeFunc = deploymentOnNode
deploymentOnPVCFunc = deploymentOnPVC
shouldCheckOkToStopFunc = cephclient.OSDUpdateShouldCheckOkToStop
)
type updateConfig struct {
cluster *Cluster
provisionConfig *provisionConfig
queue *updateQueue // these OSDs need updated
numUpdatesNeeded int // the number of OSDs that needed updating
deployments *existenceList // these OSDs have existing deployments
}
func (c *Cluster) newUpdateConfig(
provisionConfig *provisionConfig,
queue *updateQueue,
deployments *existenceList,
) *updateConfig {
return &updateConfig{
c,
provisionConfig,
queue,
queue.Len(),
deployments,
}
}
func (c *updateConfig) progress() (completed, initial int) {
return (c.numUpdatesNeeded - c.queue.Len()), c.numUpdatesNeeded
}
func (c *updateConfig) doneUpdating() bool {
return c.queue.Len() == 0
}
func (c *updateConfig) updateExistingOSDs(errs *provisionErrors) {
ctx := context.TODO()
if c.doneUpdating() {
return // no more OSDs to update
}
osdIDQuery, _ := c.queue.Pop()
var osdIDs []int
var err error
if !shouldCheckOkToStopFunc(c.cluster.context, c.cluster.clusterInfo) {
// If we should not check ok-to-stop, then only process one OSD at a time. There are likely
// less than 3 OSDs in the cluster or the cluster is on a single node. E.g., in CI :wink:.
osdIDs = []int{osdIDQuery}
} else {
osdIDs, err = cephclient.OSDOkToStop(c.cluster.context, c.cluster.clusterInfo, osdIDQuery, maxUpdatesInParallel)
if err != nil {
if c.cluster.spec.ContinueUpgradeAfterChecksEvenIfNotHealthy {
logger.Infof("OSD %d is not ok-to-stop but 'continueUpgradeAfterChecksEvenIfNotHealthy' is true, so continuing to update it", osdIDQuery)
osdIDs = []int{osdIDQuery} // make sure to update the queried OSD
} else {
logger.Infof("OSD %d is not ok-to-stop. will try updating it again later", osdIDQuery)
c.queue.Push(osdIDQuery) // push back onto queue to make sure we retry it later
return
}
}
}
logger.Debugf("updating OSDs: %v", osdIDs)
updatedDeployments := make([]*appsv1.Deployment, 0, len(osdIDs))
listIDs := []string{} // use this to build the k8s api selector query
for _, osdID := range osdIDs {
if !c.deployments.Exists(osdID) {
logger.Debugf("not updating deployment for OSD %d that is newly created", osdID)
continue
}
// osdIDQuery which has been popped off the queue but it does need to be updated
if osdID != osdIDQuery && !c.queue.Exists(osdID) {
logger.Debugf("not updating deployment for OSD %d that is not in the update queue. the OSD has already been updated", osdID)
continue
}
depName := deploymentName(osdID)
dep, err := c.cluster.context.Clientset.AppsV1().Deployments(c.cluster.clusterInfo.Namespace).Get(ctx, depName, metav1.GetOptions{})
if err != nil {
errs.addError("failed to update OSD %d. failed to find existing deployment %q. %v", osdID, depName, err)
continue
}
osdInfo, err := c.cluster.getOSDInfo(dep)
if err != nil {
errs.addError("failed to update OSD %d. failed to extract OSD info from existing deployment %q. %v", osdID, depName, err)
continue
}
nodeOrPVCName, err := getNodeOrPVCName(dep)
if err != nil {
errs.addError("%v", errors.Wrapf(err, "failed to update OSD %d", osdID))
continue
}
var updatedDep *appsv1.Deployment
if osdIsOnPVC(dep) {
logger.Infof("updating OSD %d on PVC %q", osdID, nodeOrPVCName)
updatedDep, err = deploymentOnPVCFunc(c.cluster, osdInfo, nodeOrPVCName, c.provisionConfig)
message := fmt.Sprintf("Processing OSD %d on PVC %q", osdID, nodeOrPVCName)
updateConditionFunc(c.cluster.context, c.cluster.clusterInfo.NamespacedName(), cephv1.ConditionProgressing, v1.ConditionTrue, cephv1.ClusterProgressingReason, message)
} else {
if !c.cluster.ValidStorage.NodeExists(nodeOrPVCName) {
// node will not reconcile, so don't update the deployment
// allow the OSD health checker to remove the OSD
logger.Warningf(
"not updating OSD %d on node %q. node no longer exists in the storage spec. "+
"if the user wishes to remove OSDs from the node, they must do so manually. "+
"Rook will not remove OSDs from nodes that are removed from the storage spec in order to prevent accidental data loss",
osdID, nodeOrPVCName)
continue
}
logger.Infof("updating OSD %d on node %q", osdID, nodeOrPVCName)
updatedDep, err = deploymentOnNodeFunc(c.cluster, osdInfo, nodeOrPVCName, c.provisionConfig)
message := fmt.Sprintf("Processing OSD %d on node %q", osdID, nodeOrPVCName)
updateConditionFunc(c.cluster.context, c.cluster.clusterInfo.NamespacedName(), cephv1.ConditionProgressing, v1.ConditionTrue, cephv1.ClusterProgressingReason, message)
}
if err != nil {
errs.addError("%v", errors.Wrapf(err, "failed to update OSD %d", osdID))
continue
}
updatedDeployments = append(updatedDeployments, updatedDep)
listIDs = append(listIDs, strconv.Itoa(osdID))
}
// when waiting on deployments to be updated, only list OSDs we intend to update specifically by ID
listFunc := c.cluster.getFuncToListDeploymentsWithIDs(listIDs)
failures := updateMultipleDeploymentsAndWaitFunc(c.cluster.context.Clientset, updatedDeployments, listFunc)
for _, f := range failures {
errs.addError("%v", errors.Wrapf(f.Error, "failed to update OSD deployment %q", f.ResourceName))
}
// If there were failures, don't retry them. If it's a transitory k8s/etcd issue, the next
// reconcile should succeed. If it's a different issue, it will always error.
c.queue.Remove(osdIDs)
}
// getOSDUpdateInfo returns an update queue of OSDs which need updated and an existence list of OSD
// Deployments which already exist.
func (c *Cluster) getOSDUpdateInfo(errs *provisionErrors) (*updateQueue, *existenceList, error) {
ctx := context.TODO()
namespace := c.clusterInfo.Namespace
selector := fmt.Sprintf("%s=%s", k8sutil.AppAttr, AppName)
listOpts := metav1.ListOptions{
// list only rook-ceph-osd Deployments
LabelSelector: selector,
}
deps, err := c.context.Clientset.AppsV1().Deployments(namespace).List(ctx, listOpts)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to query existing OSD deployments to see if they need updated")
}
updateQueue := newUpdateQueueWithCapacity(len(deps.Items))
existenceList := newExistenceListWithCapacity(len(deps.Items))
for i := range deps.Items {
id, err := getOSDID(&deps.Items[i]) // avoid implicit memory aliasing by indexing
if err != nil {
// add a question to the user AFTER the error text to help them recover from user error
errs.addError("%v. did a user create their own deployment with label %q?", selector, err)
continue
}
// all OSD deployments should be marked as existing
existenceList.Add(id)
updateQueue.Push(id)
}
return updateQueue, existenceList, nil
}
// An updateQueue keeps track of OSDs which need updated.
type updateQueue struct {
q []int // just a list of OSD IDs
}
// Create a new updateQueue with capacity reserved.
func newUpdateQueueWithCapacity(cap int) *updateQueue {
return &updateQueue{
q: make([]int, 0, cap),
}
}
func newUpdateQueueWithIDs(ids ...int) *updateQueue {
return &updateQueue{
q: ids,
}
}
// Len returns the length of the queue.
func (q *updateQueue) Len() int {
return len(q.q)
}
// Push pushes an item onto the end of the queue.
func (q *updateQueue) Push(osdID int) {
q.q = append(q.q, osdID)
}
// Pop pops an item off the beginning of the queue.
// Returns -1 and ok=false if the queue is empty. Otherwise, returns an OSD ID and ok=true.
func (q *updateQueue) Pop() (osdID int, ok bool) {
if q.Len() == 0 {
return -1, false
}
osdID = q.q[0]
q.q = q.q[1:]
return osdID, true
}
// Exists returns true if the item exists in the queue.
func (q *updateQueue) Exists(osdID int) bool {
for _, id := range q.q {
if id == osdID {
return true
}
}
return false
}
// Remove removes the items from the queue if they exist.
func (q *updateQueue) Remove(osdIDs []int) {
shouldRemove := func(rid int) bool {
for _, id := range osdIDs {
if id == rid {
return true
}
}
return false
}
lastIdx := 0
for idx, osdID := range q.q {
if !shouldRemove(osdID) {
// do removal by shifting slice items that should be kept into the next good position in
// the slice, and then reduce the slice capacity to match the number of kept items
q.q[lastIdx] = q.q[idx]
lastIdx++
}
}
q.q = q.q[:lastIdx]
}
// An existenceList keeps track of which OSDs already have Deployments created for them that is
// queryable in O(1) time.
type existenceList struct {
m map[int]bool
}
// Create a new existenceList with capacity reserved.
func newExistenceListWithCapacity(cap int) *existenceList {
return &existenceList{
m: make(map[int]bool, cap),
}
}
func newExistenceListWithIDs(ids ...int) *existenceList {
e := newExistenceListWithCapacity(len(ids))
for _, id := range ids {
e.Add(id)
}
return e
}
// Len returns the length of the existence list, the number of existing items.
func (e *existenceList) Len() int {
return len(e.m)
}
// Add adds an item to the existenceList.
func (e *existenceList) Add(osdID int) {
e.m[osdID] = true
}
// Exists returns true if an item is recorded in the existence list or false if it does not.
func (e *existenceList) Exists(osdID int) bool {
_, ok := e.m[osdID]
return ok
}
// return a function that will list only OSD deployments with the IDs given
func (c *Cluster) getFuncToListDeploymentsWithIDs(osdIDs []string) func() (*appsv1.DeploymentList, error) {
ctx := context.TODO()
selector := fmt.Sprintf("ceph-osd-id in (%s)", strings.Join(osdIDs, ", "))
listOpts := metav1.ListOptions{
LabelSelector: selector, // e.g. 'ceph-osd-id in (1, 3, 5, 7, 9)'
}
return func() (*appsv1.DeploymentList, error) {
return c.context.Clientset.AppsV1().Deployments(c.clusterInfo.Namespace).List(ctx, listOpts)
}
}
@@ -0,0 +1,713 @@
/*
Copyright 2021 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 osd
import (
"context"
"fmt"
"strconv"
"testing"
"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"
cephclientfake "github.com/rook/rook/pkg/daemon/ceph/client/fake"
"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"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
)
func Test_updateExistingOSDs(t *testing.T) {
namespace := "my-namespace"
logger.SetLevel(capnslog.DEBUG)
oldUpdateFunc := updateMultipleDeploymentsAndWaitFunc
oldNodeFunc := deploymentOnNodeFunc
oldPVCFunc := deploymentOnPVCFunc
oldConditionFunc := updateConditionFunc
oldShouldCheckFunc := shouldCheckOkToStopFunc
defer func() {
updateMultipleDeploymentsAndWaitFunc = oldUpdateFunc
deploymentOnNodeFunc = oldNodeFunc
deploymentOnPVCFunc = oldPVCFunc
updateConditionFunc = oldConditionFunc
shouldCheckOkToStopFunc = oldShouldCheckFunc
}()
var executor *exectest.MockExecutor // will be defined later
// inputs
var (
updateQueue *updateQueue
existingDeployments *existenceList
clientset *fake.Clientset
)
// behavior control
var (
updateInjectFailures k8sutil.Failures // return failures from mocked updateDeploymentAndWaitFunc
returnOkToStopIDs []int // return these IDs are ok-to-stop (or not ok to stop if empty)
forceUpgradeIfUnhealthy bool
)
// intermediates (created from inputs)
var (
ctx *clusterd.Context
c *Cluster
updateConfig *updateConfig
)
// outputs
var (
osdToBeQueried int // this OSD ID should be queried
deploymentsUpdated []string // updateDeploymentAndWaitFunc adds deployments to this list
osdsOnPVCs []int // deploymentOnPVCFunc adds OSD IDs to this list
osdsOnNodes []int // deploymentOnPVCFunc adds OSD IDs to this list
errs *provisionErrors
)
doSetup := func() {
// set up intermediates
ctx = &clusterd.Context{
Clientset: clientset,
Executor: executor,
}
clusterInfo := &cephclient.ClusterInfo{
Namespace: namespace,
CephVersion: cephver.Pacific,
}
clusterInfo.SetName("mycluster")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
spec := cephv1.ClusterSpec{
ContinueUpgradeAfterChecksEvenIfNotHealthy: forceUpgradeIfUnhealthy,
}
c = New(ctx, clusterInfo, spec, "rook/rook:master")
config := c.newProvisionConfig()
updateConfig = c.newUpdateConfig(config, updateQueue, existingDeployments)
// prepare outputs
deploymentsUpdated = []string{}
osdsOnPVCs = []int{}
osdsOnNodes = []int{}
errs = newProvisionErrors()
}
// stub out the conditionExportFunc to do nothing. we do not have a fake Rook interface that
// allows us to interact with a CephCluster resource like the fake K8s clientset.
updateConditionFunc = func(c *clusterd.Context, namespaceName types.NamespacedName, conditionType cephv1.ConditionType, status corev1.ConditionStatus, reason cephv1.ClusterReasonType, message string) {
// do nothing
}
shouldCheckOkToStopFunc = func(context *clusterd.Context, clusterInfo *cephclient.ClusterInfo) bool {
// always check. if shouldCheckOkToStop is not implemented correctly, single-node CI tests
// will fail, which is a more thorough test than we could make in unit tests.
return true
}
updateMultipleDeploymentsAndWaitFunc =
func(
clientset kubernetes.Interface,
deployments []*appsv1.Deployment,
listFunc func() (*appsv1.DeploymentList, error),
) k8sutil.Failures {
for _, d := range deployments {
deploymentsUpdated = append(deploymentsUpdated, d.Name)
}
return updateInjectFailures
}
executor = &exectest.MockExecutor{
MockExecuteCommandWithOutputFile: func(command string, outFileArg string, args ...string) (string, error) {
t.Logf("command: %s %v", command, args)
if fmt.Sprintf("%s %s %s", command, args[0], args[1]) != "ceph osd ok-to-stop" {
panic(fmt.Sprintf("unexpected command %q with args %v", command, args))
}
queriedID := args[2]
if strconv.Itoa(osdToBeQueried) != queriedID {
err := errors.Errorf("OSD %d should have been queried, but %s was queried instead", osdToBeQueried, queriedID)
t.Error(err)
return "", err
}
if len(returnOkToStopIDs) > 0 {
return cephclientfake.OsdOkToStopOutput(osdToBeQueried, returnOkToStopIDs, true), nil
}
return cephclientfake.OsdOkToStopOutput(osdToBeQueried, []int{}, true), errors.Errorf("induced error")
},
}
// simple wrappers to allow us to count how many OSDs on nodes/PVCs are identified
deploymentOnNodeFunc = func(c *Cluster, osd OSDInfo, nodeName string, config *provisionConfig) (*appsv1.Deployment, error) {
osdsOnNodes = append(osdsOnNodes, osd.ID)
return deploymentOnNode(c, osd, nodeName, config)
}
deploymentOnPVCFunc = func(c *Cluster, osd OSDInfo, pvcName string, config *provisionConfig) (*appsv1.Deployment, error) {
osdsOnPVCs = append(osdsOnPVCs, osd.ID)
return deploymentOnPVC(c, osd, pvcName, config)
}
addDeploymentOnNode := func(nodeName string, osdID int) {
d := getDummyDeploymentOnNode(clientset, c, nodeName, osdID)
_, err := clientset.AppsV1().Deployments(namespace).Create(context.TODO(), d, metav1.CreateOptions{})
if err != nil {
panic(err)
}
}
addDeploymentOnPVC := func(pvcName string, osdID int) {
d := getDummyDeploymentOnPVC(clientset, c, pvcName, osdID)
_, err := clientset.AppsV1().Deployments(namespace).Create(context.TODO(), d, metav1.CreateOptions{})
if err != nil {
panic(err)
}
}
t.Run("no items in the update queue should be a noop", func(t *testing.T) {
clientset = fake.NewSimpleClientset()
updateQueue = newUpdateQueueWithIDs()
existingDeployments = newExistenceListWithIDs(0, 2, 4, 6)
forceUpgradeIfUnhealthy = false
updateInjectFailures = k8sutil.Failures{}
doSetup()
addDeploymentOnNode("node0", 0)
addDeploymentOnPVC("pvc2", 2)
addDeploymentOnNode("node1", 4)
addDeploymentOnPVC("pvc6", 6)
osdToBeQueried = -1 // this will make any OSD query fail
updateConfig.updateExistingOSDs(errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, deploymentsUpdated, []string{})
})
t.Run("ok to stop one OSD at a time", func(t *testing.T) {
clientset = fake.NewSimpleClientset()
// reminder that updateQueue pops things off the queue from the front, so the leftmost item
// will be the one queried
updateQueue = newUpdateQueueWithIDs(0, 2, 4, 6)
existingDeployments = newExistenceListWithIDs(0, 2, 4, 6)
forceUpgradeIfUnhealthy = false
updateInjectFailures = k8sutil.Failures{}
doSetup()
addDeploymentOnNode("node0", 0)
addDeploymentOnPVC("pvc2", 2)
addDeploymentOnNode("node1", 4)
addDeploymentOnPVC("pvc6", 6)
for _, i := range []int{0, 2, 4, 6} {
osdToBeQueried = i
returnOkToStopIDs = []int{i}
deploymentsUpdated = []string{}
updateConfig.updateExistingOSDs(errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, deploymentsUpdated, []string{deploymentName(i)})
}
assert.ElementsMatch(t, osdsOnNodes, []int{0, 4})
assert.ElementsMatch(t, osdsOnPVCs, []int{2, 6})
assert.Equal(t, 0, updateQueue.Len()) // should be done with updates
})
t.Run("ok to stop 3 OSDs at a time", func(t *testing.T) {
clientset = fake.NewSimpleClientset()
updateQueue = newUpdateQueueWithIDs(0, 2, 4, 6)
existingDeployments = newExistenceListWithIDs(0, 2, 4, 6)
forceUpgradeIfUnhealthy = false
updateInjectFailures = k8sutil.Failures{}
doSetup()
addDeploymentOnNode("node0", 0)
addDeploymentOnPVC("pvc2", 2)
addDeploymentOnNode("node1", 4)
addDeploymentOnPVC("pvc6", 6)
osdToBeQueried = 0
returnOkToStopIDs = []int{0, 4, 6}
updateConfig.updateExistingOSDs(errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, deploymentsUpdated,
[]string{deploymentName(0), deploymentName(4), deploymentName(6)})
// should NOT be done with updates
// this also tests that updateQueue.Len() directly affects doneUpdating()
assert.Equal(t, 1, updateQueue.Len())
assert.False(t, updateConfig.doneUpdating())
deploymentsUpdated = []string{}
osdToBeQueried = 2
returnOkToStopIDs = []int{2}
updateConfig.updateExistingOSDs(errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, deploymentsUpdated,
[]string{deploymentName(2)})
// should be done with updates
// this also tests that updateQueue.Len() directly affects doneUpdating()
assert.Equal(t, 0, updateQueue.Len())
assert.True(t, updateConfig.doneUpdating())
})
t.Run("ok to stop more OSDs than are in the update queue", func(t *testing.T) {
clientset = fake.NewSimpleClientset()
updateQueue = newUpdateQueueWithIDs(2, 0)
existingDeployments = newExistenceListWithIDs(6, 4, 2, 0)
forceUpgradeIfUnhealthy = false
updateInjectFailures = k8sutil.Failures{}
doSetup()
addDeploymentOnNode("node0", 0)
addDeploymentOnPVC("pvc2", 2)
addDeploymentOnNode("node1", 4)
addDeploymentOnPVC("pvc6", 6)
osdToBeQueried = 2
returnOkToStopIDs = []int{2, 4, 6}
updateConfig.updateExistingOSDs(errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, deploymentsUpdated, []string{deploymentName(2)})
deploymentsUpdated = []string{}
osdToBeQueried = 0
returnOkToStopIDs = []int{0, 6}
updateConfig.updateExistingOSDs(errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, deploymentsUpdated, []string{deploymentName(0)})
assert.Equal(t, 0, updateQueue.Len()) // should be done with updates
})
t.Run("ok to stop OSDS not in existence list (newly-created OSDs)", func(t *testing.T) {
clientset = fake.NewSimpleClientset()
updateQueue = newUpdateQueueWithIDs(2, 0)
existingDeployments = newExistenceListWithIDs(2, 0)
forceUpgradeIfUnhealthy = false
updateInjectFailures = k8sutil.Failures{}
doSetup()
addDeploymentOnNode("node0", 0)
addDeploymentOnPVC("pvc2", 2)
addDeploymentOnNode("node1", 4)
addDeploymentOnPVC("pvc6", 6)
osdToBeQueried = 2
returnOkToStopIDs = []int{2, 4, 6}
updateConfig.updateExistingOSDs(errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, deploymentsUpdated, []string{deploymentName(2)})
deploymentsUpdated = []string{}
osdToBeQueried = 0
returnOkToStopIDs = []int{0, 6}
updateConfig.updateExistingOSDs(errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, deploymentsUpdated, []string{deploymentName(0)})
assert.Equal(t, 0, updateQueue.Len()) // should be done with updates
})
t.Run("not ok to stop OSD", func(t *testing.T) {
clientset = fake.NewSimpleClientset()
updateQueue = newUpdateQueueWithIDs(2)
existingDeployments = newExistenceListWithIDs(0, 2, 4, 6)
forceUpgradeIfUnhealthy = false
updateInjectFailures = k8sutil.Failures{}
doSetup()
addDeploymentOnNode("node0", 0)
addDeploymentOnPVC("pvc2", 2)
addDeploymentOnNode("node1", 4)
addDeploymentOnPVC("pvc6", 6)
osdToBeQueried = 2
returnOkToStopIDs = []int{} // not ok to stop
updateConfig.updateExistingOSDs(errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, deploymentsUpdated, []string{})
assert.Equal(t, 1, updateQueue.Len()) // the OSD should have been requeued
osdToBeQueried = 2
returnOkToStopIDs = []int{2}
updateConfig.updateExistingOSDs(errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, deploymentsUpdated, []string{deploymentName(2)})
assert.Equal(t, 0, updateQueue.Len()) // the OSD should now have been removed from the queue
})
t.Run("continueUpgradesAfterChecksEvenIfUnhealthy = true", func(t *testing.T) {
clientset = fake.NewSimpleClientset()
updateQueue = newUpdateQueueWithIDs(2)
existingDeployments = newExistenceListWithIDs(0, 2, 4, 6)
forceUpgradeIfUnhealthy = true // FORCE UPDATES
updateInjectFailures = k8sutil.Failures{}
doSetup()
addDeploymentOnNode("node0", 0)
addDeploymentOnPVC("pvc2", 2)
addDeploymentOnNode("node1", 4)
addDeploymentOnPVC("pvc6", 6)
osdToBeQueried = 2
returnOkToStopIDs = []int{} // NOT ok-to-stop
updateConfig.updateExistingOSDs(errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, deploymentsUpdated, []string{deploymentName(2)})
assert.Equal(t, 0, updateQueue.Len()) // should be done with updates
})
t.Run("failures updating deployments", func(t *testing.T) {
clientset = fake.NewSimpleClientset()
updateQueue = newUpdateQueueWithIDs(0, 2, 4, 6)
existingDeployments = newExistenceListWithIDs(0, 2, 4, 6)
forceUpgradeIfUnhealthy = false
updateInjectFailures = k8sutil.Failures{}
doSetup()
addDeploymentOnNode("node0", 0)
addDeploymentOnPVC("pvc2", 2)
addDeploymentOnNode("node1", 4)
addDeploymentOnPVC("pvc6", 6)
osdToBeQueried = 0
returnOkToStopIDs = []int{0, 6}
updateInjectFailures = k8sutil.Failures{
{ResourceName: deploymentName(6), Error: errors.Errorf("induced failure updating OSD 6")},
}
updateConfig.updateExistingOSDs(errs)
assert.Equal(t, 1, errs.len())
assert.ElementsMatch(t, deploymentsUpdated,
[]string{deploymentName(0), deploymentName(6)})
deploymentsUpdated = []string{}
osdToBeQueried = 2
returnOkToStopIDs = []int{2, 4}
updateInjectFailures = k8sutil.Failures{
{ResourceName: deploymentName(2), Error: errors.Errorf("induced failure updating OSD 2")},
{ResourceName: deploymentName(4), Error: errors.Errorf("induced failure waiting for OSD 4")},
}
updateConfig.updateExistingOSDs(errs)
assert.Equal(t, 3, errs.len()) // errors should be appended to the same provisionErrors struct
assert.ElementsMatch(t, deploymentsUpdated,
[]string{deploymentName(2), deploymentName(4)})
assert.Zero(t, updateQueue.Len()) // errors should not be requeued
})
t.Run("failure due to OSD deployment with bad info", func(t *testing.T) {
clientset = fake.NewSimpleClientset()
updateQueue = newUpdateQueueWithIDs(0, 6)
existingDeployments = newExistenceListWithIDs(0, 2, 4, 6)
forceUpgradeIfUnhealthy = false
updateInjectFailures = k8sutil.Failures{}
doSetup()
addDeploymentOnNode("node0", 0)
addDeploymentOnPVC("pvc2", 2)
addDeploymentOnNode("node1", 4)
addDeploymentOnPVC("pvc6", 6)
// give OSD 6 bad info by removing env vars from primary container
deploymentClient := clientset.AppsV1().Deployments(namespace)
d, err := deploymentClient.Get(context.TODO(), deploymentName(6), metav1.GetOptions{})
if err != nil {
panic(err)
}
d.Spec.Template.Spec.Containers[0].Env = []corev1.EnvVar{}
_, err = deploymentClient.Update(context.TODO(), d, metav1.UpdateOptions{})
if err != nil {
panic(err)
}
deploymentsUpdated = []string{}
osdToBeQueried = 0
returnOkToStopIDs = []int{0, 6}
updateConfig.updateExistingOSDs(errs)
assert.Equal(t, 1, errs.len())
assert.ElementsMatch(t, deploymentsUpdated,
[]string{deploymentName(0)})
assert.Zero(t, updateQueue.Len()) // errors should not be requeued
})
t.Run("do not update OSDs on nodes removed from the storage spec", func(t *testing.T) {
clientset = fake.NewSimpleClientset()
// reminder that updateQueue pops things off the queue from the front, so the leftmost item
// will be the one queried
updateQueue = newUpdateQueueWithIDs(0, 4)
existingDeployments = newExistenceListWithIDs(0, 4)
forceUpgradeIfUnhealthy = false
updateInjectFailures = k8sutil.Failures{}
doSetup()
addDeploymentOnNode("node0", 0)
addDeploymentOnNode("node1", 4)
// Remove "node0" from valid storage (user no longer wants it)
assert.Equal(t, "node0", c.ValidStorage.Nodes[0].Name)
c.ValidStorage.Nodes = c.ValidStorage.Nodes[1:]
t.Logf("valid storage nodes: %+v", c.ValidStorage.Nodes)
osdToBeQueried = 0
returnOkToStopIDs = []int{0, 4}
updateConfig.updateExistingOSDs(errs)
assert.Zero(t, errs.len())
assert.ElementsMatch(t, deploymentsUpdated, []string{deploymentName(4)})
assert.ElementsMatch(t, osdsOnNodes, []int{4})
assert.ElementsMatch(t, osdsOnPVCs, []int{})
assert.Equal(t, 0, updateQueue.Len()) // should be done with updates
})
}
func Test_getOSDUpdateInfo(t *testing.T) {
namespace := "rook-ceph"
cephImage := "ceph/ceph:v15"
// NOTE: all tests share the same clientset
clientset := fake.NewSimpleClientset()
ctx := &clusterd.Context{
Clientset: clientset,
}
clusterInfo := &cephclient.ClusterInfo{
Namespace: namespace,
CephVersion: cephver.Nautilus,
}
clusterInfo.SetName("mycluster")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
spec := cephv1.ClusterSpec{
CephVersion: cephv1.CephVersionSpec{Image: cephImage},
}
c := New(ctx, clusterInfo, spec, "rook/rook:master")
var errs *provisionErrors
var d *appsv1.Deployment
t.Run("cluster with no existing deployments", func(t *testing.T) {
errs = newProvisionErrors()
updateQueue, existenceList, err := c.getOSDUpdateInfo(errs)
assert.NoError(t, err)
assert.Zero(t, errs.len())
assert.Zero(t, updateQueue.Len())
assert.Zero(t, existenceList.Len())
})
t.Run("cluster in namespace with existing deployments, but none are OSDs", func(t *testing.T) {
// random deployment in this namespace
addTestDeployment(clientset, "non-rook-deployment", namespace, map[string]string{})
// mon.a in this namespace
l := controller.CephDaemonAppLabels("rook-ceph-mon", namespace, "mon", "a", true)
addTestDeployment(clientset, "rook-ceph-mon-a", namespace, l)
// osd.1 and 3 in another namespace (another Rook cluster)
clusterInfo2 := &cephclient.ClusterInfo{
Namespace: "other-namespace",
CephVersion: cephver.Nautilus,
}
clusterInfo2.SetName("other-cluster")
clusterInfo2.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
c2 := New(ctx, clusterInfo2, spec, "rook/rook:master")
// osd.1 on PVC in "other-namespace"
d = getDummyDeploymentOnPVC(clientset, c2, "pvc1", 1)
createDeploymentOrPanic(clientset, d)
// osd.3 on Node in "other-namespace"
d = getDummyDeploymentOnNode(clientset, c2, "node3", 3)
createDeploymentOrPanic(clientset, d)
errs = newProvisionErrors()
updateQueue, existenceList, err := c.getOSDUpdateInfo(errs)
assert.NoError(t, err)
assert.Zero(t, errs.len())
assert.Zero(t, updateQueue.Len())
assert.Zero(t, existenceList.Len())
})
t.Run("cluster in namespace with existing OSD deployments", func(t *testing.T) {
// osd.0 on PVC in this namespace
d = getDummyDeploymentOnPVC(clientset, c, "pvc0", 0)
createDeploymentOrPanic(clientset, d)
// osd.2 on Node in this namespace
d = getDummyDeploymentOnNode(clientset, c, "node2", 2)
createDeploymentOrPanic(clientset, d)
errs = newProvisionErrors()
updateQueue, existenceList, err := c.getOSDUpdateInfo(errs)
assert.NoError(t, err)
assert.Zero(t, errs.len())
assert.Equal(t, 2, updateQueue.Len())
assert.True(t, updateQueue.Exists(0))
assert.True(t, updateQueue.Exists(2))
assert.Equal(t, 2, existenceList.Len())
assert.True(t, existenceList.Exists(0))
assert.True(t, existenceList.Exists(2))
})
t.Run("existing OSD deployment with no OSD ID", func(t *testing.T) {
l := map[string]string{k8sutil.AppAttr: AppName}
addTestDeployment(clientset, "rook-ceph-osd-NOID", namespace, l)
errs = newProvisionErrors()
updateQueue, existenceList, err := c.getOSDUpdateInfo(errs)
assert.NoError(t, err)
assert.Equal(t, 1, errs.len())
// should have same update queue and existence list as last test
assert.Equal(t, 2, updateQueue.Len())
assert.Equal(t, 2, existenceList.Len())
})
t.Run("failure to list OSD deployments", func(t *testing.T) {
// reset the test to check that an error is reported if listing OSD deployments fails
test.PrependFailReactor(t, clientset, "list", "deployments")
ctx = &clusterd.Context{
Clientset: clientset,
}
c = New(ctx, clusterInfo, spec, "rook/rook:master")
errs = newProvisionErrors()
_, _, err := c.getOSDUpdateInfo(errs)
fmt.Println(err)
assert.Error(t, err)
})
}
func addTestDeployment(clientset *fake.Clientset, name, namespace string, labels map[string]string) {
d := &appsv1.Deployment{}
d.SetName(name)
d.SetNamespace(namespace)
d.SetLabels(labels)
createDeploymentOrPanic(clientset, d)
}
func createDeploymentOrPanic(clientset *fake.Clientset, d *appsv1.Deployment) {
_, err := clientset.AppsV1().Deployments(d.Namespace).Create(context.TODO(), d, metav1.CreateOptions{})
if err != nil {
panic(err)
}
}
func Test_updateQueue(t *testing.T) {
q := newUpdateQueueWithCapacity(2)
assert.Equal(t, 2, cap(q.q))
assert.Zero(t, q.Len())
testPop := func(osdID int) {
t.Helper()
id, ok := q.Pop()
assert.Equal(t, osdID, id)
assert.True(t, ok)
}
assertEmpty := func() {
t.Helper()
id, ok := q.Pop()
assert.Equal(t, -1, id)
assert.False(t, ok)
}
// assert empty behavior initially
assertEmpty()
// test basic functionality
q.Push(0)
assert.Equal(t, 1, q.Len())
testPop(0)
assertEmpty()
// test that queue can hold more items than initial capacity
// and that items are Pushed/Popped in FIFO order
q.Push(1)
q.Push(2)
q.Push(3)
assert.Equal(t, 3, q.Len())
assert.True(t, q.Exists(1))
assert.True(t, q.Exists(2))
assert.True(t, q.Exists(3))
testPop(1)
testPop(2)
testPop(3)
assertEmpty()
// Test removing queue items via q.Remove
for _, i := range []int{4, 5, 6, 7, 8} {
q.Push(i)
assert.True(t, q.Exists(i))
}
assert.Equal(t, 5, q.Len())
q.Remove([]int{
1, 2, 3, // non-existent items shouldn't affect queue
4, // remove first item
6, // remove a middle item
8, // remove last item
})
assert.Equal(t, 2, q.Len())
assert.False(t, q.Exists(1))
assert.False(t, q.Exists(2))
assert.False(t, q.Exists(3))
assert.True(t, q.Exists(5))
assert.False(t, q.Exists(6))
assert.True(t, q.Exists(7))
assert.False(t, q.Exists(8))
testPop(5)
// items pushed back onto the queue after removal should not get old values
q.Push(9)
q.Push(10)
assert.Equal(t, 3, q.Len())
assert.False(t, q.Exists(5))
assert.True(t, q.Exists(7))
assert.True(t, q.Exists(9))
assert.True(t, q.Exists(10))
testPop(7)
testPop(9)
testPop(10)
assertEmpty()
}
func Test_existenceList(t *testing.T) {
l := newExistenceListWithCapacity(2)
// Assert zero item does not exist initially
assert.False(t, l.Exists(0))
assert.Zero(t, l.Len())
// Assert basic functionality
l.Add(1)
assert.True(t, l.Exists(1))
assert.False(t, l.Exists(0))
assert.False(t, l.Exists(2))
assert.Equal(t, 1, l.Len())
// assert that more items can be added than initial capacity
l.Add(0)
l.Add(2)
l.Add(3)
assert.True(t, l.Exists(0))
assert.True(t, l.Exists(1)) // 1 should still exist from before
assert.True(t, l.Exists(2))
assert.True(t, l.Exists(3))
assert.False(t, l.Exists(4))
assert.Equal(t, 4, l.Len())
// assert that the same item can be added twice (though this should never happen for OSDs IRL)
l.Add(1)
assert.True(t, l.Exists(1))
assert.Equal(t, 4, l.Len())
}
+5 -9
View File
@@ -132,23 +132,19 @@ func watchControllerPredicate(rookContext *clusterd.Context) predicate.Funcs {
}
diff := cmp.Diff(objOld.Spec, objNew.Spec, resourceQtyComparer)
if diff != "" {
// Set the cancellation flag to stop any ongoing orchestration
rookContext.RequestCancelOrchestration.Set()
logger.Infof("CR has changed for %q. diff=%s", objNew.Name, diff)
// If spec Images are different we cancel any on-going orchestration
if objOld.Spec.CephVersion.Image != objNew.Spec.CephVersion.Image {
// Set the cancellation flag to stop any ongoing orchestration
rookContext.RequestCancelOrchestration.Set()
logger.Info("upgrade requested, cancelling any ongoing orchestration")
}
return true
} else if objOld.GetDeletionTimestamp() != objNew.GetDeletionTimestamp() {
// Set the cancellation flag to stop any ongoing orchestration
rookContext.RequestCancelOrchestration.Set()
logger.Infof("CR %q is going be deleted, cancelling any ongoing orchestration", objNew.Name)
return true
} else if objOld.GetGeneration() != objNew.GetGeneration() {
logger.Debugf("skipping resource %q update with unchanged spec", objNew.Name)
}
+1 -1
View File
@@ -385,7 +385,7 @@ func CheckPodMemory(name string, resources v1.ResourceRequirements, cephPodMinim
// This means LIMIT and REQUEST are either identical or different but still we use LIMIT as a reference
if uint64(podMemoryLimit.Value()) < display.MbTob(cephPodMinimumMemory) {
// allow the configuration if less than the min, but print a warning
logger.Warningf("running the %q daemon(s) with %dmb of ram, but at least %dmb is recommended", name, display.BToMb(uint64(podMemoryLimit.Value())), cephPodMinimumMemory)
logger.Warningf("running the %q daemon(s) with %dMB of ram, but at least %dMB is recommended", name, display.BToMb(uint64(podMemoryLimit.Value())), cephPodMinimumMemory)
}
// This means LIMIT < REQUEST
+2 -2
View File
@@ -450,7 +450,7 @@ func startDrivers(clientset kubernetes.Interface, rookclientset rookclient.Inter
if err != nil {
return errors.Wrapf(err, "failed to apply network config to rbd plugin provisioner deployment: %+v", rbdProvisionerDeployment)
}
err = k8sutil.CreateDeployment(clientset, csiRBDProvisioner, namespace, rbdProvisionerDeployment)
_, err = k8sutil.CreateOrUpdateDeployment(clientset, rbdProvisionerDeployment)
if err != nil {
return errors.Wrapf(err, "failed to start rbd provisioner deployment: %+v", rbdProvisionerDeployment)
}
@@ -510,7 +510,7 @@ func startDrivers(clientset kubernetes.Interface, rookclientset rookclient.Inter
if err != nil {
return errors.Wrapf(err, "failed to apply network config to cephfs plugin provisioner deployment: %+v", cephfsProvisionerDeployment)
}
err = k8sutil.CreateDeployment(clientset, csiCephFSProvisioner, namespace, cephfsProvisionerDeployment)
_, err = k8sutil.CreateOrUpdateDeployment(clientset, cephfsProvisionerDeployment)
if err != nil {
return errors.Wrapf(err, "failed to start cephfs provisioner deployment: %+v", cephfsProvisionerDeployment)
}
+1 -1
View File
@@ -130,7 +130,7 @@ func createWebhookDeployment(ctx context.Context, context *clusterd.Context, adm
antiAffinity := csi.GetPodAntiAffinity(k8sutil.AppAttr, appName)
admissionControllerDeployment := getDeployment(ctx, context, secretVolume, antiAffinity, admissionImage, admission_parameters, secretVolumeMount)
err := k8sutil.CreateDeployment(context.Clientset, appName, namespace, &admissionControllerDeployment)
_, err := k8sutil.CreateOrUpdateDeployment(context.Clientset, &admissionControllerDeployment)
if err != nil {
return errors.Wrap(err, "failed to create admission-controller deployment")
}
+4 -5
View File
@@ -20,15 +20,14 @@ import (
"context"
"fmt"
apps "k8s.io/api/apps/v1"
v1 "k8s.io/api/apps/v1"
appsv1 "k8s.io/api/apps/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
// CreateDaemonSet creates
func CreateDaemonSet(name, namespace string, clientset kubernetes.Interface, ds *apps.DaemonSet) error {
func CreateDaemonSet(name, namespace string, clientset kubernetes.Interface, ds *appsv1.DaemonSet) error {
ctx := context.TODO()
_, err := clientset.AppsV1().DaemonSets(namespace).Create(ctx, ds, metav1.CreateOptions{})
if err != nil {
@@ -57,7 +56,7 @@ func DeleteDaemonset(clientset kubernetes.Interface, namespace, name string) err
// AddRookVersionLabelToDaemonSet adds or updates a label reporting the Rook version which last
// modified a DaemonSet.
func AddRookVersionLabelToDaemonSet(d *v1.DaemonSet) {
func AddRookVersionLabelToDaemonSet(d *appsv1.DaemonSet) {
if d == nil {
return
}
@@ -70,7 +69,7 @@ func AddRookVersionLabelToDaemonSet(d *v1.DaemonSet) {
// GetDaemonsets returns a list of daemonsets names labels matching a given selector
// example of a label selector might be "app=rook-ceph-mon, mon!=b"
// more: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
func GetDaemonsets(clientset kubernetes.Interface, namespace, labelSelector string) (*apps.DaemonSetList, error) {
func GetDaemonsets(clientset kubernetes.Interface, namespace, labelSelector string) (*appsv1.DaemonSetList, error) {
listOptions := metav1.ListOptions{LabelSelector: labelSelector}
ctx := context.TODO()
daemonsets, err := clientset.AppsV1().DaemonSets(namespace).List(ctx, listOptions)
+278 -15
View File
@@ -22,10 +22,10 @@ import (
"time"
"github.com/banzaicloud/k8s-objectmatcher/patch"
"github.com/pkg/errors"
"github.com/rook/rook/pkg/clusterd"
apps "k8s.io/api/apps/v1"
v1 "k8s.io/api/apps/v1"
"github.com/rook/rook/pkg/util"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
@@ -33,6 +33,11 @@ import (
"k8s.io/client-go/kubernetes"
)
var (
waitForDeploymentPeriod = 2 * time.Second
waitForDeploymentTimeout = 60 * time.Second
)
// GetDeploymentImage returns the version of the image running in the pod spec for the desired container
func GetDeploymentImage(clientset kubernetes.Interface, namespace, name, container string) (string, error) {
ctx := context.TODO()
@@ -44,7 +49,7 @@ func GetDeploymentImage(clientset kubernetes.Interface, namespace, name, contain
}
// GetDeploymentSpecImage returns the image name from the spec
func GetDeploymentSpecImage(clientset kubernetes.Interface, d apps.Deployment, container string, initContainer bool) (string, error) {
func GetDeploymentSpecImage(clientset kubernetes.Interface, d appsv1.Deployment, container string, initContainer bool) (string, error) {
image, err := GetSpecContainerImage(d.Spec.Template.Spec, container, initContainer)
if err != nil {
return "", err
@@ -61,7 +66,7 @@ func GetDeploymentSpecImage(clientset kubernetes.Interface, d apps.Deployment, c
// 2. verify that we can continue the update procedure
// Basically, we go one resource by one and check if we can stop and then if the resource has been successfully updated
// we check if we can go ahead and move to the next one.
func UpdateDeploymentAndWait(clusterContext *clusterd.Context, modifiedDeployment *apps.Deployment, namespace string, verifyCallback func(action string) error) error {
func UpdateDeploymentAndWait(clusterContext *clusterd.Context, modifiedDeployment *appsv1.Deployment, namespace string, verifyCallback func(action string) error) error {
ctx := context.TODO()
currentDeployment, err := clusterContext.Clientset.AppsV1().Deployments(namespace).Get(ctx, modifiedDeployment.Name, metav1.GetOptions{})
if err != nil {
@@ -111,7 +116,7 @@ func UpdateDeploymentAndWait(clusterContext *clusterd.Context, modifiedDeploymen
return nil
}
func WaitForDeploymentToStart(clusterdContext *clusterd.Context, deployment *v1.Deployment) error {
func WaitForDeploymentToStart(clusterdContext *clusterd.Context, deployment *appsv1.Deployment) error {
// wait for the deployment to be restarted up to 300s
sleepTime := 3
attempts := 100
@@ -129,7 +134,7 @@ func WaitForDeploymentToStart(clusterdContext *clusterd.Context, deployment *v1.
// If ProgressDeadlineExceeded is reached let's fail earlier
// This can happen if one of the deployment cannot be scheduled on a node and stays in "pending" state
for _, condition := range d.Status.Conditions {
if condition.Type == v1.DeploymentProgressing && condition.Reason == "ProgressDeadlineExceeded" {
if condition.Type == appsv1.DeploymentProgressing && condition.Reason == "ProgressDeadlineExceeded" {
return fmt.Errorf("gave up waiting for deployment %q to update because %q", deployment.Name, condition.Reason)
}
}
@@ -140,10 +145,232 @@ func WaitForDeploymentToStart(clusterdContext *clusterd.Context, deployment *v1.
return fmt.Errorf("gave up waiting for deployment %q to update", deployment.Name)
}
// DeploymentNames returns a list of the names of deployments in the deployment list
func DeploymentNames(deployments *appsv1.DeploymentList) (names []string) {
for _, d := range deployments.Items {
names = append(names, d.Name)
}
return names
}
// DeploymentsUpdated is a mapping from deployment name to the observed generation of the old
// deployment which was updated.
type DeploymentsUpdated map[string]int64
// Names returns the names of the deployments which were updated.
func (d *DeploymentsUpdated) Names() (names []string) {
for name := range *d {
names = append(names, name)
}
return names
}
type Failure struct {
ResourceName string
Error error
}
type Failures []Failure
func (failures *Failures) CollatedErrors() error {
var err error
for _, f := range *failures {
err = fmt.Errorf("%v. %v", f.Error, err)
}
return err
}
// UpdateMultipleDeployments updates multiple deployments and returns DeploymentsUpdated map of
// deployment names which were updated successfully and the observed generation of the deployments
// before they were updated. If deployments are already up to date, they are not reported in the
// DeploymentsUpdated map.
// The DeploymentsUpdated map can be used with the WaitForDeploymentsToUpdate function.
// Also returns a list of failures. Each failure returned includes the name of the deployment which
// could not be updated and the error experienced when attempting to update the deployment.
func UpdateMultipleDeployments(
clientset kubernetes.Interface,
deployments []*appsv1.Deployment,
) (DeploymentsUpdated, Failures, *int32) {
deploymentsUpdated := DeploymentsUpdated{}
failures := Failures{}
var maxProgressDeadlineSeconds *int32
for _, dep := range deployments {
oldDep, newDep, err := updateDeployment(clientset, dep)
if err != nil {
failures = append(failures, Failure{
ResourceName: dep.Name,
Error: errors.Wrapf(err, "failed to update deployment %q", dep.Name),
})
continue
}
if newDep == nil {
// deployment was not updated
continue
}
deploymentsUpdated[newDep.Name] = oldDep.Status.ObservedGeneration
maxProgressDeadlineSeconds = maxInt32Ptr(maxProgressDeadlineSeconds, oldDep.Spec.ProgressDeadlineSeconds)
maxProgressDeadlineSeconds = maxInt32Ptr(maxProgressDeadlineSeconds, newDep.Spec.ProgressDeadlineSeconds)
}
return deploymentsUpdated, failures, maxProgressDeadlineSeconds
}
// WaitForDeploymentsToUpdate waits for all deployments to update. It returns a list of failures.
// Each failure includes the name of the deployment which was not updated within the timeout and an
// error indicating the reason why.
func WaitForDeploymentsToUpdate(
deploymentsUpdated DeploymentsUpdated,
progressDeadlineSeconds *int32,
listFunc func() (*appsv1.DeploymentList, error),
) Failures {
// do not modify the input!
waitingOn := DeploymentsUpdated{}
for k, v := range deploymentsUpdated {
waitingOn[k] = v
}
failures := Failures{}
waitFunc := func() (done bool, err error) {
deployments, err := listFunc()
if err != nil {
return false, errors.Wrap(err, "failed to list deployments")
}
if len(deployments.Items) < len(waitingOn) {
// This could be because the listFunc is written incorrectly
logger.Warningf(
"number of deployments listed (%d) is less than number of deployments we are waiting on to be updated (%d). listed: %+v. waiting on: %+v. ",
len(deployments.Items), len(waitingOn), DeploymentNames(deployments), waitingOn.Names())
}
for i, dep := range deployments.Items { // deployment loop
oldGeneration, ok := waitingOn[dep.Name]
if !ok {
// we are not waiting on this deployment to finish
continue
}
// If ProgressDeadlineExceeded is reached, fail earlier. This can happen if a deployment
// cannot be scheduled on a node and stays in "pending" state.
// Index deployment to prevent implicit memory aliasing
if err := progressDeadlineExceeded(&deployments.Items[i]); err != nil {
failures = append(failures, Failure{
ResourceName: dep.Name,
Error: errors.Wrapf(err, "progress deadline exceeded on deployment %q", dep.Name),
})
delete(waitingOn, dep.Name) // don't keep waiting on this deployment
continue // deployment loop
}
// Index deployment to prevent implicit memory aliasing
if deploymentIsDoneUpdating(&deployments.Items[i], oldGeneration) {
delete(waitingOn, dep.Name) // done waiting on this deployment
}
}
if len(waitingOn) == 0 {
return true, nil
}
return false, nil
}
timeout := waitForDeploymentTimeout
if progressDeadlineSeconds != nil {
// make the timeout double the progress deadline since the pod is both stopping and starting
timeout = 2 * time.Duration(*progressDeadlineSeconds) * time.Second
}
err := util.RetryWithTimeout(waitFunc, waitForDeploymentPeriod, timeout, "deployments to be updated")
if err != nil {
// the retry function doesn't return (true, error), so this must be a timeout error
logger.Errorf("%v", err)
}
// process remaining items in the waitingOn list to mark which deployments timed out
for depName := range waitingOn {
failures = append(failures, Failure{
ResourceName: depName,
Error: errors.Errorf("timed out waiting on deployment %q", depName),
})
}
return failures
}
func UpdateMultipleDeploymentsAndWait(
clientset kubernetes.Interface,
deployments []*appsv1.Deployment,
listFunc func() (*appsv1.DeploymentList, error),
) Failures {
depsUpdated, updateFailures, maxProgressDeadline := UpdateMultipleDeployments(clientset, deployments)
waitFailures := WaitForDeploymentsToUpdate(depsUpdated, maxProgressDeadline, listFunc)
return append(updateFailures, waitFailures...)
}
func deploymentIsDoneUpdating(d *appsv1.Deployment, oldObservedGeneration int64) bool {
return d.Status.ObservedGeneration != oldObservedGeneration && d.Status.UpdatedReplicas > 0 && d.Status.ReadyReplicas > 0
}
// return error if progress deadline exceeded condition on the deployment
func progressDeadlineExceeded(d *appsv1.Deployment) error {
for _, condition := range d.Status.Conditions {
if condition.Type == appsv1.DeploymentProgressing && condition.Reason == "ProgressDeadlineExceeded" {
return fmt.Errorf("gave up waiting for deployment %q to update because %q", d.Name, condition.Reason)
}
}
return nil
}
func updateDeployment(
clientset kubernetes.Interface,
deployment *appsv1.Deployment,
) (oldDeployment, newDeployment *appsv1.Deployment, err error) {
ctx := context.TODO()
namespace := deployment.Namespace
oldDeployment, err = clientset.AppsV1().Deployments(namespace).Get(ctx, deployment.Name, metav1.GetOptions{})
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to get existing deployment %q", deployment.Name)
}
// Check whether the current deployment and newly generated one are identical
patchChanged := false
patchResult, err := patch.DefaultPatchMaker.Calculate(oldDeployment, deployment)
if err != nil {
logger.Warningf("failed to calculate diff between current deployment %q and newly generated one. assuming it changed. %v", oldDeployment.Name, err)
patchChanged = true
} else if !patchResult.IsEmpty() {
patchChanged = true
}
if patchChanged {
// Set hash annotation to the newly generated deployment
err := patch.DefaultAnnotator.SetLastAppliedAnnotation(deployment)
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to set hash annotation on deployment %q", deployment.Name)
}
newDeployment, err := clientset.AppsV1().Deployments(namespace).Update(ctx, deployment, metav1.UpdateOptions{})
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to update deployment %q", deployment.Name)
}
return oldDeployment, newDeployment, nil
}
logger.Debugf("deployment %q did not change. nothing to update", deployment.Name)
return oldDeployment, nil, nil
}
// GetDeployments returns a list of deployment names labels matching a given selector
// example of a label selector might be "app=rook-ceph-mon, mon!=b"
// more: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
func GetDeployments(clientset kubernetes.Interface, namespace, labelSelector string) (*apps.DeploymentList, error) {
func GetDeployments(clientset kubernetes.Interface, namespace, labelSelector string) (*appsv1.DeploymentList, error) {
listOptions := metav1.ListOptions{LabelSelector: labelSelector}
ctx := context.TODO()
deployments, err := clientset.AppsV1().Deployments(namespace).List(ctx, listOptions)
@@ -208,7 +435,7 @@ func WaitForDeploymentImage(clientset kubernetes.Interface, namespace, label, co
// AddRookVersionLabelToDeployment adds or updates a label reporting the Rook version which last
// modified a deployment.
func AddRookVersionLabelToDeployment(d *v1.Deployment) {
func AddRookVersionLabelToDeployment(d *appsv1.Deployment) {
if d == nil {
return
}
@@ -225,7 +452,7 @@ func AddRookVersionLabelToObjectMeta(meta *metav1.ObjectMeta) {
addRookVersionLabel(meta.Labels)
}
func AddLabelToDeployment(key, value string, d *v1.Deployment) {
func AddLabelToDeployment(key, value string, d *appsv1.Deployment) {
if d == nil {
return
}
@@ -259,16 +486,52 @@ func addLabel(key, value string, labels map[string]string) {
labels[key] = value
}
func CreateDeployment(clientset kubernetes.Interface, name, namespace string, dep *apps.Deployment) error {
// CreateDeployment creates a deployment with a last applied hash annotation added
func CreateDeployment(clientset kubernetes.Interface, dep *appsv1.Deployment) (*appsv1.Deployment, error) {
ctx := context.TODO()
_, err := clientset.AppsV1().Deployments(namespace).Create(ctx, dep, metav1.CreateOptions{})
// Set hash annotation to the newly generated deployment
err := patch.DefaultAnnotator.SetLastAppliedAnnotation(dep)
if err != nil {
return nil, errors.Wrapf(err, "failed to set hash annotation on deployment %q", dep.Name)
}
return clientset.AppsV1().Deployments(dep.Namespace).Create(ctx, dep, metav1.CreateOptions{})
}
func CreateOrUpdateDeployment(clientset kubernetes.Interface, dep *appsv1.Deployment) (*appsv1.Deployment, error) {
ctx := context.TODO()
newDep, err := CreateDeployment(clientset, dep)
if err != nil {
if k8serrors.IsAlreadyExists(err) {
_, err = clientset.AppsV1().Deployments(namespace).Update(ctx, dep, metav1.UpdateOptions{})
// annotation was added in CreateDeployment to dep passed by reference
newDep, err = clientset.AppsV1().Deployments(dep.Namespace).Update(ctx, dep, metav1.UpdateOptions{})
}
if err != nil {
return fmt.Errorf("failed to start %s deployment: %+v\n%+v", name, err, dep)
return nil, errors.Wrapf(err, "failed to create or update deployment %q: %+v", dep.Name, dep)
}
}
return err
return newDep, nil
}
func maxInt32Ptr(a, b *int32) *int32 {
var ret int32
if a == nil && b == nil {
return nil
}
if a == nil && b != nil {
ret = *b
return &ret
}
if a != nil && b == nil {
ret = *a
return &ret
}
if *b > *a {
ret = *b
return &ret
}
ret = *a
return &ret
}
+482
View File
@@ -0,0 +1,482 @@
/*
Copyright 2018 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 k8sutil
import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
)
func TestUpdateMultipleDeploymentsAndWait(t *testing.T) {
// UpdateMultipleDeployments and WaitForDeploymentsToUpdate are thoroughly tested
// UpdateMultipleDeploymentsAndWait is a simple wrapper for both of their functionality
// Merely do basic integration testing on this to ensure they are connected properly
namespace := "ns"
oldPeriod := waitForDeploymentPeriod
oldTimeout := waitForDeploymentTimeout
defer func() {
waitForDeploymentPeriod = oldPeriod
waitForDeploymentTimeout = oldTimeout
}()
waitForDeploymentPeriod = 1 * time.Millisecond
waitForDeploymentTimeout = 3 * time.Millisecond
// inputs
var clientset *fake.Clientset
var deployments []*appsv1.Deployment
var listFunc func() (*appsv1.DeploymentList, error)
// outputs
var failures Failures
// a barebones deployment
deployment := func(name string) *appsv1.Deployment {
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
}
}
// a barebones deployment with some modification
modifiedDeployment := func(name string) *appsv1.Deployment {
d := deployment(name)
d.Spec.Selector = &metav1.LabelSelector{
MatchLabels: map[string]string{"hi": "mom"},
}
return d
}
timesCalled := 0
// generate a status that is not ready when first called but becomes ready later
status := func(timesCalled int) appsv1.DeploymentStatus {
if timesCalled < 0 {
timesCalled = 0
}
return appsv1.DeploymentStatus{
ObservedGeneration: int64(timesCalled),
UpdatedReplicas: int32(timesCalled),
ReadyReplicas: int32(timesCalled),
Conditions: []appsv1.DeploymentCondition{
{Type: appsv1.DeploymentProgressing, Reason: "DoinStuff"},
},
}
}
// generate a deployment with no errors and status=status(timesCalled)
okayDeployment := func(name string, timesCalled int) appsv1.Deployment {
return appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Status: status(timesCalled),
}
}
addProgressDeadlineExceeded := func(d *appsv1.Deployment) {
d.Status.Conditions = append(d.Status.Conditions, appsv1.DeploymentCondition{
Type: appsv1.DeploymentProgressing, Reason: "ProgressDeadlineExceeded"})
}
clientset = fake.NewSimpleClientset()
depsUpdated := []string{}
var deploymentReactor k8stesting.ReactionFunc = func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
switch action := action.(type) {
case k8stesting.UpdateActionImpl:
obj := action.GetObject()
d, ok := obj.(*appsv1.Deployment)
if !ok {
panic("err! action not a deployment")
}
depsUpdated = append(depsUpdated, d.Name)
default:
panic(fmt.Sprintf("action %v not understood", action))
}
return false, nil, nil
}
clientset.PrependReactor("update", "deployments", deploymentReactor)
t.Run("integration", func(t *testing.T) {
createDeploymentOrDie(clientset, deployment("d1"))
createDeploymentOrDie(clientset, deployment("d2"))
createDeploymentOrDie(clientset, deployment("d3"))
createDeploymentOrDie(clientset, deployment("d4"))
deployments = []*appsv1.Deployment{
modifiedDeployment("d1"), // should be updated (and successfully)
modifiedDeployment("d2"), // should be updated (but gets progress deadline exceeded)
modifiedDeployment("d3"), // should be updated (but never becomes ready)
deployment("d4"), // no changes so should not be updated
deployment("d5"), // does not exist so should be a failure to update
}
listFunc = func() (*appsv1.DeploymentList, error) {
d2 := okayDeployment("d2", 0)
if timesCalled >= 1 {
addProgressDeadlineExceeded(&d2)
}
l := &appsv1.DeploymentList{
Items: []appsv1.Deployment{
okayDeployment("d1", timesCalled), // becomes ready second time called
d2,
okayDeployment("d3", 0), // never becomes ready
},
}
timesCalled++
return l, nil
}
failures = UpdateMultipleDeploymentsAndWait(clientset, deployments, listFunc)
assert.Len(t, failures, 3)
assert.ElementsMatch(t,
[]string{failures[0].ResourceName, failures[1].ResourceName, failures[2].ResourceName},
[]string{"d2", "d3", "d5"})
assert.ElementsMatch(t, depsUpdated, []string{"d1", "d2", "d3"})
})
}
func TestUpdateMultipleDeployments(t *testing.T) {
namespace := "ns"
// a barebones deployment
deployment := func(name string, pds *int32) *appsv1.Deployment {
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: appsv1.DeploymentSpec{
ProgressDeadlineSeconds: pds,
},
}
}
// a barebones deployment with some modification
modifiedDeployment := func(name string, pds *int32) *appsv1.Deployment {
d := deployment(name, pds)
d.Spec.Selector = &metav1.LabelSelector{
MatchLabels: map[string]string{"hi": "mom"},
}
return d
}
// inputs
var clientset *fake.Clientset
var deployments []*appsv1.Deployment
// outputs
var deploymentsUpdated DeploymentsUpdated
var failures Failures
var pds *int32 // progressDeadlineSeconds
t.Run("no deployments to be updated", func(t *testing.T) {
clientset = fake.NewSimpleClientset()
deployments = []*appsv1.Deployment{}
deploymentsUpdated, failures, pds = UpdateMultipleDeployments(clientset, deployments)
assert.Len(t, deploymentsUpdated, 0)
assert.Len(t, failures, 0)
})
t.Run("update deployments successfully", func(t *testing.T) {
clientset = fake.NewSimpleClientset()
createDeploymentOrDie(clientset, deployment("d1", nil))
createDeploymentOrDie(clientset, deployment("d2", nil))
createDeploymentOrDie(clientset, deployment("d3", nil))
deployments = []*appsv1.Deployment{
modifiedDeployment("d1", nil),
modifiedDeployment("d2", nil),
modifiedDeployment("d3", nil),
}
deploymentsUpdated, failures, pds = UpdateMultipleDeployments(clientset, deployments)
assert.Len(t, deploymentsUpdated, 3)
assert.Len(t, failures, 0)
assert.Contains(t, deploymentsUpdated, "d1")
assert.Contains(t, deploymentsUpdated, "d2")
assert.Contains(t, deploymentsUpdated, "d3")
assert.Nil(t, pds)
})
t.Run("do not update deployments with no modifications", func(t *testing.T) {
// clientset with preexisting and updated deployments from previous test
d3 := modifiedDeployment("d3", newInt32(5))
// d3.ObjectMeta.Labels = map[string]string{
// "new": "label",
// }
deployments = []*appsv1.Deployment{
modifiedDeployment("d1", nil), // should not be updated
// d2 from before should also not be updated
d3, // should be updated
}
deploymentsUpdated, failures, pds = UpdateMultipleDeployments(clientset, deployments)
assert.Len(t, deploymentsUpdated, 1)
assert.Len(t, failures, 0)
assert.Contains(t, deploymentsUpdated, "d3")
assert.EqualValues(t, 5, *pds)
})
t.Run("failures if deployments do not exist", func(t *testing.T) {
clientset = fake.NewSimpleClientset()
createDeploymentOrDie(clientset, deployment("d1", newInt32(30)))
createDeploymentOrDie(clientset, deployment("d3", newInt32(30)))
deployments = []*appsv1.Deployment{
modifiedDeployment("d1", newInt32(30)),
modifiedDeployment("d2", newInt32(30)),
modifiedDeployment("d3", newInt32(30)),
modifiedDeployment("d4", newInt32(30)),
}
deploymentsUpdated, failures, pds = UpdateMultipleDeployments(clientset, deployments)
assert.Len(t, deploymentsUpdated, 2)
assert.Len(t, failures, 2)
assert.Contains(t, deploymentsUpdated, "d1")
assert.Contains(t, deploymentsUpdated, "d3")
assert.ElementsMatch(t, []string{failures[0].ResourceName, failures[1].ResourceName}, []string{"d2", "d4"})
assert.EqualValues(t, 30, *pds)
})
}
func TestWaitForDeploymentsToUpdate(t *testing.T) {
// NOTE: do not actually test progressDeadlineExceeded as an input because we don't want to make
// this unit test run for multiple seconds.
oldPeriod := waitForDeploymentPeriod
oldTimeout := waitForDeploymentTimeout
defer func() {
waitForDeploymentPeriod = oldPeriod
waitForDeploymentTimeout = oldTimeout
}()
waitForDeploymentPeriod = 1 * time.Millisecond
waitForDeploymentTimeout = 3 * time.Millisecond
timesCalled := 0
// generate a status that is not ready when first called but becomes ready later
status := func(timesCalled int) appsv1.DeploymentStatus {
if timesCalled < 0 {
timesCalled = 0
}
return appsv1.DeploymentStatus{
ObservedGeneration: int64(timesCalled),
UpdatedReplicas: int32(timesCalled),
ReadyReplicas: int32(timesCalled),
Conditions: []appsv1.DeploymentCondition{
{Type: appsv1.DeploymentProgressing, Reason: "DoinStuff"},
},
}
}
// generate a deployment with no errors and status=status(timesCalled)
okayDeployment := func(name string, timesCalled int) appsv1.Deployment {
return appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Status: status(timesCalled),
}
}
addProgressDeadlineExceeded := func(d *appsv1.Deployment) {
d.Status.Conditions = append(d.Status.Conditions, appsv1.DeploymentCondition{
Type: appsv1.DeploymentProgressing, Reason: "ProgressDeadlineExceeded"})
}
// inputs
var deploymentsUpdated DeploymentsUpdated
var progressDeadlineSeconds *int32
var listFunc func() (*appsv1.DeploymentList, error)
// outputs
var failures Failures
runTest := func() {
timesCalled = 0
failures = WaitForDeploymentsToUpdate(deploymentsUpdated, progressDeadlineSeconds, listFunc)
}
t.Run("not waiting on any deployments", func(t *testing.T) {
deploymentsUpdated = DeploymentsUpdated{}
listFunc = func() (*appsv1.DeploymentList, error) {
l := &appsv1.DeploymentList{
Items: []appsv1.Deployment{
okayDeployment("d1", timesCalled), // becomes ready second time called
okayDeployment("d2", timesCalled-1), // becomes ready third time called
okayDeployment("d3", timesCalled), // becomes ready second time called but only generation changed on third time
},
}
timesCalled++
return l, nil
}
runTest()
assert.Len(t, failures, 0)
})
t.Run("wait successful", func(t *testing.T) {
deploymentsUpdated = DeploymentsUpdated{
"d1": 0,
"d2": 0,
"d3": 1,
}
listFunc = func() (*appsv1.DeploymentList, error) {
l := &appsv1.DeploymentList{
Items: []appsv1.Deployment{
okayDeployment("d1", timesCalled), // becomes ready second time called
okayDeployment("d2", timesCalled-1), // becomes ready third time called
okayDeployment("d3", timesCalled), // becomes ready second time called but only generation changed on third time
},
}
timesCalled++
return l, nil
}
runTest()
assert.Len(t, failures, 0)
})
t.Run("some failures", func(t *testing.T) {
deploymentsUpdated = DeploymentsUpdated{
"d1": 0,
"d2": 0,
"d3": 0,
}
listFunc = func() (*appsv1.DeploymentList, error) {
l := &appsv1.DeploymentList{
Items: []appsv1.Deployment{
okayDeployment("d1", timesCalled), // becomes ready second time called
okayDeployment("d2", 0), // never becomes ready
okayDeployment("d3", 0), // never becomes ready
},
}
timesCalled++
return l, nil
}
runTest()
assert.Len(t, failures, 2)
assert.ElementsMatch(t, []string{failures[0].ResourceName, failures[1].ResourceName}, []string{"d2", "d3"})
assert.Error(t, failures[0].Error)
assert.Error(t, failures[1].Error)
})
t.Run("one deployment never listed", func(t *testing.T) {
deploymentsUpdated = DeploymentsUpdated{
"d1": 0,
"d2": 0,
"d3": 0,
}
listFunc = func() (*appsv1.DeploymentList, error) {
l := &appsv1.DeploymentList{
Items: []appsv1.Deployment{
okayDeployment("d1", timesCalled), // becomes ready second time called
okayDeployment("d2", timesCalled-1), // becomes ready third time called
},
}
timesCalled++
return l, nil
}
runTest()
assert.Len(t, failures, 1)
assert.ElementsMatch(t, []string{failures[0].ResourceName}, []string{"d3"})
assert.Error(t, failures[0].Error)
})
t.Run("fail to list deployments", func(t *testing.T) {
deploymentsUpdated = DeploymentsUpdated{
"d1": 0,
"d2": 0,
"d3": 0,
}
listFunc = func() (*appsv1.DeploymentList, error) {
return nil, fmt.Errorf("fake error listing deployments")
}
runTest()
assert.Len(t, failures, 3)
assert.ElementsMatch(t,
[]string{failures[0].ResourceName, failures[1].ResourceName, failures[2].ResourceName},
[]string{"d1", "d2", "d3"})
assert.Error(t, failures[0].Error)
assert.Error(t, failures[1].Error)
assert.Error(t, failures[2].Error)
})
t.Run("deployment progress deadline exceeded", func(t *testing.T) {
deploymentsUpdated = DeploymentsUpdated{
"d1": 0,
"d2": 0,
"d3": 0,
}
listFunc = func() (*appsv1.DeploymentList, error) {
d2 := okayDeployment("d2", 0)
d3 := okayDeployment("d3", 0)
if timesCalled > 2 {
addProgressDeadlineExceeded(&d2)
addProgressDeadlineExceeded(&d3)
}
l := &appsv1.DeploymentList{
Items: []appsv1.Deployment{
okayDeployment("d1", timesCalled), // becomes ready second time called
d2,
d3,
},
}
timesCalled++
return l, nil
}
runTest()
assert.Len(t, failures, 2)
assert.ElementsMatch(t, []string{failures[0].ResourceName, failures[1].ResourceName}, []string{"d2", "d3"})
assert.Error(t, failures[0].Error)
assert.Error(t, failures[1].Error)
})
}
func Test_maxInt32Ptr(t *testing.T) {
t.Run("both nil", func(t *testing.T) {
assert.Nil(t, maxInt32Ptr(nil, nil))
})
type args struct {
a *int32
b *int32
}
tests := []struct {
name string
args args
want int32
}{
{"a nil", args{nil, newInt32(2)}, 2},
{"b nil", args{newInt32(0), nil}, 0},
{"negatives", args{newInt32(-5), newInt32(-4)}, -4},
{"neg and pos", args{newInt32(-3), newInt32(1)}, 1},
{"positives", args{newInt32(1), newInt32(3)}, 3},
{"with zero", args{newInt32(0), newInt32(2)}, 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, *maxInt32Ptr(tt.args.a, tt.args.b))
})
}
}
func newInt32(i int32) *int32 {
return &i
}
func createDeploymentOrDie(clientset *fake.Clientset, d *appsv1.Deployment) {
_, err := clientset.AppsV1().Deployments(d.Namespace).Create(context.TODO(), d, metav1.CreateOptions{})
if err != nil {
panic(err)
}
}
+1 -1
View File
@@ -76,7 +76,7 @@ func WaitForJobCompletion(clientset kubernetes.Interface, job *batch.Job, timeou
return false, nil
}
if job.Status.Failed > 0 {
return false, fmt.Errorf("job %s failed.", job.Name)
return false, fmt.Errorf("job %s failed", job.Name)
}
if job.Status.Succeeded > 0 {
return true, nil
+12
View File
@@ -159,6 +159,18 @@ func addRookVersionLabel(labels map[string]string) {
labels[RookVersionLabelKey] = value
}
// RookVersionLabelMatchesCurrent returns true if the Rook version on the resource label matches the
// current Rook version running. It returns false if the label does not match. It returns an error
// if the label does not exist.
func RookVersionLabelMatchesCurrent(labels map[string]string) (bool, error) {
v, ok := labels[RookVersionLabelKey]
if !ok {
return false, fmt.Errorf("failed to find Rook version label %q", RookVersionLabelKey)
}
expectedVersion := validateLabelValue(rookversion.Version)
return (v == expectedVersion), nil
}
// validateLabelValue replaces any invalid characters
// in the input string with a replacement character,
// and enforces other limitations for k8s label values.
+3 -4
View File
@@ -26,8 +26,7 @@ import (
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
monitoringclient "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sYAML "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/tools/clientcmd"
@@ -71,7 +70,7 @@ func CreateOrUpdateServiceMonitor(serviceMonitorDefinition *monitoringv1.Service
}
oldSm, err := client.MonitoringV1().ServiceMonitors(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
if kerrors.IsNotFound(err) {
sm, err := client.MonitoringV1().ServiceMonitors(namespace).Create(ctx, serviceMonitorDefinition, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("failed to create servicemonitor. %v", err)
@@ -114,7 +113,7 @@ func CreateOrUpdatePrometheusRule(prometheusRule *monitoringv1.PrometheusRule) (
}
promRule, err := client.MonitoringV1().PrometheusRules(namespace).Create(ctx, prometheusRule, metav1.CreateOptions{})
if err != nil {
if !errors.IsAlreadyExists(err) {
if !kerrors.IsAlreadyExists(err) {
return nil, fmt.Errorf("failed to create prometheusRules. %v", err)
}
// Get current PrometheusRule so the ResourceVersion can be set as needed
+24 -5
View File
@@ -20,7 +20,6 @@ import (
"context"
"encoding/base32"
"fmt"
"math/rand"
"strings"
"testing"
@@ -41,6 +40,7 @@ import (
// New creates a fake K8s cluster with some nodes added.
func New(t *testing.T, nodes int) *fake.Clientset {
t.Helper()
clientset := fake.NewSimpleClientset()
AddSomeReadyNodes(t, clientset, nodes)
return clientset
@@ -48,9 +48,13 @@ func New(t *testing.T, nodes int) *fake.Clientset {
// AddReadyNode adds a new Node with status "Ready" and the given name and IP.
func AddReadyNode(t *testing.T, clientset *fake.Clientset, name, ip string) {
t.Helper()
ready := v1.NodeCondition{Type: v1.NodeReady, Status: v1.ConditionTrue}
n := &v1.Node{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
corev1.LabelHostname: name,
},
Name: name,
},
Status: v1.NodeStatus{
@@ -78,6 +82,7 @@ func AddReadyNode(t *testing.T, clientset *fake.Clientset, name, ip string) {
// - name from 0 to count-1
// - ip from 0.0.0.0 to <count-1>.<count-1>.<count-1>.<count-1>
func AddSomeReadyNodes(t *testing.T, clientset *fake.Clientset, count int) {
t.Helper()
for i := 0; i < count; i++ {
name := fmt.Sprintf("node%d", i)
ip := fmt.Sprintf("%d.%d.%d.%d", i, i, i, i)
@@ -123,6 +128,7 @@ var (
// to the clientset to mimic more of what K8s does in the real world.
// - Generate a name for resources that have 'generateName' set and 'name' unset.
func NewComplexClientset(t *testing.T) *fake.Clientset {
t.Helper()
clientset := fake.NewSimpleClientset()
// Some resources are created with generateName used, and we need to capture the create
@@ -169,6 +175,7 @@ func NewComplexClientset(t *testing.T) *fake.Clientset {
// - Pod create/delete is done to the clientset tracker, so no Pod watch events will register.
// - Optionally look through the clientset Nodes to randomly assign created Pods to a node.
func PrependComplexJobReactor(t *testing.T, clientset *fake.Clientset, assignPodToNode bool) {
t.Helper()
var jobReactor k8stesting.ReactionFunc = func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
switch action := action.(type) {
@@ -228,7 +235,18 @@ func PrependComplexJobReactor(t *testing.T, clientset *fake.Clientset, assignPod
clientset.PrependReactor("*", "jobs", jobReactor)
}
// Pick a random node name from the nodes present in the fake K8s clientset cluster.
// PrependFailReactor adds a reactor with the desired verb and resource that will report a failure.
func PrependFailReactor(t *testing.T, clientset *fake.Clientset, verb, resource string) {
t.Helper()
var failReactor k8stesting.ReactionFunc = func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, fmt.Errorf("fail reactor: induced failure")
}
clientset.PrependReactor(verb, resource, failReactor)
}
var pickNodeIdx = 0 // global, used to allow round-robin node picking
// Pick a node name from the nodes present in the fake K8s clientset cluster.
func pickNode(clientset *fake.Clientset) string {
obj, err := clientset.Tracker().List(nodeGVR, nodeGVK, corev1.NamespaceAll)
if err != nil {
@@ -241,7 +259,8 @@ func pickNode(clientset *fake.Clientset) string {
if len(nodes.Items) == 0 {
panic(fmt.Errorf("pickNode: no nodes are available in the fake clientset to pick from"))
}
// #nosec G404 It's harmless since it's a test code
randIdx := rand.Intn(len(nodes.Items))
return nodes.Items[randIdx].GetName()
pickNodeIdx = pickNodeIdx % len(nodes.Items) // reset to 0 once idx is more than number of nodes
name := nodes.Items[pickNodeIdx].GetName()
pickNodeIdx++
return name
}
+45
View File
@@ -19,6 +19,8 @@ package util
import (
"fmt"
"time"
"github.com/pkg/errors"
)
// Retry will attempt the given function until it succeeds, up to the given maximum amount of retries,
@@ -41,3 +43,46 @@ func Retry(maxRetries int, delay time.Duration, f func() error) error {
<-time.After(delay)
}
}
// RetryFunc is a function that returns true when it is done and it should be retried no longer.
// It should return error if there has been an error. The error will be logged if done==false
// (should keep retrying). The error will be returned by the calling function if done==true (should
// stop retrying).
type RetryFunc func() (done bool, err error)
// RetryWithTimeout retries the RetryFunc until the timeout occurs. It will retry once more after
// the timeout to avoid race conditions at the expense of running for slightly longer than timeout
// in the timeout/error case.
// The given description will be output in log/error messages as "... waiting for <description>..."
func RetryWithTimeout(f RetryFunc, period time.Duration, timeout time.Duration, description string) error {
tt := time.After(timeout)
var done bool
var err error
for {
done, err = f()
if done {
break
}
if err != nil {
logger.Errorf("error occurred waiting for %s. retrying after %v. %v", description, period, err)
}
logger.Debugf("waiting for %s. retrying after %v", description, period)
select {
case <-time.After(period):
// go back to start of loop
case <-tt:
// timeout; try one last time
done, err = f()
if !done {
if err != nil {
return errors.Wrapf(err, "timed out waiting for %s", description)
}
return fmt.Errorf("timed out waiting for %s", description)
}
return err
}
}
return err
}
+1 -1
View File
@@ -68,7 +68,7 @@ func (s *Set) Add(newValue string) bool {
return false
}
// Add a value to the set. Returns true if the value was added, false if it already exists.
// Remove a value from the set. Returns true if the value was removed, false if it does not exist.
func (s *Set) Remove(oldValue string) bool {
if _, ok := s.values[oldValue]; ok {
delete(s.values, oldValue)