Merge pull request #6497 from sp98/osd-pdb-reconciler

ceph: OSD PDB reconciler changes
This commit is contained in:
Travis Nielsen authored and GitHub committed 2020-11-18 17:30:24 -07:00
commit 54560250e9
24 files changed
+779 -1173

No files matched your search

@@ -55,6 +55,8 @@ spec:
type: boolean
osdMaintenanceTimeout:
type: integer
pgHealthCheckTimeout:
type: integer
manageMachineDisruptionBudgets:
type: boolean
skipUpgradeChecks:
@@ -137,6 +139,8 @@ spec:
type: boolean
osdMaintenanceTimeout:
type: integer
pgHealthCheckTimeout:
type: integer
manageMachineDisruptionBudgets:
type: boolean
useAllNodes:
@@ -1372,6 +1376,8 @@ spec:
type: boolean
osdMaintenanceTimeout:
type: integer
pgHealthCheckTimeout:
type: integer
manageMachineDisruptionBudgets:
type: boolean
skipUpgradeChecks:
@@ -1413,6 +1419,8 @@ spec:
type: boolean
osdMaintenanceTimeout:
type: integer
pgHealthCheckTimeout:
type: integer
manageMachineDisruptionBudgets:
type: boolean
useAllNodes:
@@ -168,6 +168,7 @@ spec:
disruptionManagement:
managePodBudgets: false
osdMaintenanceTimeout: 30
pgHealthCheckTimeout: 0
manageMachineDisruptionBudgets: false
machineDisruptionBudgetNamespace: openshift-machine-api
# security oriented settings
@@ -211,6 +211,10 @@ spec:
# A duration in minutes that determines how long an entire failureDomain like `region/zone/host` will be held in `noout` (in addition to the
# default DOWN/OUT interval) when it is draining. This is only relevant when `managePodBudgets` is `true`. The default value is `30` minutes.
osdMaintenanceTimeout: 30
# A duration in minutes that the operator will wait for the placement groups to become healthy (active+clean) after a drain was completed and OSDs came back up.
# Operator will continue with the next drain if the timeout exceeds. It only works if `managePodBudgets` is `true`.
# No values or 0 means that the operator will wait until the placement groups are healthy before unblocking the next drain.
pgHealthCheckTimeout: 0
# If true, the operator will create and manage MachineDisruptionBudgets to ensure OSDs are only fenced when the cluster is healthy.
# Only available on OpenShift.
manageMachineDisruptionBudgets: false
@@ -57,6 +57,8 @@ spec:
type: boolean
osdMaintenanceTimeout:
type: integer
pgHealthCheckTimeout:
type: integer
manageMachineDisruptionBudgets:
type: boolean
skipUpgradeChecks:
@@ -139,6 +141,8 @@ spec:
type: boolean
osdMaintenanceTimeout:
type: integer
pgHealthCheckTimeout:
type: integer
manageMachineDisruptionBudgets:
type: boolean
useAllNodes:
@@ -65,6 +65,8 @@ spec:
type: boolean
osdMaintenanceTimeout:
type: integer
pgHealthCheckTimeout:
type: integer
manageMachineDisruptionBudgets:
type: boolean
skipUpgradeChecks:
@@ -106,6 +108,8 @@ spec:
type: boolean
osdMaintenanceTimeout:
type: integer
pgHealthCheckTimeout:
type: integer
manageMachineDisruptionBudgets:
type: boolean
useAllNodes:
+28 -17
View File
@@ -14,26 +14,37 @@
OSDs do not fit under the single PodDisruptionBudget pattern. Ceph's ability to tolerate pod disruptions in one failure domain is dependent on the overall health of the cluster.
Even if an upgrade agent were only to drain one node at a time, Ceph would have to wait until there were no undersized PGs before moving on the the next.
Therefore, we will create a PodDisruptionBudget per failure domain that does not allow any evictions by default.
When attempts to drain are detected, we will delete PodDisruption budget on one node at a time, progressing to the next only after ceph is healthy enough to avoid data loss/unavailability.
The failure domain will be determined by the smallest failure domain of all the Ceph Pools in that cluster.
We begin with creating a single PodDisruptionBudget for all the OSD with maxUnavailable=1. This will allow one OSD to go down anytime. Once the user drains a node and an OSD goes down, we determine the failure domain for the draining OSD (using the OSD deployment labels). Then we create blocking PodDisruptionBudgets (maxUnavailable=0) for all other failure domains and delete the main PodDisruptionBudget. This blocks OSDs from going down in multiple failure domains simultaneously.
Detecting drains is not easy as they are a client side operation. The client cordons the node and continuously attempts to evict all pods from the node until it succeeds.
We will use a heuristic to detect drains. We will create a canary deployment for each node with a nodeSelector for that node. Since it is likely that pod will only be
removed from the node in the event of a drain, we will rely on the assumption that if that pod is not running, that node is being drained. This will not be a dangerous assumption
as false positives for drains are not dangerous in this use case.
Once the drained OSDs are back and all the pgs are active+clean, that is, the cluster is healed, the main PodDisruptionBudget is added back and the blocking ones are deleted. User can also add a timeout for the pgs to become healthy. If the timeout exceeds, the operator will ignore the pg health, add the main PodDisruptionBudget and delete the blocking ones.
Example flow:
- A Ceph pool CRD is created.
- The Rook operator creates a PDB with maxUnvailable of 0 for each failure domain.
- A cluster upgrade agent wants to perform a kernel upgrade on the nodes.
- It attempts to drain 1 or more nodes.
- The drain attempt successfully evicts the canary pod.
- The Rook operator interprets this as a drain request that it can grant by deleting the PDB.
- The Rook operator deletes one PDB, and the blocked drain on that failure domain completes.
- The OSDs on that node comeback up and all the necessary backfilling occurs, and all the osds are active+clean.
- The Rook operator recreates the PDB on that failure domain.
- The process is repeated with the subsequent nodes/failure domains.
Detecting drains is not easy as they are a client side operation. The client cordons the node and continuously attempts to evict all pods from the node until it succeeds. Whenever an OSD goes into pending state, that is, `ReadyReplicas` count is 0, we assume that some drain operation is happening.
Example scenario:
- Zone x
- Node a
- osd.0
- osd.1
- Zone y
- Node b
- osd.2
- osd.3
- Zone z
- Node c
- osd.4
- osd.5
1. Rook Operator creates a single PDB that covers all OSDs with maxUnavailable=1.
2. When Rook Operator sees an OSD go down (for example, osd.0 goes down):
- Create a PDB for each failure domain (zones y and z) with maxUnavailable=0 where the OSD did *not* go down.
- Delete the original PDB that covers all OSDs
- Now all remaining OSDs in zone x would be allowed to be drained
3. When Rook sees the OSDs are back up and all PGs are clean
- Restore the PDB that covers all OSDs with maxUnavailable=1
- Delete the PDBs (in zone y and z) where maxUnavailable=0
An example of an operator that will attempt to do rolling upgrades of nodes is the Machine Config Operator in openshift. Based on what I have seen in
[SIG cluster lifecycle](https://github.com/kubernetes/community/tree/master/sig-cluster-lifecycle), kubernetes deployments based on cluster-api approach will be
+7 -1
View File
@@ -823,10 +823,16 @@ type DisruptionManagementSpec struct {
ManagePodBudgets bool `json:"managePodBudgets,omitempty"`
// OSDMaintenanceTimeout sets how many additional minutes the DOWN/OUT interval is for drained failure domains
// it only works if managePodBudgetss is true.
// it only works if managePodBudgets is true.
// the default is 30 minutes
OSDMaintenanceTimeout time.Duration `json:"osdMaintenanceTimeout,omitempty"`
// PGHealthCheckTimeout is the time (in minutes) that the operator will wait for the placement groups to become
// healthy (active+clean) after a drain was completed and OSDs came back up. Rook will continue with the next drain
// if the timeout exceeds. It only works if managePodBudgets is true.
// No values or 0 means that the operator will wait until the placement groups are healthy before unblocking the next drain.
PGHealthCheckTimeout time.Duration `json:"pgHealthCheckTimeout,omitempty"`
// This enables management of machinedisruptionbudgets
ManageMachineDisruptionBudgets bool `json:"manageMachineDisruptionBudgets,omitempty"`
+20 -2
View File
@@ -19,6 +19,7 @@ package osd
import (
"fmt"
"strconv"
"strings"
"github.com/rook/rook/pkg/operator/ceph/controller"
)
@@ -32,6 +33,8 @@ const (
CephDeviceSetPVCIDLabelKey = "ceph.rook.io/DeviceSetPVCId"
// OSDOverPVCLabelKey is the Rook PVC label key
OSDOverPVCLabelKey = "ceph.rook.io/pvc"
// TopologyLocationLabel is the crush location label added to OSD deployments
TopologyLocationLabel = "topology-location-%s"
)
func makeStorageClassDeviceSetPVCLabel(storageClassDeviceSetName, pvcStorageClassDeviceSetPVCId string, setIndex int) map[string]string {
@@ -42,11 +45,26 @@ func makeStorageClassDeviceSetPVCLabel(storageClassDeviceSetName, pvcStorageClas
}
}
func (c *Cluster) getOSDLabels(osdID int, failureDomainValue string, portable bool) map[string]string {
stringID := fmt.Sprintf("%d", osdID)
func (c *Cluster) getOSDLabels(osd OSDInfo, failureDomainValue string, portable bool) map[string]string {
stringID := fmt.Sprintf("%d", osd.ID)
labels := controller.CephDaemonAppLabels(AppName, c.clusterInfo.Namespace, "osd", stringID, true)
labels[OsdIdLabelKey] = stringID
labels[FailureDomainKey] = failureDomainValue
labels[portableKey] = strconv.FormatBool(portable)
for k, v := range getOSDTopologyLocationLabels(osd.Location) {
labels[k] = v
}
return labels
}
func getOSDTopologyLocationLabels(topologyLocation string) map[string]string {
labels := map[string]string{}
locations := strings.Split(topologyLocation, " ")
for _, location := range locations {
loc := strings.Split(location, "=")
if len(loc) == 2 {
labels[fmt.Sprintf(TopologyLocationLabel, loc[0])] = loc[1]
}
}
return labels
}
@@ -0,0 +1,31 @@
/*
Copyright 2020 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 (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOSDTopologyLabels(t *testing.T) {
fakeLocation := "root=default host=ocs-deviceset-gp2-1-data-0-wh5wl region=us-east-1 zone=us-east-1c"
result := getOSDTopologyLocationLabels(fakeLocation)
assert.Equal(t, "us-east-1", result["topology-location-region"])
assert.Equal(t, "ocs-deviceset-gp2-1-data-0-wh5wl", result["topology-location-host"])
assert.Equal(t, "us-east-1c", result["topology-location-zone"])
}
+3 -2
View File
@@ -318,6 +318,7 @@ func TestGetPVCHostName(t *testing.T) {
clientset := fake.NewSimpleClientset()
clusterInfo := &client.ClusterInfo{Namespace: "ns"}
c := &Cluster{context: &clusterd.Context{Clientset: clientset}, clusterInfo: clusterInfo}
osdInfo := OSDInfo{ID: 23}
pvcName := "test-pvc"
// fail to get the host name when there is no pod or deployment
@@ -330,7 +331,7 @@ func TestGetPVCHostName(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "osd-23",
Namespace: c.clusterInfo.Namespace,
Labels: c.getOSDLabels(23, "", true),
Labels: c.getOSDLabels(osdInfo, "", true),
},
}
k8sutil.AddLabelToDeployment(OSDOverPVCLabelKey, pvcName, osdDeployment)
@@ -351,7 +352,7 @@ func TestGetPVCHostName(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "osd-23",
Namespace: c.clusterInfo.Namespace,
Labels: c.getOSDLabels(23, "", true),
Labels: c.getOSDLabels(osdInfo, "", true),
},
}
osdPod.Labels = map[string]string{OSDOverPVCLabelKey: pvcName}
+2 -2
View File
@@ -518,7 +518,7 @@ func (c *Cluster) makeDeployment(osdProps osdProperties, osd OSDInfo, provisionC
podTemplateSpec := v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: AppName,
Labels: c.getOSDLabels(osd.ID, failureDomainValue, osdProps.portable),
Labels: c.getOSDLabels(osd, failureDomainValue, osdProps.portable),
},
Spec: v1.PodSpec{
RestartPolicy: v1.RestartPolicyAlways,
@@ -563,7 +563,7 @@ func (c *Cluster) makeDeployment(osdProps osdProperties, osd OSDInfo, provisionC
ObjectMeta: metav1.ObjectMeta{
Name: deploymentName,
Namespace: c.clusterInfo.Namespace,
Labels: c.getOSDLabels(osd.ID, failureDomainValue, osdProps.portable),
Labels: c.getOSDLabels(osd, failureDomainValue, osdProps.portable),
},
Spec: apps.DeploymentSpec{
Selector: &metav1.LabelSelector{
@@ -25,7 +25,6 @@ import (
"github.com/rook/rook/pkg/operator/ceph/disruption/controllerconfig"
"github.com/rook/rook/pkg/operator/ceph/disruption/machinedisruption"
"github.com/rook/rook/pkg/operator/ceph/disruption/machinelabel"
"github.com/rook/rook/pkg/operator/ceph/disruption/nodedrain"
"github.com/rook/rook/pkg/operator/ceph/file"
"github.com/rook/rook/pkg/operator/ceph/nfs"
"github.com/rook/rook/pkg/operator/ceph/object"
@@ -45,7 +44,6 @@ var (
// AddToManagerFuncsMaintenance is a list of functions to add all Controllers to the Manager (entrypoint for controller)
var AddToManagerFuncsMaintenance = []func(manager.Manager, *controllerconfig.Context) error{
nodedrain.Add,
clusterdisruption.Add,
}
@@ -18,8 +18,6 @@ package clusterdisruption
import (
"github.com/rook/rook/pkg/operator/ceph/disruption/controllerconfig"
"github.com/rook/rook/pkg/operator/ceph/disruption/nodedrain"
"github.com/rook/rook/pkg/operator/k8sutil"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
@@ -29,8 +27,6 @@ import (
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
appsv1 "k8s.io/api/apps/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/types"
)
@@ -49,11 +45,10 @@ func Add(mgr manager.Manager, context *controllerconfig.Context) error {
sharedClusterMap := &ClusterMap{}
reconcileClusterDisruption := &ReconcileClusterDisruption{
client: mgr.GetClient(),
scheme: mgrScheme,
context: context,
clusterMap: sharedClusterMap,
osdCrushLocationMap: &OSDCrushLocationMap{Context: context.ClusterdContext},
client: mgr.GetClient(),
scheme: mgrScheme,
context: context,
clusterMap: sharedClusterMap,
}
reconciler := reconcile.Reconciler(reconcileClusterDisruption)
// Create a new controller
@@ -83,74 +78,6 @@ func Add(mgr manager.Manager, context *controllerconfig.Context) error {
return err
}
// Watch for PodDisruptionBudgets and enqueue the CephCluster in the namespace
err = c.Watch(
&source.Kind{Type: &policyv1beta1.PodDisruptionBudget{}},
&handler.EnqueueRequestsFromMapFunc{
ToRequests: handler.ToRequestsFunc(func(obj handler.MapObject) []reconcile.Request {
_, ok := obj.Object.(*policyv1beta1.PodDisruptionBudget)
if !ok {
// not a pdb, returning empty
logger.Errorf("PDB handler received non-PDB")
return []reconcile.Request{}
}
labels := obj.Meta.GetLabels()
// only enqueue osdDisruptionAppLabels
_, ok = labels[osdDisruptionAppName]
if !ok {
return []reconcile.Request{}
}
// // The name will be populated in the reconcile
namespace := obj.Meta.GetNamespace()
req := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace}}
return []reconcile.Request{req}
}),
},
)
if err != nil {
return err
}
// Watch for canary Deployments created by the nodedrain controller and enqueue all Cephclusters
err = c.Watch(
&source.Kind{Type: &appsv1.Deployment{}},
&handler.EnqueueRequestsFromMapFunc{
ToRequests: handler.ToRequestsFunc(func(obj handler.MapObject) []reconcile.Request {
_, ok := obj.Object.(*appsv1.Deployment)
if !ok {
// not a Deployment, returning empty
logger.Errorf("deployment handler received non-Deployment")
return []reconcile.Request{}
}
// don't enqueue if it isn't a canary Deployment
labels := obj.Meta.GetLabels()
appLabel, ok := labels[k8sutil.AppAttr]
if !ok || appLabel != nodedrain.CanaryAppName {
return []reconcile.Request{}
}
// Enqueue all CephClusters
clusterNamespaces := sharedClusterMap.GetClusterNamespaces()
if len(clusterNamespaces) == 0 {
return []reconcile.Request{}
}
reqs := make([]reconcile.Request, 0)
for _, namespace := range clusterNamespaces {
// The name will be populated in the reconcile
reqs = append(reqs, reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace}})
}
return reqs
}),
},
)
if err != nil {
return err
}
// Watch for CephBlockPools and enqueue the CephCluster in the namespace
err = c.Watch(&source.Kind{Type: &cephv1.CephBlockPool{}}, enqueueByNamespace)
if err != nil {
@@ -1,227 +0,0 @@
/*
Copyright 2019 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 clusterdisruption
import (
"context"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/pkg/errors"
"github.com/rook/rook/pkg/clusterd"
cephClient "github.com/rook/rook/pkg/daemon/ceph/client"
"github.com/rook/rook/pkg/operator/ceph/cluster/osd"
"github.com/rook/rook/pkg/operator/k8sutil"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
func (r *ReconcileClusterDisruption) getOsdDataList(clusterInfo *cephClient.ClusterInfo, request reconcile.Request, poolFailureDomain string) ([]OsdData, error) {
osdDeploymentList := &appsv1.DeploymentList{}
namespaceListOpts := client.InNamespace(request.Namespace)
err := r.client.List(context.TODO(), osdDeploymentList, client.MatchingLabels{k8sutil.AppAttr: osd.AppName}, namespaceListOpts)
if err != nil {
return nil, errors.Wrap(err, "could not list osd deployments")
}
osds := make([]OsdData, 0)
for _, deployment := range osdDeploymentList.Items {
osdData := OsdData{Deployment: deployment}
labels := deployment.Spec.Template.ObjectMeta.GetLabels()
osdID, ok := labels[osd.OsdIdLabelKey]
if !ok {
return nil, errors.Errorf("osd %q was not labeled with %q", deployment.GetName(), osd.OsdIdLabelKey)
}
osdIDInt, err := strconv.Atoi(osdID)
if err != nil {
return nil, errors.Wrapf(err, "failed to convert osd id %q in an int", osdID)
}
crushMeta, err := r.osdCrushLocationMap.Get(clusterInfo, osdIDInt)
if err != nil {
// If the error contains that message, this means the cluster is not up and running
// No monitors are present and thus no ceph configuration has been created
//
// Or this means the ceph config hasn't been written on the operator yet
// The controller starts before we run WriteConnectionConfig()
if strings.Contains(err.Error(), "error calling conf_read_file") {
logger.Debugf("Ceph %q cluster is not ready, cannot check osd location yet.", request.Namespace)
return nil, nil
}
return nil, errors.Wrapf(err, "could not fetch info from ceph for osd %q", osdID)
}
// bypass the cache if the topology location is not populated in the cache
_, failureDomainKnown := crushMeta.Location[poolFailureDomain]
if !failureDomainKnown {
crushMeta, err = r.osdCrushLocationMap.get(clusterInfo, osdIDInt)
if err != nil {
return nil, errors.Wrapf(err, "could not fetch info from ceph for osd %q", osdID)
}
}
osdData.CrushMeta = crushMeta
osds = append(osds, osdData)
}
return osds, nil
}
// OsdData stores the deployment and the Crush Data of the osd together
type OsdData struct {
Deployment appsv1.Deployment
CrushMeta *cephClient.CrushFindResult
}
// OSDCrushLocationMap is used to maintain a cache of map of osd id to cephClientCrushhFindResults
// the crush position of osds wrt to the failureDomain is not expected to change often, so a default Resync period
// of half an hour is used, but if a use case arises where this is needed, ResyncPeriod should be made smaller.
type OSDCrushLocationMap struct {
ResyncPeriod time.Duration
Context *clusterd.Context
clusterLocationMap map[string]map[int]cachedOSDLocation
mux sync.Mutex
}
type cachedOSDLocation struct {
result *cephClient.CrushFindResult
lastSynced time.Time
}
// Get takes an osd id and returns a CrushFindResult from cache
func (o *OSDCrushLocationMap) Get(clusterInfo *cephClient.ClusterInfo, id int) (*cephClient.CrushFindResult, error) {
o.mux.Lock()
defer o.mux.Unlock()
if o.ResyncPeriod == 0 {
o.ResyncPeriod = 30 * time.Minute
}
// initialize clusterLocationMap
if len(o.clusterLocationMap) == 0 {
o.clusterLocationMap = make(map[string]map[int]cachedOSDLocation)
}
locationMap, ok := o.clusterLocationMap[clusterInfo.Namespace]
// initialize namespace map
if !ok {
o.clusterLocationMap[clusterInfo.Namespace] = make(map[int]cachedOSDLocation)
}
// sync of osd id not found in clusterNamespace
osdLocation, ok := locationMap[id]
if !ok {
osdResult, err := o.get(clusterInfo, id)
if err != nil {
return nil, errors.Wrapf(err, "failed to run `find` on osd %d in cluster %q", id, clusterInfo.Namespace)
}
o.clusterLocationMap[clusterInfo.Namespace][id] = cachedOSDLocation{result: osdResult, lastSynced: time.Now()}
return osdResult, nil
}
// sync if not synced for longer than ResyncPeriod
if time.Since(osdLocation.lastSynced) > o.ResyncPeriod {
osdResult, err := o.get(clusterInfo, id)
if err != nil {
return nil, errors.Wrapf(err, "failed to run `find` on osd %d in cluster %q", id, clusterInfo.Namespace)
}
o.clusterLocationMap[clusterInfo.Namespace][id] = cachedOSDLocation{result: osdResult, lastSynced: time.Now()}
return osdResult, nil
}
return osdLocation.result, nil
}
// uncached version
func (o *OSDCrushLocationMap) get(clusterInfo *cephClient.ClusterInfo, id int) (*cephClient.CrushFindResult, error) {
osdResult, err := cephClient.FindOSDInCrushMap(o.Context, clusterInfo, id)
if err != nil {
return nil, errors.Wrapf(err, "failed running find on osd %d", id)
}
o.clusterLocationMap[clusterInfo.Namespace][id] = cachedOSDLocation{
result: osdResult,
lastSynced: time.Now(),
}
return osdResult, nil
}
func getOSDsForNodes(osdDataList []OsdData, nodeList []*corev1.Node, failureDomainType string) ([]OsdData, error) {
nodeOsdDataList := make([]OsdData, 0)
for _, node := range nodeList {
if node == nil {
logger.Warningf("node in nodelist was nil")
continue
}
nodeTopologyMap := osd.ExtractOSDTopologyFromLabels(node.GetLabels())
for _, osdData := range osdDataList {
// get the crush location of the osd
crushFailureDomain, ok := osdData.CrushMeta.Location[failureDomainType]
if !ok {
return nil, errors.Errorf("could not find the CrushFindResult.Location[%q] for %q", failureDomainType, osdData.Deployment.GetName())
}
// get the crush location of the node
nodeFailureDomain, ok := nodeTopologyMap[failureDomainType]
if !ok {
return nil, errors.Errorf("could not find the %q failure domain on node %q", failureDomainType, node.GetName())
}
// check if the node and osd have the same crush location value for this particular crush location type
if cephClient.IsNormalizedCrushNameEqual(nodeFailureDomain, crushFailureDomain) || (failureDomainType == "host" && cephClient.IsNormalizedCrushNameEqual(node.GetName(), crushFailureDomain)) {
nodeOsdDataList = append(nodeOsdDataList, osdData)
}
}
}
return nodeOsdDataList, nil
}
func getFailureDomainMapForOsds(osdDataList []OsdData, failureDomainType string) (map[string][]OsdData, error) {
failureDomainMap := make(map[string][]OsdData)
unfoundOSDs := make([]string, 0)
var err error
for _, osdData := range osdDataList {
failureDomainValue, ok := osdData.CrushMeta.Location[failureDomainType]
if !ok {
logger.Errorf("failureDomain type %q not associated with %q", failureDomainType, osdData.Deployment.GetName())
unfoundOSDs = append(unfoundOSDs, osdData.Deployment.ObjectMeta.Name)
} else {
if len(failureDomainMap[failureDomainValue]) == 0 {
failureDomainMap[failureDomainValue] = make([]OsdData, 0)
}
failureDomainMap[failureDomainValue] = append(failureDomainMap[failureDomainValue], osdData)
}
}
if len(unfoundOSDs) > 0 {
err = errors.Errorf("failure domain type %q not associated with osds: [%q]", failureDomainType, strings.Join(unfoundOSDs, ","))
}
return failureDomainMap, err
}
func getSortedOSDMapKeys(m map[string][]OsdData) []string {
list := make([]string, len(m))
count := 0
for key := range m {
list[count] = key
count++
}
sort.Strings(list)
return list
}
@@ -1,168 +0,0 @@
/*
Copyright 2019 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 clusterdisruption
import (
"fmt"
"testing"
cephClient "github.com/rook/rook/pkg/daemon/ceph/client"
"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"
)
func TestGetOSDNodeAssociation(t *testing.T) {
hostNum := 5
hostNames := make([]string, hostNum)
for i := 0; i < hostNum; i++ {
hostNames[i] = fmt.Sprintf("worker-%d", i)
}
zoneNum := hostNum
zoneNames := make([]string, zoneNum)
for i := 0; i < zoneNum; i++ {
zoneNames[i] = fmt.Sprintf("zone-%d", i)
}
osdNum := hostNum * 3
osdNames := make([]string, osdNum)
for i := 0; i < osdNum; i++ {
osdNames[i] = fmt.Sprintf("osd-%d", i)
}
osdDataList := []OsdData{
{
Deployment: appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: osdNames[0],
},
},
CrushMeta: &cephClient.CrushFindResult{
Location: map[string]string{
"host": hostNames[0],
"zone": zoneNames[0],
},
},
},
{
Deployment: appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: osdNames[1],
},
},
CrushMeta: &cephClient.CrushFindResult{
Location: map[string]string{
"host": hostNames[1],
"zone": zoneNames[1],
},
},
},
{
Deployment: appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: osdNames[2],
},
},
CrushMeta: &cephClient.CrushFindResult{
Location: map[string]string{
"host": hostNames[1],
"zone": zoneNames[2],
},
},
},
{
Deployment: appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: osdNames[3],
},
},
CrushMeta: &cephClient.CrushFindResult{
Location: map[string]string{
"host": hostNames[1],
"zone": zoneNames[2],
},
},
},
{
Deployment: appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: osdNames[4],
},
},
CrushMeta: &cephClient.CrushFindResult{
Location: map[string]string{
"host": hostNames[2],
"zone": zoneNames[0],
},
},
},
}
nodeList := []*corev1.Node{
{
ObjectMeta: metav1.ObjectMeta{
Name: hostNames[0],
Labels: map[string]string{
corev1.LabelHostname: hostNames[0],
corev1.LabelZoneFailureDomain: zoneNames[1],
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: hostNames[2],
Labels: map[string]string{
corev1.LabelHostname: hostNames[2],
corev1.LabelZoneFailureDomain: zoneNames[2],
},
},
},
nil,
}
// osds from hostName[0] - (0)
// osds from hostNames[1] - (2,3,1)
// osds from hostNames[2] - (4)
// hosts from nodeList (0,2)
osdDataSubset, err := getOSDsForNodes(osdDataList, nodeList, "host")
assert.Nil(t, err)
assert.Len(t, osdDataSubset, 2)
failureDomainsMap, err := getFailureDomainMapForOsds(osdDataSubset, "host")
assert.Nil(t, err)
failureDomainSubset := getSortedOSDMapKeys(failureDomainsMap)
assert.Equal(t, []string{hostNames[0], hostNames[2]}, failureDomainSubset)
// osds from zone[0] - (0,4)
// osds from zone[1] - (1)
// osds from zone[2] - (2,3)
// zones from nodeList (0,2)
osdDataSubset, err = getOSDsForNodes(osdDataList, nodeList, "zone")
assert.Nil(t, err)
assert.Len(t, osdDataSubset, 3)
failureDomainsMap, err = getFailureDomainMapForOsds(osdDataSubset, "zone")
assert.Nil(t, err)
failureDomainSubset = getSortedOSDMapKeys(failureDomainsMap)
assert.Equal(t, []string{zoneNames[1], zoneNames[2]}, failureDomainSubset)
}
@@ -30,7 +30,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func createFakeReconcileClusterDisruptionr(t *testing.T, obj ...runtime.Object) *ReconcileClusterDisruption {
func createFakeReconcileClusterDisruption(t *testing.T, obj ...runtime.Object) *ReconcileClusterDisruption {
scheme := scheme.Scheme
err := policyv1beta1.AddToScheme(scheme)
if err != nil {
@@ -45,7 +45,7 @@ func createFakeReconcileClusterDisruptionr(t *testing.T, obj ...runtime.Object)
}
func TestReconcileMonPDB(t *testing.T) {
r := createFakeReconcileClusterDisruptionr(t, &cephv1.CephCluster{})
r := createFakeReconcileClusterDisruption(t, &cephv1.CephCluster{})
testCases := []struct {
label string
cephCluster *cephv1.CephCluster
@@ -23,10 +23,12 @@ import (
"time"
"github.com/pkg/errors"
cephClient "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"
"github.com/rook/rook/pkg/operator/ceph/disruption/nodedrain"
opcontroller "github.com/rook/rook/pkg/operator/ceph/controller"
"github.com/rook/rook/pkg/operator/k8sutil"
apierrors "k8s.io/apimachinery/pkg/api/errors"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -37,85 +39,149 @@ import (
policyv1beta1 "k8s.io/api/policy/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/sets"
// cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
)
const (
// PDBAppName is that app label value for pdbs targeting osds
PDBAppName = "rook-ceph-osd-pdb"
disabledPDBKey = "disabled-pdb"
disabledPDBTimeKey = "pdb-disabled-at"
// osdPDBAppName is that app label value for pdbs targeting osds
osdPDBAppName = "rook-ceph-osd"
drainingFailureDomainKey = "draining-failure-domain"
drainingFailureDomainDurationKey = "draining-failure-domain-duration"
pgHealthCheckDurationKey = "pg-health-check-duration"
// DefaultMaintenanceTimeout is the period for which a drained failure domain will remain in noout
DefaultMaintenanceTimeout = 30 * time.Minute
nooutFlag = "noout"
)
func (r *ReconcileClusterDisruption) createPDBForOSD(deployment appsv1.Deployment) error {
deploymentLabels := deployment.ObjectMeta.GetLabels()
deploymentName := deployment.ObjectMeta.Name
namespace := deployment.ObjectMeta.Namespace
osdIDLabel, ok := deploymentLabels[osd.OsdIdLabelKey]
if !ok {
return errors.Errorf("could not find id label on osd %s/%s", namespace, deploymentName)
func (r *ReconcileClusterDisruption) createPDB(pdb *policyv1beta1.PodDisruptionBudget) error {
err := r.client.Create(context.TODO(), pdb)
if err != nil && !kerrors.IsAlreadyExists(err) {
return errors.Wrapf(err, "failed to create pdb %q", pdb.Name)
}
return nil
}
func (r *ReconcileClusterDisruption) deletePDB(pdb *policyv1beta1.PodDisruptionBudget) error {
err := r.client.Delete(context.TODO(), pdb)
if err != nil && !kerrors.IsNotFound(err) {
return errors.Wrapf(err, "failed to delete pdb %q", pdb.Name)
}
return nil
}
// createOverallPDBforOSD creates a single PDB for all OSDs with maxUnavailable=1
// This allows all OSDs in a single failure domain to go down.
func (r *ReconcileClusterDisruption) createOverallPDBforOSD(namespace string) error {
cephCluster, ok := r.clusterMap.GetCluster(namespace)
if !ok {
return errors.Errorf("the namespace %s was not found in the clustermap", namespace)
return errors.Errorf("failed to find the namespace %q in the clustermap", namespace)
}
pdbRequest := types.NamespacedName{Name: osdPDBAppName, Namespace: namespace}
pdb := &policyv1beta1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Name: deploymentName,
Name: osdPDBAppName,
Namespace: namespace,
Labels: map[string]string{
k8sutil.AppAttr: PDBAppName,
osd.OsdIdLabelKey: osdIDLabel,
},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: cephCluster.APIVersion,
Kind: cephCluster.Kind,
Name: cephCluster.ObjectMeta.GetName(),
UID: cephCluster.GetUID(),
},
opcontroller.ClusterOwnerRef(cephCluster.GetName(), string(cephCluster.GetUID())),
},
},
Spec: policyv1beta1.PodDisruptionBudgetSpec{
MaxUnavailable: &intstr.IntOrString{IntVal: 1},
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{k8sutil.AppAttr: osd.AppName},
},
},
}
err := r.client.Get(context.TODO(), pdbRequest, &policyv1beta1.PodDisruptionBudget{})
if err != nil {
if apierrors.IsNotFound(err) {
logger.Info("all osds are up. pg health is active+clean")
logger.Infof("creating the main pdb %q with maxUnavailable=1 for all osd", osdPDBAppName)
return r.createPDB(pdb)
}
return errors.Wrapf(err, "failed to get pdb %q", pdb.Name)
}
return nil
}
func (r *ReconcileClusterDisruption) deleteOverallPDBforOSD(namespace string) error {
pdbRequest := types.NamespacedName{Name: osdPDBAppName, Namespace: namespace}
pdb := &policyv1beta1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Name: osdPDBAppName,
Namespace: namespace,
},
}
err := r.client.Get(context.TODO(), pdbRequest, &policyv1beta1.PodDisruptionBudget{})
if err != nil {
if apierrors.IsNotFound(err) {
return nil
}
return errors.Wrapf(err, "failed to get pdb %q", pdb.Name)
}
logger.Infof("deleting the main pdb %q with maxUnavailable=1 for all osd", osdPDBAppName)
return r.deletePDB(pdb)
}
// createBlockingPDBForOSD creates individual blocking PDBs (maxUnavailable=0) for all the OSDs in
// failure domains that are not draining
func (r *ReconcileClusterDisruption) createBlockingPDBForOSD(namespace, failureDomainType, failureDomainName string) error {
cephCluster, ok := r.clusterMap.GetCluster(namespace)
if !ok {
return errors.Errorf("failed to find the namespace %q in the clustermap", namespace)
}
pdbName := getPDBName(failureDomainType, failureDomainName)
pdbRequest := types.NamespacedName{Name: pdbName, Namespace: namespace}
pdb := &policyv1beta1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Name: pdbName,
Namespace: namespace,
OwnerReferences: []metav1.OwnerReference{
opcontroller.ClusterOwnerRef(cephCluster.GetName(), string(cephCluster.GetUID())),
},
},
Spec: policyv1beta1.PodDisruptionBudgetSpec{
MaxUnavailable: &intstr.IntOrString{IntVal: 0},
Selector: deployment.Spec.Selector,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{fmt.Sprintf(osd.TopologyLocationLabel, failureDomainType): failureDomainName},
},
},
}
err := r.client.Create(context.TODO(), pdb)
if err != nil && !kerrors.IsAlreadyExists(err) {
return errors.Wrapf(err, "could not create pdb for osd: %s in namespace %s", osdIDLabel, namespace)
err := r.client.Get(context.TODO(), pdbRequest, &policyv1beta1.PodDisruptionBudget{})
if err != nil {
if apierrors.IsNotFound(err) {
logger.Infof("creating temporary blocking pdb %q with maxUnavailable=0 for %q failure domain %q", pdbName, failureDomainType, failureDomainName)
return r.createPDB(pdb)
}
return errors.Wrapf(err, "failed to get pdb %q", pdb.Name)
}
return nil
}
func (r *ReconcileClusterDisruption) deletePDB(deployment appsv1.Deployment) error {
deploymentLabels := deployment.ObjectMeta.GetLabels()
deploymentName := deployment.ObjectMeta.Name
namespace := deployment.ObjectMeta.Namespace
osdIDLabel, ok := deploymentLabels[osd.OsdIdLabelKey]
if !ok {
return errors.Errorf("could not find id label on osd %s/%s", namespace, deploymentName)
}
func (r *ReconcileClusterDisruption) deleteBlockingPDBForOSD(namespace, failureDomainType, failureDomainName string) error {
pdbName := getPDBName(failureDomainType, failureDomainName)
pdbRequest := types.NamespacedName{Name: pdbName, Namespace: namespace}
pdb := &policyv1beta1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Name: deploymentName,
Name: pdbName,
Namespace: namespace,
},
}
err := r.client.Delete(context.TODO(), pdb)
if err != nil && !kerrors.IsNotFound(err) {
return errors.Wrapf(err, "could not delete pdb for osd: %s in namespace %s", osdIDLabel, namespace)
err := r.client.Get(context.TODO(), pdbRequest, &policyv1beta1.PodDisruptionBudget{})
if err != nil {
if apierrors.IsNotFound(err) {
return nil
}
return errors.Wrapf(err, "failed to get pdb %q", pdb.Name)
}
return nil
logger.Infof("deleting temporary blocking pdb with %q with maxUnavailable=0 for %q failure domain %q", pdbName, failureDomainType, failureDomainName)
return r.deletePDB(pdb)
}
func (r *ReconcileClusterDisruption) initializePDBState(request reconcile.Request, osdDataList []OsdData) (*corev1.ConfigMap, error) {
func (r *ReconcileClusterDisruption) initializePDBState(request reconcile.Request) (*corev1.ConfigMap, error) {
pdbStateMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: pdbStateMapName,
@@ -129,23 +195,14 @@ func (r *ReconcileClusterDisruption) initializePDBState(request reconcile.Reques
err := r.client.Get(context.TODO(), pdbStateMapRequest, pdbStateMap)
if kerrors.IsNotFound(err) {
// create configmap and PDBs for all nodes labeled by failuredomain
logger.Infof("inititalizing pod disruption budgets for osds")
// one pdb is created per OSD, but after initialization they are created/deleted in failuredomain groups
for _, osdData := range osdDataList {
err := r.createPDBForOSD(osdData.Deployment)
if err != nil {
return pdbStateMap, errors.Wrapf(err, "failed to create pdb for osd deployment %q. %v", osdData.Deployment.ObjectMeta.GetName(), err)
}
}
pdbStateMap.Data = map[string]string{disabledPDBKey: ""}
// create configmap
// create configmap to track the draining failure domain
pdbStateMap.Data = map[string]string{drainingFailureDomainKey: ""}
err := r.client.Create(context.TODO(), pdbStateMap)
if err != nil {
return pdbStateMap, errors.Wrapf(err, "could not create the PDB state map %s", pdbStateMapRequest)
return pdbStateMap, errors.Wrapf(err, "failed to create the PDB state map %q", pdbStateMapRequest)
}
} else if err != nil {
return pdbStateMap, errors.Wrapf(err, "could not get the pdbStateMap %s", pdbStateMapRequest)
return pdbStateMap, errors.Wrapf(err, "failed to get the pdbStateMap %s", pdbStateMapRequest)
}
return pdbStateMap, nil
}
@@ -154,11 +211,10 @@ func (r *ReconcileClusterDisruption) reconcilePDBsForOSDs(
clusterInfo *cephclient.ClusterInfo,
request reconcile.Request,
pdbStateMap *corev1.ConfigMap,
poolFailureDomain string,
allFailureDomainsMap,
drainingFailureDomainsMap map[string][]OsdData,
failureDomainType string,
allFailureDomains,
drainingFailureDomains []string,
) error {
drainingFailureDomains := getSortedOSDMapKeys(drainingFailureDomainsMap)
pgHealthMsg, clean, err := cephclient.IsClusterClean(r.context.ClusterdContext, clusterInfo)
if err != nil {
@@ -168,130 +224,215 @@ func (r *ReconcileClusterDisruption) reconcilePDBsForOSDs(
logger.Debugf("Ceph %q cluster not ready, cannot check Ceph status yet.", request.Namespace)
return nil
}
return errors.Wrapf(err, "could not check cluster health")
return errors.Wrapf(err, "failed to check cluster health")
}
if pdbStateMap.Data == nil {
pdbStateMap.Data = make(map[string]string)
}
_, ok := pdbStateMap.Data[disabledPDBKey]
_, ok := pdbStateMap.Data[drainingFailureDomainKey]
if !ok {
pdbStateMap.Data[disabledPDBKey] = ""
}
var disabledPDBTime time.Time
timeString, disabledPDBTimeSet := pdbStateMap.Data[disabledPDBTimeKey]
if disabledPDBTimeSet {
disabledPDBTime, err = time.Parse(time.RFC3339, timeString)
if err != nil {
logger.Errorf("could not parse timestamp %v. %v", disabledPDBTime, err)
disabledPDBTime = time.Now()
pdbStateMap.Data[disabledPDBTimeKey] = disabledPDBTime.Format(time.RFC3339)
}
}
recentlyChanged := disabledPDBTimeSet && time.Since(disabledPDBTime) < time.Minute
shouldChange := clean && !recentlyChanged
activeDrains := len(drainingFailureDomains) != 0
if activeDrains {
logger.Infof("pg health: %q. detected drains on %q: %v", pgHealthMsg, poolFailureDomain, drainingFailureDomains)
}
if shouldChange {
if activeDrains {
pdbStateMap.Data[disabledPDBKey] = drainingFailureDomains[0]
pdbStateMap.Data[disabledPDBTimeKey] = time.Now().Format(time.RFC3339)
} else {
pdbStateMap.Data[disabledPDBKey] = ""
delete(pdbStateMap.Data, disabledPDBTimeKey)
}
pdbStateMap.Data[drainingFailureDomainKey] = ""
}
err = r.updateNoout(clusterInfo, pdbStateMap, allFailureDomainsMap)
var drainingFailureDomain string
activeDrains := len(drainingFailureDomains) > 0
// create blocking PDBs for all the OSDs that are not being drained right now
if activeDrains {
drainingFailureDomain = drainingFailureDomains[0]
logger.Infof("all %q failure domains: %v. currently draining: %q. pg health: %q", failureDomainType,
allFailureDomains, drainingFailureDomain, pgHealthMsg)
err := r.handleActiveDrains(allFailureDomains, drainingFailureDomain, failureDomainType, clusterInfo.Namespace, clean)
if err != nil {
return errors.Wrap(err, "failed to handle active drains")
}
currentlyDrainingFD, ok := pdbStateMap.Data[drainingFailureDomainKey]
if !ok || drainingFailureDomain != currentlyDrainingFD {
pdbStateMap.Data[drainingFailureDomainKey] = drainingFailureDomain
pdbStateMap.Data[drainingFailureDomainDurationKey] = time.Now().Format(time.RFC3339)
}
} else if clean || r.hasPGHealthCheckTimedout(pdbStateMap) {
// reset the configMap if cluster is clean or if the timeout for PGs to become active+clean has exceeded
logger.Debugf("no drains detected in %q failure domains: %v. pg health: %q", failureDomainType, allFailureDomains, pgHealthMsg)
pdbStateMap.Data[drainingFailureDomainKey] = ""
delete(pdbStateMap.Data, drainingFailureDomainDurationKey)
delete(pdbStateMap.Data, pgHealthCheckDurationKey)
} else {
logger.Infof("all osds are up. last drained failure domain: %q. pg health: %q. waiting for PGs to be active+clean or PG health check to timeout",
pdbStateMap.Data[drainingFailureDomainKey], pgHealthMsg)
}
err = r.updateNoout(clusterInfo, pdbStateMap, allFailureDomains)
if err != nil {
logger.Errorf("could not update maintenance noout in cluster %q with ceph image. %v", request, err)
logger.Errorf("failed to update maintenance noout in cluster %q. %v", request, err)
}
err = r.client.Update(context.TODO(), pdbStateMap)
if err != nil {
return errors.Wrapf(err, "could not update %q in cluster %q", pdbStateMapName, request)
return errors.Wrapf(err, "failed to update configMap %q in cluster %q", pdbStateMapName, request)
}
drainingFailureDomain, ok := pdbStateMap.Data[disabledPDBKey]
if ok && clean && len(drainingFailureDomain) > 0 {
canaryLabels := client.MatchingLabels{k8sutil.AppAttr: nodedrain.CanaryAppName, poolFailureDomain: drainingFailureDomain}
// list and delete only if it's old
drainingCanaryList := &appsv1.DeploymentList{}
err := r.client.List(context.TODO(), drainingCanaryList, canaryLabels, client.InNamespace(r.context.OperatorNamespace))
// handle inactive drains by deleting all blocking pdb and restoring the main pdb
if pdbStateMap.Data[drainingFailureDomainKey] == "" {
err := r.handleInactiveDrains(allFailureDomains, clusterInfo.Namespace, failureDomainType)
if err != nil {
return errors.Wrapf(err, "could not list canary pods by labels %q", canaryLabels)
return errors.Wrap(err, "failed to handle inactive drains")
}
// refresh old canaries in draining failure domain
for _, drainingCanary := range drainingCanaryList.Items {
if time.Since(drainingCanary.GetCreationTimestamp().Time) > time.Minute && drainingCanary.Status.ReadyReplicas < 1 {
localdrainingCanary := drainingCanary
err := r.client.Delete(context.TODO(), &localdrainingCanary)
}
return nil
}
func (r *ReconcileClusterDisruption) handleActiveDrains(allFailureDomains []string, drainingFailureDomain,
failureDomainType, namespace string, isClean bool) error {
for _, failureDomainName := range allFailureDomains {
// create blocking PDB for failure domains not currently draining
if failureDomainName != drainingFailureDomain {
err := r.createBlockingPDBForOSD(namespace, failureDomainType, failureDomainName)
if err != nil {
return errors.Wrapf(err, "failed to create blocking pdb for %q failure domain %q", failureDomainType, failureDomainName)
}
} else {
if isClean {
err := r.deleteBlockingPDBForOSD(namespace, failureDomainType, failureDomainName)
if err != nil {
logger.Warningf("could not delete canary deployment %q in namespace %q. %v", drainingCanary.GetName(), drainingCanary.GetNamespace(), err)
return errors.Wrapf(err, "failed to delete pdb for %q failure domain %q. %v", failureDomainType, failureDomainName, err)
}
}
}
}
for failureDomain, osdDataList := range allFailureDomainsMap {
for _, osdData := range osdDataList {
var err error
if failureDomain == pdbStateMap.Data[disabledPDBKey] {
err = r.deletePDB(osdData.Deployment)
} else {
err = r.createPDBForOSD(osdData.Deployment)
}
if err != nil {
return errors.Wrapf(err, "failed to reconcile pdb for osd deployment %q. %v", osdData.Deployment.ObjectMeta.GetName(), err)
}
}
}
// delete the main PDB for OSD
// This will allow all OSDs in the currently drained failure domain to be removed.
logger.Debug("deleting main pdb with maxUnavailable=1 for all osd")
err := r.deleteOverallPDBforOSD(namespace)
if err != nil {
return errors.Wrap(err, "failed to delete the main osd pdb")
}
return nil
}
func (r *ReconcileClusterDisruption) updateNoout(clusterInfo *cephclient.ClusterInfo, pdbStateMap *corev1.ConfigMap, allFailureDomainsMap map[string][]OsdData) error {
disabledFailureDomain := pdbStateMap.Data[disabledPDBKey]
func (r *ReconcileClusterDisruption) handleInactiveDrains(allFailureDomains []string, namespace, failureDomainType string) error {
err := r.createOverallPDBforOSD(namespace)
if err != nil {
return errors.Wrap(err, "failed to create main pdb")
}
for _, failureDomainName := range allFailureDomains {
err := r.deleteBlockingPDBForOSD(namespace, failureDomainType, failureDomainName)
if err != nil {
return errors.Wrapf(err, "failed to delete pdb for %q failure domain %q. %v", failureDomainType, failureDomainName, err)
}
logger.Debugf("deleted temporary blocking pdb for %q failure domain %q.", failureDomainType, failureDomainName)
}
return nil
}
func (r *ReconcileClusterDisruption) updateNoout(clusterInfo *cephclient.ClusterInfo, pdbStateMap *corev1.ConfigMap, allFailureDomains []string) error {
drainingFailureDomain := pdbStateMap.Data[drainingFailureDomainKey]
osdDump, err := cephclient.GetOSDDump(r.context.ClusterdContext, clusterInfo)
if err != nil {
return errors.Wrapf(err, "could not get osddump for reconciling maintenance noout in namespace %s", clusterInfo.Namespace)
return errors.Wrapf(err, "failed to get osddump for reconciling maintenance noout in namespace %s", clusterInfo.Namespace)
}
for failureDomain := range allFailureDomainsMap {
disabledFailureDomainTimeStampKey := fmt.Sprintf("%s-noout-last-set-at", failureDomain)
if disabledFailureDomain == failureDomain {
for _, failureDomainName := range allFailureDomains {
drainingFailureDomainTimeStampKey := fmt.Sprintf("%s-noout-last-set-at", failureDomainName)
if drainingFailureDomain == failureDomainName {
// get the time stamp
nooutSetTimeString, ok := pdbStateMap.Data[disabledFailureDomainTimeStampKey]
nooutSetTimeString, ok := pdbStateMap.Data[drainingFailureDomainTimeStampKey]
if !ok || len(nooutSetTimeString) == 0 {
// initialize it if it's not set
pdbStateMap.Data[disabledFailureDomainTimeStampKey] = time.Now().Format(time.RFC3339)
pdbStateMap.Data[drainingFailureDomainTimeStampKey] = time.Now().Format(time.RFC3339)
}
// parse the timestamp
nooutSetTime, err := time.Parse(time.RFC3339, pdbStateMap.Data[disabledFailureDomainTimeStampKey])
nooutSetTime, err := time.Parse(time.RFC3339, pdbStateMap.Data[drainingFailureDomainTimeStampKey])
if err != nil {
return errors.Wrapf(err, "could not parse timestamp %s for failureDomain %s", pdbStateMap.Data[disabledFailureDomainTimeStampKey], nooutSetTime)
return errors.Wrapf(err, "failed to parse timestamp %s for failureDomain %s", pdbStateMap.Data[drainingFailureDomainTimeStampKey], nooutSetTime)
}
if time.Since(nooutSetTime) >= r.maintenanceTimeout {
// noout expired
if _, err := osdDump.UpdateFlagOnCrushUnit(r.context.ClusterdContext, clusterInfo, false, failureDomain, nooutFlag); err != nil {
if _, err := osdDump.UpdateFlagOnCrushUnit(r.context.ClusterdContext, clusterInfo, false, failureDomainName, nooutFlag); err != nil {
return errors.Wrapf(err, "failed to update flag on crush unit when noout expired.")
}
} else {
// set noout
if _, err := osdDump.UpdateFlagOnCrushUnit(r.context.ClusterdContext, clusterInfo, true, failureDomain, nooutFlag); err != nil {
if _, err := osdDump.UpdateFlagOnCrushUnit(r.context.ClusterdContext, clusterInfo, true, failureDomainName, nooutFlag); err != nil {
return errors.Wrapf(err, "failed to update flag on crush unit while setting noout.")
}
}
} else {
// ensure noout unset
if _, err := osdDump.UpdateFlagOnCrushUnit(r.context.ClusterdContext, clusterInfo, false, failureDomain, nooutFlag); err != nil {
if _, err := osdDump.UpdateFlagOnCrushUnit(r.context.ClusterdContext, clusterInfo, false, failureDomainName, nooutFlag); err != nil {
return errors.Wrapf(err, "failed to update flag on crush unit when ensuring noout is unset.")
}
// delete the timestamp
delete(pdbStateMap.Data, disabledFailureDomainTimeStampKey)
delete(pdbStateMap.Data, drainingFailureDomainTimeStampKey)
}
}
return nil
}
func (r *ReconcileClusterDisruption) getOSDFailureDomains(clusterInfo *cephClient.ClusterInfo, request reconcile.Request, poolFailureDomain string) ([]string, []string, error) {
osdDeploymentList := &appsv1.DeploymentList{}
namespaceListOpts := client.InNamespace(request.Namespace)
topologyLocationLabel := fmt.Sprintf(osd.TopologyLocationLabel, poolFailureDomain)
err := r.client.List(context.TODO(), osdDeploymentList, client.MatchingLabels{k8sutil.AppAttr: osd.AppName}, namespaceListOpts)
if err != nil {
return []string{}, []string{}, errors.Wrap(err, "failed to list osd deployments")
}
allFailureDomains := sets.NewString()
drainingFailureDomains := sets.NewString()
for _, deployment := range osdDeploymentList.Items {
labels := deployment.Spec.Template.ObjectMeta.GetLabels()
failureDomainName := labels[topologyLocationLabel]
if failureDomainName == "" {
return []string{}, []string{}, errors.Errorf("failed to get the topology location label %q in OSD deployment %q",
topologyLocationLabel, deployment.Name)
}
if deployment.Status.ReadyReplicas < 1 {
if !drainingFailureDomains.Has(failureDomainName) {
drainingFailureDomains.Insert(failureDomainName)
}
} else {
if !allFailureDomains.Has(failureDomainName) {
allFailureDomains.Insert(failureDomainName)
}
}
}
return allFailureDomains.List(), drainingFailureDomains.List(), nil
}
func (r *ReconcileClusterDisruption) hasPGHealthCheckTimedout(pdbStateMap *corev1.ConfigMap) bool {
if r.pgHealthCheckTimeout == 0 {
logger.Debug("pg health check timeout is not set in the cluster. waiting for PGs to get active+clean")
return false
}
timeString, ok := pdbStateMap.Data[pgHealthCheckDurationKey]
if !ok || len(timeString) == 0 {
pdbStateMap.Data[pgHealthCheckDurationKey] = time.Now().Format(time.RFC3339)
} else {
pgHealthCheckDuration, err := time.Parse(time.RFC3339, timeString)
if err != nil {
logger.Errorf("failed to parse timestamp %v. %v", pgHealthCheckDuration, err)
pdbStateMap.Data[pgHealthCheckDurationKey] = time.Now().Format(time.RFC3339)
return false
}
timeElapsed := time.Since(pgHealthCheckDuration)
if timeElapsed >= r.pgHealthCheckTimeout {
logger.Info("timed out waiting for the PGs to become active+clean")
return true
}
timeleft := r.pgHealthCheckTimeout - timeElapsed
logger.Infof("waiting for %d minute(s) for PGs to become active+clean", int(timeleft.Minutes()))
}
return false
}
func getPDBName(failureDomainType, failureDomainName string) string {
return k8sutil.TruncateNodeName(fmt.Sprintf("%s-%s-%s", osdPDBAppName, failureDomainType, "%s"), failureDomainName)
}
@@ -0,0 +1,315 @@
/*
Copyright 2020 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 clusterdisruption
import (
"context"
"fmt"
"testing"
"time"
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/pkg/client/clientset/versioned/scheme"
"github.com/rook/rook/pkg/clusterd"
"github.com/rook/rook/pkg/daemon/ceph/client"
"github.com/rook/rook/pkg/operator/ceph/disruption/controllerconfig"
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"
v1 "k8s.io/api/core/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
const (
healthyCephStatus = `{"fsid":"877a47e0-7f6c-435e-891a-76983ab8c509","health":{"checks":{},"status":"HEALTH_OK"},"election_epoch":12,"quorum":[0,1,2],"quorum_names":["a","b","c"],"monmap":{"epoch":3,"fsid":"877a47e0-7f6c-435e-891a-76983ab8c509","modified":"2020-11-02 09:58:23.015313","created":"2020-11-02 09:57:37.719235","min_mon_release":14,"min_mon_release_name":"nautilus","features":{"persistent":["kraken","luminous","mimic","osdmap-prune","nautilus"],"optional":[]},"mons":[{"rank":0,"name":"a","public_addrs":{"addrvec":[{"type":"v2","addr":"172.30.74.42:3300","nonce":0},{"type":"v1","addr":"172.30.74.42:6789","nonce":0}]},"addr":"172.30.74.42:6789/0","public_addr":"172.30.74.42:6789/0"},{"rank":1,"name":"b","public_addrs":{"addrvec":[{"type":"v2","addr":"172.30.101.61:3300","nonce":0},{"type":"v1","addr":"172.30.101.61:6789","nonce":0}]},"addr":"172.30.101.61:6789/0","public_addr":"172.30.101.61:6789/0"},{"rank":2,"name":"c","public_addrs":{"addrvec":[{"type":"v2","addr":"172.30.250.55:3300","nonce":0},{"type":"v1","addr":"172.30.250.55:6789","nonce":0}]},"addr":"172.30.250.55:6789/0","public_addr":"172.30.250.55:6789/0"}]},"osdmap":{"osdmap":{"epoch":19,"num_osds":3,"num_up_osds":3,"num_in_osds":3,"num_remapped_pgs":0}},"pgmap":{"pgs_by_state":[{"state_name":"active+clean","count":96}],"num_pgs":96,"num_pools":3,"num_objects":79,"data_bytes":81553681,"bytes_used":3255447552,"bytes_avail":1646011994112,"bytes_total":1649267441664,"read_bytes_sec":853,"write_bytes_sec":5118,"read_op_per_sec":1,"write_op_per_sec":0},"fsmap":{"epoch":9,"id":1,"up":1,"in":1,"max":1,"by_rank":[{"filesystem_id":1,"rank":0,"name":"ocs-storagecluster-cephfilesystem-b","status":"up:active","gid":14161},{"filesystem_id":1,"rank":0,"name":"ocs-storagecluster-cephfilesystem-a","status":"up:standby-replay","gid":24146}],"up:standby":0},"mgrmap":{"epoch":10,"active_gid":14122,"active_name":"a","active_addrs":{"addrvec":[{"type":"v2","addr":"10.131.0.28:6800","nonce":1},{"type":"v1","addr":"10.131.0.28:6801","nonce":1}]}}}`
unHealthyCephStatus = `{"fsid":"613975f3-3025-4802-9de1-a2280b950e75","health":{"checks":{"OSD_DOWN":{"severity":"HEALTH_WARN","summary":{"message":"1 osds down"}},"OSD_HOST_DOWN":{"severity":"HEALTH_WARN","summary":{"message":"1 host (1 osds) down"}},"PG_AVAILABILITY":{"severity":"HEALTH_WARN","summary":{"message":"Reduced data availability: 101 pgs stale"}},"POOL_APP_NOT_ENABLED":{"severity":"HEALTH_WARN","summary":{"message":"application not enabled on 1 pool(s)"}}},"status":"HEALTH_WARN","overall_status":"HEALTH_WARN"},"election_epoch":12,"quorum":[0,1,2],"quorum_names":["rook-ceph-mon0","rook-ceph-mon2","rook-ceph-mon1"],"monmap":{"epoch":3,"fsid":"613975f3-3025-4802-9de1-a2280b950e75","modified":"2017-08-11 20:13:02.075679","created":"2017-08-11 20:12:35.314510","features":{"persistent":["kraken","luminous"],"optional":[]},"mons":[{"rank":0,"name":"rook-ceph-mon0","addr":"10.3.0.45:6789/0","public_addr":"10.3.0.45:6789/0"},{"rank":1,"name":"rook-ceph-mon2","addr":"10.3.0.249:6789/0","public_addr":"10.3.0.249:6789/0"},{"rank":2,"name":"rook-ceph-mon1","addr":"10.3.0.252:6789/0","public_addr":"10.3.0.252:6789/0"}]},"osdmap":{"osdmap":{"epoch":17,"num_osds":2,"num_up_osds":1,"num_in_osds":2,"full":false,"nearfull":true,"num_remapped_pgs":0}},"pgmap":{"pgs_by_state":[{"state_name":"stale+active+clean","count":101},{"state_name":"active+clean","count":99}],"num_pgs":200,"num_pools":2,"num_objects":243,"data_bytes":976793635,"bytes_used":13611479040,"bytes_avail":19825307648,"bytes_total":33436786688},"fsmap":{"epoch":1,"by_rank":[]},"mgrmap":{"epoch":3,"active_gid":14111,"active_name":"rook-ceph-mgr0","active_addr":"10.2.73.6:6800/9","available":true,"standbys":[],"modules":["restful","status"],"available_modules":["dashboard","prometheus","restful","status","zabbix"]},"servicemap":{"epoch":1,"modified":"0.000000","services":{}}}`
)
var namespace = "rook-ceph"
var cephCluster = &cephv1.CephCluster{
ObjectMeta: metav1.ObjectMeta{Name: "ceph-cluster"}}
var pdbConfigMap = &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: pdbStateMapName, Namespace: namespace}}
func fakeOSDDeployment(id, readyReplicas int) appsv1.Deployment {
osd := appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("osd-%d", id),
Namespace: namespace,
Labels: map[string]string{
"app": "rook-ceph-osd",
},
},
Spec: appsv1.DeploymentSpec{
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"topology-location-zone": fmt.Sprintf("zone-%d", id),
},
},
},
},
Status: appsv1.DeploymentStatus{
ReadyReplicas: int32(readyReplicas),
},
}
return osd
}
func getFakeReconciler(t *testing.T, obj ...runtime.Object) *ReconcileClusterDisruption {
scheme := scheme.Scheme
err := policyv1beta1.AddToScheme(scheme)
if err != nil {
assert.Fail(t, "failed to build scheme")
}
err = appsv1.AddToScheme(scheme)
if err != nil {
assert.Fail(t, "failed to build scheme")
}
err = corev1.AddToScheme(scheme)
if err != nil {
assert.Fail(t, "failed to build scheme")
}
client := fake.NewFakeClientWithScheme(scheme, obj...)
return &ReconcileClusterDisruption{
client: client,
scheme: scheme,
clusterMap: &ClusterMap{clusterMap: map[string]*cephv1.CephCluster{namespace: cephCluster}},
}
}
func getFakeClusterInfo() *client.ClusterInfo {
sharedClusterMap := &ClusterMap{}
sharedClusterMap.UpdateClusterMap(namespace, cephCluster)
return sharedClusterMap.GetClusterInfo(namespace)
}
func TestGetOSDFailureDomains(t *testing.T) {
testcases := []struct {
label string
osds []appsv1.Deployment
expectedAllFailureDomains []string
expectedDrainingFailureDomains []string
}{
{
label: "case 1: all osds are running",
osds: []appsv1.Deployment{fakeOSDDeployment(1, 1), fakeOSDDeployment(2, 1),
fakeOSDDeployment(3, 1)},
expectedAllFailureDomains: []string{"zone-1", "zone-2", "zone-3"},
expectedDrainingFailureDomains: []string{},
},
{
label: "case 1: osd in zone-1 is pending",
osds: []appsv1.Deployment{fakeOSDDeployment(1, 0), fakeOSDDeployment(2, 1),
fakeOSDDeployment(3, 1)},
expectedAllFailureDomains: []string{"zone-2", "zone-3"},
expectedDrainingFailureDomains: []string{"zone-1"},
},
{
label: "case 1: osds in zone-1 and zone-2 are pending",
osds: []appsv1.Deployment{fakeOSDDeployment(1, 0), fakeOSDDeployment(2, 0),
fakeOSDDeployment(3, 1)},
expectedAllFailureDomains: []string{"zone-3"},
expectedDrainingFailureDomains: []string{"zone-1", "zone-2"},
},
}
for _, tc := range testcases {
r := getFakeReconciler(t, cephCluster, &corev1.ConfigMap{}, tc.osds[0].DeepCopy(),
tc.osds[1].DeepCopy(), tc.osds[2].DeepCopy())
clusterInfo := getFakeClusterInfo()
request := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace}}
allfailureDomains, drainingFailureDomains, err := r.getOSDFailureDomains(clusterInfo, request, "zone")
assert.NoError(t, err)
assert.Equal(t, tc.expectedAllFailureDomains, allfailureDomains)
assert.Equal(t, tc.expectedDrainingFailureDomains, drainingFailureDomains)
}
}
func TestGetOSDFailureDomainsError(t *testing.T) {
testcases := []struct {
label string
osds []appsv1.Deployment
expectedAllFailureDomains []string
expectedDrainingFailureDomains []string
}{
{
label: "case 1: one or more OSD deployment is missing crush location label",
osds: []appsv1.Deployment{fakeOSDDeployment(1, 1), fakeOSDDeployment(2, 1),
fakeOSDDeployment(3, 1)},
expectedAllFailureDomains: []string{},
expectedDrainingFailureDomains: []string{},
},
}
for _, tc := range testcases {
osd := tc.osds[0].DeepCopy()
osd.Spec.Template.ObjectMeta.Labels["topology-location-zone"] = ""
r := getFakeReconciler(t, cephCluster, &corev1.ConfigMap{},
tc.osds[1].DeepCopy(), tc.osds[2].DeepCopy(), osd)
clusterInfo := getFakeClusterInfo()
request := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace}}
allfailureDomains, drainingFailureDomains, err := r.getOSDFailureDomains(clusterInfo, request, "zone")
assert.Error(t, err)
assert.Equal(t, tc.expectedAllFailureDomains, allfailureDomains)
assert.Equal(t, tc.expectedDrainingFailureDomains, drainingFailureDomains)
}
}
func TestReconcilePDBForOSD(t *testing.T) {
pdbConfig := pdbConfigMap.DeepCopy()
r := getFakeReconciler(t, cephCluster, pdbConfig)
clusterInfo := getFakeClusterInfo()
request := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace}}
testcases := []struct {
label string
fakeCephStatus string
fakeOSDDump string
allFailureDomains []string
drainingFailureDomains []string
expectedOSDPDBCount int
expectedMaxUnavailableCount int
expectedDrainingFailureDomainName string
}{
{
label: "case 1: no draining failure domain and all pgs are healthy",
fakeCephStatus: healthyCephStatus,
fakeOSDDump: `{"OSDs": [{"OSD": 3, "Up": 3, "In": 3}]}`,
allFailureDomains: []string{"zone-1", "zone-2", "zone-3"},
drainingFailureDomains: []string{},
expectedOSDPDBCount: 1,
expectedMaxUnavailableCount: 1,
expectedDrainingFailureDomainName: "",
},
{
label: "case 2: zone-1 failure domain is draining and pgs are unhealthy",
fakeCephStatus: unHealthyCephStatus,
fakeOSDDump: `{"OSDs": [{"OSD": 3, "Up": 3, "In": 2}]}`,
allFailureDomains: []string{"zone-1", "zone-2", "zone-3"},
drainingFailureDomains: []string{"zone-1"},
expectedOSDPDBCount: 2,
expectedMaxUnavailableCount: 0,
expectedDrainingFailureDomainName: "zone-1",
},
{
label: "case 3: zone-1 is back online. But pgs are still unhealthy from zone-1 drain",
fakeCephStatus: unHealthyCephStatus,
fakeOSDDump: `{"OSDs": [{"OSD": 3, "Up": 3, "In": 3}]}`,
allFailureDomains: []string{"zone-1", "zone-2", "zone-3"},
drainingFailureDomains: []string{},
expectedOSDPDBCount: 2,
expectedMaxUnavailableCount: 0,
expectedDrainingFailureDomainName: "zone-1",
},
{
label: "case 4: zone-1 is back online and pgs are also healthy",
fakeCephStatus: healthyCephStatus,
fakeOSDDump: `{"OSDs": [{"OSD": 3, "Up": 3, "In": 3}]}`,
allFailureDomains: []string{"zone-1", "zone-2", "zone-3"},
drainingFailureDomains: []string{},
expectedOSDPDBCount: 1,
expectedMaxUnavailableCount: 1,
expectedDrainingFailureDomainName: "",
},
}
for _, tc := range testcases {
executor := &exectest.MockExecutor{}
executor.MockExecuteCommandWithOutputFile = func(command, outputFile string, args ...string) (string, error) {
logger.Infof("Command: %s %v", command, args)
if args[0] == "status" {
return tc.fakeCephStatus, nil
}
if args[0] == "osd" && args[1] == "dump" {
return tc.fakeOSDDump, nil
}
return "", errors.Errorf("unexpected ceph command '%v'", args)
}
r.context = &controllerconfig.Context{ClusterdContext: &clusterd.Context{Executor: executor}}
err := r.reconcilePDBsForOSDs(clusterInfo, request, pdbConfig, "zone", tc.allFailureDomains, tc.drainingFailureDomains)
assert.NoError(t, err)
// assert that pdb for osd are created correctly
existingPDBs := &policyv1beta1.PodDisruptionBudgetList{}
err = r.client.List(context.TODO(), existingPDBs)
assert.NoError(t, err)
assert.Equal(t, tc.expectedOSDPDBCount, len(existingPDBs.Items))
for _, pdb := range existingPDBs.Items {
assert.Equal(t, tc.expectedMaxUnavailableCount, pdb.Spec.MaxUnavailable.IntValue())
}
// assert that config map is updated with correct failure domain
existingConfigMaps := &corev1.ConfigMapList{}
err = r.client.List(context.TODO(), existingConfigMaps)
assert.NoError(t, err)
assert.Equal(t, tc.expectedDrainingFailureDomainName, existingConfigMaps.Items[0].Data[drainingFailureDomainKey])
}
}
func TestPGHealthcheckTimeout(t *testing.T) {
pdbConfig := pdbConfigMap.DeepCopy()
r := getFakeReconciler(t, cephCluster, pdbConfig)
clusterInfo := getFakeClusterInfo()
request := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: namespace}}
executor := &exectest.MockExecutor{}
executor.MockExecuteCommandWithOutputFile = func(command, outputFile string, args ...string) (string, error) {
logger.Infof("Command: %s %v", command, args)
if args[0] == "status" {
return unHealthyCephStatus, nil
}
if args[0] == "osd" && args[1] == "dump" {
return `{"OSDs": [{"OSD": 3, "Up": 3, "In": 3}]}`, nil
}
return "", errors.Errorf("unexpected ceph command '%v'", args)
}
r.context = &controllerconfig.Context{ClusterdContext: &clusterd.Context{Executor: executor}}
// set PG health check timeout to 10 minutes
r.pgHealthCheckTimeout = time.Duration(time.Minute * 10)
// reconcile OSD PDB with active drains (on zone-1) and unhealthy PGs
err := r.reconcilePDBsForOSDs(clusterInfo, request, pdbConfig, "zone", []string{"zone-1", "zone-2"}, []string{"zone-1"})
assert.NoError(t, err)
assert.Equal(t, "zone-1", pdbConfig.Data[drainingFailureDomainKey])
// update the pgHealthCheckDuration time by -9 minutes
pdbConfig.Data[pgHealthCheckDurationKey] = time.Now().Add(time.Duration(-7) * time.Minute).Format(time.RFC3339)
// reconcile OSD PDB with no active drains and unhealthy PGs
err = r.reconcilePDBsForOSDs(clusterInfo, request, pdbConfig, "zone", []string{"zone-1", "zone-2"}, []string{})
assert.NoError(t, err)
// assert that pdb config map was not reset as the PG health check was not timed out
assert.Equal(t, "zone-1", pdbConfig.Data[drainingFailureDomainKey])
// update the drainingFailureDomain time by -9 minutes
pdbConfig.Data[pgHealthCheckDurationKey] = time.Now().Add(time.Duration(-11) * time.Minute).Format(time.RFC3339)
// reconcile OSD PDB with no active drains and unhealthy PGs
err = r.reconcilePDBsForOSDs(clusterInfo, request, pdbConfig, "zone", []string{"zone-1", "zone-2"}, []string{})
assert.NoError(t, err)
// assert that pdb config map was reset as the PG health check was timed out
assert.Equal(t, "", pdbConfig.Data[drainingFailureDomainKey])
}
@@ -25,12 +25,8 @@ import (
"github.com/pkg/errors"
"github.com/rook/rook/pkg/operator/ceph/cluster/osd"
"github.com/rook/rook/pkg/operator/ceph/disruption/nodedrain"
"github.com/rook/rook/pkg/operator/k8sutil"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
@@ -109,43 +105,6 @@ func getMinimumFailureDomain(poolList []cephv1.PoolSpec) string {
return osd.CRUSHMapLevelsOrdered[minfailureDomainIndex]
}
func (r *ReconcileClusterDisruption) getOngoingDrains(request reconcile.Request) ([]*corev1.Node, error) {
// Get the canary deployments
canaryDeploymentList := &appsv1.DeploymentList{}
operatorNameSpaceListOpts := client.InNamespace(request.Namespace)
err := r.client.List(context.TODO(), canaryDeploymentList, client.MatchingLabels{k8sutil.AppAttr: nodedrain.CanaryAppName}, operatorNameSpaceListOpts)
if err != nil {
return nil, errors.Wrap(err, "could not list canary deployments")
}
ongoingDrains := make([]*corev1.Node, 0)
for _, deployment := range canaryDeploymentList.Items {
if deployment.Status.ReadyReplicas < 1 {
nodeHostname, ok := deployment.Spec.Template.Spec.NodeSelector[corev1.LabelHostname]
if !ok {
logger.Errorf("could not find a the nodeSelector key %q for canary deployment %q", corev1.LabelHostname, deployment.ObjectMeta.Name)
continue
}
nodeList := &corev1.NodeList{}
err = r.client.List(context.TODO(), nodeList, client.MatchingLabels{corev1.LabelHostname: nodeHostname})
if err != nil {
return nil, errors.Errorf("could not get node: %s ", nodeHostname)
} else if len(nodeList.Items) < 1 {
logger.Infof("node %q does not exist, deleting corresponding drain-canary", nodeHostname)
err = r.client.DeleteAllOf(context.TODO(), &appsv1.Deployment{}, client.MatchingLabels{corev1.LabelHostname: nodeHostname, k8sutil.AppAttr: nodedrain.CanaryAppName})
if err != nil {
return nil, errors.Wrapf(err, "could not delete drain-canary for host %q", nodeHostname)
}
} else if len(nodeList.Items) > 1 {
logger.Warningf("found more than one node with %s=%s", corev1.LabelHostname, nodeHostname)
}
ongoingDrains = append(ongoingDrains, &nodeList.Items[0])
}
}
return ongoingDrains, nil
}
// Setting naive minAvailable for RGW at: n - 1
func (r *ReconcileClusterDisruption) reconcileCephObjectStore(cephObjectStoreList *cephv1.CephObjectStoreList) error {
for _, objectStore := range cephObjectStoreList.Items {
@@ -25,18 +25,21 @@ import (
"github.com/coreos/pkg/capnslog"
cephClient "github.com/rook/rook/pkg/daemon/ceph/client"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/rook/rook/pkg/operator/ceph/disruption/controllerconfig"
"github.com/rook/rook/pkg/operator/k8sutil"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
appsv1 "k8s.io/api/apps/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"
)
const (
osdDisruptionAppName = "rook-ceph-osd-disruption"
controllerName = "clusterdisruption-controller"
controllerName = "clusterdisruption-controller"
// pdbStateMapName for the clusterdisruption pdb state map
pdbStateMapName = "rook-ceph-pdbstatemap"
)
@@ -46,17 +49,20 @@ var (
// Implement reconcile.Reconciler so the controller can reconcile objects
_ reconcile.Reconciler = &ReconcileClusterDisruption{}
// delete legacy drain canary pods and blocking OSD podDisruptionBudgets
deleteLegacyResources = true
)
// ReconcileClusterDisruption reconciles ReplicaSets
type ReconcileClusterDisruption struct {
// client can be used to retrieve objects from the APIServer.
scheme *runtime.Scheme
client client.Client
context *controllerconfig.Context
clusterMap *ClusterMap
osdCrushLocationMap *OSDCrushLocationMap
maintenanceTimeout time.Duration
scheme *runtime.Scheme
client client.Client
context *controllerconfig.Context
clusterMap *ClusterMap
maintenanceTimeout time.Duration
pgHealthCheckTimeout time.Duration
}
// Reconcile reconciles a node and ensures that it has a drain-detection deployment
@@ -111,14 +117,33 @@ func (r *ReconcileClusterDisruption) reconcile(request reconcile.Request) (recon
// feature disabled for this cluster. not requeueing
return reconcile.Result{Requeue: false}, nil
}
//signal to the nodedrain controller to start
r.context.ReconcileCanaries.Update(true)
r.maintenanceTimeout = cephCluster.Spec.DisruptionManagement.OSDMaintenanceTimeout
if deleteLegacyResources {
// delete any legacy blocking pdbs for osd
err := r.deleteLegacyPDBForOSD(clusterInfo.Namespace)
if err != nil {
return reconcile.Result{}, err
}
logger.Info("deleted all legacy blocking pdbs for osds")
// delete any legacy node drain canary pods
err = r.deleteDrainCanaryPods(clusterInfo.Namespace)
if err != nil {
return reconcile.Result{}, err
}
logger.Info("deleted all legacy node drain canary pods")
deleteLegacyResources = false
}
r.maintenanceTimeout = cephCluster.Spec.DisruptionManagement.OSDMaintenanceTimeout * time.Minute
if r.maintenanceTimeout == 0 {
r.maintenanceTimeout = DefaultMaintenanceTimeout
logger.Debugf("Using default maintenance timeout: %v", r.maintenanceTimeout)
}
r.pgHealthCheckTimeout = cephCluster.Spec.DisruptionManagement.PGHealthCheckTimeout * time.Minute
// reconcile the pools and get the failure domain
cephObjectStoreList, cephFilesystemList, poolFailureDomain, poolCount, err := r.processPools(request)
if err != nil {
@@ -148,46 +173,28 @@ func (r *ReconcileClusterDisruption) reconcile(request reconcile.Request) (recon
return reconcile.Result{}, nil
}
// get the osds with crush data populated
osdDataList, err := r.getOsdDataList(clusterInfo, request, poolFailureDomain)
// get all failure domains and the draining failure domains list
allFailureDomains, drainingFailureDomains, err := r.getOSDFailureDomains(clusterInfo, request, poolFailureDomain)
if err != nil {
return reconcile.Result{}, err
}
// get the list of nodes with ongoing drains
drainingNodes, err := r.getOngoingDrains(request)
// get the map that stores currently draining failure domain
pdbStateMap, err := r.initializePDBState(request)
if err != nil {
return reconcile.Result{}, err
}
drainingOSDs, err := getOSDsForNodes(osdDataList, drainingNodes, poolFailureDomain)
err = r.reconcilePDBsForOSDs(clusterInfo, request, pdbStateMap, poolFailureDomain, allFailureDomains, drainingFailureDomains)
if err != nil {
return reconcile.Result{}, err
}
allFailureDomainsMap, err := getFailureDomainMapForOsds(osdDataList, poolFailureDomain)
if err != nil {
logger.Error(err)
}
drainingFailureDomainsMap, err := getFailureDomainMapForOsds(drainingOSDs, poolFailureDomain)
if err != nil {
logger.Error(err)
}
// get the map that stores which PDBs are intentionally down
pdbStateMap, err := r.initializePDBState(request, osdDataList)
if err != nil {
return reconcile.Result{}, err
}
err = r.reconcilePDBsForOSDs(clusterInfo, request, pdbStateMap, poolFailureDomain, allFailureDomainsMap, drainingFailureDomainsMap)
if err != nil {
return reconcile.Result{}, err
}
disabledPDB, ok := pdbStateMap.Data[disabledPDBKey]
disabledPDB, ok := pdbStateMap.Data[drainingFailureDomainKey]
if ok && len(disabledPDB) > 0 {
return reconcile.Result{Requeue: true, RequeueAfter: 30 * time.Second}, nil
}
return reconcile.Result{}, nil
}
@@ -247,7 +254,7 @@ func (c *ClusterMap) GetCluster(namespace string) (*cephv1.CephCluster, bool) {
return cluster, true
}
// GetClusterMap returns the internal clustermap for iteration purporses
// GetClusterNamespaces returns the internal clustermap for iteration purporses
func (c *ClusterMap) GetClusterNamespaces() []string {
defer c.mux.Unlock()
c.mux.Lock()
@@ -257,3 +264,21 @@ func (c *ClusterMap) GetClusterNamespaces() []string {
}
return namespaces
}
func (r *ReconcileClusterDisruption) deleteDrainCanaryPods(namespace string) error {
err := r.client.DeleteAllOf(context.TODO(), &appsv1.Deployment{}, client.InNamespace(namespace),
client.MatchingLabels{k8sutil.AppAttr: "rook-ceph-drain-canary"})
if err != nil && !kerrors.IsNotFound(err) {
return errors.Wrap(err, "failed to delete all the legacy drain-canary pods")
}
return nil
}
func (r *ReconcileClusterDisruption) deleteLegacyPDBForOSD(namespace string) error {
err := r.client.DeleteAllOf(context.TODO(), &policyv1beta1.PodDisruptionBudget{}, client.InNamespace(namespace),
client.MatchingLabels{k8sutil.AppAttr: "rook-ceph-osd-pdb"})
if err != nil && !kerrors.IsNotFound(err) {
return errors.Wrap(err, "failed to delete all the legacy blocking podDisruptionBugets")
}
return nil
}
@@ -1,148 +0,0 @@
/*
Copyright 2019 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 nodedrain
import (
"reflect"
"github.com/rook/rook/pkg/operator/ceph/cluster/osd"
"github.com/rook/rook/pkg/operator/ceph/disruption/controllerconfig"
"github.com/rook/rook/pkg/operator/k8sutil"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
const (
controllerName = "nodedrain-controller"
// CanaryAppName is applied to nodedrain canary components (pods, deployments) with the key app
CanaryAppName = "rook-ceph-drain-canary"
NodeNameLabel = "node_name"
)
// Add adds a new Controller based on nodedrain.ReconcileNode and registers the relevant watches and handlers
func Add(mgr manager.Manager, context *controllerconfig.Context) error {
reconcileNode := &ReconcileNode{
client: mgr.GetClient(),
scheme: mgr.GetScheme(),
context: context,
}
reconciler := reconcile.Reconciler(reconcileNode)
// Create a new controller
c, err := controller.New(controllerName, mgr, controller.Options{Reconciler: reconciler})
if err != nil {
return err
}
// Watch for changes to the nodes
pred := predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
nodeOld, ok := e.ObjectOld.DeepCopyObject().(*corev1.Node)
if !ok {
return false
}
nodeNew, ok := e.ObjectNew.DeepCopyObject().(*corev1.Node)
if !ok {
return false
}
return !reflect.DeepEqual(nodeOld.Spec, nodeNew.Spec)
},
}
err = c.Watch(&source.Kind{Type: &corev1.Node{}}, &handler.EnqueueRequestForObject{}, pred)
if err != nil {
return err
}
// Watch for changes to the node canary deployments
err = c.Watch(
&source.Kind{Type: &appsv1.Deployment{}},
&handler.EnqueueRequestsFromMapFunc{
ToRequests: handler.ToRequestsFunc(func(obj handler.MapObject) []reconcile.Request {
deployment, ok := obj.Object.(*appsv1.Deployment)
if !ok {
return []reconcile.Request{}
}
labels := deployment.GetLabels()
if appName, ok := labels[k8sutil.AppAttr]; !ok || appName != CanaryAppName {
return []reconcile.Request{}
}
nodeName, ok := deployment.Spec.Template.ObjectMeta.Labels[NodeNameLabel]
if !ok {
return []reconcile.Request{}
}
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: nodeName}}
return []reconcile.Request{req}
}),
},
)
if err != nil {
return err
}
// Watch for changes to the osd pod nodename and enqueue their nodes
err = c.Watch(
&source.Kind{Type: &corev1.Pod{}},
&handler.EnqueueRequestsFromMapFunc{
ToRequests: handler.ToRequestsFunc(func(obj handler.MapObject) []reconcile.Request {
pod, ok := obj.Object.(*corev1.Pod)
if !ok {
return []reconcile.Request{}
}
labels := pod.GetLabels()
if appName, ok := labels[k8sutil.AppAttr]; !ok || appName != osd.AppName {
return []reconcile.Request{}
}
nodeName := pod.Spec.NodeName
if len(nodeName) < 1 {
return []reconcile.Request{}
}
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: nodeName}}
return []reconcile.Request{req}
}),
},
predicate.Funcs{
UpdateFunc: func(event event.UpdateEvent) bool {
oldPod, ok := event.ObjectOld.(*corev1.Pod)
if !ok {
return false
}
newPod, ok := event.ObjectNew.(*corev1.Pod)
if !ok {
return false
}
// only enqueue if the nodename has changed
if oldPod.Spec.NodeName == newPod.Spec.NodeName {
return false
}
return true
},
},
)
if err != nil {
return err
}
return nil
}
@@ -1,22 +0,0 @@
/*
Copyright 2019 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 nodedrain implements the controller for ensuring that drain detection deployments exist.
The design and purpose for drain detection is found at:
https://github.com/rook/rook/blob/master/design/ceph/ceph-managed-disruptionbudgets.md
*/
package nodedrain
@@ -1,291 +0,0 @@
/*
Copyright 2019 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 nodedrain
import (
"context"
"fmt"
"os"
"time"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/coreos/pkg/capnslog"
"github.com/pkg/errors"
"github.com/rook/rook/pkg/operator/ceph/cluster/osd"
"github.com/rook/rook/pkg/operator/ceph/disruption/controllerconfig"
"github.com/rook/rook/pkg/operator/k8sutil"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var (
logger = capnslog.NewPackageLogger("github.com/rook/rook", "nodedrain-controller")
// Implement reconcile.Reconciler so the controller can reconcile objects
_ reconcile.Reconciler = &ReconcileNode{}
)
// ReconcileNode reconciles ReplicaSets
type ReconcileNode struct {
// client can be used to retrieve objects from the APIServer.
scheme *runtime.Scheme
client client.Client
context *controllerconfig.Context
}
// Reconcile reconciles a node and ensures that it has a drain-detection deployment
// attached to it.
// The Controller will requeue the Request to be processed again if an error is non-nil or
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
func (r *ReconcileNode) Reconcile(request reconcile.Request) (reconcile.Result, error) {
// workaround because the rook logging mechanism is not compatible with the controller-runtime logging interface
result, err := r.reconcile(request)
if err != nil {
logger.Error(err)
}
return result, err
}
func (r *ReconcileNode) reconcile(request reconcile.Request) (reconcile.Result, error) {
if !r.context.ReconcileCanaries.Get() {
return reconcile.Result{}, nil
}
logger.Debugf("reconciling node: %q", request.Name)
// Create or Update the deployment default/foo
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: k8sutil.TruncateNodeName(fmt.Sprintf("%s-%%s", CanaryAppName), request.Name),
Namespace: r.context.OperatorNamespace,
},
}
node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: request.Name}}
err := r.client.Get(context.TODO(), request.NamespacedName, node)
if kerrors.IsNotFound(err) {
// delete any canary deployments if the node doesn't exist
logger.Infof("deleting drain-canary deployment for deleted node %q", request.Name)
err = r.client.Delete(context.TODO(), deploy)
if kerrors.IsNotFound(err) {
return reconcile.Result{}, nil
} else if err != nil {
return reconcile.Result{}, errors.Wrapf(err, "could not delete drain-canary deployment for deleted node %q", request.Name)
}
return reconcile.Result{}, nil
} else if err != nil {
return reconcile.Result{}, errors.Errorf("could not get node %q", request.NamespacedName)
}
nodeHostnameLabel, ok := node.ObjectMeta.Labels[corev1.LabelHostname]
if !ok {
return reconcile.Result{}, errors.Errorf("Label key %q does not exist on node %q", corev1.LabelHostname, request.NamespacedName)
}
osdPodList := &corev1.PodList{}
err = r.client.List(context.TODO(), osdPodList, client.MatchingLabels{k8sutil.AppAttr: osd.AppName})
if err != nil {
return reconcile.Result{}, errors.Wrap(err, "could not list the osd pods")
}
// map with tolerations as keys and empty struct as values for uniqueness
uniqueTolerations := controllerconfig.TolerationSet{}
occupiedByOSD := false
for _, osdPod := range osdPodList.Items {
if osdPod.Spec.NodeName == request.Name {
labels := osdPod.GetLabels()
deploymentList := &appsv1.DeploymentList{}
labelSelector := map[string]string{
osd.OsdIdLabelKey: labels[osd.OsdIdLabelKey],
k8sutil.AppAttr: osd.AppName,
}
var deployment appsv1.Deployment
err := r.client.List(context.TODO(), deploymentList, client.MatchingLabels(labelSelector), client.InNamespace(osdPod.GetNamespace()))
if err != nil || len(deploymentList.Items) < 1 {
logger.Errorf("cannot find deployment for osd id %q in namespace %q", labels[osd.OsdIdLabelKey], osdPod.GetNamespace())
} else {
deployment = deploymentList.Items[0]
}
if len(deploymentList.Items) > 1 {
logger.Errorf("found multiple deployments for osd id %q in namespace %q: %+v", labels[osd.OsdIdLabelKey], osdPod.GetNamespace(), deploymentList)
}
occupiedByOSD = true
// get the osd tolerations
for _, osdToleration := range deployment.Spec.Template.Spec.Tolerations {
if osdToleration.Key == "node.kubernetes.io/unschedulable" {
logger.Errorf(
"osd %q in namespace %q tolerates the drain taint, but the drain canary will not.",
labels[osd.OsdIdLabelKey],
labels[k8sutil.ClusterAttr],
)
} else {
uniqueTolerations.Add(osdToleration)
}
}
}
}
// CreateOrUpdate the deployment
mutateFunc := func() error {
// labels for the pod, the deployment, and the deploymentSelector
selectorLabels := map[string]string{
corev1.LabelHostname: nodeHostnameLabel,
k8sutil.AppAttr: CanaryAppName,
NodeNameLabel: node.GetName(),
}
nodeSelector := map[string]string{corev1.LabelHostname: nodeHostnameLabel}
// Deployment selector is immutable so we set this value only if
// a new object is going to be created
if deploy.ObjectMeta.CreationTimestamp.IsZero() {
deploy.Spec.Selector = &metav1.LabelSelector{
MatchLabels: selectorLabels,
}
}
ownerReferences := []metav1.OwnerReference{}
// get the operator Deployment to use as an owner reference
operatorPodKey := types.NamespacedName{Name: os.Getenv(k8sutil.PodNameEnvVar), Namespace: r.context.OperatorNamespace}
operatorDeployment, err := getDeploymentForPod(r.client, operatorPodKey)
if err != nil {
logger.Errorf("could not find rook operator deployment for pod %+v. %v", operatorPodKey, err)
} else {
operatorDeployment := operatorDeployment
controllerBool := true
operatorOwnerRef := metav1.OwnerReference{
APIVersion: operatorDeployment.APIVersion,
Kind: operatorDeployment.Kind,
UID: operatorDeployment.GetUID(),
Name: operatorDeployment.GetName(),
Controller: &controllerBool,
}
ownerReferences = append(ownerReferences, operatorOwnerRef)
}
deploy.ObjectMeta.OwnerReferences = ownerReferences
// update the deployment labels
topology := osd.ExtractOSDTopologyFromLabels(node.GetLabels())
for key, value := range topology {
selectorLabels[key] = value
}
deploy.ObjectMeta.Labels = selectorLabels
// update the Deployment pod template
deploy.Spec.Template = corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: selectorLabels},
Spec: corev1.PodSpec{
NodeSelector: nodeSelector,
Containers: newDoNothingContainers(r.context.RookImage),
Tolerations: uniqueTolerations.ToList(),
},
}
return nil
}
if occupiedByOSD {
op, err := controllerutil.CreateOrUpdate(context.TODO(), r.client, deploy, mutateFunc)
if err != nil {
return reconcile.Result{}, errors.Wrapf(err, "node reconcile failed on op %q", op)
}
msg := fmt.Sprintf("deployment successfully reconciled for node %q. operation: %q", request.Name, op)
if op == controllerutil.OperationResultCreated {
logger.Info(msg)
} else {
logger.Debug(msg)
}
} else {
logger.Debugf("not watching for drains on node %q as there are no osds running there.", request.Name)
// get the canary deployment
canarayDeployment := &appsv1.Deployment{}
key := types.NamespacedName{Name: deploy.GetName(), Namespace: deploy.GetNamespace()}
err := r.client.Get(context.TODO(), key, canarayDeployment)
if err != nil && !kerrors.IsNotFound(err) {
return reconcile.Result{}, errors.Wrapf(err, "could not fetch deployment %q", key)
} else if kerrors.IsNotFound(err) {
return reconcile.Result{}, nil
}
// delete the canary deployments that aren't triggered by drains, but are on nodes that aren't occupied by OSDs
// if it's on a draining node, we don't want to kill the canary.
if canarayDeployment.Status.ReadyReplicas > 0 {
logger.Infof("deleting drain-canary deployment on node %q as OSDs are no longer scheduled here", request.Name)
err := r.client.Delete(context.TODO(), canarayDeployment)
if err != nil {
return reconcile.Result{}, errors.Wrapf(err, "could not delete deployment %q", key)
}
}
}
return reconcile.Result{Requeue: true, RequeueAfter: time.Minute * 15}, nil
}
// returns a container that does nothing
func newDoNothingContainers(rookImage string) []corev1.Container {
return []corev1.Container{{
Image: rookImage,
Name: "sleep",
Command: []string{"/tini"},
Args: []string{"--", "sleep", "infinity"},
}}
}
func getDeploymentForPod(client client.Client, podKey types.NamespacedName) (*appsv1.Deployment, error) {
// get pod
operatorPod := &corev1.Pod{}
err := client.Get(context.TODO(), podKey, operatorPod)
if err != nil {
return nil, errors.Wrapf(err, "could not get pod %+v", podKey)
} else {
for _, rsRef := range operatorPod.ObjectMeta.OwnerReferences {
if *rsRef.Controller {
// get rs
replicaSet := &appsv1.ReplicaSet{}
replicaSetKey := types.NamespacedName{Name: rsRef.Name, Namespace: podKey.Namespace}
err := client.Get(context.TODO(), replicaSetKey, replicaSet)
if err != nil {
return nil, errors.Wrapf(err, "could not get replicaset %+v", replicaSetKey)
} else {
for _, depRef := range replicaSet.ObjectMeta.OwnerReferences {
if *depRef.Controller {
// get deployment
operatorDeployment := &appsv1.Deployment{}
operatorDeploymentKey := types.NamespacedName{Name: depRef.Name, Namespace: podKey.Namespace}
err := client.Get(context.TODO(), operatorDeploymentKey, operatorDeployment)
return operatorDeployment, err
}
}
}
}
}
}
return nil, errors.Errorf("could not get deployment for pod %+v", podKey)
}
@@ -150,6 +150,8 @@ spec:
type: boolean
osdMaintenanceTimeout:
type: integer
pgHealthCheckTimeout:
type: integer
manageMachineDisruptionBudgets:
type: boolean
skipUpgradeChecks:
@@ -232,6 +234,8 @@ spec:
type: boolean
osdMaintenanceTimeout:
type: integer
pgHealthCheckTimeout:
type: integer
manageMachineDisruptionBudgets:
type: boolean
useAllNodes:
@@ -1453,6 +1457,8 @@ spec:
type: boolean
osdMaintenanceTimeout:
type: integer
pgHealthCheckTimeout:
type: integer
manageMachineDisruptionBudgets:
type: boolean
skipUpgradeChecks:
@@ -1492,6 +1498,8 @@ spec:
type: boolean
osdMaintenanceTimeout:
type: integer
pgHealthCheckTimeout:
type: integer
manageMachineDisruptionBudgets:
type: boolean
useAllNodes:
@@ -3426,6 +3434,7 @@ spec:
disruptionManagement:
managePodBudgets: false
osdMaintenanceTimeout: 30
pgHealthCheckTimeout: 0
manageMachineDisruptionBudgets: false
machineDisruptionBudgetNamespace: openshift-machine-api`
}