Add a podDisruptionBudget for the whole cloud (#473)
This increases the minimum supported Kubernetes version to v1.21
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 8aa1552..2132bc0 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -152,6 +152,18 @@
verbs:
- get
- apiGroups:
+ - policy
+ resources:
+ - poddisruptionbudgets
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
- solr.apache.org
resources:
- solrbackups
diff --git a/controllers/controller_utils_test.go b/controllers/controller_utils_test.go
index ce29048..053c3f1 100644
--- a/controllers/controller_utils_test.go
+++ b/controllers/controller_utils_test.go
@@ -20,6 +20,7 @@
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
+ policyv1 "k8s.io/api/policy/v1"
"regexp"
solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
@@ -327,6 +328,40 @@
}).Should(MatchError("ingresses.networking.k8s.io \""+ingressName+"\" not found"), "Ingress exists when it should not")
}
+func expectPodDisruptionBudget(ctx context.Context, parentResource client.Object, podDisruptionBudgetName string, selector *metav1.LabelSelector, maxUnavailable intstr.IntOrString, additionalOffset ...int) *policyv1.PodDisruptionBudget {
+ return expectPodDisruptionBudgetWithChecks(ctx, parentResource, podDisruptionBudgetName, selector, maxUnavailable, nil, resolveOffset(additionalOffset))
+}
+
+func expectPodDisruptionBudgetWithChecks(ctx context.Context, parentResource client.Object, podDisruptionBudgetName string, selector *metav1.LabelSelector, maxUnavailable intstr.IntOrString, additionalChecks func(Gomega, *policyv1.PodDisruptionBudget), additionalOffset ...int) *policyv1.PodDisruptionBudget {
+ podDisruptionBudget := &policyv1.PodDisruptionBudget{}
+ EventuallyWithOffset(resolveOffset(additionalOffset), func(g Gomega) {
+ g.Expect(k8sClient.Get(ctx, resourceKey(parentResource, podDisruptionBudgetName), podDisruptionBudget)).To(Succeed(), "Expected ConfigMap does not exist")
+
+ // Verify the PodDisruptionBudget Spec
+ g.Expect(podDisruptionBudget.Spec.Selector).To(Equal(selector), "PodDisruptionBudget does not have the correct selector.")
+ g.Expect(podDisruptionBudget.Spec.MaxUnavailable).To(Equal(&maxUnavailable), "PodDisruptionBudget does not have the correct maxUnavailable setting.")
+
+ if additionalChecks != nil {
+ additionalChecks(g, podDisruptionBudget)
+ }
+ }).Should(Succeed())
+
+ By("recreating the PodDisruptionBudget after it is deleted")
+ ExpectWithOffset(resolveOffset(additionalOffset), k8sClient.Delete(ctx, podDisruptionBudget)).To(Succeed())
+ EventuallyWithOffset(
+ resolveOffset(additionalOffset),
+ func() (types.UID, error) {
+ newResource := &policyv1.PodDisruptionBudget{}
+ err := k8sClient.Get(ctx, resourceKey(parentResource, podDisruptionBudgetName), newResource)
+ if err != nil {
+ return "", err
+ }
+ return newResource.UID, nil
+ }).Should(And(Not(BeEmpty()), Not(Equal(podDisruptionBudget.UID))), "New PodDisruptionBudget, with new UID, not created.")
+
+ return podDisruptionBudget
+}
+
func expectConfigMap(ctx context.Context, parentResource client.Object, configMapName string, configMapData map[string]string, additionalOffset ...int) *corev1.ConfigMap {
return expectConfigMapWithChecks(ctx, parentResource, configMapName, configMapData, nil, resolveOffset(additionalOffset))
}
@@ -741,9 +776,9 @@
"testS4": "valueS4",
}
testNodeSelectors = map[string]string{
- "beta.kubernetes.io/arch": "amd64",
- "beta.kubernetes.io/os": "linux",
- "solrclouds": "true",
+ "kubernetes.io/arch": "amd64",
+ "kubernetes.io/os": "linux",
+ "solrclouds": "true",
}
testProbeLivenessNonDefaults = &corev1.Probe{
InitialDelaySeconds: 20,
diff --git a/controllers/solrcloud_controller.go b/controllers/solrcloud_controller.go
index c10a8de..9c9aacc 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -21,6 +21,7 @@
"context"
"crypto/md5"
"fmt"
+ policyv1 "k8s.io/api/policy/v1"
"k8s.io/apimachinery/pkg/runtime"
"reflect"
"sort"
@@ -73,6 +74,7 @@
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="",resources=configmaps/status,verbs=get
//+kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;delete
+//+kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=zookeeper.pravega.io,resources=zookeeperclusters,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=zookeeper.pravega.io,resources=zookeeperclusters/status,verbs=get
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
@@ -457,6 +459,33 @@
}
}
+ // PodDistruptionBudget(s)
+ pdb := util.GeneratePodDisruptionBudget(instance, pvcLabelSelector)
+
+ // Check if the PodDistruptionBudget already exists
+ pdbLogger := logger.WithValues("podDisruptionBudget", pdb.Name)
+ foundPDB := &policyv1.PodDisruptionBudget{}
+ err = r.Get(ctx, types.NamespacedName{Name: pdb.Name, Namespace: pdb.Namespace}, foundPDB)
+ if err != nil && errors.IsNotFound(err) {
+ pdbLogger.Info("Creating PodDisruptionBudget")
+ if err = controllerutil.SetControllerReference(instance, pdb, r.Scheme); err == nil {
+ err = r.Create(ctx, pdb)
+ }
+ } else if err == nil {
+ var needsUpdate bool
+ needsUpdate, err = util.OvertakeControllerRef(instance, foundPDB, r.Scheme)
+ needsUpdate = util.CopyPodDisruptionBudgetFields(pdb, foundPDB, pdbLogger) || needsUpdate
+
+ // Update the found PodDistruptionBudget and write the result back if there are any changes
+ if needsUpdate && err == nil {
+ pdbLogger.Info("Updating PodDisruptionBudget")
+ err = r.Update(ctx, foundPDB)
+ }
+ }
+ if err != nil {
+ return requeueOrNot, err
+ }
+
extAddressabilityOpts := instance.Spec.SolrAddressability.External
if extAddressabilityOpts != nil && extAddressabilityOpts.Method == solrv1beta1.Ingress {
// Generate Ingress
@@ -893,7 +922,8 @@
Owns(&appsv1.StatefulSet{}).
Owns(&corev1.Service{}).
Owns(&corev1.Secret{}). /* for authentication */
- Owns(&netv1.Ingress{})
+ Owns(&netv1.Ingress{}).
+ Owns(&policyv1.PodDisruptionBudget{})
var err error
ctrlBuilder, err = r.indexAndWatchForProvidedConfigMaps(mgr, ctrlBuilder)
diff --git a/controllers/solrcloud_controller_test.go b/controllers/solrcloud_controller_test.go
index 1d5903f..3f05089 100644
--- a/controllers/solrcloud_controller_test.go
+++ b/controllers/solrcloud_controller_test.go
@@ -28,6 +28,7 @@
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/util/intstr"
"strconv"
"strings"
)
@@ -146,10 +147,14 @@
By("making sure no Ingress was created")
expectNoIngress(ctx, solrCloud, solrCloud.CommonIngressName())
+
+ By("testing the PodDisruptionBudget")
+ expectPodDisruptionBudget(ctx, solrCloud, solrCloud.StatefulSetName(), statefulSet.Spec.Selector, intstr.FromString(util.DefaultMaxPodsUnavailable))
})
})
FContext("Solr Cloud with Custom Kube Options", func() {
+ three := intstr.FromInt(3)
BeforeEach(func() {
replicas := int32(4)
solrCloud.Spec = solrv1beta1.SolrCloudSpec{
@@ -164,7 +169,10 @@
},
},
UpdateStrategy: solrv1beta1.SolrUpdateStrategy{
- Method: solrv1beta1.StatefulSetUpdate,
+ Method: solrv1beta1.StatefulSetUpdate,
+ ManagedUpdateOptions: solrv1beta1.ManagedUpdateOptions{
+ MaxPodsUnavailable: &three,
+ },
RestartSchedule: "@every 30m",
},
SolrGCTune: "gc Options",
@@ -280,6 +288,9 @@
Expect(headlessService.Spec.Ports[0].Protocol).To(Equal(corev1.ProtocolTCP), "Wrong protocol on headless Service")
Expect(headlessService.Spec.Ports[0].AppProtocol).ToNot(BeNil(), "AppProtocol on headless Service should not be nil")
Expect(*headlessService.Spec.Ports[0].AppProtocol).To(Equal("http"), "Wrong appProtocol on headless Service")
+
+ By("testing the PodDisruptionBudget")
+ expectPodDisruptionBudget(ctx, solrCloud, solrCloud.StatefulSetName(), statefulSet.Spec.Selector, three)
})
})
diff --git a/controllers/util/common.go b/controllers/util/common.go
index d58e0a1..b887f6d 100644
--- a/controllers/util/common.go
+++ b/controllers/util/common.go
@@ -18,6 +18,7 @@
package util
import (
+ policyv1 "k8s.io/api/policy/v1"
"reflect"
"strconv"
"strings"
@@ -668,6 +669,30 @@
return requireUpdate
}
+// CopyPodDisruptionBudgetFields copies the owned fields from one PodDisruptionBudget to another
+func CopyPodDisruptionBudgetFields(from, to *policyv1.PodDisruptionBudget, logger logr.Logger) bool {
+ logger = logger.WithValues("kind", "PodDisruptionBudget")
+ requireUpdate := CopyLabelsAndAnnotations(&from.ObjectMeta, &to.ObjectMeta, logger)
+
+ if !DeepEqualWithNils(to.Spec.MinAvailable, from.Spec.MinAvailable) {
+ requireUpdate = true
+ logger.Info("Update required because field changed", "field", "Spec.MinAvailable", "from", to.Spec.MinAvailable, "to", from.Spec.MinAvailable)
+ to.Spec.MinAvailable = from.Spec.MinAvailable
+ }
+ if !DeepEqualWithNils(to.Spec.MaxUnavailable, from.Spec.MaxUnavailable) {
+ requireUpdate = true
+ logger.Info("Update required because field changed", "field", "Spec.MaxUnavailable", "from", to.Spec.MaxUnavailable, "to", from.Spec.MaxUnavailable)
+ to.Spec.MaxUnavailable = from.Spec.MaxUnavailable
+ }
+ if !DeepEqualWithNils(to.Spec.Selector, from.Spec.Selector) {
+ requireUpdate = true
+ logger.Info("Update required because field changed", "field", "Spec.Selector", "from", to.Spec.Selector, "to", from.Spec.Selector)
+ to.Spec.Selector = from.Spec.Selector
+ }
+
+ return requireUpdate
+}
+
// OvertakeControllerRef makes sure that the controlled object has the owner as the controller ref.
// If the object has a different controller, then that ref will be downgraded to an "owner" and the new controller ref will be added
func OvertakeControllerRef(owner metav1.Object, controlled metav1.Object, scheme *runtime.Scheme) (needsUpdate bool, err error) {
diff --git a/controllers/util/solr_pod_disruption.go b/controllers/util/solr_pod_disruption.go
new file mode 100644
index 0000000..037ac7b
--- /dev/null
+++ b/controllers/util/solr_pod_disruption.go
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 util
+
+import (
+ solr "github.com/apache/solr-operator/api/v1beta1"
+ policyv1 "k8s.io/api/policy/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/util/intstr"
+)
+
+func GeneratePodDisruptionBudget(cloud *solr.SolrCloud, selector map[string]string) *policyv1.PodDisruptionBudget {
+ // For this PDB, we can use an intOrString maxUnavailable (whatever the user provides),
+ // because we are matching the labelSelector used by the statefulSet.
+ var maxUnavailable intstr.IntOrString
+ if cloud.Spec.UpdateStrategy.ManagedUpdateOptions.MaxPodsUnavailable != nil {
+ maxUnavailable = *cloud.Spec.UpdateStrategy.ManagedUpdateOptions.MaxPodsUnavailable
+ } else {
+ maxUnavailable = intstr.FromString(DefaultMaxPodsUnavailable)
+ }
+ return &policyv1.PodDisruptionBudget{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: cloud.StatefulSetName(),
+ Namespace: cloud.Namespace,
+ },
+ Spec: policyv1.PodDisruptionBudgetSpec{
+ Selector: &metav1.LabelSelector{
+ MatchLabels: selector,
+ },
+ MaxUnavailable: &maxUnavailable,
+ },
+ }
+}
+
+/*
+*
+We cannot actually use the shard topology for PDBs, because Kubernetes does not currently support a pod
+mapping to multiple PDBs. Since a Solr pod is sure to host replicas of multiple shards, then we would
+have to create multiple PDBs that cover a single pod. Therefore we can only use the Cloud PDB defined in the method
+above.
+
+Whenever we can use this, when generating PDBs, we need to label them so that a list of all PDBs for a cloud can be found easily.
+That way, when we have the list of PDBs to create/update, we will aslo know the list of PDBs that need to be deleted.
+
+Kubernetes Documentation: https://kubernetes.io/docs/tasks/run-application/configure-pdb/#arbitrary-controllers-and-selectors
+*/
+func createPodDisruptionBudgetForShard(cloud *solr.SolrCloud, collection string, shard string, nodes []string) policyv1.PodDisruptionBudget {
+ maxUnavailable, err := intstr.GetScaledValueFromIntOrPercent(
+ intstr.ValueOrDefault(cloud.Spec.UpdateStrategy.ManagedUpdateOptions.MaxShardReplicasUnavailable, intstr.FromInt(DefaultMaxShardReplicasUnavailable)),
+ len(nodes),
+ false)
+ if err != nil {
+ maxUnavailable = 1
+ }
+ // From the documentation above, Kubernetes will only accept an int minAvailable for PDBs that use custom pod selectors.
+ // Therefore, we cannot use the maxUnavailable straight from what the user provides.
+ minAvailable := intstr.FromInt(len(nodes) - maxUnavailable)
+ return policyv1.PodDisruptionBudget{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: cloud.Name + "-" + collection + "-" + shard,
+ Namespace: cloud.Namespace,
+ },
+ Spec: policyv1.PodDisruptionBudgetSpec{
+ Selector: &metav1.LabelSelector{
+ MatchExpressions: []metav1.LabelSelectorRequirement{
+ {
+ Key: "statefulset.kubernetes.io/pod-name",
+ Operator: "In",
+ Values: nodes,
+ },
+ },
+ },
+ MinAvailable: &minAvailable,
+ },
+ }
+}
diff --git a/docs/solr-cloud/solr-cloud-crd.md b/docs/solr-cloud/solr-cloud-crd.md
index 9255364..47dbce7 100644
--- a/docs/solr-cloud/solr-cloud-crd.md
+++ b/docs/solr-cloud/solr-cloud-crd.md
@@ -96,6 +96,18 @@
- **`maxPodsUnavailable`** - The `maximumPodsUnavailable` is calculated as the percentage of the total pods configured for that Solr Cloud.
- **`maxShardReplicasUnavailable`** - The `maxShardReplicasUnavailable` is calculated independently for each shard, as the percentage of the number of replicas for that shard.
+### Pod Disruption Budgets
+_Since v0.7.0_
+
+The Solr Operator will create a [`PodDisruptionBudget`](https://kubernetes.io/docs/concepts/workloads/pods/disruptions/#pod-disruption-budgets) to ensure that Kubernetes does not take down more than acceptable amount of SolrCloud nodes at a time.
+The PDB's `maxUnavailable` setting is populated from the `maxPodsUnavailable` setting in `SolrCloud.Spec.updateStrategy.managed`.
+If this option is not set, it will use the default value (`25%`).
+
+Currently, the implementation does not take shard/replica topology into account, like the update strategy does.
+So although Kubernetes might just take down 25% of a Cloud's nodes, that might represent all nodes that host a shard's replicas.
+This is ongoing work, and hopefully something the Solr Operator can protect against in the future.
+See [this discussion](https://github.com/apache/solr-operator/issues/471) for more information.
+
## Addressability
_Since v0.2.6_
diff --git a/docs/upgrade-notes.md b/docs/upgrade-notes.md
index eb3967b..633a0ae 100644
--- a/docs/upgrade-notes.md
+++ b/docs/upgrade-notes.md
@@ -27,8 +27,8 @@
### Kubernetes Versions
-| Solr Operator Version | `1.15` | `1.16` - `1.18` | `1.19` - `1.21` | `1.22`+ |
-|:---------------------:| :---: | :---: | :---: | :---: |
+| Solr Operator Version | `1.15` | `1.16` - `1.18` | `1.19` - `1.21` | `1.22`+ |
+|:---------------------:| :---: | :---: |:------------------:| :---: |
| `v0.2.6` | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
| `v0.2.7` | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: |
| `v0.2.8` | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: |
@@ -36,6 +36,7 @@
| `v0.4.x` | :x: | :heavy_check_mark: | :heavy_check_mark: | :x: |
| `v0.5.x` | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: |
| `v0.6.x` | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: |
+| `v0.7.x` | :x: | :x: | :x: | :heavy_check_mark: |
### Solr Versions
@@ -48,6 +49,7 @@
| `v0.4.x` | :grey_question: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| `v0.5.x` | :grey_question: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| `v0.6.x` | :grey_question: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
+| `v0.7.x` | :grey_question: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
Please note that this represents basic compatibility with the Solr Operator.
There may be options and features that require newer versions of Solr.
@@ -107,6 +109,15 @@
## Upgrade Warnings and Notes
+### v0.7.0
+- **Kubernetes support is now limited to 1.21+.**
+ If you are unable to use a newer version of Kubernetes, please install the `v0.6.0` version of the Solr Operator for use with Kubernetes `1.20` and below.
+ See the [version compatibility matrix](#kubernetes-versions) for more information.
+
+- `PodDisruptionBudgets` are now created alongside SolrCloud instances.
+ The maximum number of pods allowed down at any given time is aligned with the [Managed Update settings](solr-cloud/solr-cloud-crd.md#update-strategy) provided in the spec.
+ If this is not provided, the default setting (`25%`) is used.
+
### v0.6.0
- The default Solr version for the `SolrCloud` and `SolrPrometheusExporter` resources has been upgraded from `8.9` to `8.11`.
This will not affect any existing resources, as default versions are hard-written to the resources immediately.
diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml
index 55a9982..1da9c9c 100644
--- a/helm/solr-operator/Chart.yaml
+++ b/helm/solr-operator/Chart.yaml
@@ -17,7 +17,7 @@
description: The Solr Operator enables easy management of Solr resources within Kubernetes.
version: 0.7.0-prerelease
appVersion: v0.7.0-prerelease
-kubeVersion: ">= 1.19.0-0"
+kubeVersion: ">= 1.21.0-0"
home: https://solr.apache.org/operator
sources:
- https://github.com/apache/solr-operator
@@ -53,6 +53,11 @@
# Add change log for a single release here.
# Allowed syntax is described at: https://artifacthub.io/docs/topics/annotations/helm/#example
artifacthub.io/changes: |
+ - kind: changed
+ description: Minimum Kubernetes version has been upped to 1.21
+ links:
+ - name: GitHub PR
+ url: https://github.com/apache/solr-operator/pull/473
- kind: fixed
description: Fix bug with named PVCs
links:
@@ -65,6 +70,15 @@
links:
- name: GitHub PR
url: https://github.com/apache/solr-operator/pull/480
+ - kind: added
+ description: SolrClouds now have PodDisruptionBudgets enabled
+ links:
+ - name: GitHub Issue
+ url: https://github.com/apache/solr-operator/issues/471
+ - name: GitHub PR
+ url: https://github.com/apache/solr-operator/pull/473
+ - name: PodDisruptionBudget Documentation
+ url: https://kubernetes.io/docs/concepts/workloads/pods/disruptions/#pod-disruption-budgets
artifacthub.io/images: |
- name: solr-operator
image: apache/solr-operator:v0.7.0-prerelease
diff --git a/helm/solr-operator/templates/role.yaml b/helm/solr-operator/templates/role.yaml
index 956b0aa..2f122e0 100644
--- a/helm/solr-operator/templates/role.yaml
+++ b/helm/solr-operator/templates/role.yaml
@@ -156,6 +156,18 @@
verbs:
- get
- apiGroups:
+ - policy
+ resources:
+ - poddisruptionbudgets
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
- solr.apache.org
resources:
- solrbackups
diff --git a/helm/solr/Chart.yaml b/helm/solr/Chart.yaml
index 64a86bf..6b89239 100644
--- a/helm/solr/Chart.yaml
+++ b/helm/solr/Chart.yaml
@@ -17,7 +17,7 @@
description: A SolrCloud cluster running on Kubernetes via the Solr Operator
version: 0.7.0-prerelease
appVersion: 8.11.1
-kubeVersion: ">= 1.19.0-0"
+kubeVersion: ">= 1.21.0-0"
home: https://solr.apache.org
sources:
- https://github.com/apache/solr
@@ -39,6 +39,11 @@
# Add change log for a single release here.
# Allowed syntax is described at: https://artifacthub.io/docs/topics/annotations/helm/#example
artifacthub.io/changes: |
+ - kind: changed
+ description: Minimum Kubernetes version has been upped to 1.21
+ links:
+ - name: GitHub PR
+ url: https://github.com/apache/solr-operator/pull/473
- kind: added
description: Support custom annotations on created ServiceAccount
links: