Skip to content

Commit

Permalink
Quay notification add, update and delete support for ImageRepository
Browse files Browse the repository at this point in the history
  • Loading branch information
mavaras committed Jul 2, 2024
1 parent 4375ea0 commit 9b61575
Show file tree
Hide file tree
Showing 7 changed files with 787 additions and 62 deletions.
67 changes: 21 additions & 46 deletions controllers/imagerepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,15 @@ func (r *ImageRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Requ
}
}

if err = r.HandleNotifications(ctx, imageRepository); err != nil {
return ctrl.Result{}, err
}

if err := r.Client.Status().Update(ctx, imageRepository); err != nil {
log.Error(err, "failed to update image repository status")
return ctrl.Result{}, err
}

// we are adding to map only for new provision, not for some partial actions,
// so report time only if time was recorded
provisionTime, timeRecorded := metrics.RepositoryTimesForMetrics[repositoryIdForMetrics]
Expand All @@ -230,51 +239,6 @@ func (r *ImageRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Requ
return ctrl.Result{}, nil
}

func (r *ImageRepositoryReconciler) AddNotifications(ctx context.Context, imageRepository *imagerepositoryv1alpha1.ImageRepository) ([]imagerepositoryv1alpha1.NotificationStatus, error) {
log := ctrllog.FromContext(ctx).WithName("ConfigureNotifications")

if imageRepository.Spec.Notifications == nil {
// No notifications to configure
return nil, nil
}

log.Info("Configuring notifications")
notificationStatus := []imagerepositoryv1alpha1.NotificationStatus{}

for _, notification := range imageRepository.Spec.Notifications {
log.Info("Creating notification in Quay", "Title", notification.Title, "Event", notification.Event, "Method", notification.Method)
quayNotification, err := r.QuayClient.CreateNotification(
r.QuayOrganization,
imageRepository.Spec.Image.Name,
quay.Notification{
Title: notification.Title,
Event: string(notification.Event),
Method: string(notification.Method),
Config: quay.NotificationConfig{
Url: notification.Config.Url,
},
EventConfig: quay.NotificationEventConfig{},
})
if err != nil {
log.Error(err, "failed to create notification", "Title", notification.Title, "Event", notification.Event, "Method", notification.Method)
return nil, err
}
notificationStatus = append(
notificationStatus,
imagerepositoryv1alpha1.NotificationStatus{
UUID: quayNotification.UUID,
Title: notification.Title,
})

log.Info("Notification added",
"Title", notification.Title,
"Event", notification.Event,
"Method", notification.Method,
"QuayNotification", quayNotification)
}
return notificationStatus, nil
}

// ProvisionImageRepository creates image repository, robot account(s) and secret(s) to access the image repository.
// If labels with Application and Component name are present, robot account with pull only access
// will be created and pull token will be propagated to all environments via Remote Secret.
Expand Down Expand Up @@ -364,7 +328,7 @@ func (r *ImageRepositoryReconciler) ProvisionImageRepository(ctx context.Context
}

var notificationStatus []imagerepositoryv1alpha1.NotificationStatus
if notificationStatus, err = r.AddNotifications(ctx, imageRepository); err != nil {
if notificationStatus, err = r.SetNotifications(ctx, imageRepository); err != nil {
return err
}

Expand Down Expand Up @@ -684,3 +648,14 @@ func getRandomString(length int) string {
}
return hex.EncodeToString(bytes)[0:length]
}

func (r *ImageRepositoryReconciler) UpdateImageRepositoryStatusMessage(ctx context.Context, imageRepository *imagerepositoryv1alpha1.ImageRepository, statusMessage string) error {
log := ctrllog.FromContext(ctx)
imageRepository.Status.Message = statusMessage
if err := r.Client.Status().Update(ctx, imageRepository); err != nil {
log.Error(err, "failed to update image repository status")
return err
}

return nil
}
252 changes: 248 additions & 4 deletions controllers/imagerepository_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/konflux-ci/image-controller/pkg/quay"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"regexp"
"strings"
"time"

"github.com/konflux-ci/image-controller/pkg/quay"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"

