Skip to content

Commit

Permalink
Quay notification support for ImageRepository
Browse files Browse the repository at this point in the history
The ImageRepository CR is extended to allow defining a Quay
notifications. The notification schema is based on Quay API schema and
supports webhook and email configuration.

Quay notification is created using the rest API client and added to a CR
status field.

JIRA: ISV-4756

Example:

apiVersion: appstudio.redhat.com/v1alpha1
kind: ImageRepository
metadata:
  name: imagerepository-sample
  namespace: image-controller-system
spec:
  notifications:
    - title: MyNotif
      event: repo_push
      method: webhook
      config:
        url: https://foo.com

Signed-off-by: Ales Raszka <[email protected]>
  • Loading branch information
Allda committed May 23, 2024
1 parent abe39f6 commit 3275b7d
Show file tree
Hide file tree
Showing 14 changed files with 463 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Build the manager binary
# For more details and updates, refer to
# https://catalog.redhat.com/software/containers/ubi9/go-toolset/61e5c00b4ec9945c18787690
FROM registry.access.redhat.com/ubi9/go-toolset:1.20.10 as builder
FROM registry.access.redhat.com/ubi9/go-toolset:1.21.9-1 as builder

# Copy the Go Modules manifests
COPY go.mod go.mod
Expand Down
45 changes: 45 additions & 0 deletions api/v1alpha1/imagerepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ type ImageRepositorySpec struct {
// Credentials management.
// +optional
Credentials *ImageCredentials `json:"credentials,omitempty"`

// Notifications defines configuration for image repository notifications.
// +optional
Notifications []Notifications `json:"notifications,omitempty"`
}

// ImageParameters describes requested image repository configuration.
Expand Down Expand Up @@ -62,6 +66,37 @@ type ImageCredentials struct {
RegenerateToken *bool `json:"regenerate-token,omitempty"`
}

type Notifications struct {
Title string `json:"title,omitempty"`
// +kubebuilder:validation:Enum=repo_push
Event NotificationEvent `json:"event,omitempty"`
// +kubebuilder:validation:Enum=email;webhook
Method NotificationMethod `json:"method,omitempty"`
Config NotificationConfig `json:"config,omitempty"`
}

type NotificationEvent string

const (
NotificationEventRepoPush NotificationEvent = "repo_push"
)

type NotificationMethod string

const (
NotificationMethodEmail NotificationMethod = "email"
NotificationMethodWebhook NotificationMethod = "webhook"
)

type NotificationConfig struct {
// Email is the email address to send notifications to.
// +optional
Email string `json:"email,omitempty"`
// Webhook is the URL to send notifications to.
// +optional
Url string `json:"url,omitempty"`
}

