Skip to content

Commit c4cb85f

Browse files
yuwenmajustinsb
andauthored
make status.condition patchable (kubernetes-sigs#319)
* make `status.condition` patchable * Proposed tweaks to pr 319 * test: shorten the reconcile duration * Reintroduce abnormal conditions * cleanup; klog error if `statusConditions` is not used. --------- Co-authored-by: Justin SB <justinsb@google.com>
1 parent c62cd2a commit c4cb85f

File tree

9 files changed

+144
-52
lines changed

9 files changed

+144
-52
lines changed

pkg/patterns/addon/pkg/apis/v1alpha1/common_types.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,11 @@ type CommonSpec struct {
4242
Channel string `json:"channel,omitempty"`
4343
}
4444

45-
//go:generate go run ../../../../../../vendor/k8s.io/code-generator/cmd/deepcopy-gen/main.go -O zz_generated.deepcopy -i ./... -h ../../../../../../hack/boilerplate.go.txt
46-
// +k8s:deepcopy-gen=true
47-
4845
// CommonStatus is a set of status attributes that must be exposed on all addons.
4946
type CommonStatus struct {
5047
Healthy bool `json:"healthy"`
5148
Errors []string `json:"errors,omitempty"`
5249
Phase string `json:"phase,omitempty"`
53-
// Conditions follows the API specification "Conditions" properties.
54-
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
55-
Conditions []metav1.Condition `json:"conditions,omitempty"`
5650
}
5751

5852
// Patchable is a trait for addon CRDs that expose a raw set of Patches to be
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha1
18+
19+
import (
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
)
22+
23+
// StatusConditions defines the fields to support status.conditions.
24+
type StatusConditions struct {
25+
// Conditions follows the API specification "Conditions" properties.
26+
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
27+
Conditions []metav1.Condition `json:"conditions,omitempty"`
28+
}

pkg/patterns/addon/pkg/apis/v1alpha1/zz_generated.deepcopy.go

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package status
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
"k8s.io/apimachinery/pkg/runtime"
9+
"k8s.io/klog/v2"
10+
)
11+
12+
// GetConditions pulls out the `status.conditions` field from runtime.Object
13+
func GetConditions(instance runtime.Object) ([]metav1.Condition, error) {
14+
statusVal := reflect.ValueOf(instance).Elem().FieldByName("Status")
15+
if !statusVal.IsValid() {
16+
return nil, fmt.Errorf("status field not found")
17+
}
18+
conditionsVal := statusVal.FieldByName("Conditions")
19+
if !conditionsVal.IsValid() {
20+
klog.Errorf("unable to find `status.condition` in %T", instance)
21+
return nil, nil
22+
}
23+
24+
v := conditionsVal.Interface()
25+
conditions, ok := v.([]metav1.Condition)
26+
if !ok {
27+
return nil, fmt.Errorf("unexpecetd type for status.conditions; got %T, want []metav1.Condition", v)
28+
}
29+
return conditions, nil
30+
}
31+
32+
// SetConditions sets the newConditions to runtime.Object `status.conditions` field.
33+
func SetConditions(instance runtime.Object, newConditions []metav1.Condition) error {
34+
statusVal := reflect.ValueOf(instance).Elem().FieldByName("Status")
35+
if !statusVal.IsValid() {
36+
// Status not ready.
37+
return fmt.Errorf("status field not found")
38+
}
39+
conditionsVal := statusVal.FieldByName("Conditions")
40+
if !conditionsVal.IsValid() {
41+
klog.Errorf("unable to find `status.condition` in %T", instance)
42+
return nil
43+
}
44+
45+
newConditionsVal := reflect.ValueOf(newConditions)
46+
if !conditionsVal.CanSet() {
47+
return fmt.Errorf("cannot set status.conditions field")
48+
}
49+
if !newConditionsVal.CanConvert(conditionsVal.Type()) {
50+
return fmt.Errorf("cannot set type %v to status.conditions type %v", newConditionsVal.Type(), conditionsVal.Type())
51+
}
52+
conditionsVal.Set(newConditionsVal)
53+
54+
return nil
55+
}

pkg/patterns/addon/pkg/status/conditions.go

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ package status
33
import (
44
"strings"
55

6-
"k8s.io/apimachinery/pkg/api/meta"
76
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
87
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
9-
addonsv1alpha1 "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/apis/v1alpha1"
108
)
119

