Skip to content

Commit 9eb596e

Browse files
committed
Remove stale remediated condition when in-sync
Remediation can roll back to a version that matches with the next good config. In such situation, release will be in-sync and no action will be performed. The status conditions will continue to show Remediated=True and Released=False. Check and remove stale Remediated condition and add a Released=True condition with message constructed from the latest release. Introduce replaceCondition() to replaces target condition with a replacement condition, retaining the transition time. This helps ensure that the last transition time of releases don't change when a release is marked from remediated to released. Signed-off-by: Sunny <darkowlzz@protonmail.com>
1 parent fe8569b commit 9eb596e

File tree

2 files changed

+213
-9
lines changed

2 files changed

+213
-9
lines changed

internal/reconcile/atomic_release.go

+49
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
"helm.sh/helm/v3/pkg/kube"
2727
corev1 "k8s.io/api/core/v1"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2829
"k8s.io/client-go/tools/record"
2930
ctrl "sigs.k8s.io/controller-runtime"
3031

@@ -315,6 +316,18 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state
315316
return NewUpgrade(r.configFactory, r.eventRecorder), nil
316317
}
317318

319+
// Since the release is in-sync, remove any remediated condition if
320+
// present and replace it with upgrade succeeded condition.
321+
// This can happen when the current release, which is the result of a
322+
// rollback remediation, matches the new desired configuration due to
323+
// having the same chart version and values. As a result, we are already
324+
// in-sync without performing a release action.
325+
if conditions.IsTrue(req.Object, v2.RemediatedCondition) {
326+
cur := req.Object.Status.History.Latest()
327+
msg := fmt.Sprintf(fmtUpgradeSuccess, cur.FullReleaseName(), cur.VersionedChartName())
328+
replaceCondition(req.Object, v2.RemediatedCondition, v2.ReleasedCondition, v2.UpgradeSucceededReason, msg, metav1.ConditionTrue)
329+
}
330+
318331
return nil, nil
319332
case ReleaseStatusLocked:
320333
log.Info(msgWithReason("release locked", state.Reason))
@@ -378,6 +391,21 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state
378391
return nil, nil
379392
case ReleaseStatusUntested:
380393
log.Info(msgWithReason("release has not been tested", state.Reason))
394+
395+
// Since an untested release indicates that the release is already
396+
// in-sync, remove any remediated condition if present and replace it
397+
// with upgrade succeeded condition.
398+
// This can happen when an untested current release, which is the result
399+
// of a rollback remediation, matches the new desired configuration due
400+
// to having the same chart version and values, and has test enabled. As
401+
// a result, we are already in-sync without performing a release action,
402+
// the existing release needs to undergo testing.
403+
if conditions.IsTrue(req.Object, v2.RemediatedCondition) {
404+
cur := req.Object.Status.History.Latest()
405+
msg := fmt.Sprintf(fmtUpgradeSuccess, cur.FullReleaseName(), cur.VersionedChartName())
406+
replaceCondition(req.Object, v2.RemediatedCondition, v2.ReleasedCondition, v2.UpgradeSucceededReason, msg, metav1.ConditionTrue)
407+
}
408+
381409
return NewTest(r.configFactory, r.eventRecorder), nil
382410
case ReleaseStatusFailed:
383411
log.Info(msgWithReason("release is in a failed state", state.Reason))
@@ -491,3 +519,24 @@ func timeoutForAction(action ActionReconciler, obj *v2.HelmRelease) time.Duratio
491519
return obj.GetTimeout().Duration
492520
}
493521
}
522+
523+
// replaceCondition replaces existing target condition with replacement
524+
// condition, if present, for the given values, retaining the
525+
// LastTransitionTime.
526+
func replaceCondition(obj *v2.HelmRelease, target string, replacement string, reason string, msg string, status metav1.ConditionStatus) {
527+
c := conditions.Get(obj, target)
528+
if c != nil {
529+
// Remove any existing replacement condition to retain the
530+
// LastTransitionTime set here. If the state of the new condition
531+
// changes an existing condition, the LastTransitionTime is updated to
532+
// the current time.
533+
// Refer https://github.com/fluxcd/pkg/blob/runtime/v0.43.0/runtime/conditions/setter.go#L54-L55.
534+
conditions.Delete(obj, replacement)
535+
c.Status = status
536+
c.Type = replacement
537+
c.Reason = reason
538+
c.Message = msg
539+
conditions.Set(obj, c)
540+
conditions.Delete(obj, target)
541+
}
542+
}

