diff --git a/cache/cache.go b/cache/cache.go index 842e61aa..d9290445 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -35,6 +35,16 @@ func SetupComponentCache(mgr ctrl.Manager) error { "spec.application", componentIndexFunc) } +// SetupReleaseCache adds a new index field to be able to search Releases by ReleasePlan name. +func SetupReleaseCache(mgr ctrl.Manager) error { + releaseIndexFunc := func(obj client.Object) []string { + return []string{obj.(*v1alpha1.Release).Spec.ReleasePlan} + } + + return mgr.GetCache().IndexField(context.Background(), &v1alpha1.Release{}, + "spec.releasePlan", releaseIndexFunc) +} + // SetupReleasePlanCache adds a new index field to be able to search ReleasePlans by target. func SetupReleasePlanCache(mgr ctrl.Manager) error { releasePlanIndexFunc := func(obj client.Object) []string { diff --git a/controllers/release/controller.go b/controllers/release/controller.go index ae2851a2..d6d569a2 100644 --- a/controllers/release/controller.go +++ b/controllers/release/controller.go @@ -113,6 +113,9 @@ func (c *Controller) SetupCache(mgr ctrl.Manager) error { if err := cache.SetupComponentCache(mgr); err != nil { return err } + if err := cache.SetupReleaseCache(mgr); err != nil { + return err + } // NOTE: Both the release and releaseplan controller need this ReleasePlanAdmission cache. However, it only needs to be added // once to the manager, so only one controller should add it. If it is removed here, it should be added to the ReleasePlan controller. diff --git a/loader/loader.go b/loader/loader.go index 1c42e13b..b7b309a4 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -3,6 +3,8 @@ package loader import ( "context" "fmt" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" "os" "strings" @@ -29,6 +31,7 @@ type ObjectLoader interface { GetEnterpriseContractPolicy(ctx context.Context, cli client.Client, releasePlanAdmission *v1alpha1.ReleasePlanAdmission) (*ecapiv1alpha1.EnterpriseContractPolicy, error) GetMatchingReleasePlanAdmission(ctx context.Context, cli client.Client, releasePlan *v1alpha1.ReleasePlan) (*v1alpha1.ReleasePlanAdmission, error) GetMatchingReleasePlans(ctx context.Context, cli client.Client, releasePlanAdmission *v1alpha1.ReleasePlanAdmission) (*v1alpha1.ReleasePlanList, error) + GetPreviousRelease(ctx context.Context, cli client.Client, release *v1alpha1.Release) (*v1alpha1.Release, error) GetRelease(ctx context.Context, cli client.Client, name, namespace string) (*v1alpha1.Release, error) GetRoleBindingFromReleaseStatus(ctx context.Context, cli client.Client, release *v1alpha1.Release) (*rbac.RoleBinding, error) GetReleasePipelineRun(ctx context.Context, cli client.Client, release *v1alpha1.Release) (*tektonv1.PipelineRun, error) @@ -170,6 +173,42 @@ func (l *loader) GetMatchingReleasePlans(ctx context.Context, cli client.Client, return releasePlans, nil } +// GetPreviousRelease returns the Release that was created just before the given Release. +// If no previous Release is found, a NotFound error is returned. +func (l *loader) GetPreviousRelease(ctx context.Context, cli client.Client, release *v1alpha1.Release) (*v1alpha1.Release, error) { + releases := &v1alpha1.ReleaseList{} + err := cli.List(ctx, releases, + client.InNamespace(release.Namespace), + client.MatchingFields{"spec.releasePlan": release.Spec.ReleasePlan}) + if err != nil { + return nil, err + } + + var previousRelease *v1alpha1.Release + + // Find the previous release + for i, possiblePreviousRelease := range releases.Items { + // Ignore the release passed as argument and any release created after that one + if possiblePreviousRelease.Name == release.Name || + possiblePreviousRelease.CreationTimestamp.After(release.CreationTimestamp.Time) { + continue + } + if previousRelease == nil || possiblePreviousRelease.CreationTimestamp.After(previousRelease.CreationTimestamp.Time) { + previousRelease = &releases.Items[i] + } + } + + if previousRelease == nil { + return nil, errors.NewNotFound( + schema.GroupResource{ + Group: v1alpha1.GroupVersion.Group, + Resource: release.GetObjectKind().GroupVersionKind().Kind, + }, release.Name) + } + + return previousRelease, nil +} + // GetRelease returns the Release with the given name and namespace. If the Release is not found or the Get operation // fails, an error will be returned. func (l *loader) GetRelease(ctx context.Context, cli client.Client, name, namespace string) (*v1alpha1.Release, error) { diff --git a/loader/loader_mock.go b/loader/loader_mock.go index 6ddc137a..ce90d6e1 100644 --- a/loader/loader_mock.go +++ b/loader/loader_mock.go @@ -21,6 +21,7 @@ const ( EnterpriseContractPolicyContextKey MatchedReleasePlansContextKey MatchedReleasePlanAdmissionContextKey + PreviousReleaseContextKey ProcessingResourcesContextKey ReleaseContextKey ReleasePipelineRunContextKey @@ -97,6 +98,14 @@ func (l *mockLoader) GetMatchingReleasePlans(ctx context.Context, cli client.Cli return toolkit.GetMockedResourceAndErrorFromContext(ctx, MatchedReleasePlansContextKey, &v1alpha1.ReleasePlanList{}) } +// GetPreviousRelease returns the resource and error passed as values of the context. +func (l *mockLoader) GetPreviousRelease(ctx context.Context, cli client.Client, release *v1alpha1.Release) (*v1alpha1.Release, error) { + if ctx.Value(PreviousReleaseContextKey) == nil { + return l.loader.GetPreviousRelease(ctx, cli, release) + } + return toolkit.GetMockedResourceAndErrorFromContext(ctx, PreviousReleaseContextKey, &v1alpha1.Release{}) +} + // GetRelease returns the resource and error passed as values of the context. func (l *mockLoader) GetRelease(ctx context.Context, cli client.Client, name, namespace string) (*v1alpha1.Release, error) { if ctx.Value(ReleaseContextKey) == nil { diff --git a/loader/loader_mock_test.go b/loader/loader_mock_test.go index d71353d9..b8296c3c 100644 --- a/loader/loader_mock_test.go +++ b/loader/loader_mock_test.go @@ -111,6 +111,21 @@ var _ = Describe("Release Adapter", Ordered, func() { }) }) + When("calling GetPreviousRelease", func() { + It("returns the resource and error from the context", func() { + release := &v1alpha1.Release{} + mockContext := toolkit.GetMockedContext(ctx, []toolkit.MockData{ + { + ContextKey: PreviousReleaseContextKey, + Resource: release, + }, + }) + resource, err := loader.GetPreviousRelease(mockContext, nil, release) + Expect(resource).To(Equal(release)) + Expect(err).To(BeNil()) + }) + }) + When("calling GetRelease", func() { It("returns the resource and error from the context", func() { release := &v1alpha1.Release{} diff --git a/loader/loader_test.go b/loader/loader_test.go index b7508444..19aef8f6 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strings" + "time" tektonutils "github.com/konflux-ci/release-service/tekton/utils" @@ -20,6 +21,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" ) var _ = Describe("Release Adapter", Ordered, func() { @@ -238,6 +240,83 @@ var _ = Describe("Release Adapter", Ordered, func() { }) }) + When("calling GetPreviousRelease", func() { + var newerRelease, mostRecentRelease *v1alpha1.Release + + AfterEach(func() { + k8sClient.Delete(ctx, newerRelease) + k8sClient.Delete(ctx, mostRecentRelease) + + // Wait until the releases are gone + Eventually(func() bool { + releases := &v1alpha1.ReleaseList{} + err := k8sClient.List(ctx, releases, + client.InNamespace(release.Namespace), + client.MatchingFields{"spec.releasePlan": release.Spec.ReleasePlan}) + return err == nil && len(releases.Items) == 1 + }).Should(BeTrue()) + }) + + It("returns a NotFound error if no previous release is found", func() { + returnedObject, err := loader.GetPreviousRelease(ctx, k8sClient, release) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + Expect(returnedObject).To(BeNil()) + }) + + It("returns the previous release if found", func() { + // We need a new release with a more recent creation timestamp + time.Sleep(2 * time.Second) + + newerRelease = release.DeepCopy() + newerRelease.Name = "newer-release" + newerRelease.ResourceVersion = "" + Expect(k8sClient.Create(ctx, newerRelease)).To(Succeed()) + + // Wait until the new release is cached + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: newerRelease.Name, Namespace: newerRelease.Namespace}, newerRelease) + }).Should(Succeed()) + + returnedObject, err := loader.GetPreviousRelease(ctx, k8sClient, newerRelease) + Expect(err).ToNot(HaveOccurred()) + Expect(returnedObject).ToNot(BeNil()) + Expect(returnedObject.Name).To(Equal(release.Name)) + }) + + It("returns the previous release if multiple releases are found", func() { + // We need two new releases with a more recent creation timestamp + time.Sleep(2 * time.Second) + + newerRelease = release.DeepCopy() + newerRelease.Name = "newer-release" + newerRelease.ResourceVersion = "" + Expect(k8sClient.Create(ctx, newerRelease)).To(Succeed()) + + // Wait until the new release is cached + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: newerRelease.Name, Namespace: newerRelease.Namespace}, newerRelease) + }).Should(Succeed()) + + time.Sleep(2 * time.Second) + + mostRecentRelease = release.DeepCopy() + mostRecentRelease.Name = "most-recent-release" + mostRecentRelease.ResourceVersion = "" + Expect(k8sClient.Create(ctx, mostRecentRelease)).To(Succeed()) + + // Wait until the new release is cached + Eventually(func() error { + return k8sClient.Get(ctx, client.ObjectKey{Name: mostRecentRelease.Name, Namespace: mostRecentRelease.Namespace}, mostRecentRelease) + }).Should(Succeed()) + + returnedObject, err := loader.GetPreviousRelease(ctx, k8sClient, mostRecentRelease) + Expect(err).ToNot(HaveOccurred()) + Expect(returnedObject).ToNot(BeNil()) + Expect(returnedObject.Name).To(Equal(newerRelease.Name)) + }) + }) + When("calling GetRelease", func() { It("returns the requested release", func() { returnedObject, err := loader.GetRelease(ctx, k8sClient, release.Name, release.Namespace) diff --git a/loader/suite_test.go b/loader/suite_test.go index 84f298bc..36224097 100644 --- a/loader/suite_test.go +++ b/loader/suite_test.go @@ -110,6 +110,7 @@ var _ = BeforeSuite(func() { defer GinkgoRecover() Expect(cache.SetupComponentCache(mgr)).To(Succeed()) + Expect(cache.SetupReleaseCache(mgr)).To(Succeed()) Expect(cache.SetupReleasePlanCache(mgr)).To(Succeed()) Expect(cache.SetupReleasePlanAdmissionCache(mgr)).To(Succeed())