From 542a007646887c7b8ddbb11887c47498255459fb Mon Sep 17 00:00:00 2001 From: David Eads Date: Wed, 1 Jul 2020 15:40:28 -0400 Subject: [PATCH] add helpers for managing conditions --- .../k8s.io/apimachinery/pkg/api/meta/BUILD | 2 + .../apimachinery/pkg/api/meta/conditions.go | 101 ++++++++ .../pkg/api/meta/conditions_test.go | 218 ++++++++++++++++++ 3 files changed, 321 insertions(+) create mode 100644 staging/src/k8s.io/apimachinery/pkg/api/meta/conditions.go create mode 100644 staging/src/k8s.io/apimachinery/pkg/api/meta/conditions_test.go diff --git a/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD b/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD index 22055c01a8b..9d8aa0c2454 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD +++ b/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD @@ -9,6 +9,7 @@ load( go_test( name = "go_default_test", srcs = [ + "conditions_test.go", "meta_test.go", "multirestmapper_test.go", "priority_test.go", @@ -27,6 +28,7 @@ go_test( go_library( name = "go_default_library", srcs = [ + "conditions.go", "doc.go", "errors.go", "firsthit_restmapper.go", diff --git a/staging/src/k8s.io/apimachinery/pkg/api/meta/conditions.go b/staging/src/k8s.io/apimachinery/pkg/api/meta/conditions.go new file mode 100644 index 00000000000..934790dcb16 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/meta/conditions.go @@ -0,0 +1,101 @@ +/* +Copyright 2020 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 meta + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// SetStatusCondition sets the corresponding condition in conditions to newCondition. +// conditions must be non-nil. +// 1. if the condition of the specified type already exists (all fields of the existing condition are updated to +// newCondition, LastTransitionTime is set to now if the new status differs from the old status) +// 2. if a condition of the specified type does not exist (LastTransitionTime is set to now() if unset, and newCondition is appended) +func SetStatusCondition(conditions *[]metav1.Condition, newCondition metav1.Condition) { + if conditions == nil { + return + } + existingCondition := FindStatusCondition(*conditions, newCondition.Type) + if existingCondition == nil { + if newCondition.LastTransitionTime.IsZero() { + newCondition.LastTransitionTime = metav1.NewTime(time.Now()) + } + *conditions = append(*conditions, newCondition) + return + } + + if existingCondition.Status != newCondition.Status { + existingCondition.Status = newCondition.Status + if !newCondition.LastTransitionTime.IsZero() { + existingCondition.LastTransitionTime = newCondition.LastTransitionTime + } else { + existingCondition.LastTransitionTime = metav1.NewTime(time.Now()) + } + } + + existingCondition.Reason = newCondition.Reason + existingCondition.Message = newCondition.Message +} + +// RemoveStatusCondition removes the corresponding conditionType from conditions. +// conditions must be non-nil. +func RemoveStatusCondition(conditions *[]metav1.Condition, conditionType string) { + if conditions == nil { + return + } + newConditions := make([]metav1.Condition, 0, len(*conditions)-1) + for _, condition := range *conditions { + if condition.Type != conditionType { + newConditions = append(newConditions, condition) + } + } + + *conditions = newConditions +} + +// FindStatusCondition finds the conditionType in conditions. +func FindStatusCondition(conditions []metav1.Condition, conditionType string) *metav1.Condition { + for i := range conditions { + if conditions[i].Type == conditionType { + return &conditions[i] + } + } + + return nil +} + +// IsStatusConditionTrue returns true when the conditionType is present and set to `metav1.ConditionTrue` +func IsStatusConditionTrue(conditions []metav1.Condition, conditionType string) bool { + return IsStatusConditionPresentAndEqual(conditions, conditionType, metav1.ConditionTrue) +} + +// IsStatusConditionFalse returns true when the conditionType is present and set to `metav1.ConditionFalse` +func IsStatusConditionFalse(conditions []metav1.Condition, conditionType string) bool { + return IsStatusConditionPresentAndEqual(conditions, conditionType, metav1.ConditionFalse) +} + +// IsStatusConditionPresentAndEqual returns true when conditionType is present and equal to status. +func IsStatusConditionPresentAndEqual(conditions []metav1.Condition, conditionType string, status metav1.ConditionStatus) bool { + for _, condition := range conditions { + if condition.Type == conditionType { + return condition.Status == status + } + } + return false +} diff --git a/staging/src/k8s.io/apimachinery/pkg/api/meta/conditions_test.go b/staging/src/k8s.io/apimachinery/pkg/api/meta/conditions_test.go new file mode 100644 index 00000000000..3384597b5f6 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/meta/conditions_test.go @@ -0,0 +1,218 @@ +/* +Copyright 2020 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 meta + +import ( + "reflect" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestSetStatusCondition(t *testing.T) { + oneHourBefore := time.Now().Add(-1 * time.Hour) + oneHourAfter := time.Now().Add(1 * time.Hour) + + tests := []struct { + name string + conditions []metav1.Condition + toAdd metav1.Condition + expected []metav1.Condition + }{ + { + name: "should-add", + conditions: []metav1.Condition{ + {Type: "first"}, + {Type: "third"}, + }, + toAdd: metav1.Condition{Type: "second", Status: metav1.ConditionTrue, LastTransitionTime: metav1.Time{Time: oneHourBefore}, Reason: "reason", Message: "message"}, + expected: []metav1.Condition{ + {Type: "first"}, + {Type: "third"}, + {Type: "second", Status: metav1.ConditionTrue, LastTransitionTime: metav1.Time{Time: oneHourBefore}, Reason: "reason", Message: "message"}, + }, + }, + { + name: "use-supplied-time", + conditions: []metav1.Condition{ + {Type: "first"}, + {Type: "second", Status: metav1.ConditionFalse}, + {Type: "third"}, + }, + toAdd: metav1.Condition{Type: "second", Status: metav1.ConditionTrue, LastTransitionTime: metav1.Time{Time: oneHourBefore}, Reason: "reason", Message: "message"}, + expected: []metav1.Condition{ + {Type: "first"}, + {Type: "second", Status: metav1.ConditionTrue, LastTransitionTime: metav1.Time{Time: oneHourBefore}, Reason: "reason", Message: "message"}, + {Type: "third"}, + }, + }, + { + name: "update-fields", + conditions: []metav1.Condition{ + {Type: "first"}, + {Type: "second", Status: metav1.ConditionTrue, LastTransitionTime: metav1.Time{Time: oneHourBefore}}, + {Type: "third"}, + }, + toAdd: metav1.Condition{Type: "second", Status: metav1.ConditionTrue, LastTransitionTime: metav1.Time{Time: oneHourAfter}, Reason: "reason", Message: "message"}, + expected: []metav1.Condition{ + {Type: "first"}, + {Type: "second", Status: metav1.ConditionTrue, LastTransitionTime: metav1.Time{Time: oneHourBefore}, Reason: "reason", Message: "message"}, + {Type: "third"}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + SetStatusCondition(&test.conditions, test.toAdd) + if !reflect.DeepEqual(test.conditions, test.expected) { + t.Error(test.conditions) + } + }) + } +} + +func TestRemoveStatusCondition(t *testing.T) { + tests := []struct { + name string + conditions []metav1.Condition + conditionType string + expected []metav1.Condition + }{ + { + name: "present", + conditions: []metav1.Condition{ + {Type: "first"}, + {Type: "second"}, + {Type: "third"}, + }, + conditionType: "second", + expected: []metav1.Condition{ + {Type: "first"}, + {Type: "third"}, + }, + }, + { + name: "not-present", + conditions: []metav1.Condition{ + {Type: "first"}, + {Type: "second"}, + {Type: "third"}, + }, + conditionType: "fourth", + expected: []metav1.Condition{ + {Type: "first"}, + {Type: "second"}, + {Type: "third"}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + RemoveStatusCondition(&test.conditions, test.conditionType) + if !reflect.DeepEqual(test.conditions, test.expected) { + t.Error(test.conditions) + } + }) + } +} + +func TestFindStatusCondition(t *testing.T) { + tests := []struct { + name string + conditions []metav1.Condition + conditionType string + expected *metav1.Condition + }{ + { + name: "not-present", + conditions: []metav1.Condition{ + {Type: "first"}, + }, + conditionType: "second", + expected: nil, + }, + { + name: "present", + conditions: []metav1.Condition{ + {Type: "first"}, + {Type: "second"}, + }, + conditionType: "second", + expected: &metav1.Condition{Type: "second"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := FindStatusCondition(test.conditions, test.conditionType) + if !reflect.DeepEqual(actual, test.expected) { + t.Error(actual) + } + }) + } +} + +func TestIsStatusConditionPresentAndEqual(t *testing.T) { + tests := []struct { + name string + conditions []metav1.Condition + conditionType string + conditionStatus metav1.ConditionStatus + expected bool + }{ + { + name: "doesnt-match-true", + conditions: []metav1.Condition{ + {Type: "first", Status: metav1.ConditionUnknown}, + }, + conditionType: "first", + conditionStatus: metav1.ConditionTrue, + expected: false, + }, + { + name: "does-match-true", + conditions: []metav1.Condition{ + {Type: "first", Status: metav1.ConditionTrue}, + }, + conditionType: "first", + conditionStatus: metav1.ConditionTrue, + expected: true, + }, + { + name: "does-match-false", + conditions: []metav1.Condition{ + {Type: "first", Status: metav1.ConditionFalse}, + }, + conditionType: "first", + conditionStatus: metav1.ConditionFalse, + expected: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := IsStatusConditionPresentAndEqual(test.conditions, test.conditionType, test.conditionStatus) + if actual != test.expected { + t.Error(actual) + } + + }) + } +}