Skip to content
Open
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
99 changes: 99 additions & 0 deletions internal/cmd/controller/gitops/reconciler/gitjob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3495,3 +3495,102 @@ func TestCreateJob_AlreadyExistsIsIgnored(t *testing.T) {
t.Fatal("createJob should return false when job already exists")
}
}

// Test_PropagateAcceptedFailureToReady_SetsReadyFalse verifies that when the
// Accepted condition is False, propagateAcceptedFailureToReady forces Ready to
// False with the same message and reason. This is the core fix for
// https://github.com/rancher/fleet/issues/4865.
func Test_PropagateAcceptedFailureToReady_SetsReadyFalse(t *testing.T) {
gitrepo := &fleetv1.GitRepo{}
gitrepo.Status.Conditions = []genericcondition.GenericCondition{
{
Type: fleetv1.GitRepoAcceptedCondition,
Status: "False",
Message: "failed to look up HelmSecretNameForPaths, error: secret not found",
Reason: "Error",
},
{
Type: "Ready",
Status: "True",
},
}

propagateAcceptedFailureToReady(gitrepo)

readyCond, found := getCondition(gitrepo, "Ready")
if !found {
t.Fatal("expected Ready condition to be present")
}
if readyCond.Status != "False" {
t.Errorf("expected Ready=False, got Ready=%s", readyCond.Status)
}
if readyCond.Message != "failed to look up HelmSecretNameForPaths, error: secret not found" {
t.Errorf("unexpected Ready message: %s", readyCond.Message)
}
}

// Test_PropagateAcceptedFailureToReady_AddsReadyWhenMissing verifies that when
// Ready is absent and Accepted=False, a Ready=False condition is added.
func Test_PropagateAcceptedFailureToReady_AddsReadyWhenMissing(t *testing.T) {
gitrepo := &fleetv1.GitRepo{}
gitrepo.Status.Conditions = []genericcondition.GenericCondition{
{
Type: fleetv1.GitRepoAcceptedCondition,
Status: "False",
Message: "missing cabundle secret",
Reason: "Error",
},
}

propagateAcceptedFailureToReady(gitrepo)

readyCond, found := getCondition(gitrepo, "Ready")
if !found {
t.Fatal("expected Ready condition to be added")
}
if readyCond.Status != "False" {
t.Errorf("expected Ready=False, got Ready=%s", readyCond.Status)
}
if readyCond.Message != "missing cabundle secret" {
t.Errorf("unexpected Ready message: %s", readyCond.Message)
}
}

// Test_PropagateAcceptedFailureToReady_NoOpWhenAcceptedTrue verifies that when
// Accepted=True, the Ready condition is not changed by propagateAcceptedFailureToReady.
func Test_PropagateAcceptedFailureToReady_NoOpWhenAcceptedTrue(t *testing.T) {
gitrepo := &fleetv1.GitRepo{}
gitrepo.Status.Conditions = []genericcondition.GenericCondition{
{
Type: fleetv1.GitRepoAcceptedCondition,
Status: "True",
},
{
Type: "Ready",
Status: "True",
},
}

propagateAcceptedFailureToReady(gitrepo)

readyCond, found := getCondition(gitrepo, "Ready")
if !found {
t.Fatal("expected Ready condition to be present")
}
if readyCond.Status != "True" {
t.Errorf("expected Ready=True (unchanged), got Ready=%s", readyCond.Status)
}
}

// Test_PropagateAcceptedFailureToReady_NoOpWhenNoAccepted verifies that when
// no Accepted condition is present, propagateAcceptedFailureToReady is a no-op.
func Test_PropagateAcceptedFailureToReady_NoOpWhenNoAccepted(t *testing.T) {
gitrepo := &fleetv1.GitRepo{}
// No Accepted condition, no Ready condition - simulates fresh GitRepo with 0/0 bundles.
propagateAcceptedFailureToReady(gitrepo)

_, found := getCondition(gitrepo, "Ready")
if found {
t.Error("expected no Ready condition to be added when no Accepted condition is present")
}
}
49 changes: 49 additions & 0 deletions internal/cmd/controller/gitops/reconciler/status_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ func (r *StatusReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{}, err
}

// If the Accepted condition is False (e.g. missing secret, cabundle failure,
// restriction violation), propagate that failure to the Ready condition.
// Without this, a GitRepo with no bundles (because the job never ran) would
// report Ready=True because the empty bundle summary satisfies 0/0 == ready.
propagateAcceptedFailureToReady(gitrepo)

if err := r.updateStatus(ctx, orig, gitrepo); err != nil {
logger.Error(err, "Reconcile failed update to git repo status", "status", gitrepo.Status)
return ctrl.Result{RequeueAfter: durations.GitRepoStatusDelay}, nil
Expand Down Expand Up @@ -247,6 +253,49 @@ bundles:
return nil
}

// propagateAcceptedFailureToReady ensures the Ready condition reflects an
// Accepted=False error. When a pre-job error (missing secret, cabundle failure,
// restriction violation) prevents any bundle from being created, the bundle
// summary is 0/0, which would normally resolve to Ready=True. By copying the
// Accepted condition's message onto Ready we surface the real error.
func propagateAcceptedFailureToReady(gitrepo *fleet.GitRepo) {
var acceptedMsg string
var acceptedReason string
acceptedFalse := false
for _, c := range gitrepo.Status.Conditions {
if c.Type == fleet.GitRepoAcceptedCondition && c.Status == v1.ConditionFalse {
acceptedFalse = true
acceptedMsg = c.Message
acceptedReason = c.Reason
break
}
}
if !acceptedFalse {
return
}

found := false
newConditions := make([]genericcondition.GenericCondition, 0, len(gitrepo.Status.Conditions))
for _, c := range gitrepo.Status.Conditions {
if c.Type == string(fleet.Ready) {
c.Status = v1.ConditionFalse
c.Message = acceptedMsg
c.Reason = acceptedReason
found = true
}
newConditions = append(newConditions, c)
Comment on lines +279 to +286

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure this could introduce an undesiredLastUpdateTime update.

Let's say the first iteration of the status reconciler computes the desired state and, because there are no Bundles it considers Ready should be true.
Later this new function propagates the message and reason from the Accepted condition and status is now False.
That's fine.

The problem I see (I would need to confirm testing it myself) is that for the next reconcile it re-checks again, there are no Bundles and the Ready condition is found as False.
When that happens, wrangler updates the LastUpdateTime when it sets the condition back to True.
Later the new function only preserves the message and reason, so the LastUpdateTime has been updated (when it shouldn't).

I think the tests are not exercising this particular scenario. I think it should be also covered


Also, what happens if the Ready condition was already False?
We would be overwritting unconditionally the message and reason to the Accepted values, right?

}
if !found {
newConditions = append(newConditions, genericcondition.GenericCondition{
Type: string(fleet.Ready),
Status: v1.ConditionFalse,
Message: acceptedMsg,
Reason: acceptedReason,
})
}
gitrepo.Status.Conditions = newConditions

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Doesn't this invalidate/remove all other conditions that may be existing on this object?
I also expect the Ready condition to be a summary of other conditions, so maybe a good approach for this state is to introduce a new one?

Not that familiar with the API to judge, but I feel like it would be a more clean approach.

}

type forcedDelayingSource[R comparable] struct {
source.TypedSource[R]
delay time.Duration
Expand Down