Skip to content

Commit 18ec675

Browse files
authored
add new status hint: 'conditions' to specify extra conditions to be met (#100)
1 parent 8603504 commit 18ec675

File tree

5 files changed

+222
-7
lines changed

5 files changed

+222
-7
lines changed

pkg/status/analyzer.go

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ SPDX-License-Identifier: Apache-2.0
66
package status
77

88
import (
9+
"fmt"
10+
"regexp"
911
"strings"
1012

1113
"github.com/iancoleman/strcase"
@@ -40,22 +42,38 @@ func NewStatusAnalyzer(reconcilerName string) StatusAnalyzer {
4042

4143
// Implement the StatusAnalyzer interface.
4244
func (s *statusAnalyzer) ComputeStatus(object *unstructured.Unstructured) (Status, error) {
45+
var extraConditions []string
46+
4347
if hint, ok := object.GetAnnotations()[s.reconcilerName+"/"+types.AnnotationKeySuffixStatusHint]; ok {
4448
object = object.DeepCopy()
45-
4649
for _, hint := range strings.Split(hint, ",") {
47-
switch strcase.ToKebab(hint) {
50+
var key, value string
51+
var hasValue bool
52+
if match := regexp.MustCompile(`^([^=]+)=(.*)$`).FindStringSubmatch(hint); match != nil {
53+
key = match[1]
54+
value = match[2]
55+
hasValue = true
56+
} else {
57+
key = hint
58+
}
59+
switch strcase.ToKebab(key) {
4860
case types.StatusHintHasObservedGeneration:
61+
if hasValue {
62+
return UnknownStatus, fmt.Errorf("status hint %s does not take a value", types.StatusHintHasObservedGeneration)
63+
}
4964
_, found, err := unstructured.NestedInt64(object.Object, "status", "observedGeneration")
5065
if err != nil {
5166
return UnknownStatus, err
5267
}
5368
if !found {
54-
if err := unstructured.SetNestedField(object.Object, -1, "status", "observedGeneration"); err != nil {
69+
if err := unstructured.SetNestedField(object.Object, int64(-1), "status", "observedGeneration"); err != nil {
5570
return UnknownStatus, err
5671
}
5772
}
5873
case types.StatusHintHasReadyCondition:
74+
if hasValue {
75+
return UnknownStatus, fmt.Errorf("status hint %s does not take a value", types.StatusHintHasReadyCondition)
76+
}
5977
foundReadyCondition := false
6078
conditions, found, err := unstructured.NestedSlice(object.Object, "status", "conditions")
6179
if err != nil {
@@ -85,6 +103,13 @@ func (s *statusAnalyzer) ComputeStatus(object *unstructured.Unstructured) (Statu
85103
return UnknownStatus, err
86104
}
87105
}
106+
case types.StatusHintConditions:
107+
if !hasValue {
108+
return UnknownStatus, fmt.Errorf("status hint %s requires a value", types.StatusHintConditions)
109+
}
110+
extraConditions = append(extraConditions, strings.Split(value, ";")...)
111+
default:
112+
return UnknownStatus, fmt.Errorf("unknown status hint %s", key)
88113
}
89114
}
90115
}
@@ -93,11 +118,33 @@ func (s *statusAnalyzer) ComputeStatus(object *unstructured.Unstructured) (Statu
93118
if err != nil {
94119
return UnknownStatus, err
95120
}
121+
status := Status(res.Status)
122+
123+
if status == CurrentStatus && len(extraConditions) > 0 {
124+
objc, err := kstatus.GetObjectWithConditions(object.UnstructuredContent())
125+
if err != nil {
126+
return UnknownStatus, err
127+
}
128+
for _, condition := range extraConditions {
129+
found := false
130+
for _, cond := range objc.Status.Conditions {
131+
if cond.Type == condition {
132+
found = true
133+
if cond.Status != corev1.ConditionTrue {
134+
status = InProgressStatus
135+
}
136+
}
137+
}
138+
if !found {
139+
status = InProgressStatus
140+
}
141+
}
142+
}
96143

97144
switch object.GroupVersionKind() {
98145
case schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "Job"}:
99146
// other than kstatus we want to consider jobs as InProgress if its pods are still running, resp. did not (yet) finish successfully
100-
if res.Status == kstatus.CurrentStatus {
147+
if status == CurrentStatus {
101148
done := false
102149
objc, err := kstatus.GetObjectWithConditions(object.UnstructuredContent())
103150
if err != nil {
@@ -114,10 +161,10 @@ func (s *statusAnalyzer) ComputeStatus(object *unstructured.Unstructured) (Statu
114161
}
115162
}
116163
if !done {
117-
res.Status = kstatus.InProgressStatus
164+
status = InProgressStatus
118165
}
119166
}
120167
}
121168

122-
return Status(res.Status), nil
169+
return status, nil
123170
}

pkg/status/analyzer_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package status_test
7+
8+
import (
9+
"strings"
10+
11+
corev1 "k8s.io/api/core/v1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14+
"k8s.io/apimachinery/pkg/runtime"
15+
kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status"
16+
17+
. "github.com/onsi/ginkgo/v2"
18+
. "github.com/onsi/gomega"
19+
"github.com/sap/component-operator-runtime/pkg/status"
20+
)
21+
22+
var _ = Describe("testing: analyzer.go", func() {
23+
var analyzer status.StatusAnalyzer
24+
25+
BeforeEach(func() {
26+
analyzer = status.NewStatusAnalyzer("test")
27+
})
28+
29+
Context("xxx", func() {
30+
BeforeEach(func() {
31+
})
32+
33+
DescribeTable("testing: ComputeStatus()",
34+
func(generation int, observedGeneration int, conditions map[kstatus.ConditionType]corev1.ConditionStatus, hintObservedGeneration bool, hintReadyCondition bool, hintConditions []string, expectedStatus status.Status) {
35+
obj := Object{
36+
ObjectMeta: metav1.ObjectMeta{
37+
Generation: int64(generation),
38+
},
39+
Status: ObjectStatus{
40+
ObservedGeneration: int64(observedGeneration),
41+
},
42+
}
43+
for name, status := range conditions {
44+
obj.Status.Conditions = append(obj.Status.Conditions, kstatus.Condition{
45+
Type: name,
46+
Status: status,
47+
})
48+
}
49+
var hints []string
50+
if hintObservedGeneration {
51+
hints = append(hints, "has-observed-generation")
52+
}
53+
if hintReadyCondition {
54+
hints = append(hints, "has-ready-condition")
55+
}
56+
if len(hintConditions) > 0 {
57+
hints = append(hints, "conditions="+strings.Join(hintConditions, ";"))
58+
}
59+
if len(hints) > 0 {
60+
obj.Annotations = map[string]string{
61+
"test/status-hint": strings.Join(hints, ","),
62+
}
63+
}
64+
unstructuredContent, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&obj)
65+
Expect(err).NotTo(HaveOccurred())
66+
unstructuredObj := &unstructured.Unstructured{Object: unstructuredContent}
67+
68+
computedStatus, err := analyzer.ComputeStatus(unstructuredObj)
69+
Expect(err).NotTo(HaveOccurred())
70+
71+
Expect(computedStatus).To(Equal(expectedStatus))
72+
},
73+
74+
Entry(nil, 3, 0, nil, false, false, nil, status.CurrentStatus),
75+
Entry(nil, 3, 1, nil, false, false, nil, status.InProgressStatus),
76+
Entry(nil, 3, 3, nil, false, false, nil, status.CurrentStatus),
77+
Entry(nil, 3, 0, nil, true, false, nil, status.InProgressStatus),
78+
Entry(nil, 3, 1, nil, true, false, nil, status.InProgressStatus),
79+
Entry(nil, 3, 3, nil, true, false, nil, status.CurrentStatus),
80+
81+
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionUnknown}, false, false, nil, status.InProgressStatus),
82+
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionUnknown}, false, false, nil, status.InProgressStatus),
83+
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionUnknown}, false, false, nil, status.InProgressStatus),
84+
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionUnknown}, true, false, nil, status.InProgressStatus),
85+
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionUnknown}, true, false, nil, status.InProgressStatus),
86+
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionUnknown}, true, false, nil, status.InProgressStatus),
87+
88+
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionFalse}, false, false, nil, status.InProgressStatus),
89+
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionFalse}, false, false, nil, status.InProgressStatus),
90+
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionFalse}, false, false, nil, status.InProgressStatus),
91+
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionFalse}, true, false, nil, status.InProgressStatus),
92+
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionFalse}, true, false, nil, status.InProgressStatus),
93+
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionFalse}, true, false, nil, status.InProgressStatus),
94+
95+
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionTrue}, false, false, nil, status.CurrentStatus),
96+
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionTrue}, false, false, nil, status.InProgressStatus),
97+
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionTrue}, false, false, nil, status.CurrentStatus),
98+
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionTrue}, true, false, nil, status.InProgressStatus),
99+
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionTrue}, true, false, nil, status.InProgressStatus),
100+
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionTrue}, true, false, nil, status.CurrentStatus),
101+
102+
Entry(nil, 3, 0, nil, false, true, nil, status.InProgressStatus),
103+
Entry(nil, 3, 1, nil, false, true, nil, status.InProgressStatus),
104+
Entry(nil, 3, 3, nil, false, true, nil, status.InProgressStatus),
105+
Entry(nil, 3, 0, nil, true, true, nil, status.InProgressStatus),
106+
Entry(nil, 3, 1, nil, true, true, nil, status.InProgressStatus),
107+
Entry(nil, 3, 3, nil, true, true, nil, status.InProgressStatus),
108+
109+
Entry(nil, 3, 0, nil, false, false, []string{"Test"}, status.InProgressStatus),
110+
Entry(nil, 3, 1, nil, false, false, []string{"Test"}, status.InProgressStatus),
111+
Entry(nil, 3, 3, nil, false, false, []string{"Test"}, status.InProgressStatus),
112+
Entry(nil, 3, 0, nil, true, false, []string{"Test"}, status.InProgressStatus),
113+
Entry(nil, 3, 1, nil, true, false, []string{"Test"}, status.InProgressStatus),
114+
Entry(nil, 3, 3, nil, true, false, []string{"Test"}, status.InProgressStatus),
115+
116+
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, false, false, []string{"Test"}, status.InProgressStatus),
117+
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, false, false, []string{"Test"}, status.InProgressStatus),
118+
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, false, false, []string{"Test"}, status.InProgressStatus),
119+
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, true, false, []string{"Test"}, status.InProgressStatus),
120+
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, true, false, []string{"Test"}, status.InProgressStatus),
121+
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, true, false, []string{"Test"}, status.InProgressStatus),
122+
123+
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, false, false, []string{"Test"}, status.InProgressStatus),
124+
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, false, false, []string{"Test"}, status.InProgressStatus),
125+
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, false, false, []string{"Test"}, status.InProgressStatus),
126+
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, true, false, []string{"Test"}, status.InProgressStatus),
127+
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, true, false, []string{"Test"}, status.InProgressStatus),
128+
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, true, false, []string{"Test"}, status.InProgressStatus),
129+
130+
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionTrue}, false, false, []string{"Test"}, status.CurrentStatus),
131+
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionTrue}, false, false, []string{"Test"}, status.InProgressStatus),
132+
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionTrue}, false, false, []string{"Test"}, status.CurrentStatus),
133+
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionTrue}, true, false, []string{"Test"}, status.InProgressStatus),
134+
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionTrue}, true, false, []string{"Test"}, status.InProgressStatus),
135+
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionTrue}, true, false, []string{"Test"}, status.CurrentStatus),
136+
)
137+
})
138+
})
139+
140+
type Object struct {
141+
metav1.ObjectMeta `json:"metadata,omitempty"`
142+
Status ObjectStatus `json:"status"`
143+
}
144+
145+
type ObjectStatus struct {
146+
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
147+
Conditions []kstatus.Condition `json:"conditions,omitempty"`
148+
}