1210
const (
@@ -15,27 +13,7 @@ const (
1513
ReadyType = "Ready"
1614
)
1715

18-
// SetInProgress set the present condition to a single condition with type "Ready" and status "false". This means
19-
// the current resources is still reconciling. If any deployment manifests are abnormal, their abnormal status condition will
20-
// be recorded in the `message` field.
21-
func SetInProgress(commonStatus *addonsv1alpha1.CommonStatus, abnormalTrueConditions []status.Condition) {
22-
setCondition(metav1.ConditionFalse, commonStatus, abnormalTrueConditions)
23-
}
24-
25-
// SetReady set the present condition to a single condition with type "Ready" and status "true". This means
26-
// all the deployment manifests are reconciled.
27-
func SetReady(commonStatus *addonsv1alpha1.CommonStatus, abnormalTrueConditions []status.Condition) {
28-
setCondition(metav1.ConditionTrue, commonStatus, abnormalTrueConditions)
29-
}
30-
31-
func setCondition(status metav1.ConditionStatus, commonStatus *addonsv1alpha1.CommonStatus, abnormalTrueConditions []status.Condition) {
32-
newCondition := new(abnormalTrueConditions)
33-
newCondition.Status = status
34-
newCondition.Type = ReadyType
35-
meta.SetStatusCondition(&commonStatus.Conditions, newCondition)
36-
}
37-
38-
// new returns a Condition object with human-readable message and reason.
16+
// buildReadyCondition returns a Condition object with human-readable message and reason.
3917
// The "reason" should be "Normal" if no deployment manifests have abnormal conditions, or "ManifestsNotReady"
4018
// as long as one deployment manifest has an abnormal condition.
4119
// The "message" contains each abnormal condition's "reason" and "message".
@@ -46,22 +24,31 @@ func setCondition(status metav1.ConditionStatus, commonStatus *addonsv1alpha1.Co
4624
// message: |-
4725
// apps/v1, Kind=Deployment/argocd/argocd-repo-server:Deployment does not have minimum availability.
4826
// apps/v1, Kind=Deployment/argocd/argocd-server:Deployment does not have minimum availability.
49-
func new(conditions []status.Condition) metav1.Condition {
50-
if len(conditions) == 0 {
51-
return metav1.Condition{
52-
Reason: NormalReason,
53-
Message: "all manifests are reconciled.",
54-
}
27+
func buildReadyCondition(isReady bool, abnormalConditions []status.Condition) metav1.Condition {
28+
var readyCondition metav1.Condition
29+
readyCondition.Type = ReadyType
30+
31+
if isReady {
32+
readyCondition.Status = metav1.ConditionTrue
33+
} else {
34+
readyCondition.Status = metav1.ConditionFalse
5535
}
56-
abnormalMessage := ""
57-
for _, cond := range conditions {
58-
if cond.Message != "" {
59-
abnormalMessage += cond.Message + "\n"
36+
37+
if len(abnormalConditions) == 0 {
38+
readyCondition.Reason = NormalReason
39+
readyCondition.Message = "all manifests are reconciled."
40+
} else {
41+
var messages []string
42+
for _, cond := range abnormalConditions {
43+
if cond.Message != "" {
44+
messages = append(messages, cond.Message)
45+
}
6046
}
61-
}
6247

63-
return metav1.Condition{
64-
Reason: AbnormalReason,
65-
Message: strings.TrimSuffix(abnormalMessage, "\n"),
48+
readyCondition.Reason = AbnormalReason
49+
50+
readyCondition.Message = strings.Join(messages, "\n")
6651
}
52+
53+
return readyCondition
6754
}

pkg/patterns/addon/pkg/status/kstatus.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package status
33
import (
44
"context"
55

6+
"k8s.io/apimachinery/pkg/api/meta"
67
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
78
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
89
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -27,6 +28,11 @@ func (k *kstatusAggregator) BuildStatus(ctx context.Context, info *declarative.S
2728
log.Error(err, "error retrieving status")
2829
return err
2930
}
31+
conditions, err := GetConditions(info.Subject)
32+
if err != nil {
33+
log.Error(err, "error retrieving status.conditions")
34+
return err
35+
}
3036

3137
shouldComputeHealthFromObjects := info.Manifest != nil && info.LiveObjects != nil
3238
if info.Err != nil {
@@ -78,17 +84,20 @@ func (k *kstatusAggregator) BuildStatus(ctx context.Context, info *declarative.S
7884
}
7985

8086
// Summarize all the deployment manifests statuses to a single results.
81-
aggregatedPhase := aggregateStatus(statusMap)
8287
// Update the Conditions for the declarativeObject status.
83-
if aggregatedPhase == status.CurrentStatus {
84-
SetReady(&currentStatus, abnormalConditions)
85-
} else {
86-
SetInProgress(&currentStatus, abnormalConditions)
87-
}
88+
aggregatedPhase := aggregateStatus(statusMap)
89+
isReady := aggregatedPhase == status.CurrentStatus
90+
readyCondition := buildReadyCondition(isReady, abnormalConditions)
91+
92+
meta.SetStatusCondition(&conditions, readyCondition)
93+
8894
currentStatus.Phase = string(aggregatedPhase)
95+
if err := SetConditions(info.Subject, conditions); err != nil {
96+
return err
97+
}
8998
}
9099
currentStatus.Healthy = currentStatus.Phase == string(status.CurrentStatus)
91-
if err := utils.SetCommonStatus(info.Subject, currentStatus); err != nil {
100+
if err = utils.SetCommonStatus(info.Subject, currentStatus); err != nil {
92101
return err
93102
}
94103
return nil

pkg/test/testreconciler/simpletest/controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func testSimpleReconciler(h *testharness.Harness, testdir string, applier applie
138138

139139
mgrContext, mgrStop := context.WithCancel(ctx)
140140
go func() {
141-
time.Sleep(2 * time.Second)
141+
time.Sleep(1 * time.Second)
142142
// time.Sleep(200 * time.Second)
143143
mgrStop()
144144
}()

pkg/test/testreconciler/simpletest/v1alpha1/types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ type SimpleTestSpec struct {
2929

3030
// SimpleTestStatus defines the observed state of Guestbook
3131
type SimpleTestStatus struct {
32-
addonv1alpha1.CommonStatus `json:",inline"`
32+
addonv1alpha1.CommonStatus `json:",inline"`
33+
addonv1alpha1.StatusConditions `json:",inline"`
3334
}
3435

3536
//+kubebuilder:object:root=true

pkg/test/testreconciler/simpletest/v1alpha1/zz_generated.deepcopy.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)