Skip to content

Commit

Permalink
Implement email notifications on BillingEntity update
Browse files Browse the repository at this point in the history
  • Loading branch information
Aline Abler committed Apr 20, 2023
1 parent b98b98c commit 55343ff
Show file tree
Hide file tree
Showing 16 changed files with 457 additions and 60 deletions.
7 changes: 7 additions & 0 deletions apis/billing/v1/billingentity_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import (
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"
)

const (
// ConditionEmailSent is set when the update notification email has been sent
ConditionEmailSent = "EmailSent"
ConditionReasonSendFailed = "SendFailed"
ConditionReasonUpdated = "Updated"
)

// +kubebuilder:object:root=true

// BillingEntity is a representation of an APPUiO Cloud BillingEntity
Expand Down
3 changes: 2 additions & 1 deletion apis/user/v1/invitation_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ const (
// ConditionRedeemed is set when the invitation has been redeemed
ConditionRedeemed = "Redeemed"
// ConditionEmailSent is set when the invitation email has been sent
ConditionEmailSent = "EmailSent"
ConditionEmailSent = "EmailSent"
ConditionReasonSendFailed = "SendFailed"
)

// +kubebuilder:object:root=true
Expand Down
10 changes: 6 additions & 4 deletions apiserver/billing/odoostorage/odoo/odoo8/odoo8.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ var (

"email",
"phone",
"x_control_api_meta_status",
)
accountingContactUpdateAllowedFields = newSet(
"x_invoice_contact",
"x_control_api_meta_status",
"email",
)
)
Expand Down Expand Up @@ -291,10 +291,12 @@ func mapPartnersToBillingEntity(ctx context.Context, company model.Partner, acco
name := odooIDToK8sID(accounting.ID)

var status billingv1.BillingEntityStatus
err := json.Unmarshal([]byte(accounting.Status.Value), &status)
if accounting.Status.Value != "" {
err := json.Unmarshal([]byte(accounting.Status.Value), &status)

if err != nil {
l.Error(err, "Could not unmarshal BillingEntityStatus", "billingEntityName", name, "rawStatus", accounting.Status.Value)
if err != nil {
l.Error(err, "Could not unmarshal BillingEntityStatus", "billingEntityName", name, "rawStatus", accounting.Status.Value)
}
}

return billingv1.BillingEntity{
Expand Down
10 changes: 10 additions & 0 deletions apiserver/billing/odoostorage/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package odoostorage
import (
"context"
"fmt"
"reflect"

apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
Expand Down Expand Up @@ -38,5 +40,13 @@ func (s *billingEntityStorage) Update(ctx context.Context, name string, objInfo
}
}

if !reflect.DeepEqual(newBE.Spec, oldBE.Spec) {
apimeta.SetStatusCondition(&newBE.Status.Conditions, metav1.Condition{
Status: metav1.ConditionFalse,
Type: billingv1.ConditionEmailSent,
Reason: billingv1.ConditionReasonUpdated,
})
}

return newBE, false, s.storage.Update(ctx, newBE)
}
23 changes: 23 additions & 0 deletions config/rbac/controller/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ rules:
- get
- list
- watch
- apiGroups:
- rbac.appuio.io
resources:
- billingentities/status
verbs:
- get
- patch
- update
- apiGroups:
- rbac.appuio.io
resources:
Expand Down Expand Up @@ -189,6 +197,21 @@ rules:
- patch
- update
- watch
- apiGroups:
- user.appuio.io
resources:
- billingentities
verbs:
- get
- list
- apiGroups:
- user.appuio.io
resources:
- billingentities/status
verbs:
- get
- patch
- update
- apiGroups:
- user.appuio.io
resources:
Expand Down
113 changes: 103 additions & 10 deletions controller.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"flag"
"os"
"text/template"
Expand All @@ -10,6 +11,7 @@ import (
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"

"github.com/Masterminds/sprig/v3"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
Expand All @@ -19,6 +21,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/metrics"
"sigs.k8s.io/controller-runtime/pkg/webhook"

"github.com/robfig/cron/v3"
"github.com/spf13/cobra"

billingv1 "github.com/appuio/control-api/apis/billing/v1"
Expand All @@ -35,14 +38,22 @@ import (
const (
defaultInvitationEmailTemplate = `Hello developer of great software, Kubernetes engineer or fellow human,
A user of APPUiO Cloud has invited you to join them. Follow https://portal.dev/invitations/{{.Invitation.ObjectMeta.Name}}?token={{.Invitation.Status.Token}} to accept this invitation.
A user of APPUiO Cloud has invited you to join them. Follow https://portal.dev/invitations/{{.Object.ObjectMeta.Name}}?token={{.Object.Status.Token}} to accept this invitation.
APPUiO Cloud is a shared Kubernetes offering based on OpenShift provided by https://vshn.ch.
Unsure what to do next? Accept this invitation using the link above, login to one of the zones listed at https://portal.appuio.cloud/zones, deploy your application. A getting started guide on how to do so, is available at https://docs.appuio.cloud/user/tutorials/getting-started.html. To learn more about APPUiO Cloud in general, please visit https://appuio.cloud.
If you have any problems or questions, please email us at [email protected].
All the best
Your APPUiO Cloud Team`
defaultBillingEntityEmailTemplate = `Good time of day!
A user of APPUiO Cloud has updated billing entity {{.Object.ObjectMeta.Name}} ({{.Object.Spec.Name}}).
See https://erp.vshn.net/web#id={{ trimPrefix "be-" .Object.ObjectMeta.Name }}&view_type=form&model=res.partner&menu_id=74&action=60 for details.
All the best
Your APPUiO Cloud Team`
)
Expand Down Expand Up @@ -86,6 +97,11 @@ func ControllerCommand() *cobra.Command {
invEmailMailgunUrl := cmd.Flags().String("mailgun-url", "https://api.eu.mailgun.net/v3", "API base URL for your Mailgun account")
invEmailMailgunTestMode := cmd.Flags().Bool("mailgun-test-mode", false, "If set, do not actually send e-mails")

billingEntityEmailBodyTemplate := cmd.Flags().String("billingentity-email-body-template", defaultBillingEntityEmailTemplate, "Body for billing entity modification update mails")
billingEntityEmailRecipient := cmd.Flags().String("billingentity-email-recipient", "", "Recipient e-mail address for billing entity modification update mails")
billingEntityEmailSubject := cmd.Flags().String("billingentity-email-subject", "An APPUiO Billing Entity has been updated", "Subject for billing entity modification update mails")
billingEntityCronInterval := cmd.Flags().String("billingentity-email-cron-interval", "@every 1m", "Cron interval for how frequently billing entity update e-mails are sent")

cmd.Run = func(*cobra.Command, []string) {
scheme := runtime.NewScheme()
setupLog := ctrl.Log.WithName("setup")
Expand All @@ -100,29 +116,59 @@ func ControllerCommand() *cobra.Command {
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
ctx := ctrl.SetupSignalHandler()

bt, err := template.New("emailBody").Parse(*emailBodyTemplate)
bt, err := template.New("emailBody").Funcs(sprig.FuncMap()).Parse(*emailBodyTemplate)
if err != nil {
setupLog.Error(err, "Failed to parse email body template")
setupLog.Error(err, "Failed to parse email body template for invitations")
os.Exit(1)
}
bodyRenderer := &mailsenders.InvitationRenderer{Template: bt}
bet, err := template.New("emailBody").Funcs(sprig.FuncMap()).Parse(*billingEntityEmailBodyTemplate)
if err != nil {
setupLog.Error(err, "Failed to parse email body template for billing entity e-mails")
os.Exit(1)
}
invitationBodyRenderer := &mailsenders.Renderer{Template: bt}
billingEntityBodyRenderer := &mailsenders.Renderer{Template: bet}

var mailSender mailsenders.MailSender
var invMailSender mailsenders.MailSender
var beMailSender mailsenders.MailSender
if *invEmailBackend == "mailgun" {
b := mailsenders.NewMailgunSender(
*invEmailMailgunDomain,
*invEmailMailgunToken,
*invEmailMailgunUrl,
*invEmailSender,
bodyRenderer,
invitationBodyRenderer,
*invEmailSubject,
*invEmailMailgunTestMode,
)
mailSender = &b
invMailSender = &b
if *billingEntityEmailRecipient != "" {
be := mailsenders.NewMailgunSender(
*invEmailMailgunDomain,
*invEmailMailgunToken,
*invEmailMailgunUrl,
*invEmailSender,
billingEntityBodyRenderer,
*billingEntityEmailSubject,
*invEmailMailgunTestMode,
)
beMailSender = &be
} else {
// fall back to stdout if no recipient e-mail is given
beMailSender = &mailsenders.StdoutSender{
Subject: *billingEntityEmailSubject,
Body: billingEntityBodyRenderer,
}
}
invMailSender = &b
} else {
mailSender = &mailsenders.StdoutSender{
invMailSender = &mailsenders.StdoutSender{
Subject: *invEmailSubject,
Body: bodyRenderer,
Body: invitationBodyRenderer,
}
beMailSender = &mailsenders.StdoutSender{
Subject: *billingEntityEmailSubject,
Body: billingEntityBodyRenderer,
}
}

Expand All @@ -135,7 +181,7 @@ func ControllerCommand() *cobra.Command {
*invTokenValidFor,
*redeemedInvitationTTL,
*invEmailBaseRetryDelay,
mailSender,
invMailSender,
ctrl.Options{
Scheme: scheme,
MetricsBindAddress: *metricsAddr,
Expand All @@ -150,11 +196,23 @@ func ControllerCommand() *cobra.Command {
os.Exit(1)
}

cron, err := setupCron(
ctx,
*billingEntityCronInterval,
mgr,
beMailSender,
*billingEntityEmailRecipient,
)

cron.Start()

setupLog.Info("starting manager")
if err := mgr.Start(ctx); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
setupLog.Info("Stopping...")
<-cron.Stop().Done()
}

return cmd
Expand Down Expand Up @@ -283,3 +341,38 @@ func setupManager(
}
return mgr, err
}

func setupCron(
ctx context.Context,
crontab string,
mgr ctrl.Manager,
beMailSender mailsenders.MailSender,
beMailRecipient string,
) (*cron.Cron, error) {

bemail := controllers.NewBillingEntityEmailCronJob(
mgr.GetClient(),
mgr.GetEventRecorderFor("invitation-email-controller"),
mgr.GetScheme(),
beMailSender,
beMailRecipient,
)

metrics.Registry.MustRegister(bemail.GetMetrics())
syncLog := ctrl.Log.WithName("cron")

c := cron.New()
_, err := c.AddFunc(crontab, func() {
err := bemail.Run(ctx)

if err == nil {
return
}
syncLog.Error(err, "Error during periodic job")

})
if err != nil {
return nil, err
}
return c, nil
}
Loading

0 comments on commit 55343ff

Please sign in to comment.