Skip to content

Commit

Permalink
feat(reminder): Adding reminder notifier for rb
Browse files Browse the repository at this point in the history
Signed-off-by: Vincent Boutour <[email protected]>
  • Loading branch information
ViBiOh committed Oct 30, 2021
1 parent 04977b6 commit a35d388
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 23 deletions.
16 changes: 14 additions & 2 deletions cmd/notifier/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"flag"
"fmt"
"os"

"github.com/ViBiOh/httputils/v4/pkg/db"
Expand Down Expand Up @@ -33,6 +34,8 @@ func main() {
dockerConfig := docker.Flags(fs, "docker")
notifierConfig := notifier.Flags(fs, "notifier")

notificationType := fs.String("notification", "daily", "Notification type. \"daily\" or \"reminder\"")

logger.Fatal(fs.Parse(os.Args[1:]))

logger.Global(logger.New(loggerConfig))
Expand All @@ -57,8 +60,17 @@ func main() {

logger.Info("Starting notifier...")

if err := notifierApp.Notify(context.Background()); err != nil {
logger.Fatal(err)
switch *notificationType {
case "daily":
if err := notifierApp.Notify(context.Background()); err != nil {
logger.Fatal(err)
}
case "reminder":
if err := notifierApp.Remind(context.Background()); err != nil {
logger.Fatal(err)
}
default:
logger.Fatal(fmt.Errorf("unknown notification type `%s`", *notificationType))
}

logger.Info("Notifier ended!")
Expand Down
48 changes: 48 additions & 0 deletions infra/reminder.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: ketchup-remindeer
namespace: default
spec:
interval: 120m
chart:
spec:
chart: cron
version: "0.0.19"
sourceRef:
kind: HelmRepository
name: vibioh
namespace: default
interval: 120m
install:
crds: Skip
maxHistory: 3
upgrade:
remediation:
retries: 3
rollback:
cleanupOnFail: true
values:
nameOverride: ketchup-remindeer
schedule: "0 16 * * 5" # manager is in UTC
image:
name: vibioh/ketchup
tag: "202110251847" # {"$imagepolicy": "default:flux-ketchup-web:tag"}
config:
KETCHUP_DB_HOST: ketchup-db
KETCHUP_DB_NAME: ketchup
KETCHUP_DB_PORT: "80"
KETCHUP_DB_USER: ketchup
KETCHUP_LOGGER_JSON: "true"
secrets:
KETCHUP_DB_PASS: AgAw+m39PXVIRub8NNvEenJC1CuqSraj0//3FUlhc5NV4cLT0lr15v7oIsd+LyLD65iirArC+03AJ7NQ6HQ/G4a0ARY/mljIFw82+lvZXCF22KvUrJ9CXRY/4ugARluGNwNbUmxO+2frON7G8Ek4GzxyUQK54h2esRWf9Ev7yTz/tfWCEg2JteMWMJ340glCIGUlY1V23GA2ucYmvSBEx8Svifs20lRFrWO0GHs5/gLYP9z4oFbEHO3fLH6a3WM0xiPZQ0I1wnJApRTKe/qraihlCxSz1PfJyELeoP3pEeuJ7M/J8kr/ryJDeI8f4ZSnJ1nbeP6e2VhWd/trGeNBnDbgnVwv/Ycsl15GC3OpD+qEuQtYGLa/zJS7GriLsQ+4uPkXPPhr45nhpiMouInnkw05ZX68V5c/aWDTVY3NNUAPuWL/VV0zq7OLDtokLiD/wS8XsG+XtewVMx6opvDYXqyUBbg2tbW96xdyCnRWfhE/LWdxirRaYKirGV4zRjD6icgd7xgkmzDngk1CfoSXVfUzklMI+P0ekE8Sds1zxKKdsIPwQ/dmorC0Uv98JYc194Ax6Kk7+9q5MBwUuNlU3IOCQuEF72MVsJM3rfSWou+m28bEFIiI6fI7DDCNmvZ5bhDva/KN7A8bLAltpv+ITCv5IJW9ejNVRiy90gdTm4GUPA9XZTDNdpU425DyCKQFOU+XPSpBsR2CDsZnyPMBlFqDnO3m7Mc1QOhENQO986KIEt/WRwQGXb5j+hz/L+Bn9co1SE3nfr8jxFV2CIk=
KETCHUP_MAILER_URL: AgAxlp+YpMSMQPJFQ/mpzfIBL7b3481XO1QQgm6cys/xql6u49mbF1YBQeoFSE1telRhPbfFcbAT5PXuz6Jljr2kSg8iTT5L1u+96fxUYGoLPLfTsq1stPvohALgDxAPs8qiaw+OvLVszfTOBVtTXwJpTi7/08XVrVcKgKuFPG4GPB71EVsXEV1yK+UxJLjSICM1OQ8pIzVxBWwLYn2rVB/zcRVnYtjWQN8dPerQvG4iF40HfdN1VfPf58OOZsIavTTjIh10jO0t/+k0iR7B5W239uYA2EI8foySF/geNjxvpw7HRQvZs44pXVDGdsF/5LB138AqrdegMJ6/1/G7yGS0fEx6y7SjEcUb7CIjZJCZNd5GsonYPQa+dja61RLb4NSHT5QTnw5s0zeTjWE2JpuGHlWihxQ9NKGSY/oLUMLWbsnbfzs9Z++ZP8Bq1UyYApWcFQBcQdmJcqmuLL6dtKbzDtHTtPmE8aDqq2mSXhKX8j5TDmGM2uG9a/ijFV3IgUdTRZaYDM9LEqOctVpAcYgfTytaf3ADxOJKHVBpvRTMOsByzJ5TGruw8p2gcwSgOO1kHcNbR7n28mDHIkGYMevrdzvLKENMYCV/97rUrK7CSLFxia47ITe/q5ak8V94EyoklTESs+YT1jNF+EkNb7nL1r6axNiAnbQizQ8J4078dSofrFPzlDY6s0jmUOMt8q3I+d0MpkNAQ0vhuqXy0OBj4w3D2Lpi2tqEFJiQ8aXDH5j3IOr4MSHg27KL+hjfzxs0JVVFbMIAyOSBKZThoOls9DlmqNmBBegaEhPR2ehT1BpsMBbq
containers:
- name: job
command:
- /notifier
- -notification
- "reminder"
resources:
limits:
memory: 128Mi
13 changes: 9 additions & 4 deletions pkg/mocks/ketchup_service.go

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

13 changes: 9 additions & 4 deletions pkg/mocks/ketchup_store.go

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

15 changes: 15 additions & 0 deletions pkg/mocks/user_store.go

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

5 changes: 3 additions & 2 deletions pkg/model/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type UserService interface {
//go:generate mockgen -destination ../mocks/user_store.go -mock_names UserStore=UserStore -package mocks github.com/ViBiOh/ketchup/pkg/model UserStore
type UserStore interface {
DoAtomic(context.Context, func(context.Context) error) error
ListReminderUsers(ctx context.Context) ([]User, error)
GetByLoginID(context.Context, uint64) (User, error)
GetByEmail(context.Context, string) (User, error)
Create(context.Context, User) (uint64, error)
Expand Down Expand Up @@ -83,7 +84,7 @@ type RepositoryStore interface {
type KetchupService interface {
List(ctx context.Context, pageSize uint, last string) ([]Ketchup, uint64, error)
ListForRepositories(ctx context.Context, repositories []Repository, frequency KetchupFrequency) ([]Ketchup, error)
ListOutdatedByFrequency(ctx context.Context, frequency KetchupFrequency) ([]Ketchup, error)
ListOutdatedByFrequency(ctx context.Context, frequency KetchupFrequency, users ...User) ([]Ketchup, error)
Create(ctx context.Context, item Ketchup) (Ketchup, error)
Update(ctx context.Context, oldPattern string, item Ketchup) (Ketchup, error)
UpdateAll(ctx context.Context) error
Expand All @@ -97,7 +98,7 @@ type KetchupStore interface {
DoAtomic(ctx context.Context, action func(context.Context) error) error
List(ctx context.Context, page uint, last string) ([]Ketchup, uint64, error)
ListByRepositoriesID(ctx context.Context, ids []uint64, frequency KetchupFrequency) ([]Ketchup, error)
ListOutdatedByFrequency(ctx context.Context, frequency KetchupFrequency) ([]Ketchup, error)
ListOutdatedByFrequency(ctx context.Context, frequency KetchupFrequency, usersIds ...uint64) ([]Ketchup, error)
GetByRepository(ctx context.Context, id uint64, pattern string, forUpdate bool) (Ketchup, error)
Create(ctx context.Context, o Ketchup) (uint64, error)
Update(ctx context.Context, o Ketchup, oldPattern string) error
Expand Down
40 changes: 34 additions & 6 deletions pkg/notifier/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ func (a App) Notify(ctx context.Context) error {
return fmt.Errorf("unable to get ketchup to notify: %w", err)
}

if err := a.sendNotification(userCtx, ketchupsToNotify); err != nil {
return err
if err := a.sendNotification(userCtx, "ketchup", ketchupsToNotify); err != nil {
return fmt.Errorf("unable to send notification: %s", err)
}

if len(a.pushURL) != 0 {
Expand Down Expand Up @@ -161,7 +161,7 @@ func (a App) getKetchupToNotify(ctx context.Context, releases []model.Release) (
}

logger.Info("%d weekly ketchups to notify", len(weeklyKetchups))
a.addWeeklyKetchups(weeklyKetchups, userToNotify)
a.groupKetchupsToUsers(weeklyKetchups, userToNotify)
}

logger.Info("%d users to notify", len(userToNotify))
Expand Down Expand Up @@ -207,7 +207,7 @@ func (a App) syncReleasesByUser(releases []model.Release, ketchups []model.Ketch
return usersToNotify
}

func (a App) addWeeklyKetchups(ketchups []model.Ketchup, usersToNotify map[model.User][]model.Release) {
func (a App) groupKetchupsToUsers(ketchups []model.Ketchup, usersToNotify map[model.User][]model.Release) {
for _, ketchup := range ketchups {
ketchupVersion, err := semver.Parse(ketchup.Version)
if err != nil {
Expand Down Expand Up @@ -240,7 +240,7 @@ func (a App) handleKetchupNotification(ketchup model.Ketchup, version string) {
}
}

func (a App) sendNotification(ctx context.Context, ketchupToNotify map[model.User][]model.Release) error {
func (a App) sendNotification(ctx context.Context, template string, ketchupToNotify map[model.User][]model.Release) error {
if len(ketchupToNotify) == 0 {
return nil
}
Expand All @@ -259,7 +259,7 @@ func (a App) sendNotification(ctx context.Context, ketchupToNotify map[model.Use
"releases": releases,
}

mr := mailerModel.NewMailRequest().Template("ketchup").From("[email protected]").As("Ketchup").To(user.Email).Data(payload)
mr := mailerModel.NewMailRequest().Template(template).From("[email protected]").As("Ketchup").To(user.Email).Data(payload)
subject := fmt.Sprintf("Ketchup - %d new release", len(releases))
if len(releases) > 1 {
subject += "s"
Expand All @@ -273,3 +273,31 @@ func (a App) sendNotification(ctx context.Context, ketchupToNotify map[model.Use

return nil
}

// Remind users for new ketchup
func (a App) Remind(ctx context.Context) error {
userCtx := authModel.StoreUser(ctx, authModel.NewUser(a.loginID, "scheduler"))

usersToRemind, err := a.userService.ListReminderUsers(userCtx)
if err != nil {
return fmt.Errorf("unable to get reminder users: %s", err)
}

remindKetchups, err := a.ketchupService.ListOutdatedByFrequency(userCtx, model.Daily, usersToRemind...)
if err != nil {
return fmt.Errorf("unable to get daily ketchups to remind: %s", err)
}

if len(remindKetchups) == 0 {
return nil
}

usersToNotify := make(map[model.User][]model.Release)
a.groupKetchupsToUsers(remindKetchups, usersToNotify)

if err := a.sendNotification(userCtx, "ketchup_remind", usersToNotify); err != nil {
return fmt.Errorf("unable to send remind notification: %s", err)
}

return nil
}
2 changes: 1 addition & 1 deletion pkg/notifier/notifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ func TestSendNotification(t *testing.T) {
mailerApp.EXPECT().Enabled().Return(false)
}

gotErr := tc.instance.sendNotification(tc.args.ctx, tc.args.ketchupToNotify)
gotErr := tc.instance.sendNotification(tc.args.ctx, "ketchup", tc.args.ketchupToNotify)

failed := false

Expand Down
9 changes: 7 additions & 2 deletions pkg/service/ketchup/ketchup.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,13 @@ func (a App) ListForRepositories(ctx context.Context, repositories []model.Repos
}

// ListOutdatedByFrequency ketchups outdated
func (a App) ListOutdatedByFrequency(ctx context.Context, frequency model.KetchupFrequency) ([]model.Ketchup, error) {
list, err := a.ketchupStore.ListOutdatedByFrequency(ctx, frequency)
func (a App) ListOutdatedByFrequency(ctx context.Context, frequency model.KetchupFrequency, users ...model.User) ([]model.Ketchup, error) {
usersIds := make([]uint64, len(users))
for i, user := range users {
usersIds[i] = user.ID
}

list, err := a.ketchupStore.ListOutdatedByFrequency(ctx, frequency, usersIds...)
if err != nil {
return nil, httpModel.WrapInternal(fmt.Errorf("unable to list outdated by frequency: %w", err))
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/service/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ func (a App) StoreInContext(ctx context.Context) context.Context {
return model.StoreUser(ctx, item)
}

// ListReminderUsers list users that need a reminder
func (a App) ListReminderUsers(ctx context.Context) ([]model.User, error) {
return a.userStore.ListReminderUsers(ctx)
}

// Create user
func (a App) Create(ctx context.Context, item model.User) (model.User, error) {
if err := a.check(ctx, model.User{}, item); err != nil {
Expand Down
12 changes: 10 additions & 2 deletions pkg/store/ketchup/ketchup.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ WHERE
`

// ListOutdatedByFrequency lists outdated ketchup by frequency id
func (a App) ListOutdatedByFrequency(ctx context.Context, frequency model.KetchupFrequency) ([]model.Ketchup, error) {
func (a App) ListOutdatedByFrequency(ctx context.Context, frequency model.KetchupFrequency, userIds ...uint64) ([]model.Ketchup, error) {
var list []model.Ketchup

scanner := func(rows pgx.Rows) error {
Expand Down Expand Up @@ -226,7 +226,15 @@ func (a App) ListOutdatedByFrequency(ctx context.Context, frequency model.Ketchu
return nil
}

return list, a.db.List(ctx, scanner, listOutdatedByFrequencyQuery, strings.ToLower(frequency.String()))
query := listOutdatedByFrequencyQuery
params := []interface{}{strings.ToLower(frequency.String())}

if len(userIds) > 0 {
query += " AND k.user_id IN ($2)"
params = append(params, userIds)
}

return list, a.db.List(ctx, scanner, query, params...)
}

const getQuery = `
Expand Down
29 changes: 29 additions & 0 deletions pkg/store/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,35 @@ func (a App) GetByLoginID(ctx context.Context, loginID uint64) (model.User, erro
return item, a.db.Get(ctx, scanner, getByLoginIDQuery, loginID)
}

const listReminderUsers = `
SELECT
id,
email,
login_id
FROM
ketchup.user
WHERE
reminder IS TRUE
`

// ListReminderUsers retrieve user with reminders
func (a App) ListReminderUsers(ctx context.Context) ([]model.User, error) {
var list []model.User

scanner := func(rows pgx.Rows) error {
var item model.User

if err := rows.Scan(&item.ID, &item.Email, &item.Login.ID); err != nil {
return err
}

list = append(list, item)
return nil
}

return list, a.db.List(ctx, scanner, listReminderUsers)
}

const insertQuery = `
INSERT INTO
ketchup.user
Expand Down
1 change: 1 addition & 0 deletions sql/ddl.sql
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ CREATE TABLE ketchup.user (
id BIGINT NOT NULL DEFAULT nextval('ketchup.user_seq'),
email TEXT NOT NULL,
login_id BIGINT NOT NULL REFERENCES auth.login(id) ON DELETE CASCADE,
reminder BOOL NOT NULL,
creation_date TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER SEQUENCE ketchup.user_seq OWNED BY ketchup.user.id;
Expand Down
2 changes: 2 additions & 0 deletions sql/migration_2021-11-30_1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE ketchup.user
ADD COLUMN reminder BOOL NOT NULL DEFAULT FALSE;

0 comments on commit a35d388

Please sign in to comment.