diff --git a/charts/operator/crds/monitoring.googleapis.com_clusterrules.yaml b/charts/operator/crds/monitoring.googleapis.com_clusterrules.yaml index 3c48ff4a92..327e1fc777 100644 --- a/charts/operator/crds/monitoring.googleapis.com_clusterrules.yaml +++ b/charts/operator/crds/monitoring.googleapis.com_clusterrules.yaml @@ -124,6 +124,44 @@ spec: type: object status: description: Most recently observed status of the resource. + properties: + conditions: + description: Represents the latest available observations of a podmonitor's + current state. + items: + description: MonitoringCondition describes the condition of a PodMonitoring. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human-readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: MonitoringConditionType is the type of MonitoringCondition. + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: The generation observed by the controller. + format: int64 + type: integer type: object required: - spec diff --git a/charts/operator/crds/monitoring.googleapis.com_globalrules.yaml b/charts/operator/crds/monitoring.googleapis.com_globalrules.yaml index ee6566e0a0..4ae39f0490 100644 --- a/charts/operator/crds/monitoring.googleapis.com_globalrules.yaml +++ b/charts/operator/crds/monitoring.googleapis.com_globalrules.yaml @@ -123,6 +123,44 @@ spec: type: object status: description: Most recently observed status of the resource. + properties: + conditions: + description: Represents the latest available observations of a podmonitor's + current state. + items: + description: MonitoringCondition describes the condition of a PodMonitoring. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human-readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: MonitoringConditionType is the type of MonitoringCondition. + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: The generation observed by the controller. + format: int64 + type: integer type: object required: - spec diff --git a/charts/operator/crds/monitoring.googleapis.com_rules.yaml b/charts/operator/crds/monitoring.googleapis.com_rules.yaml index b293b8f520..4bff18b6fa 100644 --- a/charts/operator/crds/monitoring.googleapis.com_rules.yaml +++ b/charts/operator/crds/monitoring.googleapis.com_rules.yaml @@ -124,6 +124,44 @@ spec: type: object status: description: Most recently observed status of the resource. + properties: + conditions: + description: Represents the latest available observations of a podmonitor's + current state. + items: + description: MonitoringCondition describes the condition of a PodMonitoring. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human-readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: MonitoringConditionType is the type of MonitoringCondition. + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: The generation observed by the controller. + format: int64 + type: integer type: object required: - spec diff --git a/doc/api.md b/doc/api.md index 571f31e3b3..70a9cf1943 100644 --- a/doc/api.md +++ b/doc/api.md @@ -1385,7 +1385,7 @@ monitoring resource was created successfully.

-(Appears in: ClusterNodeMonitoring, PodMonitoringStatus) +(Appears in: ClusterNodeMonitoring, PodMonitoringStatus, RulesStatus)

MonitoringStatus holds status information of a monitoring resource.

@@ -2425,6 +2425,31 @@ RulesStatus

RulesStatus contains status information for a Rules resource.

+ + + + + + + + + + + + + +
FieldDescription
+MonitoringStatus
+ + +MonitoringStatus + + +
+

+(Members of MonitoringStatus are embedded into this type.) +

+

