Skip to content

Commit

Permalink
Update Workspace Notifications reconciliation logic (#477)
Browse files Browse the repository at this point in the history
  • Loading branch information
arybolovlev authored Oct 18, 2024
1 parent 19029a3 commit 83479af
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 171 deletions.
5 changes: 5 additions & 0 deletions .changes/unreleased/ENHANCEMENTS-477-20240821-134240.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: ENHANCEMENTS
body: '`Workspace`: Update Notifications reconciliation logic to reduce the number of API calls.'
time: 2024-08-21T13:42:40.942036+02:00
custom:
PR: "477"
279 changes: 109 additions & 170 deletions controllers/workspace_controller_notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,46 +14,26 @@ import (
"github.com/hashicorp/hcp-terraform-operator/internal/slice"
)

func hasNotificationItem(s []tfc.NotificationConfiguration, f tfc.NotificationConfiguration) (int, bool) {
for i, v := range s {
if cmp.Equal(v, f, cmpopts.IgnoreFields(tfc.NotificationConfiguration{},
"CreatedAt", "DeliveryResponses", "EmailAddresses", "EmailUsers", "Enabled", "ID", "Subscribable", "Token", "Triggers", "URL", "UpdatedAt")) {
func hasNotificationItem(notificaions []tfc.NotificationConfiguration, notificaion tfc.NotificationConfiguration) (int, bool) {
for i, n := range notificaions {
if notificaion.Name == n.Name && notificaion.DestinationType == n.DestinationType {
return i, true
}

}
return -1, false
}

func notificationsDifference(a, b []tfc.NotificationConfiguration) []tfc.NotificationConfiguration {
var d []tfc.NotificationConfiguration
bc := make([]tfc.NotificationConfiguration, len(b))

copy(bc, b)

for _, av := range a {
i, t := hasNotificationItem(bc, av)
if t {
bc = slice.RemoveFromSlice(bc, i)
} else {
d = append(d, av)
}
}

return d
}

func (r *WorkspaceReconciler) getOrgMembers(ctx context.Context, w *workspaceInstance) (map[string]*tfc.User, error) {
eu := make(map[string]*tfc.User)
e := make([]string, 0)
emailUsers := make(map[string]*tfc.User)
emails := make([]string, 0)
for _, n := range w.instance.Spec.Notifications {
for _, ne := range n.EmailUsers {
eu[ne] = nil
e = append(e, ne)
}
emails = append(emails, n.EmailUsers...)
}
if len(emails) == 0 {
return emailUsers, nil
}
listOpts := &tfc.OrganizationMembershipListOptions{
Emails: e,
Emails: emails,
ListOptions: tfc.ListOptions{
PageSize: maxPageSize,
},
Expand All @@ -64,14 +44,14 @@ func (r *WorkspaceReconciler) getOrgMembers(ctx context.Context, w *workspaceIns
return nil, err
}
for _, m := range members.Items {
eu[m.Email] = &tfc.User{ID: m.User.ID}
emailUsers[m.Email] = &tfc.User{ID: m.User.ID}
}
if members.NextPage == 0 {
break
}
listOpts.PageNumber = members.NextPage
}
return eu, nil
return emailUsers, nil
}

func (r *WorkspaceReconciler) getInstanceNotifications(ctx context.Context, w *workspaceInstance) ([]tfc.NotificationConfiguration, error) {
Expand All @@ -84,36 +64,39 @@ func (r *WorkspaceReconciler) getInstanceNotifications(ctx context.Context, w *w
return []tfc.NotificationConfiguration{}, err
}

o := make([]tfc.NotificationConfiguration, len(w.instance.Spec.Notifications))
notifications := make([]tfc.NotificationConfiguration, len(w.instance.Spec.Notifications))

for i, n := range w.instance.Spec.Notifications {
var eu []*tfc.User
for _, e := range n.EmailUsers {
if v, ok := orgEmailUsers[e]; ok && v != nil {
eu = append(eu, v)
for i, notification := range w.instance.Spec.Notifications {
var emailUsers []*tfc.User
for _, emailUser := range notification.EmailUsers {
if v, ok := orgEmailUsers[emailUser]; ok {
emailUsers = append(emailUsers, v)
} else {
w.log.Error(err, "Reconcile Notifications", "msg", fmt.Sprintf("user %s was not found", emailUser))
return []tfc.NotificationConfiguration{}, fmt.Errorf("user %s was not found", emailUser)
}
}
nt := make([]string, len(n.Triggers))
for i, t := range n.Triggers {
nt[i] = string(t)
triggers := make([]string, len(notification.Triggers))
for i, t := range notification.Triggers {
triggers[i] = string(t)
}
o[i] = tfc.NotificationConfiguration{
Name: n.Name,
DestinationType: n.Type,
URL: n.URL,
Enabled: n.Enabled,
Token: n.Token,
Triggers: nt,
EmailAddresses: n.EmailAddresses,
EmailUsers: eu,
notifications[i] = tfc.NotificationConfiguration{
Name: notification.Name,
DestinationType: notification.Type,
URL: notification.URL,
Enabled: notification.Enabled,
Token: notification.Token,
Triggers: triggers,
EmailAddresses: notification.EmailAddresses,
EmailUsers: emailUsers,
}
}

return o, nil
return notifications, nil
}

func (r *WorkspaceReconciler) getWorkspaceNotifications(ctx context.Context, w *workspaceInstance) ([]tfc.NotificationConfiguration, error) {
var o []tfc.NotificationConfiguration
var notifications []tfc.NotificationConfiguration

listOpts := &tfc.NotificationConfigurationListOptions{
ListOptions: tfc.ListOptions{
Expand All @@ -125,17 +108,17 @@ func (r *WorkspaceReconciler) getWorkspaceNotifications(ctx context.Context, w *
if err != nil {
return nil, err
}
for _, n := range wn.Items {
o = append(o, tfc.NotificationConfiguration{
ID: n.ID,
Name: n.Name,
DestinationType: n.DestinationType,
URL: n.URL,
Enabled: n.Enabled,
Token: n.Token,
Triggers: n.Triggers,
EmailAddresses: n.EmailAddresses,
EmailUsers: n.EmailUsers,
for _, notification := range wn.Items {
notifications = append(notifications, tfc.NotificationConfiguration{
ID: notification.ID,
Name: notification.Name,
DestinationType: notification.DestinationType,
URL: notification.URL,
Enabled: notification.Enabled,
Token: notification.Token,
Triggers: notification.Triggers,
EmailAddresses: notification.EmailAddresses,
EmailUsers: notification.EmailUsers,
})
}
if wn.NextPage == 0 {
Expand All @@ -144,98 +127,63 @@ func (r *WorkspaceReconciler) getWorkspaceNotifications(ctx context.Context, w *
listOpts.PageNumber = wn.NextPage
}

return o, nil
}

func getNotificationsToCreate(spec, ws []tfc.NotificationConfiguration) []tfc.NotificationConfiguration {
return notificationsDifference(spec, ws)
}

func getNotificationsToUpdate(spec, ws []tfc.NotificationConfiguration) []tfc.NotificationConfiguration {
o := []tfc.NotificationConfiguration{}

if len(spec) == 0 || len(ws) == 0 {
return o
}

for _, sv := range spec {
for _, wv := range ws {
if sv.Name == wv.Name {
if !cmp.Equal(sv, wv, cmpopts.IgnoreFields(tfc.NotificationConfiguration{},
"CreatedAt", "DeliveryResponses", "ID", "Subscribable", "UpdatedAt")) {
sv.ID = wv.ID
o = append(o, sv)
}
}
}
}

return o
}

func getNotificationsToDelete(spec, ws []tfc.NotificationConfiguration) []tfc.NotificationConfiguration {
return notificationsDifference(ws, spec)
return notifications, nil
}

func (r *WorkspaceReconciler) createNotifications(ctx context.Context, w *workspaceInstance, create []tfc.NotificationConfiguration) error {
for _, c := range create {
w.log.Info("Reconcile Notifications", "msg", "creating notificaion")
nt := make([]tfc.NotificationTriggerType, len(c.Triggers))
for i, t := range c.Triggers {
nt[i] = tfc.NotificationTriggerType(t)
}
_, err := w.tfClient.Client.NotificationConfigurations.Create(ctx, w.instance.Status.WorkspaceID, tfc.NotificationConfigurationCreateOptions{
Name: &c.Name,
DestinationType: &c.DestinationType,
URL: &c.URL,
Enabled: &c.Enabled,
Token: &c.Token,
Triggers: nt,
EmailUsers: c.EmailUsers,
EmailAddresses: c.EmailAddresses,
})
if err != nil {
w.log.Error(err, "Reconcile Notifications", "msg", fmt.Sprintf("failed to create a new notification %q", c.ID))
return err
}
func (w *workspaceInstance) createNotification(ctx context.Context, notificaion tfc.NotificationConfiguration) error {
w.log.Info("Reconcile Notifications", "msg", "creating notificaion")
nt := make([]tfc.NotificationTriggerType, len(notificaion.Triggers))
for i, t := range notificaion.Triggers {
nt[i] = tfc.NotificationTriggerType(t)
}
_, err := w.tfClient.Client.NotificationConfigurations.Create(ctx, w.instance.Status.WorkspaceID, tfc.NotificationConfigurationCreateOptions{
Name: &notificaion.Name,
DestinationType: &notificaion.DestinationType,
URL: &notificaion.URL,
Enabled: &notificaion.Enabled,
Token: &notificaion.Token,
Triggers: nt,
EmailUsers: notificaion.EmailUsers,
EmailAddresses: notificaion.EmailAddresses,
})
if err != nil {
w.log.Error(err, "Reconcile Notifications", "msg", fmt.Sprintf("failed to create a new notification %q", notificaion.ID))
return err
}

return nil
}

func (r *WorkspaceReconciler) updateNotifications(ctx context.Context, w *workspaceInstance, update []tfc.NotificationConfiguration) error {
for _, u := range update {
w.log.Info("Reconcile Notifications", "msg", fmt.Sprintf("updating notificaion %q", u.ID))
nt := make([]tfc.NotificationTriggerType, len(u.Triggers))
for i, t := range u.Triggers {
nt[i] = tfc.NotificationTriggerType(t)
}
_, err := w.tfClient.Client.NotificationConfigurations.Update(ctx, u.ID, tfc.NotificationConfigurationUpdateOptions{
Name: &u.Name,
Enabled: &u.Enabled,
Token: &u.Token,
Triggers: nt,
URL: &u.URL,
EmailAddresses: u.EmailAddresses,
EmailUsers: u.EmailUsers,
func (w *workspaceInstance) updateNotification(ctx context.Context, sn, wn tfc.NotificationConfiguration) error {
if !cmp.Equal(sn, wn, cmpopts.IgnoreFields(tfc.NotificationConfiguration{}, "ID", "CreatedAt", "DeliveryResponses", "UpdatedAt", "Subscribable")) {
w.log.Info("Reconcile Notifications", "msg", fmt.Sprintf("updating notificaion %q", wn.ID))
triggers := make([]tfc.NotificationTriggerType, len(sn.Triggers))
for i, t := range sn.Triggers {
triggers[i] = tfc.NotificationTriggerType(t)
}
_, err := w.tfClient.Client.NotificationConfigurations.Update(ctx, wn.ID, tfc.NotificationConfigurationUpdateOptions{
Name: &sn.Name,
URL: &sn.URL,
Enabled: &sn.Enabled,
Token: &sn.Token,
Triggers: triggers,
EmailAddresses: sn.EmailAddresses,
EmailUsers: sn.EmailUsers,
})
if err != nil {
w.log.Error(err, "Reconcile Notifications", "msg", fmt.Sprintf("failed to update notificaion %q", u.ID))
w.log.Error(err, "Reconcile Notifications", "msg", fmt.Sprintf("failed to update notificaion %q", wn.ID))
return err
}
}

return nil
}

func (r *WorkspaceReconciler) deleteNotifications(ctx context.Context, w *workspaceInstance, delete []tfc.NotificationConfiguration) error {
for _, d := range delete {
w.log.Info("Reconcile Notifications", "msg", fmt.Sprintf("deleting notificaion %q", d.ID))
err := w.tfClient.Client.NotificationConfigurations.Delete(ctx, d.ID)
if err != nil {
w.log.Error(err, "Reconcile Notifications", "msg", fmt.Sprintf("failed to delete notificaions %q", d.ID))
return err
}
func (w *workspaceInstance) deleteNotification(ctx context.Context, notification tfc.NotificationConfiguration) error {
w.log.Info("Reconcile Notifications", "msg", fmt.Sprintf("deleting notificaion %q", notification.ID))
if err := w.tfClient.Client.NotificationConfigurations.Delete(ctx, notification.ID); err != nil {
w.log.Error(err, "Reconcile Notifications", "msg", fmt.Sprintf("failed to delete notificaion %q", notification.ID))
return err
}

return nil
Expand All @@ -244,50 +192,41 @@ func (r *WorkspaceReconciler) deleteNotifications(ctx context.Context, w *worksp
func (r *WorkspaceReconciler) reconcileNotifications(ctx context.Context, w *workspaceInstance) error {
w.log.Info("Reconcile Notifications", "msg", "new reconciliation event")

spec, err := r.getInstanceNotifications(ctx, w)
workspaceNotifications, err := r.getWorkspaceNotifications(ctx, w)
if err != nil {
w.log.Error(err, "Reconcile Notifications", "msg", "failed to get instance notificaions")
w.log.Error(err, "Reconcile Notifications", "msg", "failed to get workspace notifications")
return err
}

ws, err := r.getWorkspaceNotifications(ctx, w)
specNotifications, err := r.getInstanceNotifications(ctx, w)
if err != nil {
w.log.Error(err, "Reconcile Notifications", "msg", "failed to get workspace notifications")
w.log.Error(err, "Reconcile Notifications", "msg", "failed to get instance notificaions")
return err
}

delete := getNotificationsToDelete(spec, ws)
if len(delete) > 0 {
w.log.Info("Reconcile Notifications", "msg", fmt.Sprintf("deleting %d notifications", len(delete)))
err := r.deleteNotifications(ctx, w, delete)
if err != nil {
w.log.Error(err, "Reconcile Notifications", "msg", "failed to delete a notificaion")
return err
}
if len(specNotifications) == 0 && len(workspaceNotifications) == 0 {
w.log.Info("Reconcile Notifications", "msg", "there are no notifications both in spec and workspace")
return nil
}

create := getNotificationsToCreate(spec, ws)
if len(create) > 0 {
w.log.Info("Reconcile Notifications", "msg", fmt.Sprintf("creating %d notifications", len(create)))
err := r.createNotifications(ctx, w, create)
if err != nil {
w.log.Error(err, "Reconcile Notifications", "msg", "failed to create a notificaion")
return err
}
}
w.log.Info("Reconcile Notifications", "msg", fmt.Sprintf("there are %d notifications in spec", len(specNotifications)))
w.log.Info("Reconcile Notifications", "msg", fmt.Sprintf("there are %d notifications in workspace", len(workspaceNotifications)))

ws, err = r.getWorkspaceNotifications(ctx, w)
if err != nil {
w.log.Error(err, "Reconcile Notifications", "msg", "failed to get workspace notifications")
return err
for _, sn := range specNotifications {
if i, ok := hasNotificationItem(workspaceNotifications, sn); ok {
if err := w.updateNotification(ctx, sn, workspaceNotifications[i]); err != nil {
return err
}
workspaceNotifications = slice.RemoveFromSlice(workspaceNotifications, i)
} else {
if err := w.createNotification(ctx, sn); err != nil {
return err
}
}
}

update := getNotificationsToUpdate(spec, ws)
if len(update) > 0 {
w.log.Info("Reconcile Notifications", "msg", fmt.Sprintf("updating %d notifications", len(update)))
err := r.updateNotifications(ctx, w, update)
if err != nil {
w.log.Error(err, "Reconcile Notifications", "msg", "failed to update a notificaion")
for _, wn := range workspaceNotifications {
if err := w.deleteNotification(ctx, wn); err != nil {
return err
}
}
Expand Down
3 changes: 2 additions & 1 deletion controllers/workspace_controller_notifications_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
appv1alpha2 "github.com/hashicorp/hcp-terraform-operator/api/v1alpha2"
)

var _ = Describe("Workspace controller", Label("Notifications"), Ordered, func() {
var _ = Describe("Workspace controller", Ordered, func() {
var (
instance *appv1alpha2.Workspace
namespacedName types.NamespacedName
Expand Down Expand Up @@ -67,6 +67,7 @@ var _ = Describe("Workspace controller", Label("Notifications"), Ordered, func()
},
},
Name: workspace,
Description: "Notifications",
Notifications: []appv1alpha2.Notification{},
},
Status: appv1alpha2.WorkspaceStatus{},
Expand Down

0 comments on commit 83479af

Please sign in to comment.