Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 10 additions & 1 deletion pkg/controllers/monitoring/monitoring-stack/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"

stack "github.com/rhobs/observability-operator/pkg/apis/monitoring/v1alpha1"
"github.com/rhobs/observability-operator/pkg/status"

"github.com/go-logr/logr"
monv1 "github.com/rhobs/obo-prometheus-operator/pkg/apis/monitoring/v1"
)

type resourceManager struct {
Expand Down Expand Up @@ -184,7 +188,12 @@ func (rm resourceManager) updateStatus(ctx context.Context, req ctrl.Request, ms
logger.Info("Failed to get prometheus object", "err", err)
return ctrl.Result{RequeueAfter: 2 * time.Second}
}
ms.Status.Conditions = updateConditions(ms, prom, recError)

ms.Status.Conditions, err = status.UpdateConditions(ms, operands, recError)
if err != nil {
logger.Info("Failed to update status conditions", "err", err)
return ctrl.Result{RequeueAfter: 2 * time.Second}
}
err = rm.k8sClient.Status().Update(ctx, ms)
if err != nil {
logger.Info("Failed to update status", "err", err)
Expand Down
207 changes: 207 additions & 0 deletions pkg/status/conditions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package status

import (
"fmt"

"github.com/rhobs/observability-operator/pkg/apis/monitoring/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
AvailableReason = "MonitoringStackAvailable"
ReconciledReason = "MonitoringStackReconciled"
FailedToReconcileReason = "FailedToReconcile"
ResourceSelectorIsNil = "ResourceSelectorNil"
AvailableMessage = "Monitoring Stack is available"
SuccessfullyReconciledMessage = "Monitoring Stack is successfully reconciled"
ResourceSelectorIsNilMessage = "No resources will be discovered, ResourceSelector is nil"
ResourceDiscoveryOnMessage = "Resource discovery is operational"
NoReason = "None"
available = "Available"
reconciled = "Reconciled"
)

func UpdateConditions(stackObj client.Object, operands []Operand, recError error) ([]v1alpha1.Condition, error) {
var availableCon v1alpha1.Condition
var reconciledCon v1alpha1.Condition
conditions, err := getConditionsFromObject(stackObj)
if err != nil {
return nil, err
}
for _, opr := range operands {
if opr.affectsAvailability {
availableCon = updateAvailable(conditions, opr, stackObj.GetGeneration())
}
if opr.affectsReconciled {
reconciledCon = updateReconciled(conditions, opr, stackObj.GetGeneration(), recError)
}
}

resourceDiscoveryCon, err := updateResourceDiscovery(stackObj)
if err != nil {
return nil, err
}

return []v1alpha1.Condition{
availableCon,
reconciledCon,
*resourceDiscoveryCon,
}, nil
}

// updateAvailable gets existing "Available" condition and updates its parameters
// based on the operand "Available" condition
func updateAvailable(conditions []v1alpha1.Condition, opr Operand, generation int64) v1alpha1.Condition {
ac, err := getConditionByType(conditions, v1alpha1.AvailableCondition)
if err != nil {
ac = v1alpha1.Condition{
Type: v1alpha1.AvailableCondition,
Status: v1alpha1.ConditionUnknown,
Reason: NoReason,
LastTransitionTime: metav1.Now(),
}
}

operandAvailable, err := opr.getConditionByType(available)

if err != nil {
ac.Status = v1alpha1.ConditionUnknown
ac.Reason = fmt.Sprintf("%sNotAvailable", opr.name)
ac.Message = fmt.Sprintf("Cannot read %s status conditions", opr.name)
ac.LastTransitionTime = metav1.Now()
return ac
}
// MonitoringStack status will not be updated if there is a difference between the operand generation
// and the operand ObservedGeneration. This can occur, for example, in the case of an invalid operand configuration.
if operandAvailable.ObservedGeneration != opr.Object.GetGeneration() {
return ac
}

if operandAvailable.Status != "True" {
ac.Status = prometheusStatusToMSStatus(operandAvailable.Status)
if operandAvailable.Status == "Degraded" {
ac.Reason = fmt.Sprintf("%sDegraded", opr.name)
} else {
ac.Reason = fmt.Sprintf("%sNotAvailable", opr.name)
}
ac.Message = operandAvailable.Message
ac.LastTransitionTime = metav1.Now()
return ac
}
ac.Status = v1alpha1.ConditionTrue
ac.Reason = AvailableReason
ac.Message = AvailableMessage
ac.ObservedGeneration = generation
ac.LastTransitionTime = metav1.Now()
return ac
}

// updateReconciled updates "Reconciled" conditions based on the provided error value and
// the operand "Reconciled" condition
func updateReconciled(conditions []v1alpha1.Condition, opr Operand, generation int64, reconcileErr error) v1alpha1.Condition {
rc, cErr := getConditionByType(conditions, v1alpha1.ReconciledCondition)
if cErr != nil {
rc = v1alpha1.Condition{
Type: v1alpha1.ReconciledCondition,
Status: v1alpha1.ConditionUnknown,
Reason: NoReason,
LastTransitionTime: metav1.Now(),
}
}
if reconcileErr != nil {
rc.Status = v1alpha1.ConditionFalse
rc.Message = reconcileErr.Error()
rc.Reason = FailedToReconcileReason
rc.LastTransitionTime = metav1.Now()
return rc
}
operandReconciled, reconcileErr := opr.getConditionByType(reconciled)

if reconcileErr != nil {
rc.Status = v1alpha1.ConditionUnknown
rc.Reason = fmt.Sprintf("%sNotReconciled", opr.name)
rc.Message = fmt.Sprintf("Cannot read %s status conditions", opr.name)
rc.LastTransitionTime = metav1.Now()
return rc
}

if operandReconciled.ObservedGeneration != opr.Object.GetGeneration() {
return rc
}

if operandReconciled.Status != "True" {
rc.Status = prometheusStatusToMSStatus(operandReconciled.Status)
rc.Reason = fmt.Sprintf("%sNotReconciled", opr.name)
rc.Message = operandReconciled.Message
rc.LastTransitionTime = metav1.Now()
return rc
}
rc.Status = v1alpha1.ConditionTrue
rc.Reason = ReconciledReason
rc.Message = SuccessfullyReconciledMessage
rc.ObservedGeneration = generation
rc.LastTransitionTime = metav1.Now()
return rc
}

func getConditionByType(conditions []v1alpha1.Condition, t v1alpha1.ConditionType) (v1alpha1.Condition, error) {
for _, c := range conditions {
if c.Type == t {
return c, nil
}
}
return v1alpha1.Condition{}, fmt.Errorf("ERROR: condition type %v not found", t)
}

func getConditionsFromObject(o client.Object) ([]v1alpha1.Condition, error) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we could avoid this function by using some interface like:

type StatusReporting interface {
	Conditions() []Condition
	Geeneration() int64
}

And require types like MonitoringStack to implement this interface.

@tremes tremes May 2, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jan--f I was updating this PR this morning a bit (locally so far) and I think this ^^ can probably make few things more simple here.
What do you think about this idea? The Operand type introduced here can then use this StatusReporting type instead of client.Object (but it's just theory. I haven't tried yet)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm actually the theory was not correct :) I guess it won't help much with the conditions reading from the operands.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I see.
Could we perhaps use generics to implement the update* functions? The idea would be to simply provide a helper function that implements what's described in https://docs.google.com/presentation/d/1f-0UtZB8RvIlw49pBYCLfLfWeRflUYo2bJvkKpL2sRo/edit#slide=id.g28d2c6c7c1e_0_10
wdyt?

we certainly could make do without this, but maybe this is nice chance to testdrive golang generics :)

unstrObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o)
if err != nil {
return nil, err
}
var conditions []v1alpha1.Condition
untypedCon, ok, err := unstructured.NestedSlice(unstrObj, "status", "conditions")
// if no conditions found, return empty conditions
if !ok {
return conditions, nil
}
if err != nil {
return nil, err
}

for _, untypedC := range untypedCon {
cMap, ok := untypedC.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("converting to map[string]interface{}: %v", untypedC)
}
conditions = append(conditions, v1alpha1.Condition{
Type: v1alpha1.ConditionType(convert[string](cMap["type"])),
Reason: convert[string](cMap["reason"]),
Status: v1alpha1.ConditionStatus(convert[string](cMap["status"])),
Message: convert[string](cMap["message"]),
ObservedGeneration: convert[int64](cMap["observedGeneration"]),
LastTransitionTime: convert[metav1.Time](cMap["lastTransitionTime"]),
})
}
return conditions, nil

}

func prometheusStatusToMSStatus(ps string) v1alpha1.ConditionStatus {
switch ps {
// Prometheus "Available" condition with status "Degraded" is reported as "Available" condition
// with status false
case "Degraded":
return v1alpha1.ConditionFalse
case "True":
return v1alpha1.ConditionTrue
case "False":
return v1alpha1.ConditionFalse
case "Unknown":
return v1alpha1.ConditionUnknown
default:
return v1alpha1.ConditionUnknown
}
}
Loading