Add conditions to PDB status

This commit is contained in:
Morten Torkildsen 2021-03-03 19:40:47 -08:00
parent 466e730259
commit 1e2a7f381f
11 changed files with 424 additions and 29 deletions

View File

@ -77,6 +77,10 @@ type PodDisruptionBudgetStatus struct {
// total number of pods counted by this disruption budget // total number of pods counted by this disruption budget
ExpectedPods int32 ExpectedPods int32
// Conditions contain conditions for PDB
// +optional
Conditions []metav1.Condition
} }
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View File

@ -23,8 +23,10 @@ import (
"strings" "strings"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
policyapiv1beta1 "k8s.io/api/policy/v1beta1"
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
unversionedvalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" unversionedvalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
appsvalidation "k8s.io/kubernetes/pkg/apis/apps/validation" appsvalidation "k8s.io/kubernetes/pkg/apis/apps/validation"
@ -40,7 +42,6 @@ import (
// with any errors. // with any errors.
func ValidatePodDisruptionBudget(pdb *policy.PodDisruptionBudget) field.ErrorList { func ValidatePodDisruptionBudget(pdb *policy.PodDisruptionBudget) field.ErrorList {
allErrs := ValidatePodDisruptionBudgetSpec(pdb.Spec, field.NewPath("spec")) allErrs := ValidatePodDisruptionBudgetSpec(pdb.Spec, field.NewPath("spec"))
allErrs = append(allErrs, ValidatePodDisruptionBudgetStatus(pdb.Status, field.NewPath("status"))...)
return allErrs return allErrs
} }
@ -68,10 +69,16 @@ func ValidatePodDisruptionBudgetSpec(spec policy.PodDisruptionBudgetSpec, fldPat
return allErrs return allErrs
} }
// ValidatePodDisruptionBudgetStatus validates a PodDisruptionBudgetStatus and returns an ErrorList // ValidatePodDisruptionBudgetStatusUpdate validates a PodDisruptionBudgetStatus and returns an ErrorList
// with any errors. // with any errors.
func ValidatePodDisruptionBudgetStatus(status policy.PodDisruptionBudgetStatus, fldPath *field.Path) field.ErrorList { func ValidatePodDisruptionBudgetStatusUpdate(status, oldStatus policy.PodDisruptionBudgetStatus, fldPath *field.Path, apiVersion schema.GroupVersion) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
allErrs = append(allErrs, unversionedvalidation.ValidateConditions(status.Conditions, fldPath.Child("conditions"))...)
// Don't run other validations for v1beta1 since we don't want to introduce
// new validations retroactively.
if apiVersion == policyapiv1beta1.SchemeGroupVersion {
return allErrs
}
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.DisruptionsAllowed), fldPath.Child("disruptionsAllowed"))...) allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.DisruptionsAllowed), fldPath.Child("disruptionsAllowed"))...)
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.CurrentHealthy), fldPath.Child("currentHealthy"))...) allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.CurrentHealthy), fldPath.Child("currentHealthy"))...)
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.DesiredHealthy), fldPath.Child("desiredHealthy"))...) allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.DesiredHealthy), fldPath.Child("desiredHealthy"))...)

View File

