Skip to content
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
59 changes: 53 additions & 6 deletions pkg/status/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ SPDX-License-Identifier: Apache-2.0
package status

import (
"fmt"
"regexp"
"strings"

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

// Implement the StatusAnalyzer interface.
func (s *statusAnalyzer) ComputeStatus(object *unstructured.Unstructured) (Status, error) {
var extraConditions []string

if hint, ok := object.GetAnnotations()[s.reconcilerName+"/"+types.AnnotationKeySuffixStatusHint]; ok {
object = object.DeepCopy()

for _, hint := range strings.Split(hint, ",") {
switch strcase.ToKebab(hint) {
var key, value string
var hasValue bool
if match := regexp.MustCompile(`^([^=]+)=(.*)$`).FindStringSubmatch(hint); match != nil {
key = match[1]
value = match[2]
hasValue = true
} else {
key = hint
}
switch strcase.ToKebab(key) {
case types.StatusHintHasObservedGeneration:
if hasValue {
return UnknownStatus, fmt.Errorf("status hint %s does not take a value", types.StatusHintHasObservedGeneration)
}
_, found, err := unstructured.NestedInt64(object.Object, "status", "observedGeneration")
if err != nil {
return UnknownStatus, err
}
if !found {
if err := unstructured.SetNestedField(object.Object, -1, "status", "observedGeneration"); err != nil {
if err := unstructured.SetNestedField(object.Object, int64(-1), "status", "observedGeneration"); err != nil {
return UnknownStatus, err
}
}
case types.StatusHintHasReadyCondition:
if hasValue {
return UnknownStatus, fmt.Errorf("status hint %s does not take a value", types.StatusHintHasReadyCondition)
}
foundReadyCondition := false
conditions, found, err := unstructured.NestedSlice(object.Object, "status", "conditions")
if err != nil {
Expand Down Expand Up @@ -85,6 +103,13 @@ func (s *statusAnalyzer) ComputeStatus(object *unstructured.Unstructured) (Statu
return UnknownStatus, err
}
}
case types.StatusHintConditions:
if !hasValue {
return UnknownStatus, fmt.Errorf("status hint %s requires a value", types.StatusHintConditions)
}
extraConditions = append(extraConditions, strings.Split(value, ";")...)
default:
return UnknownStatus, fmt.Errorf("unknown status hint %s", key)
}
}
}
Expand All @@ -93,11 +118,33 @@ func (s *statusAnalyzer) ComputeStatus(object *unstructured.Unstructured) (Statu
if err != nil {
return UnknownStatus, err
}
status := Status(res.Status)

if status == CurrentStatus && len(extraConditions) > 0 {
objc, err := kstatus.GetObjectWithConditions(object.UnstructuredContent())
if err != nil {
return UnknownStatus, err
}
for _, condition := range extraConditions {
found := false
for _, cond := range objc.Status.Conditions {
if cond.Type == condition {
found = true
if cond.Status != corev1.ConditionTrue {
status = InProgressStatus
}
}
}
if !found {
status = InProgressStatus
}
}
}

switch object.GroupVersionKind() {
case schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "Job"}:
// other than kstatus we want to consider jobs as InProgress if its pods are still running, resp. did not (yet) finish successfully
if res.Status == kstatus.CurrentStatus {
if status == CurrentStatus {
done := false
objc, err := kstatus.GetObjectWithConditions(object.UnstructuredContent())
if err != nil {
Expand All @@ -114,10 +161,10 @@ func (s *statusAnalyzer) ComputeStatus(object *unstructured.Unstructured) (Statu
}
}
if !done {
res.Status = kstatus.InProgressStatus
status = InProgressStatus
}
}
}

return Status(res.Status), nil
return status, nil
}
148 changes: 148 additions & 0 deletions pkg/status/analyzer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors
SPDX-License-Identifier: Apache-2.0
*/

package status_test

import (
"strings"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/sap/component-operator-runtime/pkg/status"
)

var _ = Describe("testing: analyzer.go", func() {
var analyzer status.StatusAnalyzer

BeforeEach(func() {
analyzer = status.NewStatusAnalyzer("test")
})

Context("xxx", func() {
BeforeEach(func() {
})

DescribeTable("testing: ComputeStatus()",
func(generation int, observedGeneration int, conditions map[kstatus.ConditionType]corev1.ConditionStatus, hintObservedGeneration bool, hintReadyCondition bool, hintConditions []string, expectedStatus status.Status) {
obj := Object{
ObjectMeta: metav1.ObjectMeta{
Generation: int64(generation),
},
Status: ObjectStatus{
ObservedGeneration: int64(observedGeneration),
},
}
for name, status := range conditions {
obj.Status.Conditions = append(obj.Status.Conditions, kstatus.Condition{
Type: name,
Status: status,
})
}
var hints []string
if hintObservedGeneration {
hints = append(hints, "has-observed-generation")
}
if hintReadyCondition {
hints = append(hints, "has-ready-condition")
}
if len(hintConditions) > 0 {
hints = append(hints, "conditions="+strings.Join(hintConditions, ";"))
}
if len(hints) > 0 {
obj.Annotations = map[string]string{
"test/status-hint": strings.Join(hints, ","),
}
}
unstructuredContent, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&obj)
Expect(err).NotTo(HaveOccurred())
unstructuredObj := &unstructured.Unstructured{Object: unstructuredContent}

computedStatus, err := analyzer.ComputeStatus(unstructuredObj)
Expect(err).NotTo(HaveOccurred())

Expect(computedStatus).To(Equal(expectedStatus))
},

Entry(nil, 3, 0, nil, false, false, nil, status.CurrentStatus),
Entry(nil, 3, 1, nil, false, false, nil, status.InProgressStatus),
Entry(nil, 3, 3, nil, false, false, nil, status.CurrentStatus),
Entry(nil, 3, 0, nil, true, false, nil, status.InProgressStatus),
Entry(nil, 3, 1, nil, true, false, nil, status.InProgressStatus),
Entry(nil, 3, 3, nil, true, false, nil, status.CurrentStatus),

Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionUnknown}, false, false, nil, status.InProgressStatus),
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionUnknown}, false, false, nil, status.InProgressStatus),
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionUnknown}, false, false, nil, status.InProgressStatus),
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionUnknown}, true, false, nil, status.InProgressStatus),
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionUnknown}, true, false, nil, status.InProgressStatus),
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionUnknown}, true, false, nil, status.InProgressStatus),

Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionFalse}, false, false, nil, status.InProgressStatus),
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionFalse}, false, false, nil, status.InProgressStatus),
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionFalse}, false, false, nil, status.InProgressStatus),
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionFalse}, true, false, nil, status.InProgressStatus),
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionFalse}, true, false, nil, status.InProgressStatus),
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionFalse}, true, false, nil, status.InProgressStatus),

Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionTrue}, false, false, nil, status.CurrentStatus),
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionTrue}, false, false, nil, status.InProgressStatus),
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionTrue}, false, false, nil, status.CurrentStatus),
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionTrue}, true, false, nil, status.InProgressStatus),
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionTrue}, true, false, nil, status.InProgressStatus),
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Ready": corev1.ConditionTrue}, true, false, nil, status.CurrentStatus),

