Skip to content

Commit 3b2256c

Browse files
authored
Add status conditions helpers (#1143)
* pkg/status: add status conditions helpers * doc/user-guide.md: added section about managing CR status conditions
1 parent 487696a commit 3b2256c

File tree

4 files changed

+498
-0
lines changed

4 files changed

+498
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Added
44

55
- Add a new option to set the minimum log level that triggers stack trace generation in logs (`--zap-stacktrace-level`) ([#2319](https://github.com/operator-framework/operator-sdk/pull/2319))
6+
- Added `pkg/status` with several new types and interfaces that can be used in `Status` structs to simplify handling of [status conditions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties). ([#1143](https://github.com/operator-framework/operator-sdk/pull/1143))
67

78
### Changed
89

doc/user-guide.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,35 @@ $ kubectl delete -f deploy/service_account.yaml
428428

429429
## Advanced Topics
430430

431+
### Manage CR status conditions
432+
433+
An often-used pattern is to include `Conditions` in the status of custom resources. Conditions represent the latest available observations of an object's state (see the [Kubernetes API conventionsdocumentation][typical-status-properties] for more information).
434+
435+
The `Conditions` field added to the `MemcachedStatus` struct simplifies the management of your CR's conditions. It:
436+
- Enables callers to add and remove conditions.
437+
- Ensures that there are no duplicates.
438+
- Sorts the conditions deterministically to avoid unnecessary repeated reconciliations.
439+
- Automatically handles the each condition's `LastTransitionTime`.
440+
- Provides helper methods to make it easy to determine the state of a condition.
441+
442+
To use conditions in your custom resource, add a Conditions field to the Status struct in `_types.go`:
443+
444+
```Go
445+
import (
446+
"github.com/operator-framework/operator-sdk/pkg/status"
447+
)
448+
449+
type MyAppStatus struct {
450+
// Conditions represent the latest available observations of an object's state
451+
Conditions status.Conditions `json:"conditions"`
452+
}
453+
```
454+
455+
<!--
456+
TODO(joelanford): add a link to the Conditions godoc once the initial PR is merged
457+
-->
458+
Then, in your controller, you can use `Conditions` methods to make it easier to set and remove conditions or check their current values.
459+
431460
### Adding 3rd Party Resources To Your Operator
432461

433462
The operator's Manager supports the Core Kubernetes resource types as found in the client-go [scheme][scheme_package] package and will also register the schemes of all custom resource types defined in your project under `pkg/apis`.
@@ -737,3 +766,4 @@ When the operator is not running in a cluster, the Manager will return an error
737766
[quay_link]: https://quay.io
738767
[multi-namespaced-cache-builder]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/cache#MultiNamespacedCacheBuilder
739768
[scheme_builder]: https://godoc.org/sigs.k8s.io/controller-runtime/pkg/scheme#Builder
769+
[typical-status-properties]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

pkg/status/conditions.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// Copyright 2020 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package status
16+
17+
import (
18+
"encoding/json"
19+
"sort"
20+
21+
corev1 "k8s.io/api/core/v1"
22+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
kubeclock "k8s.io/apimachinery/pkg/util/clock"
24+
)
25+
26+
// clock is used to set status condition timestamps.
27+
// This variable makes it easier to test conditions.
28+
var clock kubeclock.Clock = &kubeclock.RealClock{}
29+
30+
// ConditionType is the type of the condition and is typically a CamelCased
31+
// word or short phrase.
32+
//
33+
// Condition types should indicate state in the "abnormal-true" polarity. For
34+
// example, if the condition indicates when a policy is invalid, the "is valid"
35+
// case is probably the norm, so the condition should be called "Invalid".
36+
type ConditionType string
37+
38+
// ConditionReason is intended to be a one-word, CamelCase representation of
39+
// the category of cause of the current status. It is intended to be used in
40+
// concise output, such as one-line kubectl get output, and in summarizing
41+
// occurrences of causes.
42+
type ConditionReason string
43+
44+
// Condition represents an observation of an object's state. Conditions are an
45+
// extension mechanism intended to be used when the details of an observation
46+
// are not a priori known or would not apply to all instances of a given Kind.
47+
//
48+
// Conditions should be added to explicitly convey properties that users and
49+
// components care about rather than requiring those properties to be inferred
50+
// from other observations. Once defined, the meaning of a Condition can not be
51+
// changed arbitrarily - it becomes part of the API, and has the same
52+
// backwards- and forwards-compatibility concerns of any other part of the API.
53+
type Condition struct {
54+
Type ConditionType `json:"type"`
55+
Status corev1.ConditionStatus `json:"status"`
56+
Reason ConditionReason `json:"reason,omitempty"`
57+
Message string `json:"message,omitempty"`
58+
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
59+
}
60+
61+
// IsTrue Condition whether the condition status is "True".
62+
func (c Condition) IsTrue() bool {
63+
return c.Status == corev1.ConditionTrue
64+
}
65+
66+
// IsFalse returns whether the condition status is "False".
67+
func (c Condition) IsFalse() bool {
68+
return c.Status == corev1.ConditionFalse
69+
}
70+
71+
// IsUnknown returns whether the condition status is "Unknown".
72+
func (c Condition) IsUnknown() bool {
73+
return c.Status == corev1.ConditionUnknown
74+
}
75+
76+
// DeepCopy returns a deep copy of the condition
77+
func (c *Condition) DeepCopy() *Condition {
78+
if c == nil {
79+
return nil
80+
}
81+
out := *c
82+
return &out
83+
}
84+
85+
// Conditions is a set of Condition instances.
86+
//
87+
// +kubebuilder:validation:Type=array
88+
type Conditions map[ConditionType]Condition
89+
90+
// NewConditions initializes a set of conditions with the given list of
91+
// conditions.
92+
func NewConditions(conds ...Condition) Conditions {
93+
conditions := Conditions{}
94+
for _, c := range conds {
95+
conditions.SetCondition(c)
96+
}
97+
return conditions
98+
}
99+
100+
// IsTrueFor searches the set of conditions for a condition with the given
101+
// ConditionType. If found, it returns `condition.IsTrue()`. If not found,
102+
// it returns false.
103+
func (conditions Conditions) IsTrueFor(t ConditionType) bool {
104+
if condition, ok := conditions[t]; ok {
105+
return condition.IsTrue()
106+
}
107+
return false
108+
}
109+
110+
// IsFalseFor searches the set of conditions for a condition with the given
111+
// ConditionType. If found, it returns `condition.IsFalse()`. If not found,
112+
// it returns false.
113+
func (conditions Conditions) IsFalseFor(t ConditionType) bool {
114+
if condition, ok := conditions[t]; ok {
115+
return condition.IsFalse()
116+
}
117+
return false
118+
}
119+
120+
// IsUnknownFor searches the set of conditions for a condition with the given
121+
// ConditionType. If found, it returns `condition.IsUnknown()`. If not found,
122+
// it returns true.
123+
func (conditions Conditions) IsUnknownFor(t ConditionType) bool {
124+
if condition, ok := conditions[t]; ok {
125+
return condition.IsUnknown()
126+
}
127+
return true
128+
}
129+
130+
// SetCondition adds (or updates) the set of conditions with the given
131+
// condition. It returns a boolean value indicating whether the set condition
132+
// is new or was a change to the existing condition with the same type.
133+
func (conditions *Conditions) SetCondition(newCond Condition) bool {
134+
if conditions == nil || *conditions == nil {
135+
*conditions = make(map[ConditionType]Condition)
136+
}
137+
newCond.LastTransitionTime = metav1.Time{Time: clock.Now()}
138+
139+
if condition, ok := (*conditions)[newCond.Type]; ok {
140+
// If the condition status didn't change, use the existing
141+
// condition's last transition time.
142+
if condition.Status == newCond.Status {
143+
newCond.LastTransitionTime = condition.LastTransitionTime
144+
}
145+
changed := condition.Status != newCond.Status ||
146+
condition.Reason != newCond.Reason ||
147+
condition.Message != newCond.Message
148+
(*conditions)[newCond.Type] = newCond
149+
return changed
150+
}
151+
(*conditions)[newCond.Type] = newCond
152+
return true
153+
}
154+
155+
// GetCondition searches the set of conditions for the condition with the given
156+
// ConditionType and returns it. If the matching condition is not found,
157+
// GetCondition returns nil.
158+
func (conditions Conditions) GetCondition(t ConditionType) *Condition {
159+
if condition, ok := conditions[t]; ok {
160+
return &condition
161+
}
162+
return nil
163+
}
164+
165+
// RemoveCondition removes the condition with the given ConditionType from
166+
// the conditions set. If no condition with that type is found, RemoveCondition
167+
// returns without performing any action. If the passed condition type is not
168+
// found in the set of conditions, RemoveCondition returns false.
169+
func (conditions *Conditions) RemoveCondition(t ConditionType) bool {
170+
if conditions == nil || *conditions == nil {
171+
return false
172+
}
173+
if _, ok := (*conditions)[t]; ok {
174+
delete(*conditions, t)
175+
return true
176+
}
177+
return false
178+
}
179+
180+
// MarshalJSON marshals the set of conditions as a JSON array, sorted by
181+
// condition type.
182+
func (conditions Conditions) MarshalJSON() ([]byte, error) {
183+
conds := []Condition{}
184+
for _, condition := range conditions {
185+
conds = append(conds, condition)
186+
}
187+
sort.Slice(conds, func(a, b int) bool {
188+
return conds[a].Type < conds[b].Type
189+
})
190+
return json.Marshal(conds)
191+
}
192+
193+
// UnmarshalJSON unmarshals the JSON data into the set of Conditions.
194+
func (conditions *Conditions) UnmarshalJSON(data []byte) error {
195+
*conditions = make(map[ConditionType]Condition)
196+
conds := []Condition{}
197+
if err := json.Unmarshal(data, &conds); err != nil {
198+
return err
199+
}
200+
for _, condition := range conds {
201+
(*conditions)[condition.Type] = condition
202+
}
203+
return nil
204+
}

0 commit comments

Comments
 (0)