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
10 changes: 10 additions & 0 deletions pkg/controller/create_incomplete_reason_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ package controller
import (
"context"
"errors"
"github.com/krateo-platformops/unstructured-runtime/pkg/tools/unstructured/condition"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -117,4 +118,13 @@ func TestCreateIncomplete_ConditionCarriesTheReason(t *testing.T) {
require.Contains(t, joined, "remove the",
"the remediation advice was lost: the message must carry BOTH what to do and why, "+
"conditions were: "+joined)

// #93: READY itself names the refusal, with the cause and the remediation. It used to be a bare
// Creating, indistinguishable from a healthy create in progress, with the cause reachable only
// on Synced.
ready := unstructuredtools.GetCondition(got, condition.TypeReady, condition.ReasonCreateOutcomeUnknown)
require.NotNil(t, ready, "Ready must be False/CreateOutcomeUnknown; conditions were: "+joined)
require.Equal(t, metav1.ConditionFalse, ready.Status)
require.Contains(t, ready.Message, reason, "Ready's message must carry the existence check's error")
require.Contains(t, ready.Message, "remove the", "Ready's message must carry the remediation")
}
5 changes: 4 additions & 1 deletion pkg/controller/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,10 @@ func (c *Controller) processItem(ctx context.Context, obj interface{}) (err erro
c.recordEvent(el, event.Warning(reasonCannotInitialize, actionProcessEvent, wrapped))

err = unstructuredtools.SetConditions(el,
condition.Creating(),
// #93: its own reason, carrying the cause and the remediation. A bare Creating read
// like a healthy create in progress, while this refusal lasts until the existence
// check succeeds or a human clears the marker.
condition.CreateOutcomeUnknown(wrapped.Error()),
condition.ReconcileError(wrapped),
)
if err != nil {
Expand Down
21 changes: 21 additions & 0 deletions pkg/tools/unstructured/condition/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ const (
// PAUSED, so the value is the last one observed and is not being refreshed. Status stays True —
// a paused resource is still available, it just is not converging (core-provider#241).
ReasonReconcilePausedReady string = "ReconcilePaused"
// ReasonCreateOutcomeUnknown: a create was started and its outcome cannot be determined (the
// krateo.io/external-create-pending marker is set and the existence check failed), so the
// runtime refuses to act rather than risk creating the resource twice (unstructured-runtime#93).
// It was a bare Creating, which reads like a healthy create in progress. The message carries the
// existence check's error and the remediation. It clears when a later existence check succeeds
// (retried on every resync; a transient cause clears by itself) or when a human, having verified
// the external state, removes the marker; a permanent cause (a kind with no read verb, say)
// needs the human.
ReasonCreateOutcomeUnknown string = "CreateOutcomeUnknown"
)

// Reasons a resource is or is not synced.
Expand Down Expand Up @@ -188,6 +197,18 @@ func ObserveFailed(message string) metav1.Condition {
}
}

// CreateOutcomeUnknown is Ready=False/CreateOutcomeUnknown with message: see
// ReasonCreateOutcomeUnknown.
func CreateOutcomeUnknown(message string) metav1.Condition {
return metav1.Condition{
Type: TypeReady,
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonCreateOutcomeUnknown,
Message: message,
}
}

func FailWithReason(reason string) metav1.Condition {
return metav1.Condition{
Type: TypeReady,
Expand Down
Loading