@ -19,10 +19,13 @@ package validation
import ( import (
"fmt" "fmt"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
api "k8s.io/kubernetes/pkg/apis/core" api "k8s.io/kubernetes/pkg/apis/core"
@ -98,26 +101,150 @@ func TestValidateMinAvailablePodAndMaxUnavailableDisruptionBudgetSpec(t *testing
} }
func TestValidatePodDisruptionBudgetStatus(t *testing.T) { func TestValidatePodDisruptionBudgetStatus(t *testing.T) {
successCases := []policy.PodDisruptionBudgetStatus{ const expectNoErrors = false
{DisruptionsAllowed: 10}, const expectErrors = true
{CurrentHealthy: 5}, testCases := []struct {
{DesiredHealthy: 3}, name string
{ExpectedPods: 2}} pdbStatus policy.PodDisruptionBudgetStatus
for _, c := range successCases { expectErrForVersion map[schema.GroupVersion]bool
errors := ValidatePodDisruptionBudgetStatus(c, field.NewPath("status")) }{
if len(errors) > 0 { {
t.Errorf("unexpected failure %v for %v", errors, c) name: "DisruptionsAllowed: 10",
} pdbStatus: policy.PodDisruptionBudgetStatus{
DisruptionsAllowed: 10,
},
expectErrForVersion: map[schema.GroupVersion]bool{
policy.SchemeGroupVersion: expectNoErrors,
policyv1beta1.SchemeGroupVersion: expectNoErrors,
},
},
{
name: "CurrentHealthy: 5",
pdbStatus: policy.PodDisruptionBudgetStatus{
CurrentHealthy: 5,
},
expectErrForVersion: map[schema.GroupVersion]bool{
policy.SchemeGroupVersion: expectNoErrors,
policyv1beta1.SchemeGroupVersion: expectNoErrors,
},
},
{
name: "DesiredHealthy: 3",
pdbStatus: policy.PodDisruptionBudgetStatus{
DesiredHealthy: 3,
},
expectErrForVersion: map[schema.GroupVersion]bool{
policy.SchemeGroupVersion: expectNoErrors,
policyv1beta1.SchemeGroupVersion: expectNoErrors,
},
},
{
name: "ExpectedPods: 2",
pdbStatus: policy.PodDisruptionBudgetStatus{
ExpectedPods: 2,
},
expectErrForVersion: map[schema.GroupVersion]bool{
policy.SchemeGroupVersion: expectNoErrors,
policyv1beta1.SchemeGroupVersion: expectNoErrors,
},
},
{
name: "DisruptionsAllowed: -10",
pdbStatus: policy.PodDisruptionBudgetStatus{
DisruptionsAllowed: -10,
},
expectErrForVersion: map[schema.GroupVersion]bool{
policy.SchemeGroupVersion: expectErrors,
policyv1beta1.SchemeGroupVersion: expectNoErrors,
},
},
{
name: "CurrentHealthy: -5",
pdbStatus: policy.PodDisruptionBudgetStatus{
CurrentHealthy: -5,
},
expectErrForVersion: map[schema.GroupVersion]bool{
policy.SchemeGroupVersion: expectErrors,
policyv1beta1.SchemeGroupVersion: expectNoErrors,
},
},
{
name: "DesiredHealthy: -3",
pdbStatus: policy.PodDisruptionBudgetStatus{
DesiredHealthy: -3,
},
expectErrForVersion: map[schema.GroupVersion]bool{
policy.SchemeGroupVersion: expectErrors,
policyv1beta1.SchemeGroupVersion: expectNoErrors,
},
},
{
name: "ExpectedPods: -2",
pdbStatus: policy.PodDisruptionBudgetStatus{
ExpectedPods: -2,
},
expectErrForVersion: map[schema.GroupVersion]bool{
policy.SchemeGroupVersion: expectErrors,
policyv1beta1.SchemeGroupVersion: expectNoErrors,
},
},
{
name: "Conditions valid",
pdbStatus: policy.PodDisruptionBudgetStatus{
Conditions: []metav1.Condition{
{
Type: policyv1beta1.DisruptionAllowedCondition,
Status: metav1.ConditionTrue,
LastTransitionTime: metav1.Time{
Time: time.Now().Add(-5 * time.Minute),
},
Reason: policyv1beta1.SufficientPodsReason,
Message: "message",
ObservedGeneration: 3,
},
},
},
expectErrForVersion: map[schema.GroupVersion]bool{
policy.SchemeGroupVersion: expectNoErrors,
policyv1beta1.SchemeGroupVersion: expectNoErrors,
},
},
{
name: "Conditions not valid",
pdbStatus: policy.PodDisruptionBudgetStatus{
Conditions: []metav1.Condition{
{
Type: policyv1beta1.DisruptionAllowedCondition,
Status: metav1.ConditionTrue,
},
{
Type: policyv1beta1.DisruptionAllowedCondition,
Status: metav1.ConditionFalse,
},
},
},
expectErrForVersion: map[schema.GroupVersion]bool{
policy.SchemeGroupVersion: expectErrors,
policyv1beta1.SchemeGroupVersion: expectErrors,
},
},
} }
failureCases := []policy.PodDisruptionBudgetStatus{
{DisruptionsAllowed: -10}, for _, tc := range testCases {
{CurrentHealthy: -5}, for apiVersion, expectErrors := range tc.expectErrForVersion {
{DesiredHealthy: -3}, t.Run(fmt.Sprintf("apiVersion: %s, %s", apiVersion.String(), tc.name), func(t *testing.T) {
{ExpectedPods: -2}} errors := ValidatePodDisruptionBudgetStatusUpdate(tc.pdbStatus, policy.PodDisruptionBudgetStatus{},
for _, c := range failureCases { field.NewPath("status"), apiVersion)
errors := ValidatePodDisruptionBudgetStatus(c, field.NewPath("status")) errCount := len(errors)
if len(errors) == 0 {
t.Errorf("unexpected success for %v", c) if errCount > 0 && !expectErrors {
t.Errorf("unexpected failure %v for %v", errors, tc.pdbStatus)
}
if errCount == 0 && expectErrors {
t.Errorf("expected errors but didn't one for %v", tc.pdbStatus)
}
})
} }
} }
} }

View File

@ -49,6 +49,7 @@ import (
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record" "k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue" "k8s.io/client-go/util/workqueue"
pdbhelper "k8s.io/component-helpers/apps/poddisruptionbudget"
"k8s.io/klog/v2" "k8s.io/klog/v2"
podutil "k8s.io/kubernetes/pkg/api/v1/pod" podutil "k8s.io/kubernetes/pkg/api/v1/pod"
"k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/controller"
@ -63,7 +64,9 @@ import (
// If the controller is running on a different node it is important that the two nodes have synced // If the controller is running on a different node it is important that the two nodes have synced
// clock (via ntp for example). Otherwise PodDisruptionBudget controller may not provide enough // clock (via ntp for example). Otherwise PodDisruptionBudget controller may not provide enough
// protection against unwanted pod disruptions. // protection against unwanted pod disruptions.
const DeletionTimeout = 2 * 60 * time.Second const (
DeletionTimeout = 2 * 60 * time.Second
)
type updater func(*policy.PodDisruptionBudget) error type updater func(*policy.PodDisruptionBudget) error
@ -579,7 +582,7 @@ func (dc *DisruptionController) sync(key string) error {
} }
if err != nil { if err != nil {
klog.Errorf("Failed to sync pdb %s/%s: %v", pdb.Namespace, pdb.Name, err) klog.Errorf("Failed to sync pdb %s/%s: %v", pdb.Namespace, pdb.Name, err)
return dc.failSafe(pdb) return dc.failSafe(pdb, err)
} }
return nil return nil
@ -774,9 +777,21 @@ func (dc *DisruptionController) buildDisruptedPodMap(pods []*v1.Pod, pdb *policy
// implement the "fail open" part of the design since if we manage to update // implement the "fail open" part of the design since if we manage to update
// this field correctly, we will prevent the /evict handler from approving an // this field correctly, we will prevent the /evict handler from approving an
// eviction when it may be unsafe to do so. // eviction when it may be unsafe to do so.
func (dc *DisruptionController) failSafe(pdb *policy.PodDisruptionBudget) error { func (dc *DisruptionController) failSafe(pdb *policy.PodDisruptionBudget, err error) error {
newPdb := pdb.DeepCopy() newPdb := pdb.DeepCopy()
newPdb.Status.DisruptionsAllowed = 0 newPdb.Status.DisruptionsAllowed = 0
if newPdb.Status.Conditions == nil {
newPdb.Status.Conditions = make([]metav1.Condition, 0)
}
apimeta.SetStatusCondition(&newPdb.Status.Conditions, metav1.Condition{
Type: policy.DisruptionAllowedCondition,
Status: metav1.ConditionFalse,
Reason: policy.SyncFailedReason,
Message: err.Error(),
ObservedGeneration: newPdb.Status.ObservedGeneration,
})
return dc.getUpdater()(newPdb) return dc.getUpdater()(newPdb)
} }
@ -797,7 +812,8 @@ func (dc *DisruptionController) updatePdbStatus(pdb *policy.PodDisruptionBudget,
pdb.Status.ExpectedPods == expectedCount && pdb.Status.ExpectedPods == expectedCount &&
pdb.Status.DisruptionsAllowed == disruptionsAllowed && pdb.Status.DisruptionsAllowed == disruptionsAllowed &&
apiequality.Semantic.DeepEqual(pdb.Status.DisruptedPods, disruptedPods) && apiequality.Semantic.DeepEqual(pdb.Status.DisruptedPods, disruptedPods) &&
pdb.Status.ObservedGeneration == pdb.Generation { pdb.Status.ObservedGeneration == pdb.Generation &&
pdbhelper.ConditionsAreUpToDate(pdb) {
return nil return nil
} }
@ -811,6 +827,8 @@ func (dc *DisruptionController) updatePdbStatus(pdb *policy.PodDisruptionBudget,
ObservedGeneration: pdb.Generation, ObservedGeneration: pdb.Generation,
} }
pdbhelper.UpdateDisruptionAllowedCondition(newPdb)
return dc.getUpdater()(newPdb) return dc.getUpdater()(newPdb)
} }

View File

@ -32,6 +32,7 @@ import (
policy "k8s.io/api/policy/v1beta1" policy "k8s.io/api/policy/v1beta1"
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/meta/testrestmapper" "k8s.io/apimachinery/pkg/api/meta/testrestmapper"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -73,6 +74,8 @@ func (ps *pdbStates) Get(key string) policy.PodDisruptionBudget {
func (ps *pdbStates) VerifyPdbStatus(t *testing.T, key string, disruptionsAllowed, currentHealthy, desiredHealthy, expectedPods int32, func (ps *pdbStates) VerifyPdbStatus(t *testing.T, key string, disruptionsAllowed, currentHealthy, desiredHealthy, expectedPods int32,
disruptedPodMap map[string]metav1.Time) { disruptedPodMap map[string]metav1.Time) {
actualPDB := ps.Get(key) actualPDB := ps.Get(key)
actualConditions := actualPDB.Status.Conditions
actualPDB.Status.Conditions = nil
expectedStatus := policy.PodDisruptionBudgetStatus{ expectedStatus := policy.PodDisruptionBudgetStatus{
DisruptionsAllowed: disruptionsAllowed, DisruptionsAllowed: disruptionsAllowed,
CurrentHealthy: currentHealthy, CurrentHealthy: currentHealthy,
@ -86,6 +89,22 @@ func (ps *pdbStates) VerifyPdbStatus(t *testing.T, key string, disruptionsAllowe
debug.PrintStack() debug.PrintStack()
t.Fatalf("PDB %q status mismatch. Expected %+v but got %+v.", key, expectedStatus, actualStatus) t.Fatalf("PDB %q status mismatch. Expected %+v but got %+v.", key, expectedStatus, actualStatus)
} }
cond := apimeta.FindStatusCondition(actualConditions, policy.DisruptionAllowedCondition)
if cond == nil {
t.Fatalf("Expected condition %q, but didn't find it", policy.DisruptionAllowedCondition)
}
if disruptionsAllowed > 0 {
if cond.Status != metav1.ConditionTrue {
t.Fatalf("Expected condition %q to have status %q, but was %q",
policy.DisruptionAllowedCondition, metav1.ConditionTrue, cond.Status)
}
} else {
if cond.Status != metav1.ConditionFalse {
t.Fatalf("Expected condition %q to have status %q, but was %q",
policy.DisruptionAllowedCondition, metav1.ConditionFalse, cond.Status)
}
}
} }
func (ps *pdbStates) VerifyDisruptionAllowed(t *testing.T, key string, disruptionsAllowed int32) { func (ps *pdbStates) VerifyDisruptionAllowed(t *testing.T, key string, disruptionsAllowed int32) {

View File

@ -33,6 +33,7 @@ import (
"k8s.io/apiserver/pkg/util/dryrun" "k8s.io/apiserver/pkg/util/dryrun"
policyclient "k8s.io/client-go/kubernetes/typed/policy/v1beta1" policyclient "k8s.io/client-go/kubernetes/typed/policy/v1beta1"
"k8s.io/client-go/util/retry" "k8s.io/client-go/util/retry"
pdbhelper "k8s.io/component-helpers/apps/poddisruptionbudget"
podutil "k8s.io/kubernetes/pkg/api/pod" podutil "k8s.io/kubernetes/pkg/api/pod"
api "k8s.io/kubernetes/pkg/apis/core" api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/policy" "k8s.io/kubernetes/pkg/apis/policy"
@ -331,6 +332,10 @@ func (r *EvictionREST) checkAndDecrement(namespace string, podName string, pdb p
} }
pdb.Status.DisruptionsAllowed-- pdb.Status.DisruptionsAllowed--
if pdb.Status.DisruptionsAllowed == 0 {
pdbhelper.UpdateDisruptionAllowedCondition(&pdb)
}
// If this is a dry-run, we don't need to go any further than that. // If this is a dry-run, we don't need to go any further than that.
if dryRun == true { if dryRun == true {
return nil return nil

View File

@ -23,6 +23,7 @@ import (
policyv1beta1 "k8s.io/api/policy/v1beta1" policyv1beta1 "k8s.io/api/policy/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -505,6 +506,103 @@ func TestEvictionDryRun(t *testing.T) {
} }
} }
func TestEvictionPDBStatus(t *testing.T) {
testcases := []struct {
name string
pdb *policyv1beta1.PodDisruptionBudget
expectedDisruptionsAllowed int32
expectedReason string
}{
{
name: "pdb status is updated after eviction",
pdb: &policyv1beta1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"},
Spec: policyv1beta1.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"a": "true"}}},
Status: policyv1beta1.PodDisruptionBudgetStatus{
DisruptionsAllowed: 1,
Conditions: []metav1.Condition{
{
Type: policyv1beta1.DisruptionAllowedCondition,
Reason: policyv1beta1.SufficientPodsReason,
Status: metav1.ConditionTrue,
},
},
},
},
expectedDisruptionsAllowed: 0,
expectedReason: policyv1beta1.InsufficientPodsReason,
},
{
name: "condition reason is only updated if AllowedDisruptions becomes 0",
pdb: &policyv1beta1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"},
Spec: policyv1beta1.PodDisruptionBudgetSpec{Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"a": "true"}}},
Status: policyv1beta1.PodDisruptionBudgetStatus{
DisruptionsAllowed: 3,
Conditions: []metav1.Condition{
{
Type: policyv1beta1.DisruptionAllowedCondition,
Reason: policyv1beta1.SufficientPodsReason,
Status: metav1.ConditionTrue,
},
},
},
},
expectedDisruptionsAllowed: 2,
expectedReason: policyv1beta1.SufficientPodsReason,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
testContext := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
storage, _, statusStorage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
client := fake.NewSimpleClientset(tc.pdb)
for _, podName := range []string{"foo-1", "foo-2"} {
pod := validNewPod()
pod.Labels = map[string]string{"a": "true"}
pod.ObjectMeta.Name = podName
pod.Spec.NodeName = "foo"
newPod, err := storage.Create(testContext, pod, nil, &metav1.CreateOptions{})
if err != nil {
t.Error(err)
}
(newPod.(*api.Pod)).Status.Phase = api.PodRunning
_, _, err = statusStorage.Update(testContext, pod.Name, rest.DefaultUpdatedObjectInfo(newPod),
nil, nil, false, &metav1.UpdateOptions{})
if err != nil {
t.Error(err)
}
}
evictionRest := newEvictionStorage(storage.Store, client.PolicyV1beta1())
eviction := &policy.Eviction{ObjectMeta: metav1.ObjectMeta{Name: "foo-1", Namespace: "default"}, DeleteOptions: &metav1.DeleteOptions{}}
_, err := evictionRest.Create(testContext, "foo-1", eviction, nil, &metav1.CreateOptions{})
if err != nil {
t.Fatalf("Failed to run eviction: %v", err)
}
existingPDB, err := client.PolicyV1beta1().PodDisruptionBudgets(metav1.NamespaceDefault).Get(context.TODO(), tc.pdb.Name, metav1.GetOptions{})
if err != nil {
t.Errorf("%#v", err)
return
}
if want, got := tc.expectedDisruptionsAllowed, existingPDB.Status.DisruptionsAllowed; got != want {
t.Errorf("expected DisruptionsAllowed to be %d, but got %d", want, got)
}
cond := apimeta.FindStatusCondition(existingPDB.Status.Conditions, policyv1beta1.DisruptionAllowedCondition)
if want, got := tc.expectedReason, cond.Reason; want != got {
t.Errorf("expected Reason to be %q, but got %q", want, got)
}
})
}
}
func resource(resource string) schema.GroupResource { func resource(resource string) schema.GroupResource {
return schema.GroupResource{Group: "", Resource: resource} return schema.GroupResource{Group: "", Resource: resource}
} }