internal/reconcile/atomic_release_test.go

+164-9
Original file line numberDiff line numberDiff line change
@@ -1015,15 +1015,16 @@ func TestAtomicRelease_Reconcile_Scenarios(t *testing.T) {
10151015

10161016
func TestAtomicRelease_actionForState(t *testing.T) {
10171017
tests := []struct {
1018-
name string
1019-
releases []*helmrelease.Release
1020-
annotations map[string]string
1021-
spec func(spec *v2.HelmReleaseSpec)
1022-
status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
1023-
state ReleaseState
1024-
want ActionReconciler
1025-
wantEvent *corev1.Event
1026-
wantErr error
1018+
name string
1019+
releases []*helmrelease.Release
1020+
annotations map[string]string
1021+
spec func(spec *v2.HelmReleaseSpec)
1022+
status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
1023+
state ReleaseState
1024+
want ActionReconciler
1025+
wantEvent *corev1.Event
1026+
wantErr error
1027+
assertConditions []metav1.Condition
10271028
}{
10281029
{
10291030
name: "in-sync release does not trigger any action",
@@ -1053,6 +1054,25 @@ func TestAtomicRelease_actionForState(t *testing.T) {
10531054
},
10541055
want: &Upgrade{},
10551056
},
1057+
{
1058+
name: "in-sync release with stale remediated condition",
1059+
status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
1060+
return v2.HelmReleaseStatus{
1061+
History: v2.Snapshots{
1062+
{Version: 1},
1063+
},
1064+
Conditions: []metav1.Condition{
1065+
*conditions.FalseCondition(v2.ReleasedCondition, v2.UpgradeFailedReason, "upgrade failed"),
1066+
*conditions.TrueCondition(v2.RemediatedCondition, v2.RollbackSucceededReason, "rolled back"),
1067+
},
1068+
}
1069+
},
1070+
state: ReleaseState{Status: ReleaseStatusInSync},
1071+
want: nil,
1072+
assertConditions: []metav1.Condition{
1073+
*conditions.TrueCondition(v2.ReleasedCondition, v2.UpgradeSucceededReason, "upgrade succeeded"),
1074+
},
1075+
},
10561076
{
10571077
name: "locked release triggers unlock action",
10581078
state: ReleaseState{Status: ReleaseStatusLocked},
@@ -1245,6 +1265,25 @@ func TestAtomicRelease_actionForState(t *testing.T) {
12451265
state: ReleaseState{Status: ReleaseStatusUntested},
12461266
want: &Test{},
12471267
},
1268+
{
1269+
name: "untested release with stale remediated condition",
1270+
status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
1271+
return v2.HelmReleaseStatus{
1272+
History: v2.Snapshots{
1273+
{Version: 1},
1274+
},
1275+
Conditions: []metav1.Condition{
1276+
*conditions.FalseCondition(v2.ReleasedCondition, v2.UpgradeFailedReason, "upgrade failed"),
1277+
*conditions.TrueCondition(v2.RemediatedCondition, v2.RollbackSucceededReason, "rolled back"),
1278+
},
1279+
}
1280+
},
1281+
state: ReleaseState{Status: ReleaseStatusUntested},
1282+
want: &Test{},
1283+
assertConditions: []metav1.Condition{
1284+
*conditions.TrueCondition(v2.ReleasedCondition, v2.UpgradeSucceededReason, "upgrade succeeded"),
1285+
},
1286+
},
12481287
{
12491288
name: "failed release without active remediation triggers upgrade",
12501289
state: ReleaseState{Status: ReleaseStatusFailed},
@@ -1513,6 +1552,122 @@ func TestAtomicRelease_actionForState(t *testing.T) {
15131552
} else {
15141553
g.Expect(recorder.GetEvents()).To(BeEmpty())
15151554
}
1555+
1556+
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
1557+
})
1558+
}
1559+
}
1560+
1561+
func Test_replaceCondition(t *testing.T) {
1562+
g := NewWithT(t)
1563+
timestamp, err := time.Parse(time.UnixDate, "Wed Feb 25 11:06:39 GMT 2015")
1564+
g.Expect(err).ToNot(HaveOccurred())
1565+
1566+
tests := []struct {
1567+
name string
1568+
conditions []metav1.Condition
1569+
target string
1570+
replacement string
1571+
wantConditions []metav1.Condition
1572+
}{
1573+
{
1574+
name: "both conditions exist",
1575+
conditions: []metav1.Condition{
1576+
{
1577+
Type: v2.ReleasedCondition,
1578+
Status: metav1.ConditionFalse,
1579+
Reason: v2.UpgradeFailedReason,
1580+
Message: "upgrade failed",
1581+
ObservedGeneration: 1,
1582+
LastTransitionTime: metav1.NewTime(timestamp),
1583+
},
1584+
{
1585+
Type: v2.RemediatedCondition,
1586+
Status: metav1.ConditionTrue,
1587+
Reason: v2.RollbackSucceededReason,
1588+
Message: "rollback",
1589+
ObservedGeneration: 1,
1590+
LastTransitionTime: metav1.NewTime(timestamp),
1591+
},
1592+
},
1593+
target: v2.RemediatedCondition,
1594+
replacement: v2.ReleasedCondition,
1595+
wantConditions: []metav1.Condition{
1596+
{
1597+
Type: v2.ReleasedCondition,
1598+
Status: metav1.ConditionTrue,
1599+
Reason: v2.UpgradeSucceededReason,
1600+
Message: "foo",
1601+
ObservedGeneration: 1,
1602+
LastTransitionTime: metav1.NewTime(timestamp),
1603+
},
1604+
},
1605+
},
1606+
{
1607+
name: "no existing replacement condition",
1608+
conditions: []metav1.Condition{
1609+
{
1610+
Type: v2.RemediatedCondition,
1611+
Status: metav1.ConditionTrue,
1612+
Reason: v2.RollbackSucceededReason,
1613+
Message: "rollback",
1614+
ObservedGeneration: 1,
1615+
LastTransitionTime: metav1.NewTime(timestamp),
1616+
},
1617+
},
1618+
target: v2.RemediatedCondition,
1619+
replacement: v2.ReleasedCondition,
1620+
wantConditions: []metav1.Condition{
1621+
{
1622+
Type: v2.ReleasedCondition,
1623+
Status: metav1.ConditionTrue,
1624+
Reason: v2.UpgradeSucceededReason,
1625+
Message: "foo",
1626+
ObservedGeneration: 1,
1627+
LastTransitionTime: metav1.NewTime(timestamp),
1628+
},
1629+
},
1630+
},
1631+
{
1632+
name: "no existing target condition",
1633+
conditions: []metav1.Condition{
1634+
{
1635+
Type: v2.ReleasedCondition,
1636+
Status: metav1.ConditionFalse,
1637+
Reason: v2.UpgradeFailedReason,
1638+
Message: "upgrade failed",
1639+
ObservedGeneration: 1,
1640+
LastTransitionTime: metav1.NewTime(timestamp),
1641+
},
1642+
},
1643+
target: v2.RemediatedCondition,
1644+
replacement: v2.ReleasedCondition,
1645+
wantConditions: []metav1.Condition{
1646+
{
1647+
Type: v2.ReleasedCondition,
1648+
Status: metav1.ConditionFalse,
1649+
Reason: v2.UpgradeFailedReason,
1650+
Message: "upgrade failed",
1651+
ObservedGeneration: 1,
1652+
LastTransitionTime: metav1.NewTime(timestamp),
1653+
},
1654+
},
1655+
},
1656+
{
1657+
name: "no existing target and replacement conditions",
1658+
target: v2.RemediatedCondition,
1659+
replacement: v2.ReleasedCondition,
1660+
},
1661+
}
1662+
for _, tt := range tests {
1663+
t.Run(tt.name, func(t *testing.T) {
1664+
g := NewWithT(t)
1665+
1666+
obj := &v2.HelmRelease{}
1667+
obj.Generation = 1
1668+
obj.Status.Conditions = tt.conditions
1669+
replaceCondition(obj, tt.target, tt.replacement, v2.UpgradeSucceededReason, "foo", metav1.ConditionTrue)
1670+
g.Expect(obj.Status.Conditions).To(Equal(tt.wantConditions))
15161671
})
15171672
}
15181673
}

0 commit comments

Comments
 (0)