Entry(nil, 3, 0, nil, false, true, nil, status.InProgressStatus),
Entry(nil, 3, 1, nil, false, true, nil, status.InProgressStatus),
Entry(nil, 3, 3, nil, false, true, nil, status.InProgressStatus),
Entry(nil, 3, 0, nil, true, true, nil, status.InProgressStatus),
Entry(nil, 3, 1, nil, true, true, nil, status.InProgressStatus),
Entry(nil, 3, 3, nil, true, true, nil, status.InProgressStatus),

Entry(nil, 3, 0, nil, false, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 1, nil, false, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 3, nil, false, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 0, nil, true, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 1, nil, true, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 3, nil, true, false, []string{"Test"}, status.InProgressStatus),

Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, false, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, false, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, false, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, true, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, true, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, true, false, []string{"Test"}, status.InProgressStatus),

Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, false, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, false, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, false, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, true, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, true, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionFalse}, true, false, []string{"Test"}, status.InProgressStatus),

Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionTrue}, false, false, []string{"Test"}, status.CurrentStatus),
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionTrue}, false, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionTrue}, false, false, []string{"Test"}, status.CurrentStatus),
Entry(nil, 3, 0, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionTrue}, true, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 1, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionTrue}, true, false, []string{"Test"}, status.InProgressStatus),
Entry(nil, 3, 3, map[kstatus.ConditionType]corev1.ConditionStatus{"Test": corev1.ConditionTrue}, true, false, []string{"Test"}, status.CurrentStatus),
)
})
})

type Object struct {
metav1.ObjectMeta `json:"metadata,omitempty"`
Status ObjectStatus `json:"status"`
}

type ObjectStatus struct {
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
Conditions []kstatus.Condition `json:"conditions,omitempty"`
}
18 changes: 18 additions & 0 deletions pkg/status/suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and component-operator-runtime contributors
SPDX-License-Identifier: Apache-2.0
*/

package status_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestComponent(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Component Suite")
}
1 change: 1 addition & 0 deletions pkg/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ const (
const (
StatusHintHasObservedGeneration = "has-observed-generation"
StatusHintHasReadyCondition = "has-ready-condition"
StatusHintConditions = "conditions"
)
3 changes: 2 additions & 1 deletion website/content/en/docs/concepts/dependents.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ To support such cases, the `Generator` implementation can set the following anno
- `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
- `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:
- `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)
- `has-ready-condition`: tells the framework to count with a ready condition; if it is absent, the condition state will be considered as `Unknown`
- `has-ready-condition`: tells the framework to count with a ready condition; if it is absent, the condition status will be considered as `Unknown`
- `conditions`: semicolon-separated list of additional conditions that must be present and have a `True` status in order to make the overall status ready

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