Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quay notification support for ImageRepository #112

Merged
merged 1 commit into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
)
Allda marked this conversation as resolved.
Show resolved Hide resolved

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should check it before calling this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check what?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imageRepository.Spec.Notifications?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aha, I didn't want to add extra complexity to the parent function and instead return early from this one in case notifications are not set.

// 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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will return an error if this function is called second time.
See how, for example, CreateRobotAccount is implemented.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated code to not create a duplicated entries if a notification already exists.

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to add the notifications on image repository creation only or at any point?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the current usecase that we have it is fine to create a notification on CR creation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has to be implemented in follow up PR.

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
29 changes: 29 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(BeFalse())

waitImageRepositoryFinalizerOnImageRepository(resourceKey)

Expand All @@ -117,6 +125,7 @@ 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(0))

remoteSecretKey := types.NamespacedName{Name: imageRepository.Status.Credentials.PushRemoteSecretName, Namespace: imageRepository.Namespace}
remoteSecret := waitRemoteSecretExist(remoteSecretKey)
Expand Down Expand Up @@ -316,12 +325,28 @@ 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{
ApplicationNameLabelName: defaultComponentApplication,
ComponentNameLabelName: defaultComponentName,
},
Notifications: []imagerepositoryv1alpha1.Notifications{
{
Title: "test-notification",
Event: imagerepositoryv1alpha1.NotificationEventRepoPush,
Method: imagerepositoryv1alpha1.NotificationMethodWebhook,
Config: imagerepositoryv1alpha1.NotificationConfig{
Url: "http://test-url",
},
},
},
}

if updateComponentAnnotation {
Expand All @@ -340,6 +365,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 +397,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
12 changes: 7 additions & 5 deletions controllers/suite_util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ const (
)

type imageRepositoryConfig struct {
ResourceKey *types.NamespacedName
ImageName string
Visibility string
Labels map[string]string
Annotations map[string]string
ResourceKey *types.NamespacedName
ImageName string
Visibility string
Labels map[string]string
Annotations map[string]string
Notifications []imagerepositoryv1alpha1.Notifications
}

func getImageRepositoryConfig(config imageRepositoryConfig) *imagerepositoryv1alpha1.ImageRepository {
Expand Down Expand Up @@ -93,6 +94,7 @@ func getImageRepositoryConfig(config imageRepositoryConfig) *imagerepositoryv1al
Name: config.ImageName,
Visibility: imagerepositoryv1alpha1.ImageVisibility(visibility),
},
Notifications: config.Notifications,
},
}
}
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to do the update?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't able to run in locally with the previous version and since the new version was already available I bumped it up.


toolchain go1.21.9

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