View File

@ -21,7 +21,9 @@ import (
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/storage/names" "k8s.io/apiserver/pkg/storage/names"
"k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/policy" "k8s.io/kubernetes/pkg/apis/policy"
@ -109,7 +111,13 @@ func (podDisruptionBudgetStatusStrategy) PrepareForUpdate(ctx context.Context, o
// ValidateUpdate is the default update validation for an end user updating status // ValidateUpdate is the default update validation for an end user updating status
func (podDisruptionBudgetStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { func (podDisruptionBudgetStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
// TODO: Validate status updates. var apiVersion schema.GroupVersion
return field.ErrorList{} if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found {
// return validation.ValidatePodDisruptionBudgetStatusUpdate(obj.(*policy.PodDisruptionBudget), old.(*policy.PodDisruptionBudget)) apiVersion = schema.GroupVersion{
Group: requestInfo.APIVersion,
Version: requestInfo.APIGroup,
}
}
return validation.ValidatePodDisruptionBudgetStatusUpdate(obj.(*policy.PodDisruptionBudget).Status,
old.(*policy.PodDisruptionBudget).Status, field.NewPath("status"), apiVersion)
} }

View File

@ -77,8 +77,44 @@ type PodDisruptionBudgetStatus struct {
// total number of pods counted by this disruption budget // total number of pods counted by this disruption budget
ExpectedPods int32 `json:"expectedPods" protobuf:"varint,6,opt,name=expectedPods"` ExpectedPods int32 `json:"expectedPods" protobuf:"varint,6,opt,name=expectedPods"`
// Conditions contain conditions for PDB. The disruption controller sets the
// DisruptionAllowed condition. The following are known values for the reason field
// (additional reasons could be added in the future):
// - SyncFailed: The controller encountered an error and wasn't able to compute
// the number of allowed disruptions. Therefore no disruptions are
// allowed and the status of the condition will be False.
// - InsufficientPods: The number of pods are either at or below the number
// required by the PodDisruptionBudget. No disruptions are
// allowed and the status of the condition will be False.
// - SufficientPods: There are more pods than required by the PodDisruptionBudget.
// The condition will be True, and the number of allowed
// disruptions are provided by the disruptionsAllowed property.
//
// +optional
// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type", protobuf:"bytes,7,rep,name=conditions"`
} }
const (
// DisruptionAllowedCondition is a condition set by the disruption controller
// that signal whether any of the pods covered by the PDB can be disrupted.
DisruptionAllowedCondition = "DisruptionAllowed"
// SyncFailedReason is set on the DisruptionAllowed condition if reconcile
// of the PDB failed and therefore disruption of pods are not allowed.
SyncFailedReason = "SyncFailed"
// SufficientPodsReason is set on the DisruptionAllowed condition if there are
// more pods covered by the PDB than required and at least one can be disrupted.
SufficientPodsReason = "SufficientPods"
// InsufficientPodsReason is set on the DisruptionAllowed condition if the number
// of pods are equal to or fewer than required by the PDB.
InsufficientPodsReason = "InsufficientPods"
)
// +genclient // +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.5 // +k8s:prerelease-lifecycle-gen:introduced=1.5

View File

@ -0,0 +1,8 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- sig-apps-api-approvers
reviewers:
- sig-apps-api-reviewers
labels:
- sig/apps

View File

@ -0,0 +1,65 @@
/*
Copyright 2021 The Kubernetes Authors.
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 poddisruptionbudget
import (
policy "k8s.io/api/policy/v1beta1"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// UpdateDisruptionAllowedCondition updates the DisruptionAllowed condition
// on a PodDisruptionBudget based on the value of the DisruptionsAllowed field.
func UpdateDisruptionAllowedCondition(pdb *policy.PodDisruptionBudget) {
if pdb.Status.Conditions == nil {
pdb.Status.Conditions = make([]metav1.Condition, 0)
}
if pdb.Status.DisruptionsAllowed > 0 {
apimeta.SetStatusCondition(&pdb.Status.Conditions, metav1.Condition{
Type: policy.DisruptionAllowedCondition,
Reason: policy.SufficientPodsReason,
Status: metav1.ConditionTrue,
ObservedGeneration: pdb.Status.ObservedGeneration,
})
} else {
apimeta.SetStatusCondition(&pdb.Status.Conditions, metav1.Condition{
Type: policy.DisruptionAllowedCondition,
Reason: policy.InsufficientPodsReason,
Status: metav1.ConditionFalse,
ObservedGeneration: pdb.Status.ObservedGeneration,
})
}
}
// ConditionsAreUpToDate checks whether the status and reason for the
// DisruptionAllowed condition are set to the correct values based on the
// DisruptionsAllowed field.
func ConditionsAreUpToDate(pdb *policy.PodDisruptionBudget) bool {
cond := apimeta.FindStatusCondition(pdb.Status.Conditions, policy.DisruptionAllowedCondition)
if cond == nil {
return false
}
if pdb.Status.ObservedGeneration != pdb.Generation {
return false
}
if pdb.Status.DisruptionsAllowed > 0 {
return cond.Status == metav1.ConditionTrue && cond.Reason == policy.SufficientPodsReason
}
return cond.Status == metav1.ConditionFalse && cond.Reason == policy.InsufficientPodsReason
}