diff --git a/api/v1/conditions.go b/api/v1/conditions.go index b3eb01fef..1874fdc54 100644 --- a/api/v1/conditions.go +++ b/api/v1/conditions.go @@ -24,6 +24,7 @@ const ( ReasonCriteriaNotMatched = "CriteriaNotMatched" ReasonCriteriaExpressionError = "CriteriaExpressionError" ReasonCriteriaMatched = "CriteriaMatched" + ReasonProgressing = "InProgress" ) // ConditionedObject defines interface for objects that can have conditions diff --git a/api/v1/criteria_conditions.go b/api/v1/criteria_conditions.go new file mode 100644 index 000000000..17212d830 --- /dev/null +++ b/api/v1/criteria_conditions.go @@ -0,0 +1,103 @@ +package v1 + +import ( + "fmt" + "sort" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ConditionedResource is a ConditionedObject that also exposes generation tracking for status updates. +// +k8s:deepcopy-gen=false +type ConditionedResource interface { + ConditionedObject + GetGeneration() int64 +} + +// FormatClusterLabels returns a stable, human-readable summary of cluster labels for status messages. +// Keys are sorted to make comparisons deterministic. +func FormatClusterLabels(labels map[string]string) string { + if len(labels) == 0 { + return "(none)" + } + keys := make([]string, 0, len(labels)) + for k := range labels { + keys = append(keys, k) + } + sort.Strings(keys) + parts := make([]string, 0, len(keys)) + for _, k := range keys { + parts = append(parts, fmt.Sprintf("%s=%s", k, labels[k])) + } + return strings.Join(parts, ", ") +} + +// NewCriteriaCondition constructs a metav1.Condition encapsulating the outcome of criteria expression evaluation. +// The condition uses the shared Reason constants defined in conditions.go for consistent signaling across CRDs. +func NewCriteriaCondition(conditionType string, generation int64, matched bool, expression string, evaluationErr error, clusterLabels map[string]string) metav1.Condition { + labelSummary := FormatClusterLabels(clusterLabels) + + condition := metav1.Condition{ + Type: conditionType, + ObservedGeneration: generation, + LastTransitionTime: metav1.Now(), + } + + switch { + case expression == "": + condition.Status = metav1.ConditionTrue + condition.Reason = ReasonCriteriaMatched + condition.Message = "No criteria expression configured; defaulting to match" + case evaluationErr != nil: + condition.Status = metav1.ConditionFalse + condition.Reason = ReasonCriteriaExpressionError + condition.Message = fmt.Sprintf("Criteria expression %q failed: %v (cluster labels: %s)", expression, evaluationErr, labelSummary) + case matched: + condition.Status = metav1.ConditionTrue + condition.Reason = ReasonCriteriaMatched + condition.Message = fmt.Sprintf("Criteria expression %q matched cluster labels: %s", expression, labelSummary) + default: + condition.Status = metav1.ConditionFalse + condition.Reason = ReasonCriteriaNotMatched + condition.Message = fmt.Sprintf("Criteria expression %q evaluated to false for cluster labels: %s", expression, labelSummary) + } + + return condition +} + +// NewOwnerCondition constructs a metav1.Condition for owner-type signaling (execution success/failure) with sensible defaults. +func NewOwnerCondition(conditionType string, generation int64, status metav1.ConditionStatus, reason, message string) metav1.Condition { + condition := metav1.Condition{ + Type: conditionType, + Status: status, + Message: message, + ObservedGeneration: generation, + LastTransitionTime: metav1.Now(), + } + + if reason != "" { + condition.Reason = reason + } else { + condition.Reason = defaultOwnerReason(status) + } + + return condition +} + +// SetOwnerCondition applies an owner-style condition to the provided resource using the supplied parameters. +func SetOwnerCondition(resource ConditionedResource, conditionType string, status metav1.ConditionStatus, reason, message string) { + condition := NewOwnerCondition(conditionType, resource.GetGeneration(), status, reason, message) + resource.SetCondition(condition) +} + +func defaultOwnerReason(status metav1.ConditionStatus) string { + switch status { + case metav1.ConditionTrue: + return ConditionCompleted + case metav1.ConditionFalse: + return ConditionFailed + default: + return ReasonProgressing + } +} diff --git a/api/v1/criteria_conditions_test.go b/api/v1/criteria_conditions_test.go new file mode 100644 index 000000000..9384593de --- /dev/null +++ b/api/v1/criteria_conditions_test.go @@ -0,0 +1,57 @@ +package v1 + +import ( + "errors" + "testing" + + meta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stretchr/testify/require" +) + +func TestFormatClusterLabels(t *testing.T) { + labels := map[string]string{"environment": "prod", "region": "eastus"} + formatted := FormatClusterLabels(labels) + require.Equal(t, "environment=prod, region=eastus", formatted) + + require.Equal(t, "(none)", FormatClusterLabels(nil)) +} + +func TestNewCriteriaCondition(t *testing.T) { + labels := map[string]string{"region": "westus"} + + cond := NewCriteriaCondition("test", 7, true, "region == 'westus'", nil, labels) + require.Equal(t, metav1.ConditionTrue, cond.Status) + require.Equal(t, ReasonCriteriaMatched, cond.Reason) + require.Contains(t, cond.Message, "region == 'westus'") + require.Equal(t, int64(7), cond.ObservedGeneration) + + cond = NewCriteriaCondition("test", 7, false, "region == 'westus'", nil, labels) + require.Equal(t, metav1.ConditionFalse, cond.Status) + require.Equal(t, ReasonCriteriaNotMatched, cond.Reason) + + cond = NewCriteriaCondition("test", 7, false, "", nil, labels) + require.Equal(t, metav1.ConditionTrue, cond.Status) + require.Equal(t, ReasonCriteriaMatched, cond.Reason) + require.Equal(t, "No criteria expression configured; defaulting to match", cond.Message) + + cond = NewCriteriaCondition("test", 7, false, "region == 'westus'", errors.New("parse"), labels) + require.Equal(t, metav1.ConditionFalse, cond.Status) + require.Equal(t, ReasonCriteriaExpressionError, cond.Reason) + require.Contains(t, cond.Message, "parse") +} + +func TestSetOwnerConditionHelper(t *testing.T) { + fn := &Function{} + fn.Generation = 3 + + SetOwnerCondition(fn, "demo", metav1.ConditionTrue, "", "completed") + + cond := meta.FindStatusCondition(fn.Status.Conditions, "demo") + require.NotNil(t, cond) + require.Equal(t, metav1.ConditionTrue, cond.Status) + require.Equal(t, ConditionCompleted, cond.Reason) + require.Equal(t, "completed", cond.Message) + require.Equal(t, int64(3), cond.ObservedGeneration) +} diff --git a/api/v1/function_types.go b/api/v1/function_types.go index e785a452a..afdf16d36 100644 --- a/api/v1/function_types.go +++ b/api/v1/function_types.go @@ -18,7 +18,6 @@ package v1 import ( "fmt" - "sort" "strings" meta "k8s.io/apimachinery/pkg/api/meta" @@ -209,48 +208,6 @@ func (f *Function) SetDatabaseMatchCondition(matched bool, configuredDB, availab // SetCriteriaMatchCondition records the outcome of CriteriaExpression evaluation. An optional clusterLabels map may be // provided to enrich messages for debugging. Passing nil is permitted and treated as "no labels". func (f *Function) SetCriteriaMatchCondition(matched bool, expression string, err error, clusterLabels map[string]string) { - labelSummary := FormatClusterLabels(clusterLabels) - reason := ReasonCriteriaMatched - status := metav1.ConditionTrue - message := fmt.Sprintf("Criteria expression %q matched cluster labels: %s", expression, labelSummary) - if expression == "" { - reason = ReasonCriteriaMatched - message = "No criteria expression configured; defaulting to match" - } - if err != nil { - reason = ReasonCriteriaExpressionError - status = metav1.ConditionFalse - message = fmt.Sprintf("Criteria expression %q failed: %v (cluster labels: %s)", expression, err, labelSummary) - } else if !matched { - reason = ReasonCriteriaNotMatched - status = metav1.ConditionFalse - message = fmt.Sprintf("Criteria expression %q evaluated to false for cluster labels: %s", expression, labelSummary) - } - condition := metav1.Condition{ - Type: FunctionCriteriaMatch, - Status: status, - Reason: reason, - Message: message, - ObservedGeneration: f.GetGeneration(), - LastTransitionTime: metav1.Now(), - } + condition := NewCriteriaCondition(FunctionCriteriaMatch, f.GetGeneration(), matched, expression, err, clusterLabels) meta.SetStatusCondition(&f.Status.Conditions, condition) } - -// FormatClusterLabels returns a stable, human-readable summary of cluster labels for status messages. -// Keys are sorted to make comparisons deterministic. -func FormatClusterLabels(labels map[string]string) string { - if len(labels) == 0 { - return "(none)" - } - keys := make([]string, 0, len(labels)) - for k := range labels { - keys = append(keys, k) - } - sort.Strings(keys) - parts := make([]string, 0, len(keys)) - for _, k := range keys { - parts = append(parts, fmt.Sprintf("%s=%s", k, labels[k])) - } - return strings.Join(parts, ", ") -} diff --git a/api/v1/managementcommand_types.go b/api/v1/managementcommand_types.go index 6d60b23e7..9383fd9e9 100644 --- a/api/v1/managementcommand_types.go +++ b/api/v1/managementcommand_types.go @@ -5,9 +5,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. -const ManagementCommandConditionOwner = "managementcommand.adx-mon.azure.com" +const ( + ManagementCommandConditionOwner = "managementcommand.adx-mon.azure.com" + ManagementCommandCriteriaMatch = "managementcommand.adx-mon.azure.com/CriteriaMatch" + ManagementCommandExecuted = "managementcommand.adx-mon.azure.com/Executed" +) + +const ( + ReasonManagementCommandExecutionSucceeded = "ExecutionSucceeded" + ReasonManagementCommandExecutionFailed = "ExecutionFailed" + ReasonManagementCommandCriteriaError = "CriteriaEvaluationFailed" +) // ManagementCommandSpec defines the desired state of ManagementCommand type ManagementCommandSpec struct { @@ -51,20 +59,47 @@ func (m *ManagementCommand) GetCondition() *metav1.Condition { } func (m *ManagementCommand) SetCondition(c metav1.Condition) { + if c.Type == "" { + c.Type = ManagementCommandConditionOwner + } + + existing := meta.FindStatusCondition(m.Status.Conditions, c.Type) + if c.Reason == "" { + if existing != nil && existing.Reason != "" { + c.Reason = existing.Reason + } else { + c.Reason = defaultOwnerReason(c.Status) + } + } + if c.Message == "" && existing != nil { + c.Message = existing.Message + } if c.ObservedGeneration == 0 { - c.Reason = "Created" - } else { - c.Reason = "Updated" + c.ObservedGeneration = m.GetGeneration() } - if c.Status == metav1.ConditionFalse { - c.Reason = "Failed" + if c.LastTransitionTime.IsZero() { + c.LastTransitionTime = metav1.Now() } - c.ObservedGeneration = m.GetGeneration() - c.Type = ManagementCommandConditionOwner meta.SetStatusCondition(&m.Status.Conditions, c) } +// SetCriteriaMatchCondition records the criteria evaluation state for the ManagementCommand. +func (m *ManagementCommand) SetCriteriaMatchCondition(matched bool, expression string, evaluationErr error, clusterLabels map[string]string) { + condition := NewCriteriaCondition(ManagementCommandCriteriaMatch, m.GetGeneration(), matched, expression, evaluationErr, clusterLabels) + m.SetCondition(condition) +} + +// SetExecutionCondition updates both the ManagementCommandExecuted condition and the owner condition to reflect execution state. +func (m *ManagementCommand) SetExecutionCondition(status metav1.ConditionStatus, reason, message string) { + execution := NewOwnerCondition(ManagementCommandExecuted, m.GetGeneration(), status, reason, message) + m.SetCondition(execution) + + owner := execution + owner.Type = ManagementCommandConditionOwner + m.SetCondition(owner) +} + //+kubebuilder:object:root=true //+kubebuilder:subresource:status diff --git a/api/v1/managementcommand_types_test.go b/api/v1/managementcommand_types_test.go new file mode 100644 index 000000000..5e44a99eb --- /dev/null +++ b/api/v1/managementcommand_types_test.go @@ -0,0 +1,79 @@ +package v1 + +import ( + "testing" + + meta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stretchr/testify/require" +) + +func TestManagementCommandSetConditionDefaults(t *testing.T) { + cmd := &ManagementCommand{} + cmd.Generation = 4 + + cmd.SetCondition(metav1.Condition{Status: metav1.ConditionTrue}) + owner := meta.FindStatusCondition(cmd.Status.Conditions, ManagementCommandConditionOwner) + require.NotNil(t, owner) + require.Equal(t, metav1.ConditionTrue, owner.Status) + require.Equal(t, ConditionCompleted, owner.Reason) + require.Equal(t, int64(4), owner.ObservedGeneration) + + // Simulate UpdateStatus call with empty reason/message should preserve existing reason/message + ownerReason := "custom" + ownerMessage := "custom message" + cmd.SetExecutionCondition(metav1.ConditionFalse, ownerReason, ownerMessage) + + cmd.SetCondition(metav1.Condition{Status: metav1.ConditionFalse}) + owner = meta.FindStatusCondition(cmd.Status.Conditions, ManagementCommandConditionOwner) + require.Equal(t, ownerReason, owner.Reason) + require.Equal(t, ownerMessage, owner.Message) +} + +func TestManagementCommandCriteriaMatchCondition(t *testing.T) { + labels := map[string]string{"environment": "prod"} + cmd := &ManagementCommand{} + cmd.Generation = 2 + + cmd.SetCriteriaMatchCondition(true, "environment == 'prod'", nil, labels) + crit := meta.FindStatusCondition(cmd.Status.Conditions, ManagementCommandCriteriaMatch) + require.NotNil(t, crit) + require.Equal(t, metav1.ConditionTrue, crit.Status) + require.Equal(t, ReasonCriteriaMatched, crit.Reason) + + cmd.SetCriteriaMatchCondition(false, "environment == 'prod'", assertiveErr("boom"), labels) + crit = meta.FindStatusCondition(cmd.Status.Conditions, ManagementCommandCriteriaMatch) + require.NotNil(t, crit) + require.Equal(t, metav1.ConditionFalse, crit.Status) + require.Equal(t, ReasonCriteriaExpressionError, crit.Reason) + require.Contains(t, crit.Message, "boom") +} + +func TestManagementCommandExecutionCondition(t *testing.T) { + cmd := &ManagementCommand{} + cmd.Generation = 10 + + cmd.SetExecutionCondition(metav1.ConditionFalse, ReasonManagementCommandExecutionFailed, "failed") + exec := meta.FindStatusCondition(cmd.Status.Conditions, ManagementCommandExecuted) + require.NotNil(t, exec) + require.Equal(t, metav1.ConditionFalse, exec.Status) + require.Equal(t, ReasonManagementCommandExecutionFailed, exec.Reason) + require.Equal(t, "failed", exec.Message) + + owner := meta.FindStatusCondition(cmd.Status.Conditions, ManagementCommandConditionOwner) + require.NotNil(t, owner) + require.Equal(t, metav1.ConditionFalse, owner.Status) + require.Equal(t, ReasonManagementCommandExecutionFailed, owner.Reason) + require.Equal(t, "failed", owner.Message) + + cmd.SetExecutionCondition(metav1.ConditionTrue, ReasonManagementCommandExecutionSucceeded, "ok") + exec = meta.FindStatusCondition(cmd.Status.Conditions, ManagementCommandExecuted) + require.Equal(t, metav1.ConditionTrue, exec.Status) + require.Equal(t, ReasonManagementCommandExecutionSucceeded, exec.Reason) + require.Equal(t, "ok", exec.Message) + + owner = meta.FindStatusCondition(cmd.Status.Conditions, ManagementCommandConditionOwner) + require.Equal(t, metav1.ConditionTrue, owner.Status) + require.Equal(t, ReasonManagementCommandExecutionSucceeded, owner.Reason) +} diff --git a/ingestor/adx/tasks.go b/ingestor/adx/tasks.go index c0a935a23..aea3e25b9 100644 --- a/ingestor/adx/tasks.go +++ b/ingestor/adx/tasks.go @@ -19,7 +19,7 @@ import ( "github.com/Azure/adx-mon/pkg/kustoutil" "github.com/Azure/adx-mon/pkg/logger" "github.com/Azure/azure-kusto-go/kusto" - "github.com/Azure/azure-kusto-go/kusto/data/errors" + kustoerrors "github.com/Azure/azure-kusto-go/kusto/data/errors" "github.com/Azure/azure-kusto-go/kusto/kql" meta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -218,7 +218,7 @@ func (t *SyncFunctionsTask) Run(ctx context.Context) error { stmt := kql.New(".execute database script with (ThrowOnErrors=true) <| ").AddUnsafe(function.Spec.Body) if _, err := t.kustoCli.Mgmt(ctx, stmt); err != nil { parsed := kustoutil.ParseError(err) - if !errors.Retry(err) { + if !kustoerrors.Retry(err) { logger.Errorf("Permanent failure to create function %s.%s: %v", function.Spec.Database, function.Name, err) function.SetReconcileCondition(metav1.ConditionFalse, "KustoExecutionFailed", parsed) if err = t.updateKQLFunctionStatus(ctx, function, v1.PermanentFailure, err); err != nil { @@ -322,18 +322,24 @@ func (t *ManagementCommandTask) Run(ctx context.Context) error { if err := t.store.List(ctx, managementCommands, storage.FilterCompleted); err != nil { return fmt.Errorf("failed to list management commands: %w", err) } - for _, command := range managementCommands.Items { + for i := range managementCommands.Items { + command := &managementCommands.Items[i] // ManagementCommands database is optional as not all commands are scoped at the database level if command.Spec.Database != "" && command.Spec.Database != t.kustoCli.Database() { continue } - if expr := command.Spec.CriteriaExpression; expr != "" { - ok, err := celutil.EvaluateCriteriaExpression(t.ClusterLabels, expr) + expression := strings.TrimSpace(command.Spec.CriteriaExpression) + if expression == "" { + command.SetCriteriaMatchCondition(true, expression, nil, t.ClusterLabels) + } else { + ok, err := celutil.EvaluateCriteriaExpression(t.ClusterLabels, expression) if err != nil { err = fmt.Errorf("criteriaExpression evaluation failed: %w", err) logger.Errorf("ManagementCommand %s/%s criteriaExpression error: %v", command.Namespace, command.Name, err) - if updErr := t.store.UpdateStatus(ctx, &command, err); updErr != nil { + command.SetCriteriaMatchCondition(false, expression, err, t.ClusterLabels) + command.SetExecutionCondition(metav1.ConditionFalse, v1.ReasonManagementCommandCriteriaError, err.Error()) + if updErr := t.store.UpdateStatus(ctx, command, err); updErr != nil { logger.Errorf("Failed to update management command status for %s/%s: %v", command.Namespace, command.Name, updErr) } continue @@ -342,8 +348,15 @@ func (t *ManagementCommandTask) Run(ctx context.Context) error { if logger.IsDebug() { logger.Debugf("Skipping management command %s/%s due to criteriaExpression evaluating to false", command.Namespace, command.Name) } + command.SetCriteriaMatchCondition(false, expression, nil, t.ClusterLabels) + message := fmt.Sprintf("Management command skipped because criteria expression evaluated to false for cluster labels: %s", v1.FormatClusterLabels(t.ClusterLabels)) + command.SetExecutionCondition(metav1.ConditionFalse, v1.ReasonCriteriaNotMatched, message) + if updErr := t.store.UpdateStatus(ctx, command, fmt.Errorf("%s", message)); updErr != nil { + logger.Errorf("Failed to persist criteria mismatch for %s/%s: %v", command.Namespace, command.Name, updErr) + } continue } + command.SetCriteriaMatchCondition(true, expression, nil, t.ClusterLabels) } var stmt *kql.Builder @@ -353,15 +366,25 @@ func (t *ManagementCommandTask) Run(ctx context.Context) error { stmt = kql.New(".execute database script with (ThrowOnErrors = true) <|").AddUnsafe(command.Spec.Body) } if _, err := t.kustoCli.Mgmt(ctx, stmt); err != nil { - logger.Errorf("Failed to execute management command %s.%s: %v", command.Spec.Database, command.Name, err) - if err = t.store.UpdateStatus(ctx, &command, err); err != nil { - logger.Errorf("Failed to update management command status: %v", err) - } - } else { - logger.Infof("Successfully executed management command %s.%s", command.Spec.Database, command.Name) - if err := t.store.UpdateStatus(ctx, &command, nil); err != nil { - logger.Errorf("Failed to update success status: %v", err) + parsed := kustoutil.ParseError(err) + logger.Errorf("Failed to execute management command %s/%s: %v", command.Namespace, command.Name, err) + command.SetExecutionCondition(metav1.ConditionFalse, v1.ReasonManagementCommandExecutionFailed, parsed) + if updErr := t.store.UpdateStatus(ctx, command, fmt.Errorf("%s", parsed)); updErr != nil { + logger.Errorf("Failed to update management command status: %v", updErr) } + continue + } + + logger.Infof("Successfully executed management command %s.%s", command.Spec.Database, command.Name) + message := "Management command executed successfully" + if endpoint := strings.TrimSpace(t.kustoCli.Endpoint()); endpoint != "" { + message = fmt.Sprintf("Management command executed against %s", endpoint) + } else if command.Spec.Database != "" { + message = fmt.Sprintf("Management command executed against database %s", command.Spec.Database) + } + command.SetExecutionCondition(metav1.ConditionTrue, v1.ReasonManagementCommandExecutionSucceeded, message) + if err := t.store.UpdateStatus(ctx, command, nil); err != nil { + logger.Errorf("Failed to update success status: %v", err) } } diff --git a/ingestor/adx/tasks_test.go b/ingestor/adx/tasks_test.go index 91d1b6e49..71cd00ce7 100644 --- a/ingestor/adx/tasks_test.go +++ b/ingestor/adx/tasks_test.go @@ -1042,7 +1042,23 @@ func TestManagementCommandCriteriaExpression(t *testing.T) { task := NewManagementCommandsTask(handler, exec, map[string]string{"region": "westus"}) require.NoError(t, task.Run(ctx)) require.Empty(t, exec.stmts, "management command should be skipped when criteriaExpression is false") - require.Empty(t, handler.updatedObjects, "status should remain unchanged when skipping") + require.Len(t, handler.updatedObjects, 1) + cmd := handler.updatedObjects[0].(*adxmonv1.ManagementCommand) + crit := apimeta.FindStatusCondition(cmd.Status.Conditions, v1.ManagementCommandCriteriaMatch) + require.NotNil(t, crit) + require.Equal(t, metav1.ConditionFalse, crit.Status) + require.Equal(t, v1.ReasonCriteriaNotMatched, crit.Reason) + + execCond := apimeta.FindStatusCondition(cmd.Status.Conditions, v1.ManagementCommandExecuted) + require.NotNil(t, execCond) + require.Equal(t, metav1.ConditionFalse, execCond.Status) + require.Equal(t, v1.ReasonCriteriaNotMatched, execCond.Reason) + require.Contains(t, execCond.Message, "criteria expression evaluated to false") + + owner := apimeta.FindStatusCondition(cmd.Status.Conditions, v1.ManagementCommandConditionOwner) + require.NotNil(t, owner) + require.Equal(t, metav1.ConditionFalse, owner.Status) + require.Equal(t, v1.ReasonCriteriaNotMatched, owner.Reason) }) t.Run("records error when expression evaluation fails", func(t *testing.T) { @@ -1063,11 +1079,24 @@ func TestManagementCommandCriteriaExpression(t *testing.T) { require.NoError(t, task.Run(ctx)) require.Empty(t, exec.stmts, "management command should not execute when criteriaExpression errors") require.Len(t, handler.updatedObjects, 1) - cmd := handler.updatedObjects[0].(*v1.ManagementCommand) - condition := apimeta.FindStatusCondition(cmd.Status.Conditions, adxmonv1.ManagementCommandConditionOwner) - require.NotNil(t, condition) - require.Equal(t, metav1.ConditionFalse, condition.Status) - require.Contains(t, condition.Message, "criteriaExpression evaluation failed") + cmd := handler.updatedObjects[0].(*adxmonv1.ManagementCommand) + crit := apimeta.FindStatusCondition(cmd.Status.Conditions, v1.ManagementCommandCriteriaMatch) + require.NotNil(t, crit) + require.Equal(t, metav1.ConditionFalse, crit.Status) + require.Equal(t, v1.ReasonCriteriaExpressionError, crit.Reason) + require.Contains(t, crit.Message, "criteriaExpression evaluation failed") + + execCond := apimeta.FindStatusCondition(cmd.Status.Conditions, v1.ManagementCommandExecuted) + require.NotNil(t, execCond) + require.Equal(t, metav1.ConditionFalse, execCond.Status) + require.Equal(t, v1.ReasonManagementCommandCriteriaError, execCond.Reason) + require.Contains(t, execCond.Message, "criteriaExpression evaluation failed") + + owner := apimeta.FindStatusCondition(cmd.Status.Conditions, v1.ManagementCommandConditionOwner) + require.NotNil(t, owner) + require.Equal(t, metav1.ConditionFalse, owner.Status) + require.Equal(t, v1.ReasonManagementCommandCriteriaError, owner.Reason) + require.Contains(t, owner.Message, "criteriaExpression evaluation failed") }) }