// ImageRepositoryStatus defines the observed state of ImageRepository
type ImageRepositoryStatus struct {
// State shows if image repository could be used.
Expand All @@ -79,6 +114,10 @@ type ImageRepositoryStatus struct {

// Credentials contain information related to image repository credentials.
Credentials CredentialsStatus `json:"credentials,omitempty"`

// Notifications shows the status of the notifications configuration.
// +optional
Notifications []NotificationStatus `json:"notifications,omitempty"`
}

type ImageRepositoryState string
Expand Down Expand Up @@ -126,6 +165,12 @@ type CredentialsStatus struct {
PullRemoteSecretName string `json:"pull-remote-secret,omitempty"`
}

// NotificationStatus shows the status of the notification configuration.
type NotificationStatus struct {
Title string `json:"title,omitempty"`
UUID string `json:"uuid,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

Expand Down
56 changes: 56 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions config/crd/bases/appstudio.redhat.com_imagerepositories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,34 @@ spec:
- private
type: string
type: object
notifications:
description: Notifications defines configuration for image repository
notifications.
items:
properties:
config:
properties:
email:
description: Email is the email address to send notifications
to.
type: string
url:
description: Webhook is the URL to send notifications to.
type: string
type: object
event:
enum:
- repo_push
type: string
method:
enum:
- email
- webhook
type: string
title:
type: string
type: object
type: array
type: object
status:
description: ImageRepositoryStatus defines the observed state of ImageRepository
Expand Down Expand Up @@ -140,6 +168,18 @@ spec:
contain non critical error, like failed to change image visibility,
while the state is ready and image resitory could be used.
type: string
notifications:
description: Notifications shows the status of the notifications configuration.
items:
description: NotificationStatus shows the status of the notification
configuration.
properties:
title:
type: string
uuid:
type: string
type: object
type: array
state:
description: State shows if image repository could be used. "ready"
means repository was created and usable, "failed" means that the
Expand Down
51 changes: 51 additions & 0 deletions controllers/imagerepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,51 @@ 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", "Name", imageRepository.Name, "Namespace", imageRepository.Namespace)
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 @@ -319,6 +364,11 @@ func (r *ImageRepositoryReconciler) ProvisionImageRepository(ctx context.Context
}
}

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

status := imagerepositoryv1alpha1.ImageRepositoryStatus{}
status.State = imagerepositoryv1alpha1.ImageRepositoryStateReady
status.Image.URL = quayImageURL
Expand All @@ -332,6 +382,7 @@ func (r *ImageRepositoryReconciler) ProvisionImageRepository(ctx context.Context
status.Credentials.PullRemoteSecretName = pullCredentialsInfo.RemoteSecretName
status.Credentials.PullSecretName = pullCredentialsInfo.SecretName
}
status.Notifications = notificationStatus

imageRepository.Spec.Image.Name = imageRepositoryName
controllerutil.AddFinalizer(imageRepository, ImageRepositoryFinalizer)
Expand Down
21 changes: 21 additions & 0 deletions controllers/imagerepository_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ var _ = Describe("Image repository controller", func() {
return 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{})

uploadSecretKey := types.NamespacedName{Name: "upload-secret-" + resourceKey.Name + "-image-push", Namespace: resourceKey.Namespace}
Expand All @@ -102,6 +109,7 @@ var _ = Describe("Image repository controller", func() {
Eventually(func() bool { return isCreateRepositoryInvoked }, timeout, interval).Should(BeTrue())
Eventually(func() bool { return isCreateRobotAccountInvoked }, timeout, interval).Should(BeTrue())
Eventually(func() bool { return isAddPushPermissionsToRobotAccountInvoked }, timeout, interval).Should(BeTrue())
Eventually(func() bool { return isCreateNotificationInvoked }, timeout, interval).Should(BeTrue())

waitImageRepositoryFinalizerOnImageRepository(resourceKey)

Expand All @@ -117,6 +125,9 @@ var _ = Describe("Image repository controller", func() {
Expect(imageRepository.Status.Credentials.PushRemoteSecretName).To(Equal(imageRepository.Name + "-image-push"))
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))
Expect(imageRepository.Status.Notifications[0].UUID).To(Equal("uuid"))
Expect(imageRepository.Status.Notifications[0].Title).To(Equal("test-notification"))

remoteSecretKey := types.NamespacedName{Name: imageRepository.Status.Credentials.PushRemoteSecretName, Namespace: imageRepository.Namespace}
remoteSecret := waitRemoteSecretExist(remoteSecretKey)
Expand Down Expand Up @@ -316,6 +327,12 @@ var _ = Describe("Image repository controller", func() {
}
return 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
}

imageRepositoryConfigObject := imageRepositoryConfig{
Labels: map[string]string{
Expand All @@ -340,6 +357,7 @@ var _ = Describe("Image repository controller", func() {
Eventually(func() bool { return isCreatePullRobotAccountInvoked }, timeout, interval).Should(BeTrue())
Eventually(func() bool { return isAddPushPermissionsToRobotAccountInvoked }, timeout, interval).Should(BeTrue())
Eventually(func() bool { return isAddPullPermissionsToRobotAccountInvoked }, timeout, interval).Should(BeTrue())
Eventually(func() bool { return isCreateNotificationInvoked }, timeout, interval).Should(BeTrue())

waitImageRepositoryFinalizerOnImageRepository(resourceKey)

Expand Down Expand Up @@ -371,6 +389,9 @@ var _ = Describe("Image repository controller", func() {
Expect(imageRepository.Status.Credentials.PullRemoteSecretName).To(Equal(imageRepository.Name + "-image-pull"))
Expect(imageRepository.Status.Credentials.PullSecretName).To(Equal(imageRepository.Name + "-image-pull"))
Expect(imageRepository.Status.Credentials.GenerationTimestamp).ToNot(BeNil())
Expect(imageRepository.Status.Notifications).To(HaveLen(1))
Expect(imageRepository.Status.Notifications[0].UUID).To(Equal("uuid"))
Expect(imageRepository.Status.Notifications[0].Title).To(Equal("test-notification"))

pushRemoteSecretKey := types.NamespacedName{Name: imageRepository.Status.Credentials.PushRemoteSecretName, Namespace: imageRepository.Namespace}
pushRemoteSecret := waitRemoteSecretExist(pushRemoteSecretKey)
Expand Down
10 changes: 10 additions & 0 deletions controllers/suite_util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ func getImageRepositoryConfig(config imageRepositoryConfig) *imagerepositoryv1al
Name: config.ImageName,
Visibility: imagerepositoryv1alpha1.ImageVisibility(visibility),
},
Notifications: []imagerepositoryv1alpha1.Notifications{
{
Title: "test-notification",
Event: imagerepositoryv1alpha1.NotificationEventRepoPush,
Method: imagerepositoryv1alpha1.NotificationMethodWebhook,
Config: imagerepositoryv1alpha1.NotificationConfig{
Url: "http://test-url",
},
},
},
},
}
}
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/konflux-ci/image-controller

go 1.20
go 1.21

toolchain go1.21.9

require (
github.com/go-logr/logr v1.4.1
Expand Down
Loading

0 comments on commit 3275b7d

Please sign in to comment.