Merge pull request #977 from fluxcd/backport-972-to-release/v1.0.x

[release/v1.0.x] PostRenderersDigest observation improvements
This commit is contained in:
Stefan Prodan 2024-05-09 15:31:52 +03:00 committed by GitHub
commit 999b855107
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 318 additions and 126 deletions

View File

@ -38,7 +38,9 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/diff"
"github.com/fluxcd/helm-controller/internal/digest"
interrors "github.com/fluxcd/helm-controller/internal/errors"
"github.com/fluxcd/helm-controller/internal/postrender"
)
// OwnedConditions is a list of Condition types owned by the HelmRelease object.
@ -210,6 +212,15 @@ func (r *AtomicRelease) Reconcile(ctx context.Context, req *Request) error {
// written to Ready.
summarize(req)
// remove stale post-renderers digest on successful reconciliation.
if conditions.IsReady(req.Object) {
req.Object.Status.ObservedPostRenderersDigest = ""
if req.Object.Spec.PostRenderers != nil {
// Update the post-renderers digest if the post-renderers exist.
req.Object.Status.ObservedPostRenderersDigest = postrender.Digest(digest.Canonical, req.Object.Spec.PostRenderers).String()
}
}
return nil
}

View File

@ -43,7 +43,9 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/helm-controller/internal/kube"
"github.com/fluxcd/helm-controller/internal/postrender"
"github.com/fluxcd/helm-controller/internal/release"
"github.com/fluxcd/helm-controller/internal/testutil"
)
@ -1067,6 +1069,229 @@ func TestAtomicRelease_Reconcile_Scenarios(t *testing.T) {
}
}
func TestAtomicRelease_Reconcile_PostRenderers_Scenarios(t *testing.T) {
tests := []struct {
name string
releases func(namespace string) []*helmrelease.Release
spec func(spec *v2.HelmReleaseSpec)
values map[string]interface{}
status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
wantDigest string
wantReleaseAction v2.ReleaseAction
}{
{
name: "addition of post renderers",
releases: func(namespace string) []*helmrelease.Release {
return []*helmrelease.Release{
testutil.BuildRelease(&helmrelease.MockReleaseOptions{
Name: mockReleaseName,
Namespace: namespace,
Version: 1,
Status: helmrelease.StatusDeployed,
Chart: testutil.BuildChart(),
}, testutil.ReleaseWithConfig(nil)),
}
},
spec: func(spec *v2.HelmReleaseSpec) {
spec.PostRenderers = postRenderers
},
status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
return v2.HelmReleaseStatus{
History: v2.Snapshots{
release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
},
Conditions: []metav1.Condition{
{
Type: meta.ReadyCondition,
Status: metav1.ConditionTrue,
ObservedGeneration: 1,
},
},
}
},
wantDigest: postrender.Digest(digest.Canonical, postRenderers).String(),
wantReleaseAction: v2.ReleaseActionUpgrade,
},
{
name: "config change and addition of post renderers",
releases: func(namespace string) []*helmrelease.Release {
return []*helmrelease.Release{
testutil.BuildRelease(&helmrelease.MockReleaseOptions{
Name: mockReleaseName,
Namespace: namespace,
Version: 1,
Status: helmrelease.StatusDeployed,
Chart: testutil.BuildChart(),
}, testutil.ReleaseWithConfig(nil)),
}
},
spec: func(spec *v2.HelmReleaseSpec) {
spec.PostRenderers = postRenderers
},
status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
return v2.HelmReleaseStatus{
History: v2.Snapshots{
release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
},
Conditions: []metav1.Condition{
{
Type: meta.ReadyCondition,
Status: metav1.ConditionTrue,
ObservedGeneration: 1,
},
},
}
},
values: map[string]interface{}{"foo": "baz"},
wantDigest: postrender.Digest(digest.Canonical, postRenderers).String(),
wantReleaseAction: v2.ReleaseActionUpgrade,
},
{
name: "existing and new post renderers value",
releases: func(namespace string) []*helmrelease.Release {
return []*helmrelease.Release{
testutil.BuildRelease(&helmrelease.MockReleaseOptions{
Name: mockReleaseName,
Namespace: namespace,
Version: 1,
Status: helmrelease.StatusDeployed,
Chart: testutil.BuildChart(),
}, testutil.ReleaseWithConfig(nil)),
}
},
spec: func(spec *v2.HelmReleaseSpec) {
spec.PostRenderers = postRenderers2
},
status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
return v2.HelmReleaseStatus{
History: v2.Snapshots{
release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
},
Conditions: []metav1.Condition{
{
Type: meta.ReadyCondition,
Status: metav1.ConditionTrue,
ObservedGeneration: 1,
},
},
ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(),
}
},
wantDigest: postrender.Digest(digest.Canonical, postRenderers2).String(),
wantReleaseAction: v2.ReleaseActionUpgrade,
},
{
name: "post renderers mismatch remains in sync for processed config",
releases: func(namespace string) []*helmrelease.Release {
return []*helmrelease.Release{
testutil.BuildRelease(&helmrelease.MockReleaseOptions{
Name: mockReleaseName,
Namespace: namespace,
Version: 1,
Status: helmrelease.StatusDeployed,
Chart: testutil.BuildChart(),
}, testutil.ReleaseWithConfig(nil)),
}
},
spec: func(spec *v2.HelmReleaseSpec) {
spec.PostRenderers = postRenderers2
},
status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
return v2.HelmReleaseStatus{
History: v2.Snapshots{
release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
},
Conditions: []metav1.Condition{
{
Type: meta.ReadyCondition,
Status: metav1.ConditionTrue,
ObservedGeneration: 2, // This is used to set processed config generation.
},
},
ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(),
}
},
wantDigest: postrender.Digest(digest.Canonical, postRenderers2).String(),
wantReleaseAction: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
namedNS, err := testEnv.CreateNamespace(context.TODO(), mockReleaseNamespace)
g.Expect(err).NotTo(HaveOccurred())
t.Cleanup(func() {
_ = testEnv.Delete(context.TODO(), namedNS)
})
releaseNamespace := namedNS.Name
releases := tt.releases(releaseNamespace)
obj := &v2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: mockReleaseName,
Namespace: releaseNamespace,
// Set a higher generation value to allow setting
// observations in previous generations.
Generation: 2,
},
Spec: v2.HelmReleaseSpec{
ReleaseName: mockReleaseName,
TargetNamespace: releaseNamespace,
StorageNamespace: releaseNamespace,
Timeout: &metav1.Duration{Duration: 100 * time.Millisecond},
},
}
if tt.spec != nil {
tt.spec(&obj.Spec)
}
if tt.status != nil {
obj.Status = tt.status(releases)
}
getter, err := RESTClientGetterFromManager(testEnv.Manager, obj.GetReleaseNamespace())
g.Expect(err).ToNot(HaveOccurred())
cfg, err := action.NewConfigFactory(getter,
action.WithStorage(action.DefaultStorageDriver, obj.GetStorageNamespace()),
)
g.Expect(err).ToNot(HaveOccurred())
store := helmstorage.Init(cfg.Driver)
for _, r := range releases {
g.Expect(store.Create(r)).To(Succeed())
}
// We use a fake client here to allow us to work with a minimal release
// object mock. As the fake client does not perform any validation.
// However, for the Helm storage driver to work, we need a real client
// which is therefore initialized separately above.
client := fake.NewClientBuilder().
WithScheme(testEnv.Scheme()).
WithObjects(obj).
WithStatusSubresource(&v2.HelmRelease{}).
Build()
patchHelper := patch.NewSerialPatcher(obj, client)
recorder := new(record.FakeRecorder)
req := &Request{
Object: obj,
Chart: testutil.BuildChart(),
Values: tt.values,
}
err = NewAtomicRelease(patchHelper, cfg, recorder, testFieldManager).Reconcile(context.TODO(), req)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(obj.Status.ObservedPostRenderersDigest).To(Equal(tt.wantDigest))
g.Expect(obj.Status.LastAttemptedReleaseAction).To(Equal(tt.wantReleaseAction))
})
}
}
func TestAtomicRelease_actionForState(t *testing.T) {
tests := []struct {
name string

View File

@ -28,8 +28,6 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/helm-controller/internal/postrender"
"github.com/fluxcd/helm-controller/internal/release"
"github.com/fluxcd/helm-controller/internal/storage"
)
@ -190,15 +188,6 @@ func summarize(req *Request) {
Message: conds[0].Message,
ObservedGeneration: req.Object.Generation,
})
// remove stale post-renderers digest
if conditions.Get(req.Object, meta.ReadyCondition).Status == metav1.ConditionTrue {
req.Object.Status.ObservedPostRenderersDigest = ""
if req.Object.Spec.PostRenderers != nil {
// Update the post-renderers digest if the post-renderers exist.
req.Object.Status.ObservedPostRenderersDigest = postrender.Digest(digest.Canonical, req.Object.Spec.PostRenderers).String()
}
}
}
// eventMessageWithLog returns an event message composed out of the given

