Skip to content

Add status conditions helpers #1143

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Added

- 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))
- 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))

### Changed

Expand Down
30 changes: 30 additions & 0 deletions doc/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,35 @@ $ kubectl delete -f deploy/service_account.yaml

## Advanced Topics

### Manage CR status conditions

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).

The `Conditions` field added to the `MemcachedStatus` struct simplifies the management of your CR's conditions. It:
- Enables callers to add and remove conditions.
- Ensures that there are no duplicates.
- Sorts the conditions deterministically to avoid unnecessary repeated reconciliations.
- Automatically handles the each condition's `LastTransitionTime`.
- Provides helper methods to make it easy to determine the state of a condition.

To use conditions in your custom resource, add a Conditions field to the Status struct in `_types.go`:

```Go
import (
"github.com/operator-framework/operator-sdk/pkg/status"
)

type MyAppStatus struct {
// Conditions represent the latest available observations of an object's state
Conditions status.Conditions `json:"conditions"`
}
```

<!--
TODO(joelanford): add a link to the Conditions godoc once the initial PR is merged
-->
Then, in your controller, you can use `Conditions` methods to make it easier to set and remove conditions or check their current values.

### Adding 3rd Party Resources To Your Operator

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`.
Expand Down Expand Up @@ -737,3 +766,4 @@ When the operator is not running in a cluster, the Manager will return an error
[quay_link]: https://quay.io
[multi-namespaced-cache-builder]: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/cache#MultiNamespacedCacheBuilder
[scheme_builder]: https://godoc.org/sigs.k8s.io/controller-runtime/pkg/scheme#Builder
[typical-status-properties]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
204 changes: 204 additions & 0 deletions pkg/status/conditions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// Copyright 2020 The Operator-SDK 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 status

import (
"encoding/json"
"sort"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubeclock "k8s.io/apimachinery/pkg/util/clock"
)

// clock is used to set status condition timestamps.
// This variable makes it easier to test conditions.
var clock kubeclock.Clock = &kubeclock.RealClock{}

// ConditionType is the type of the condition and is typically a CamelCased
// word or short phrase.
//
// Condition types should indicate state in the "abnormal-true" polarity. For
// example, if the condition indicates when a policy is invalid, the "is valid"
// case is probably the norm, so the condition should be called "Invalid".
type ConditionType string

// ConditionReason is intended to be a one-word, CamelCase representation of
// the category of cause of the current status. It is intended to be used in
// concise output, such as one-line kubectl get output, and in summarizing
// occurrences of causes.
type ConditionReason string

// Condition represents an observation of an object's state. Conditions are an
// extension mechanism intended to be used when the details of an observation
// are not a priori known or would not apply to all instances of a given Kind.
//
// Conditions should be added to explicitly convey properties that users and
// components care about rather than requiring those properties to be inferred
// from other observations. Once defined, the meaning of a Condition can not be
// changed arbitrarily - it becomes part of the API, and has the same
// backwards- and forwards-compatibility concerns of any other part of the API.
type Condition struct {
Type ConditionType `json:"type"`
Status corev1.ConditionStatus `json:"status"`
Reason ConditionReason `json:"reason,omitempty"`
Message string `json:"message,omitempty"`
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
}

// IsTrue Condition whether the condition status is "True".
func (c Condition) IsTrue() bool {
return c.Status == corev1.ConditionTrue
}

// IsFalse returns whether the condition status is "False".
func (c Condition) IsFalse() bool {
return c.Status == corev1.ConditionFalse
}

// IsUnknown returns whether the condition status is "Unknown".
func (c Condition) IsUnknown() bool {
return c.Status == corev1.ConditionUnknown
}

// DeepCopy returns a deep copy of the condition
func (c *Condition) DeepCopy() *Condition {
if c == nil {
return nil
}
out := *c
return &out
}

// Conditions is a set of Condition instances.
//
// +kubebuilder:validation:Type=array
type Conditions map[ConditionType]Condition

// NewConditions initializes a set of conditions with the given list of
// conditions.
func NewConditions(conds ...Condition) Conditions {
conditions := Conditions{}
for _, c := range conds {
conditions.SetCondition(c)
}
return conditions
}

// IsTrueFor searches the set of conditions for a condition with the given
// ConditionType. If found, it returns `condition.IsTrue()`. If not found,
// it returns false.
func (conditions Conditions) IsTrueFor(t ConditionType) bool {
if condition, ok := conditions[t]; ok {
return condition.IsTrue()
}
return false
}

// IsFalseFor searches the set of conditions for a condition with the given
// ConditionType. If found, it returns `condition.IsFalse()`. If not found,
// it returns false.
func (conditions Conditions) IsFalseFor(t ConditionType) bool {
if condition, ok := conditions[t]; ok {
return condition.IsFalse()
}
return false
}

// IsUnknownFor searches the set of conditions for a condition with the given
// ConditionType. If found, it returns `condition.IsUnknown()`. If not found,
// it returns true.
func (conditions Conditions) IsUnknownFor(t ConditionType) bool {
if condition, ok := conditions[t]; ok {
return condition.IsUnknown()
}
return true
}

// SetCondition adds (or updates) the set of conditions with the given
// condition. It returns a boolean value indicating whether the set condition
// is new or was a change to the existing condition with the same type.
func (conditions *Conditions) SetCondition(newCond Condition) bool {
if conditions == nil || *conditions == nil {
*conditions = make(map[ConditionType]Condition)
}
newCond.LastTransitionTime = metav1.Time{Time: clock.Now()}

if condition, ok := (*conditions)[newCond.Type]; ok {
// If the condition status didn't change, use the existing
// condition's last transition time.
if condition.Status == newCond.Status {
newCond.LastTransitionTime = condition.LastTransitionTime
}
changed := condition.Status != newCond.Status ||
condition.Reason != newCond.Reason ||
condition.Message != newCond.Message
(*conditions)[newCond.Type] = newCond
return changed
}
(*conditions)[newCond.Type] = newCond
return true
}

// GetCondition searches the set of conditions for the condition with the given
// ConditionType and returns it. If the matching condition is not found,
// GetCondition returns nil.
func (conditions Conditions) GetCondition(t ConditionType) *Condition {
if condition, ok := conditions[t]; ok {
return &condition
}
return nil
}

// RemoveCondition removes the condition with the given ConditionType from
// the conditions set. If no condition with that type is found, RemoveCondition
// returns without performing any action. If the passed condition type is not
// found in the set of conditions, RemoveCondition returns false.
func (conditions *Conditions) RemoveCondition(t ConditionType) bool {
if conditions == nil || *conditions == nil {
return false
}
if _, ok := (*conditions)[t]; ok {
delete(*conditions, t)
return true
}
return false
}

// MarshalJSON marshals the set of conditions as a JSON array, sorted by
// condition type.
func (conditions Conditions) MarshalJSON() ([]byte, error) {
conds := []Condition{}
for _, condition := range conditions {
conds = append(conds, condition)
}
sort.Slice(conds, func(a, b int) bool {
return conds[a].Type < conds[b].Type
})
return json.Marshal(conds)
}

// UnmarshalJSON unmarshals the JSON data into the set of Conditions.
func (conditions *Conditions) UnmarshalJSON(data []byte) error {
*conditions = make(map[ConditionType]Condition)
conds := []Condition{}
if err := json.Unmarshal(data, &conds); err != nil {
return err
}
for _, condition := range conds {
(*conditions)[condition.Type] = condition
}
return nil
}
Loading