Expand Down Expand Up @@ -315,6 +315,21 @@ var _ = Describe("Image repository controller", func() {
Expect(organization).To(Equal(quay.TestQuayOrg))
return &quay.Notification{UUID: "uuid"}, nil
}
isGetNotificationsInvoked := false
quay.GetNotificationsFunc = func(organization, repository string) ([]quay.Notification, error) {
isGetNotificationsInvoked = true
Expect(organization).To(Equal(quay.TestQuayOrg))
return []quay.Notification{
{
Title: "test-notification",
Event: string(imagerepositoryv1alpha1.NotificationEventRepoPush),
Method: string(imagerepositoryv1alpha1.NotificationMethodWebhook),
Config: quay.NotificationConfig{
Url: "http://test-url",
},
},
}, nil
}

imageRepositoryConfigObject := imageRepositoryConfig{
Labels: map[string]string{
Expand Down Expand Up @@ -354,6 +369,7 @@ var _ = Describe("Image repository controller", func() {
if updateComponentAnnotation {
Expect(component.Spec.ContainerImage).To(Equal(imageRepository.Status.Image.URL))
Expect(imageRepository.Annotations).To(HaveLen(0))
Eventually(func() bool { return isGetNotificationsInvoked }, timeout, interval).Should(BeTrue())
} else {
Expect(component.Spec.ContainerImage).To(BeEmpty())
}
Expand Down Expand Up @@ -455,6 +471,21 @@ var _ = Describe("Image repository controller", func() {
isRegenerateRobotAccountTokenForPushInvoked = true
return &quay.RobotAccount{Name: robotName, Token: newPushToken}, nil
}
isGetNotificationsInvoked := false
quay.GetNotificationsFunc = func(organization, repository string) ([]quay.Notification, error) {
isGetNotificationsInvoked = true
Expect(organization).To(Equal(quay.TestQuayOrg))
return []quay.Notification{
{
Title: "test-notification",
Event: string(imagerepositoryv1alpha1.NotificationEventRepoPush),
Method: string(imagerepositoryv1alpha1.NotificationMethodWebhook),
Config: quay.NotificationConfig{
Url: "http://test-url",
},
},
}, nil
}

imageRepository := getImageRepository(resourceKey)
oldTokenGenerationTimestamp := *imageRepository.Status.Credentials.GenerationTimestamp
Expand All @@ -470,6 +501,7 @@ var _ = Describe("Image repository controller", func() {
imageRepository.Status.Credentials.GenerationTimestamp != nil &&
*imageRepository.Status.Credentials.GenerationTimestamp != oldTokenGenerationTimestamp
}, timeout, interval).Should(BeTrue())
Eventually(func() bool { return isGetNotificationsInvoked }, timeout, interval).Should(BeTrue())

pushSecretKey := types.NamespacedName{Name: imageRepository.Status.Credentials.PushSecretName, Namespace: imageRepository.Namespace}
pushSecret := waitSecretExist(pushSecretKey)
Expand Down Expand Up @@ -531,6 +563,219 @@ var _ = Describe("Image repository controller", func() {
})
})

Context("Notifications", func() {

BeforeEach(func() {
quay.ResetTestQuayClient()
})

It("should prepare environment", func() {
pushToken = "push-token1234"
expectedImageName = fmt.Sprintf("%s/%s", defaultNamespace, defaultImageRepositoryName)
expectedImage = fmt.Sprintf("quay.io/%s/%s", quay.TestQuayOrg, expectedImageName)
expectedRobotAccountPrefix = strings.ReplaceAll(strings.ReplaceAll(expectedImageName, "-", "_"), "/", "_")
})

It("should provision image repository", func() {
isCreateRepositoryInvoked := false
quay.CreateRepositoryFunc = func(repository quay.RepositoryRequest) (*quay.Repository, error) {
defer GinkgoRecover()
isCreateRepositoryInvoked = true
Expect(repository.Repository).To(Equal(expectedImageName))
Expect(repository.Namespace).To(Equal(quay.TestQuayOrg))
Expect(repository.Visibility).To(Equal("public"))
Expect(repository.Description).ToNot(BeEmpty())
return &quay.Repository{Name: expectedImageName}, nil
}

isCreateNotificationInvoked := false
quay.CreateNotificationFunc = func(organization, repository string, notification quay.Notification) (*quay.Notification, error) {
isCreateNotificationInvoked = true
Expect(organization).To(Equal(quay.TestQuayOrg))
return &quay.Notification{UUID: "uuid"}, nil
}

createImageRepository(imageRepositoryConfig{
Notifications: []imagerepositoryv1alpha1.Notifications{
{
Title: "test-notification",
Event: imagerepositoryv1alpha1.NotificationEventRepoPush,
Method: imagerepositoryv1alpha1.NotificationMethodWebhook,
Config: imagerepositoryv1alpha1.NotificationConfig{
Url: "http://test-url",
},
},
},
})

Eventually(func() bool { return isCreateRepositoryInvoked }, timeout, interval).Should(BeTrue())
Eventually(func() bool { return isCreateNotificationInvoked }, timeout, interval).Should(BeTrue())

waitImageRepositoryFinalizerOnImageRepository(resourceKey)

imageRepository := getImageRepository(resourceKey)
Expect(imageRepository.Spec.Image.Name).To(Equal(expectedImageName))
Expect(imageRepository.Spec.Image.Visibility).To(Equal(imagerepositoryv1alpha1.ImageVisibilityPublic))
Expect(imageRepository.OwnerReferences).To(HaveLen(0))
Expect(imageRepository.Status.State).To(Equal(imagerepositoryv1alpha1.ImageRepositoryStateReady))
Expect(imageRepository.Status.Message).To(BeEmpty())
Expect(imageRepository.Status.Image.URL).To(Equal(expectedImage))
Expect(imageRepository.Status.Image.Visibility).To(Equal(imagerepositoryv1alpha1.ImageVisibilityPublic))
Expect(imageRepository.Status.Credentials.PushRobotAccountName).To(HavePrefix(expectedRobotAccountPrefix))
Expect(imageRepository.Status.Credentials.PushSecretName).To(Equal(imageRepository.Name + "-image-push"))
Expect(imageRepository.Status.Credentials.GenerationTimestamp).ToNot(BeNil())
Expect(imageRepository.Status.Notifications).To(HaveLen(1))
})

It("should add notification", func() {
notifications := []quay.Notification{
{
UUID: "uuid",
Title: "test-notification",
Event: string(imagerepositoryv1alpha1.NotificationEventRepoPush),
Method: string(imagerepositoryv1alpha1.NotificationMethodWebhook),
Config: quay.NotificationConfig{
Url: "http://test-url",
},
},
}
isCreateNotificationInvoked := false
quay.CreateNotificationFunc = func(organization, repository string, notification quay.Notification) (*quay.Notification, error) {
notifications = append(
notifications,
quay.Notification{
UUID: "uuid2",
Title: notification.Title,
Event: notification.Event,
Method: notification.Method,
Config: notification.Config,
},
)
isCreateNotificationInvoked = true
Expect(organization).To(Equal(quay.TestQuayOrg))
return &quay.Notification{UUID: "uuid2", Title: notification.Title}, nil
}
isGetNotificationsInvoked := false
quay.GetNotificationsFunc = func(organization, repository string) ([]quay.Notification, error) {
isGetNotificationsInvoked = true
Expect(organization).To(Equal(quay.TestQuayOrg))
return notifications, nil
}

newNotification := imagerepositoryv1alpha1.Notifications{
Title: "test-notification-2",
Event: imagerepositoryv1alpha1.NotificationEventRepoPush,
Method: imagerepositoryv1alpha1.NotificationMethodWebhook,
Config: imagerepositoryv1alpha1.NotificationConfig{
Url: "http://test-url-2",
},
}
imageRepository := getImageRepository(resourceKey)
imageRepository.Spec.Notifications = append(imageRepository.Spec.Notifications, newNotification)
Expect(k8sClient.Update(ctx, imageRepository)).To(Succeed())

Eventually(func() bool { return isCreateNotificationInvoked }, timeout, interval).Should(BeTrue())
Eventually(func() bool { return isGetNotificationsInvoked }, timeout, interval).Should(BeTrue())
Eventually(func() bool {
imageRepository := getImageRepository(resourceKey)
return len(imageRepository.Status.Notifications) == 2
}, timeout, interval).Should(BeTrue())
})

It("should delete notification", func() {
notifications := []quay.Notification{
{
UUID: "uuid",
Title: "test-notification",
Event: string(imagerepositoryv1alpha1.NotificationEventRepoPush),
Method: string(imagerepositoryv1alpha1.NotificationMethodWebhook),
Config: quay.NotificationConfig{
Url: "http://test-url",
},
},
{
UUID: "uuid2",
Title: "test-notification-2",
Event: string(imagerepositoryv1alpha1.NotificationEventRepoPush),
Method: string(imagerepositoryv1alpha1.NotificationMethodWebhook),
Config: quay.NotificationConfig{
Url: "http://test-url-2",
},
},
}
isDeleteNotificationInvoked := false
quay.DeleteNotificationFunc = func(organization, repository string, notificationUuid string) (bool, error) {
isDeleteNotificationInvoked = true
notifications = notifications[:1]
Expect(organization).To(Equal(quay.TestQuayOrg))
return true, nil
}
isGetNotificationsInvoked := false
quay.GetNotificationsFunc = func(organization, repository string) ([]quay.Notification, error) {
isGetNotificationsInvoked = true
Expect(organization).To(Equal(quay.TestQuayOrg))
return notifications, nil
}

imageRepository := getImageRepository(resourceKey)
Expect(imageRepository.Status.Notifications).To(HaveLen(2))
imageRepository.Spec.Notifications = imageRepository.Spec.Notifications[:len(imageRepository.Spec.Notifications)-1]
Expect(k8sClient.Update(ctx, imageRepository)).To(Succeed())

Eventually(func() bool { return isDeleteNotificationInvoked }, timeout, interval).Should(BeTrue())
Eventually(func() bool { return isGetNotificationsInvoked }, timeout, interval).Should(BeTrue())
Eventually(func() bool {
imageRepository := getImageRepository(resourceKey)
return len(imageRepository.Status.Notifications) == 1
}, timeout, interval).Should(BeTrue())
})

It("should update notification", func() {
updatedUrl := "http://test-url_new"
isUpdateNotificationInvoked := false
quay.UpdateNotificationFunc = func(organization, repository string, notificationUuid string, notification quay.Notification) (*quay.Notification, error) {
isUpdateNotificationInvoked = true
Expect(organization).To(Equal(quay.TestQuayOrg))
return &quay.Notification{
UUID: "uuid_new",
Title: "test-notification",
Event: string(imagerepositoryv1alpha1.NotificationEventRepoPush),
Method: string(imagerepositoryv1alpha1.NotificationMethodWebhook),
Config: quay.NotificationConfig{
Url: updatedUrl,
},
}, nil
}
isGetNotificationsInvoked := false
quay.GetNotificationsFunc = func(organization, repository string) ([]quay.Notification, error) {
isGetNotificationsInvoked = true
Expect(organization).To(Equal(quay.TestQuayOrg))
notifications := []quay.Notification{
{
UUID: "uuid",
Title: "test-notification",
Event: string(imagerepositoryv1alpha1.NotificationEventRepoPush),
Method: string(imagerepositoryv1alpha1.NotificationMethodWebhook),
Config: quay.NotificationConfig{
Url: "http://test-url",
},
},
}
return notifications, nil
}
imageRepository := getImageRepository(resourceKey)
imageRepository.Spec.Notifications[0].Config.Url = updatedUrl
Expect(k8sClient.Update(ctx, imageRepository)).To(Succeed())

Eventually(func() bool { return isUpdateNotificationInvoked }, timeout, interval).Should(BeTrue())
Eventually(func() bool { return isGetNotificationsInvoked }, timeout, interval).Should(BeTrue())
Eventually(func() bool {
imageRepository := getImageRepository(resourceKey)
return len(imageRepository.Status.Notifications) == 1 && imageRepository.Spec.Notifications[0].Config.Url == updatedUrl && imageRepository.Status.Notifications[0].UUID == "uuid_new"
}, timeout, interval).Should(BeTrue())
})
})

Context("Other image repository scenarios", func() {

BeforeEach(func() {
Expand Down Expand Up @@ -675,5 +920,4 @@ var _ = Describe("Image repository controller", func() {
Expect(k8sClient.Create(ctx, imageRepository)).ToNot(Succeed())
})
})

})
Loading

0 comments on commit 9b61575

Please sign in to comment.