View File

@ -31,8 +31,6 @@ import (
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/helm-controller/internal/postrender"
)
const (
@ -50,7 +48,7 @@ var (
Kind: "Deployment",
Name: "test",
},
Patch: `|-
Patch: `
apiVersion: apps/v1
kind: Deployment
metadata:
@ -73,7 +71,7 @@ var (
Kind: "Deployment",
Name: "test",
},
Patch: `|-
Patch: `
apiVersion: apps/v1
kind: Deployment
metadata:
@ -509,96 +507,6 @@ func Test_summarize(t *testing.T) {
},
},
},
{
name: "with postrender",
generation: 1,
status: v2.HelmReleaseStatus{
Conditions: []metav1.Condition{
{
Type: v2.ReleasedCondition,
Status: metav1.ConditionTrue,
Reason: v2.InstallSucceededReason,
Message: "Install complete",
ObservedGeneration: 1,
},
},
ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(),
},
spec: &v2.HelmReleaseSpec{
PostRenderers: postRenderers2,
},
expectedStatus: &v2.HelmReleaseStatus{
Conditions: []metav1.Condition{
{
Type: meta.ReadyCondition,
Status: metav1.ConditionTrue,
Reason: v2.InstallSucceededReason,
Message: "Install complete",
ObservedGeneration: 1,
},
{
Type: v2.ReleasedCondition,
Status: metav1.ConditionTrue,
Reason: v2.InstallSucceededReason,
Message: "Install complete",
ObservedGeneration: 1,
},
},
ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers2).String(),
},
},
{
name: "with PostRenderers and Remediaction success",
generation: 1,
status: v2.HelmReleaseStatus{
Conditions: []metav1.Condition{
{
Type: v2.ReleasedCondition,
Status: metav1.ConditionFalse,
Reason: v2.UpgradeFailedReason,
Message: "Upgrade failure",
ObservedGeneration: 1,
},
{
Type: v2.RemediatedCondition,
Status: metav1.ConditionTrue,
Reason: v2.RollbackSucceededReason,
Message: "Uninstall complete",
ObservedGeneration: 1,
},
},
ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(),
},
spec: &v2.HelmReleaseSpec{
PostRenderers: postRenderers2,
},
expectedStatus: &v2.HelmReleaseStatus{
Conditions: []metav1.Condition{
{
Type: meta.ReadyCondition,
Status: metav1.ConditionFalse,
Reason: v2.RollbackSucceededReason,
Message: "Uninstall complete",
ObservedGeneration: 1,
},
{
Type: v2.ReleasedCondition,
Status: metav1.ConditionFalse,
Reason: v2.UpgradeFailedReason,
Message: "Upgrade failure",
ObservedGeneration: 1,
},
{
Type: v2.RemediatedCondition,
Status: metav1.ConditionTrue,
Reason: v2.RollbackSucceededReason,
Message: "Uninstall complete",
ObservedGeneration: 1,
},
},
ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(),
},
},
}
for _, tt := range tests {

View File

@ -21,6 +21,8 @@ import (
"errors"
"fmt"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
"github.com/fluxcd/pkg/ssa/jsondiff"
"helm.sh/helm/v3/pkg/kube"
helmrelease "helm.sh/helm/v3/pkg/release"
@ -143,13 +145,22 @@ func DetermineReleaseState(ctx context.Context, cfg *action.ConfigFactory, req *
}
}
// Verify if postrender digest has changed
// Verify if postrender digest has changed if config has not been
// processed. For the processed or partially processed generation, the
// updated observation will only be reflected at the end of a successful
// reconciliation. Comparing here would result the reconciliation to
// get stuck in this check due to a mismatch forever. The value can't
// change without a new generation. Hence, compare the observed digest
// for new generations only.
ready := conditions.Get(req.Object, meta.ReadyCondition)
if ready != nil && ready.ObservedGeneration != req.Object.Generation {
var postrenderersDigest string
if req.Object.Spec.PostRenderers != nil {
postrenderersDigest = postrender.Digest(digest.Canonical, req.Object.Spec.PostRenderers).String()
}
if postrenderersDigest != req.Object.Status.ObservedPostRenderersDigest {
return ReleaseState{Status: ReleaseStatusOutOfSync, Reason: "postrender digest has changed"}, nil
return ReleaseState{Status: ReleaseStatusOutOfSync, Reason: "postrenderers digest has changed"}, nil
}
}
// For the further determination of test results, we look at the

View File

@ -27,8 +27,10 @@ import (
helmrelease "helm.sh/helm/v3/pkg/release"
helmstorage "helm.sh/helm/v3/pkg/storage"
helmdriver "helm.sh/helm/v3/pkg/storage/driver"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/ssa/jsondiff"
ssanormalize "github.com/fluxcd/pkg/ssa/normalize"
ssautil "github.com/fluxcd/pkg/ssa/utils"
@ -474,6 +476,13 @@ func Test_DetermineReleaseState(t *testing.T) {
release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
},
ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(),
Conditions: []metav1.Condition{
{
Type: meta.ReadyCondition,
Status: metav1.ConditionTrue,
ObservedGeneration: 1,
},
},
}
},
chart: testutil.BuildChart(),
@ -482,6 +491,41 @@ func Test_DetermineReleaseState(t *testing.T) {
Status: ReleaseStatusOutOfSync,
},
},
{
name: "postRenderers mismatch ignored for processed generation",
releases: []*helmrelease.Release{
testutil.BuildRelease(&helmrelease.MockReleaseOptions{
Name: mockReleaseName,
Namespace: mockReleaseNamespace,
Version: 1,
Status: helmrelease.StatusDeployed,
Chart: testutil.BuildChart(),
}, testutil.ReleaseWithConfig(map[string]interface{}{"foo": "bar"})),
},
spec: func(spec *v2.HelmReleaseSpec) {
spec.PostRenderers = postRenderers2
},
status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
return v2.HelmReleaseStatus{
History: v2.Snapshots{
release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
},
ObservedPostRenderersDigest: postrender.Digest(digest.Canonical, postRenderers).String(),
Conditions: []metav1.Condition{
{
Type: meta.ReadyCondition,
Status: metav1.ConditionTrue,
ObservedGeneration: 2,
},
},
}
},
chart: testutil.BuildChart(),
values: map[string]interface{}{"foo": "bar"},
want: ReleaseState{
Status: ReleaseStatusInSync,
},
},
}
for _, tt := range tests {
@ -495,6 +539,10 @@ func Test_DetermineReleaseState(t *testing.T) {
StorageNamespace: mockReleaseNamespace,
},
}
// Set a non-zero generation so that old observations can be set on
// the object status.
obj.Generation = 2
if tt.spec != nil {
tt.spec(&obj.Spec)
}