pkg/status/suite_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package status_test
7+
8+
import (
9+
"testing"
10+
11+
. "github.com/onsi/ginkgo/v2"
12+
. "github.com/onsi/gomega"
13+
)
14+
15+
func TestComponent(t *testing.T) {
16+
RegisterFailHandler(Fail)
17+
RunSpecs(t, "Component Suite")
18+
}

pkg/types/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ const (
4848
const (
4949
StatusHintHasObservedGeneration = "has-observed-generation"
5050
StatusHintHasReadyCondition = "has-ready-condition"
51+
StatusHintConditions = "conditions"
5152
)

website/content/en/docs/concepts/dependents.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ To support such cases, the `Generator` implementation can set the following anno
4242
- `mycomponent-operator.mydomain.io/delete-order` (optional): the wave by which this object will be deleted; that is, if the dependent is no longer part of the component, or if the whole component is being deleted; dependents will be deleted wave by wave; that is, objects of the same wave will be deleted in a canonical order, and the reconciler will only proceed to the next wave if all objects of previous saves are gone; specified orders can be negative or positive numbers between -32768 and 32767, objects with no explicit order set are treated as if they would specify order 0; note that the delete order is completely independent of the apply order
4343
- `mycomponent-operator.mydomain.io/status-hint` (optional): a comma-separated list of hints that may help the framework to properly identify the state of the annotated dependent object; currently, the following hints are possible:
4444
- `has-observed-generation`: tells the framework that the dependent object has a `status.observedGeneration` field, even if it is not (yet) set by the responsible controller (some controllers are known to set the observed generation lazily, with the consequence that there is a period right after creation of the dependent object, where the field is missing in the dependent's status)
45-
- `has-ready-condition`: tells the framework to count with a ready condition; if it is absent, the condition state will be considered as `Unknown`
45+
- `has-ready-condition`: tells the framework to count with a ready condition; if it is absent, the condition status will be considered as `Unknown`
46+
- `conditions`: semicolon-separated list of additional conditions that must be present and have a `True` status in order to make the overall status ready
4647

4748
Note that, in the above paragraph, `mycomponent-operator.mydomain.io` has to be replaced with whatever was passed as `name` when calling `NewReconciler()`.
4849

0 commit comments

Comments
 (0)