SampleGroup diff --git a/manifests/setup.yaml b/manifests/setup.yaml index b827933ddb..44cf7445b6 100644 --- a/manifests/setup.yaml +++ b/manifests/setup.yaml @@ -1298,6 +1298,41 @@ spec: type: object status: description: Most recently observed status of the resource. + properties: + conditions: + description: Represents the latest available observations of a podmonitor's current state. + items: + description: MonitoringCondition describes the condition of a PodMonitoring. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + format: date-time + type: string + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human-readable message indicating details about the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: MonitoringConditionType is the type of MonitoringCondition. + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: The generation observed by the controller. + format: int64 + type: integer type: object required: - spec @@ -1520,6 +1555,41 @@ spec: type: object status: description: Most recently observed status of the resource. + properties: + conditions: + description: Represents the latest available observations of a podmonitor's current state. + items: + description: MonitoringCondition describes the condition of a PodMonitoring. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + format: date-time + type: string + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human-readable message indicating details about the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: MonitoringConditionType is the type of MonitoringCondition. + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: The generation observed by the controller. + format: int64 + type: integer type: object required: - spec @@ -3378,6 +3448,41 @@ spec: type: object status: description: Most recently observed status of the resource. + properties: + conditions: + description: Represents the latest available observations of a podmonitor's current state. + items: + description: MonitoringCondition describes the condition of a PodMonitoring. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status to another. + format: date-time + type: string + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human-readable message indicating details about the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: MonitoringConditionType is the type of MonitoringCondition. + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: The generation observed by the controller. + format: int64 + type: integer type: object required: - spec diff --git a/pkg/operator/apis/monitoring/v1/monitoring_types.go b/pkg/operator/apis/monitoring/v1/monitoring_types.go index ba4f4a6f36..82e0e7645b 100644 --- a/pkg/operator/apis/monitoring/v1/monitoring_types.go +++ b/pkg/operator/apis/monitoring/v1/monitoring_types.go @@ -68,6 +68,10 @@ func NewDefaultConditions(now metav1.Time) []MonitoringCondition { } } +func (cond *MonitoringCondition) IsValid() bool { + return cond.Type != "" && cond.Status != "" +} + // MonitoringStatus holds status information of a monitoring resource. type MonitoringStatus struct { // The generation observed by the controller. @@ -77,17 +81,17 @@ type MonitoringStatus struct { Conditions []MonitoringCondition `json:"conditions,omitempty"` } -// SetMonitoringCondition merges the provided condition if the resource generation changed or there is -// a status condition state transition. -func (status *MonitoringStatus) SetMonitoringCondition(gen int64, now metav1.Time, cond *MonitoringCondition) (bool, error) { +// SetMonitoringCondition merges the provided valid condition if the resource generation changed or +// there is a status condition state transition. +func (status *MonitoringStatus) SetMonitoringCondition(gen int64, now metav1.Time, cond *MonitoringCondition) bool { var ( specChanged = status.ObservedGeneration != gen statusTransition, update bool conds = make(map[MonitoringConditionType]*MonitoringCondition) ) - if cond.Type == "" || cond.Status == "" { - return update, errInvalidCond + if !cond.IsValid() { + return false } // Set up defaults. @@ -124,5 +128,5 @@ func (status *MonitoringStatus) SetMonitoringCondition(gen int64, now metav1.Tim } } - return update, nil + return update } diff --git a/pkg/operator/apis/monitoring/v1/pod_types.go b/pkg/operator/apis/monitoring/v1/pod_types.go index 33387c5996..031f46b7ca 100644 --- a/pkg/operator/apis/monitoring/v1/pod_types.go +++ b/pkg/operator/apis/monitoring/v1/pod_types.go @@ -26,10 +26,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) -var ( - errInvalidCond = fmt.Errorf("condition needs both 'Type' and 'Status' fields set") -) - // PodMonitoringCRD represents a Kubernetes CRD that monitors Pod endpoints. type PodMonitoringCRD interface { MonitoringCRD diff --git a/pkg/operator/apis/monitoring/v1/pod_types_test.go b/pkg/operator/apis/monitoring/v1/pod_types_test.go index e39043a9cc..97d671523d 100644 --- a/pkg/operator/apis/monitoring/v1/pod_types_test.go +++ b/pkg/operator/apis/monitoring/v1/pod_types_test.go @@ -190,10 +190,7 @@ func TestSetMonitoringCondition(t *testing.T) { for _, c := range cases { t.Run(c.doc, func(t *testing.T) { got := c.curr - change, err := got.SetMonitoringCondition(c.generation, c.now, c.cond) - if err != nil { - t.Fatalf("set podmonitoring condition: %s", err) - } + change := got.SetMonitoringCondition(c.generation, c.now, c.cond) // Get resolved podmonitorings. if change != c.change { diff --git a/pkg/operator/apis/monitoring/v1/rules_types.go b/pkg/operator/apis/monitoring/v1/rules_types.go index c5de1bf7d3..5331e862ae 100644 --- a/pkg/operator/apis/monitoring/v1/rules_types.go +++ b/pkg/operator/apis/monitoring/v1/rules_types.go @@ -55,6 +55,10 @@ func (*Rules) ValidateDelete() (admission.Warnings, error) { return nil, nil } +func (r *Rules) GetMonitoringStatus() *MonitoringStatus { + return &r.Status.MonitoringStatus +} + // RulesList is a list of Rules. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type RulesList struct { @@ -100,6 +104,10 @@ func (*ClusterRules) ValidateDelete() (admission.Warnings, error) { return nil, nil } +func (r *ClusterRules) GetMonitoringStatus() *MonitoringStatus { + return &r.Status.MonitoringStatus +} + // ClusterRulesList is a list of ClusterRules. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type ClusterRulesList struct { @@ -144,6 +152,10 @@ func (*GlobalRules) ValidateDelete() (admission.Warnings, error) { return nil, nil } +func (r *GlobalRules) GetMonitoringStatus() *MonitoringStatus { + return &r.Status.MonitoringStatus +} + // GlobalRulesList is a list of GlobalRules. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type GlobalRulesList struct { @@ -192,5 +204,5 @@ type Rule struct { // RulesStatus contains status information for a Rules resource. type RulesStatus struct { - // TODO: add status information. + MonitoringStatus `json:",inline"` } diff --git a/pkg/operator/collection.go b/pkg/operator/collection.go index b61caf85ae..136dca2b80 100644 --- a/pkg/operator/collection.go +++ b/pkg/operator/collection.go @@ -153,7 +153,7 @@ func patchMonitoringStatus(ctx context.Context, kubeClient client.Client, obj cl patch := client.RawPatch(types.MergePatchType, patchBytes) if err := kubeClient.Status().Patch(ctx, obj, patch); err != nil { - return err + return fmt.Errorf("patch status: %w", err) } return nil } @@ -422,14 +422,7 @@ func (r *collectionReconciler) makeCollectorConfig(ctx context.Context, spec *mo } cfg.ScrapeConfigs = append(cfg.ScrapeConfigs, cfgs...) - change, err := pmon.Status.SetMonitoringCondition(pmon.GetGeneration(), metav1.Now(), cond) - if err != nil { - // Log an error but let operator continue to avoid getting stuck - // on a potential bad resource. - logger.Error(err, "setting podmonitoring status state", "namespace", pmon.Namespace, "name", pmon.Name) - } - - if change { + if pmon.Status.SetMonitoringCondition(pmon.GetGeneration(), metav1.Now(), cond) { r.statusUpdates = append(r.statusUpdates, &pmon) } } @@ -463,14 +456,7 @@ func (r *collectionReconciler) makeCollectorConfig(ctx context.Context, spec *mo } cfg.ScrapeConfigs = append(cfg.ScrapeConfigs, cfgs...) - change, err := cmon.Status.SetMonitoringCondition(cmon.GetGeneration(), metav1.Now(), cond) - if err != nil { - // Log an error but let operator continue to avoid getting stuck - // on a potential bad resource. - logger.Error(err, "setting clusterpodmonitoring status state", "namespace", cmon.Namespace, "name", cmon.Name) - } - - if change { + if cmon.Status.SetMonitoringCondition(cmon.GetGeneration(), metav1.Now(), cond) { r.statusUpdates = append(r.statusUpdates, &cmon) } } @@ -516,14 +502,7 @@ func (r *collectionReconciler) makeCollectorConfig(ctx context.Context, spec *mo } cfg.ScrapeConfigs = append(cfg.ScrapeConfigs, cfgs...) - change, err := cm.Status.SetMonitoringCondition(cm.GetGeneration(), metav1.Now(), cond) - if err != nil { - // Log an error but let operator continue to avoid getting stuck - // on a potential bad resource. - logger.Error(err, "setting clusternodemonitoring status state", "namespace", cm.Namespace, "name", cm.Name) - } - - if change { + if cm.Status.SetMonitoringCondition(cm.GetGeneration(), metav1.Now(), cond) { r.statusUpdates = append(r.statusUpdates, &cm) } } diff --git a/pkg/operator/collection_test.go b/pkg/operator/collection_test.go index fbc4aba9d9..a1f3b9e24e 100644 --- a/pkg/operator/collection_test.go +++ b/pkg/operator/collection_test.go @@ -56,7 +56,11 @@ func newFakeClientBuilder() *fake.ClientBuilder { return fake.NewClientBuilder(). WithScheme(testScheme). WithStatusSubresource(&monitoringv1.PodMonitoring{}). - WithStatusSubresource(&monitoringv1.ClusterPodMonitoring{}) + WithStatusSubresource(&monitoringv1.ClusterPodMonitoring{}). + WithStatusSubresource(&monitoringv1.ClusterNodeMonitoring{}). + WithStatusSubresource(&monitoringv1.Rules{}). + WithStatusSubresource(&monitoringv1.ClusterRules{}). + WithStatusSubresource(&monitoringv1.GlobalRules{}) } // Tests that the collection does not overwrite the non-managed status fields. diff --git a/pkg/operator/rules.go b/pkg/operator/rules.go index 073ac40e5f..407282489f 100644 --- a/pkg/operator/rules.go +++ b/pkg/operator/rules.go @@ -16,6 +16,7 @@ package operator import ( "context" + "errors" "fmt" "github.com/go-logr/logr" @@ -202,6 +203,7 @@ func hasGlobalRules(ctx context.Context, c client.Client) (bool, error) { return len(rules.Items) > 0, nil } +// ensureRuleConfigs updates the Prometheus Rules ConfigMap. func (r *rulesReconciler) ensureRuleConfigs(ctx context.Context, projectID, location, cluster string, compression monitoringv1.CompressionType) error { logger, _ := logr.FromContext(ctx) @@ -234,48 +236,98 @@ func (r *rulesReconciler) ensureRuleConfigs(ctx context.Context, projectID, loca if err := r.client.List(ctx, &rulesList); err != nil { return fmt.Errorf("list rules: %w", err) } - for _, rs := range rulesList.Items { + + now := metav1.Now() + conditionSuccess := &monitoringv1.MonitoringCondition{ + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionTrue, + } + var statusUpdates []monitoringv1.MonitoringCRD + + for i := range rulesList.Items { + rs := &rulesList.Items[i] result, err := rs.RuleGroupsConfig(projectID, location, cluster) if err != nil { - // TODO(freinartz): update resource condition. - logger.Error(err, "converting rules failed", "rules_namespace", rs.Namespace, "rules_name", rs.Name) + msg := "generating rule config failed" + if rs.Status.SetMonitoringCondition(rs.GetGeneration(), now, &monitoringv1.MonitoringCondition{ + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionFalse, + Message: msg, + Reason: err.Error(), + }) { + statusUpdates = append(statusUpdates, rs) + } + logger.Error(err, "convert rules", "err", err, "namespace", rs.Namespace, "name", rs.Name) + continue } filename := fmt.Sprintf("rules__%s__%s.yaml", rs.Namespace, rs.Name) if err := setConfigMapData(cm, compression, filename, result); err != nil { return err } + + if rs.Status.SetMonitoringCondition(rs.GetGeneration(), now, conditionSuccess) { + statusUpdates = append(statusUpdates, rs) + } } var clusterRulesList monitoringv1.ClusterRulesList if err := r.client.List(ctx, &clusterRulesList); err != nil { return fmt.Errorf("list cluster rules: %w", err) } - for _, rs := range clusterRulesList.Items { + for i := range clusterRulesList.Items { + rs := &clusterRulesList.Items[i] result, err := rs.RuleGroupsConfig(projectID, location, cluster) if err != nil { - // TODO(freinartz): update resource condition. - logger.Error(err, "converting rules failed", "clusterrules_name", rs.Name) + msg := "generating rule config failed" + if rs.Status.SetMonitoringCondition(rs.Generation, now, &monitoringv1.MonitoringCondition{ + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionFalse, + Message: msg, + Reason: err.Error(), + }) { + statusUpdates = append(statusUpdates, rs) + } + logger.Error(err, "convert rules", "err", err, "namespace", rs.Namespace, "name", rs.Name) + continue } filename := fmt.Sprintf("clusterrules__%s.yaml", rs.Name) if err := setConfigMapData(cm, compression, filename, result); err != nil { return err } + + if rs.Status.SetMonitoringCondition(rs.GetGeneration(), now, conditionSuccess) { + statusUpdates = append(statusUpdates, rs) + } } var globalRulesList monitoringv1.GlobalRulesList if err := r.client.List(ctx, &globalRulesList); err != nil { return fmt.Errorf("list global rules: %w", err) } - for _, rs := range globalRulesList.Items { + for i := range globalRulesList.Items { + rs := &globalRulesList.Items[i] result, err := rs.RuleGroupsConfig() if err != nil { - // TODO(freinartz): update resource condition. - logger.Error(err, "converting rules failed", "globalrules_name", rs.Name) + msg := "generating rule config failed" + if rs.Status.SetMonitoringCondition(rs.Generation, now, &monitoringv1.MonitoringCondition{ + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionFalse, + Message: msg, + Reason: err.Error(), + }) { + statusUpdates = append(statusUpdates, rs) + } + logger.Error(err, "convert rules", "err", err, "namespace", rs.Namespace, "name", rs.Name) + continue } filename := fmt.Sprintf("globalrules__%s.yaml", rs.Name) if err := setConfigMapData(cm, compression, filename, result); err != nil { return err } + + if rs.Status.SetMonitoringCondition(rs.GetGeneration(), now, conditionSuccess) { + statusUpdates = append(statusUpdates, rs) + } } // Create or update generated rule ConfigMap. @@ -286,5 +338,13 @@ func (r *rulesReconciler) ensureRuleConfigs(ctx context.Context, projectID, loca } else if err != nil { return fmt.Errorf("update generated rules: %w", err) } - return nil + + var errs []error + for _, obj := range statusUpdates { + if err := patchMonitoringStatus(ctx, r.client, obj, obj.GetMonitoringStatus()); err != nil { + errs = append(errs, err) + } + } + + return errors.Join(errs...) } diff --git a/pkg/operator/rules_test.go b/pkg/operator/rules_test.go index a82e7bae0b..1dbb2f6b36 100644 --- a/pkg/operator/rules_test.go +++ b/pkg/operator/rules_test.go @@ -17,10 +17,14 @@ package operator import ( "context" "errors" + "fmt" "testing" + "time" monitoringv1 "github.com/GoogleCloudPlatform/prometheus-engine/pkg/operator/apis/monitoring/v1" + "github.com/google/go-cmp/cmp" appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/interceptor" @@ -59,6 +63,339 @@ func TestHasRules(t *testing.T) { } } +func TestRulesStatus(t *testing.T) { + // The fake client truncates seconds, so create a time that can't be rounded down. + timeDefault := metav1.NewTime(time.Date(2024, 5, 23, 1, 23, 0, 0, time.UTC)) + timeAfter := metav1.NewTime(timeDefault.Add(time.Minute)) + var testCases []struct { + obj monitoringv1.MonitoringCRD + expectedStatus monitoringv1.MonitoringStatus + } + + addTestCases := func(name string, newObj func(objectMeta metav1.ObjectMeta, spec monitoringv1.RulesSpec, status monitoringv1.RulesStatus) monitoringv1.MonitoringCRD) { + testCases = append(testCases, []struct { + obj monitoringv1.MonitoringCRD + expectedStatus monitoringv1.MonitoringStatus + }{ + { + obj: newObj( + metav1.ObjectMeta{ + Name: fmt.Sprintf("invalid-%s-no-condition", name), + Generation: 2, + }, + monitoringv1.RulesSpec{ + Groups: []monitoringv1.RuleGroup{ + { + Name: "test-group", + Rules: []monitoringv1.Rule{ + { + Record: "test_record", + Expr: "test_expr{", + }, + }, + }, + }, + }, + monitoringv1.RulesStatus{}, + ), + expectedStatus: monitoringv1.MonitoringStatus{ + ObservedGeneration: 2, + Conditions: []monitoringv1.MonitoringCondition{ + { + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionFalse, + LastUpdateTime: timeAfter, + LastTransitionTime: timeAfter, + Message: "generating rule config failed", + }, + }, + }, + }, + { + obj: newObj( + metav1.ObjectMeta{ + Name: fmt.Sprintf("invalid-%s-outdated-condition", name), + Generation: 2, + }, + monitoringv1.RulesSpec{ + Groups: []monitoringv1.RuleGroup{ + { + Name: "test-group", + Rules: []monitoringv1.Rule{ + { + Record: "test_record", + Expr: "test_expr{", + }, + }, + }, + }, + }, + monitoringv1.RulesStatus{ + MonitoringStatus: monitoringv1.MonitoringStatus{ + ObservedGeneration: 1, + Conditions: []monitoringv1.MonitoringCondition{ + { + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionTrue, + LastUpdateTime: timeDefault, + LastTransitionTime: timeDefault, + }, + }, + }, + }, + ), + expectedStatus: monitoringv1.MonitoringStatus{ + ObservedGeneration: 2, + Conditions: []monitoringv1.MonitoringCondition{ + { + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionFalse, + LastUpdateTime: timeAfter, + LastTransitionTime: timeAfter, + Message: "generating rule config failed", + }, + }, + }, + }, + { + obj: newObj( + metav1.ObjectMeta{ + Name: fmt.Sprintf("invalid-%s-correct-condition", name), + Generation: 2, + }, + monitoringv1.RulesSpec{ + Groups: []monitoringv1.RuleGroup{ + { + Name: "test-group", + Rules: []monitoringv1.Rule{ + { + Record: "test_record", + Expr: "test_expr{", + }, + }, + }, + }, + }, + monitoringv1.RulesStatus{ + MonitoringStatus: monitoringv1.MonitoringStatus{ + ObservedGeneration: 1, + Conditions: []monitoringv1.MonitoringCondition{ + { + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionFalse, + LastUpdateTime: timeDefault, + LastTransitionTime: timeDefault, + }, + }, + }, + }, + ), + expectedStatus: monitoringv1.MonitoringStatus{ + ObservedGeneration: 2, + Conditions: []monitoringv1.MonitoringCondition{ + { + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionFalse, + LastUpdateTime: timeAfter, + LastTransitionTime: timeDefault, + Message: "generating rule config failed", + }, + }, + }, + }, + { + obj: newObj( + metav1.ObjectMeta{ + Name: fmt.Sprintf("valid-%s-no-condition", name), + Generation: 1, + }, + monitoringv1.RulesSpec{ + Groups: []monitoringv1.RuleGroup{ + { + Name: "test-group", + Rules: []monitoringv1.Rule{ + { + Record: "test_record", + Expr: "test_expr", + }, + }, + }, + }, + }, + monitoringv1.RulesStatus{}, + ), + expectedStatus: monitoringv1.MonitoringStatus{ + ObservedGeneration: 1, + Conditions: []monitoringv1.MonitoringCondition{ + { + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionTrue, + LastUpdateTime: timeAfter, + LastTransitionTime: timeAfter, + }, + }, + }, + }, + { + obj: newObj( + metav1.ObjectMeta{ + Name: fmt.Sprintf("valid-%s-outdated-condition", name), + Generation: 2, + }, + monitoringv1.RulesSpec{ + Groups: []monitoringv1.RuleGroup{ + { + Name: "test-group", + Rules: []monitoringv1.Rule{ + { + Record: "test_record", + Expr: "test_expr", + }, + }, + }, + }, + }, + monitoringv1.RulesStatus{ + MonitoringStatus: monitoringv1.MonitoringStatus{ + ObservedGeneration: 1, + Conditions: []monitoringv1.MonitoringCondition{ + { + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionFalse, + LastUpdateTime: timeDefault, + LastTransitionTime: timeDefault, + }, + }, + }, + }, + ), + expectedStatus: monitoringv1.MonitoringStatus{ + ObservedGeneration: 2, + Conditions: []monitoringv1.MonitoringCondition{ + { + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionTrue, + LastUpdateTime: timeAfter, + LastTransitionTime: timeAfter, + }, + }, + }, + }, + { + obj: newObj( + metav1.ObjectMeta{ + Name: fmt.Sprintf("valid-%s-correct-condition", name), + Generation: 2, + }, + monitoringv1.RulesSpec{ + Groups: []monitoringv1.RuleGroup{ + { + Name: "test-group", + Rules: []monitoringv1.Rule{ + { + Record: "test_record", + Expr: "test_expr", + }, + }, + }, + }, + }, + monitoringv1.RulesStatus{ + MonitoringStatus: monitoringv1.MonitoringStatus{ + ObservedGeneration: 1, + Conditions: []monitoringv1.MonitoringCondition{ + { + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionTrue, + LastUpdateTime: timeDefault, + LastTransitionTime: timeDefault, + }, + }, + }, + }, + ), + expectedStatus: monitoringv1.MonitoringStatus{ + ObservedGeneration: 2, + Conditions: []monitoringv1.MonitoringCondition{ + { + Type: monitoringv1.ConfigurationCreateSuccess, + Status: corev1.ConditionTrue, + LastUpdateTime: timeAfter, + LastTransitionTime: timeDefault, + }, + }, + }, + }, + }...) + } + addTestCases("namespaced-rule", func(objectMeta metav1.ObjectMeta, spec monitoringv1.RulesSpec, status monitoringv1.RulesStatus) monitoringv1.MonitoringCRD { + return &monitoringv1.Rules{ + ObjectMeta: objectMeta, + Spec: spec, + Status: status, + } + }) + addTestCases("cluster-rule", func(objectMeta metav1.ObjectMeta, spec monitoringv1.RulesSpec, status monitoringv1.RulesStatus) monitoringv1.MonitoringCRD { + return &monitoringv1.ClusterRules{ + ObjectMeta: objectMeta, + Spec: spec, + Status: status, + } + }) + addTestCases("global-rule", func(objectMeta metav1.ObjectMeta, spec monitoringv1.RulesSpec, status monitoringv1.RulesStatus) monitoringv1.MonitoringCRD { + return &monitoringv1.GlobalRules{ + ObjectMeta: objectMeta, + Spec: spec, + Status: status, + } + }) + + objs := make([]client.Object, 0, len(testCases)) + for _, tc := range testCases { + objs = append(objs, tc.obj) + } + kubeClient := newFakeClientBuilder(). + WithObjects(objs...). + Build() + + ctx := context.Background() + r := rulesReconciler{ + client: kubeClient, + } + + if err := r.ensureRuleConfigs(ctx, "", "", "", monitoringv1.CompressionNone); err != nil { + t.Fatal("ensure rules configs:", err) + } + + for _, tc := range testCases { + t.Run(tc.obj.GetName(), func(t *testing.T) { + objectKey := client.ObjectKeyFromObject(tc.obj) + if err := kubeClient.Get(ctx, objectKey, tc.obj); err != nil { + t.Fatal("get obj:", err) + } + + status := tc.obj.GetMonitoringStatus() + if len(status.Conditions) != 1 { + t.Fatalf("invalid %q conditions amount, expected 1 but got %d", objectKey, len(status.Conditions)) + } + condition := &status.Conditions[0] + // If time changed, normalize to the "after time", since we don't mock the process time. + if !condition.LastTransitionTime.Equal(&timeDefault) { + condition.LastTransitionTime = timeAfter + } + if !condition.LastUpdateTime.Equal(&timeDefault) { + condition.LastUpdateTime = timeAfter + } + // The message is good enough. Don't need reason. + condition.Reason = "" + + if diff := cmp.Diff(&tc.expectedStatus, status); diff != "" { + t.Errorf("expected %q condition (-want, +got): %s", objectKey, diff) + } + }) + } +} + func TestScaleRuleConsumers(t *testing.T) { var alertmanagerReplicas int32 alertManager := appsv1.StatefulSet{