Skip to content

Commit

Permalink
Feat/ add vms/email debt notification (labring#4763)
Browse files Browse the repository at this point in the history
* optimize debt log & fix named kb cluster backup pvc named.
* add vms notification for debt
* add email notification for debt
  • Loading branch information
bxy4543 authored May 29, 2024
1 parent 786985e commit 1ad3c71
Show file tree
Hide file tree
Showing 21 changed files with 1,308 additions and 286 deletions.
139 changes: 122 additions & 17 deletions controllers/account/controllers/debt_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ import (
"fmt"
"math"
"os"
"reflect"
runtime2 "runtime"
"strconv"
"strings"
"time"

"github.com/volcengine/volc-sdk-golang/service/vms"

"github.com/labring/sealos/controllers/pkg/pay"

"sigs.k8s.io/controller-runtime/pkg/handler"
Expand Down Expand Up @@ -71,9 +75,18 @@ const (

SMSAccessKeyIDEnv = "SMS_AK"
SMSAccessKeySecretEnv = "SMS_SK"
VmsAccessKeyIDEnv = "VMS_AK"
VmsAccessKeySecretEnv = "VMS_SK"
SMSEndpointEnv = "SMS_ENDPOINT"
SMSSignNameEnv = "SMS_SIGN_NAME"
SMSCodeMapEnv = "SMS_CODE_MAP"
VmsCodeMapEnv = "VMS_CODE_MAP"
VmsNumberPollEnv = "VMS_NUMBER_POLL"
SMTPHostEnv = "SMTP_HOST"
SMTPPortEnv = "SMTP_PORT"
SMTPFromEnv = "SMTP_FROM"
SMTPPasswordEnv = "SMTP_PASSWORD"
SMTPTitleEnv = "SMTP_TITLE"
)

// DebtReconciler reconciles a Debt object
Expand All @@ -86,6 +99,13 @@ type DebtReconciler struct {
logr.Logger
accountSystemNamespace string
SmsConfig *SmsConfig
VmsConfig *VmsConfig
smtpConfig *utils.SMTPConfig
}

type VmsConfig struct {
TemplateCode map[int]string
NumberPoll string
}

type SmsConfig struct {
Expand Down Expand Up @@ -150,7 +170,7 @@ func (r *DebtReconciler) reconcile(ctx context.Context, owner string) error {
return nil
}
}
r.Logger.Error(fmt.Errorf("account %s not exist", owner), "account not exist")
r.Logger.Error(fmt.Errorf("account %s not exist", owner), err.Error())
return ErrAccountNotExist
}
if account.CreateRegionID == "" {
Expand Down Expand Up @@ -430,6 +450,11 @@ var TitleTemplateEN = map[int]string{

var NoticeTemplateZH map[int]string

var (
forbidTimes = []string{"00:00-10:00", "20:00-24:00"}
UTCPlus8 = time.FixedZone("UTC+8", 8*3600)
)

func (r *DebtReconciler) sendSMSNotice(user string, oweAmount int64, noticeType int) error {
if r.SmsConfig == nil {
return nil
Expand All @@ -438,18 +463,58 @@ func (r *DebtReconciler) sendSMSNotice(user string, oweAmount int64, noticeType
if err != nil {
return fmt.Errorf("failed to get user oauth provider: %w", err)
}
if outh == nil || outh.ProviderID == "" || outh.ProviderType != pkgtypes.OauthProviderTypePhone {
r.Logger.Info("user not exist or user phone is empty, skip sms notification", "user", user)
phone, email := "", ""
for i := range outh {
if outh[i].ProviderType == pkgtypes.OauthProviderTypePhone {
phone = outh[i].ProviderID
} else if outh[i].ProviderType == pkgtypes.OauthProviderTypeEmail {
email = outh[i].ProviderID
}
}
if phone == "" && email == "" {
r.Logger.Info("user phone && email is not set, skip sms notification", "user", user)
return nil
}
oweamount := strconv.FormatInt(int64(math.Abs(math.Ceil(float64(oweAmount)/1_000_000))), 10)
return utils.SendSms(r.SmsConfig.Client, &client2.SendSmsRequest{
PhoneNumbers: tea.String(outh.ProviderID),
err = utils.SendSms(r.SmsConfig.Client, &client2.SendSmsRequest{
PhoneNumbers: tea.String(phone),
SignName: tea.String(r.SmsConfig.SmsSignName),
TemplateCode: tea.String(r.SmsConfig.SmsCode[noticeType]),
// |ownAmount/1_000_000|
TemplateParam: tea.String("{\"user_id\":\"" + user + "\",\"oweamount\":\"" + oweamount + "\"}"),
})
if err != nil {
return fmt.Errorf("failed to send sms notice: %w", err)
}
if noticeType == WarningNotice {
err = utils.SendVms(phone, r.VmsConfig.TemplateCode[noticeType], r.VmsConfig.NumberPoll, GetSendVmsTimeInUTCPlus8(time.Now()), forbidTimes)
if err != nil {
return fmt.Errorf("failed to send vms notice: %w", err)
}
if r.smtpConfig != nil {
err = r.smtpConfig.SendEmail(NoticeTemplateZH[noticeType], email)
if err != nil {
return fmt.Errorf("failed to send email notice: %w", err)
}
}
}
return nil
}

// GetSendVmsTimeInUTCPlus8 send vms time in UTC+8 10:00-20:00
func GetSendVmsTimeInUTCPlus8(t time.Time) time.Time {
nowInUTCPlus8 := t.In(UTCPlus8)
hour := nowInUTCPlus8.Hour()
if hour >= 10 && hour < 20 {
return t
}
var next10AM time.Time
if hour < 10 {
next10AM = time.Date(nowInUTCPlus8.Year(), nowInUTCPlus8.Month(), nowInUTCPlus8.Day(), 10, 0, 0, 0, UTCPlus8)
} else {
next10AM = time.Date(nowInUTCPlus8.Year(), nowInUTCPlus8.Month(), nowInUTCPlus8.Day()+1, 10, 0, 0, 0, UTCPlus8)
}
return next10AM.In(time.Local)
}

func (r *DebtReconciler) readNotice(ctx context.Context, namespaces []string, noticeTypes ...int) error {
Expand Down Expand Up @@ -572,26 +637,62 @@ func splitSmsCodeMap(codeStr string) (map[int]string, error) {
return codeMap, nil
}

func setupSmsConfig() (*SmsConfig, error) {
func (r *DebtReconciler) setupSmsConfig() error {
if err := env.CheckEnvSetting([]string{SMSAccessKeyIDEnv, SMSAccessKeySecretEnv, SMSEndpointEnv, SMSSignNameEnv, SMSCodeMapEnv}); err != nil {
return nil, fmt.Errorf("check env setting error: %w", err)
return fmt.Errorf("check env setting error: %w", err)
}

smsCodeMap, err := splitSmsCodeMap(os.Getenv(SMSCodeMapEnv))
if err != nil {
return nil, fmt.Errorf("split sms code map error: %w", err)
return fmt.Errorf("split sms code map error: %w", err)
}

smsClient, err := utils.CreateSMSClient(os.Getenv(SMSAccessKeyIDEnv), os.Getenv(SMSAccessKeySecretEnv), os.Getenv(SMSEndpointEnv))
if err != nil {
return nil, fmt.Errorf("create sms client error: %w", err)
return fmt.Errorf("create sms client error: %w", err)
}

return &SmsConfig{
r.SmsConfig = &SmsConfig{
Client: smsClient,
SmsSignName: os.Getenv(SMSSignNameEnv),
SmsCode: smsCodeMap,
}, nil
}
return nil
}

func (r *DebtReconciler) setupVmsConfig() error {
if err := env.CheckEnvSetting([]string{VmsAccessKeyIDEnv, VmsAccessKeySecretEnv, VmsNumberPollEnv}); err != nil {
return fmt.Errorf("check env setting error: %w", err)
}
vms.DefaultInstance.Client.SetAccessKey(os.Getenv(VmsAccessKeyIDEnv))
vms.DefaultInstance.Client.SetSecretKey(os.Getenv(VmsAccessKeySecretEnv))

vmsCodeMap, err := splitSmsCodeMap(os.Getenv(VmsCodeMapEnv))
if err != nil {
return fmt.Errorf("split vms code map error: %w", err)
}
r.VmsConfig = &VmsConfig{
TemplateCode: vmsCodeMap,
NumberPoll: os.Getenv(VmsNumberPollEnv),
}
return nil
}

func (r *DebtReconciler) setupSMTPConfig() error {
if err := env.CheckEnvSetting([]string{SMTPHostEnv, SMTPPortEnv, SMTPFromEnv, SMTPPasswordEnv, SMTPTitleEnv}); err != nil {
return fmt.Errorf("check env setting error: %w", err)
}
serverPort, err := strconv.Atoi(os.Getenv(SMTPPortEnv))
if err != nil {
return fmt.Errorf("invalid smtp port: %w", err)
}
r.smtpConfig = &utils.SMTPConfig{
ServerHost: os.Getenv(SMTPHostEnv),
ServerPort: serverPort,
FromEmail: os.Getenv(SMTPFromEnv),
Passwd: os.Getenv(SMTPPasswordEnv),
EmailTitle: SMTPTitleEnv,
}
return nil
}

// SetupWithManager sets up the controller with the Manager.
Expand All @@ -602,11 +703,15 @@ func (r *DebtReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.
debtDetectionCycleSecond := env.GetInt64EnvWithDefault(DebtDetectionCycleEnv, 1800)
r.DebtDetectionCycle = time.Duration(debtDetectionCycleSecond) * time.Second

smsConfig, err := setupSmsConfig()
if err != nil {
r.Logger.Error(err, "Failed to set up SMS configuration")
} else {
r.SmsConfig = smsConfig
setupList := []func() error{
r.setupSmsConfig,
r.setupVmsConfig,
r.setupSMTPConfig,
}
for i := range setupList {
if err := setupList[i](); err != nil {
r.Logger.Error(err, fmt.Sprintf("failed to set up %s", runtime2.FuncForPC(reflect.ValueOf(setupList[i]).Pointer()).Name()))
}
}

/*
Expand Down
14 changes: 14 additions & 0 deletions controllers/account/controllers/debt_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package controllers

import (
"testing"
"time"
)

func Test_splitSmsCodeMap(t *testing.T) {
Expand All @@ -37,3 +38,16 @@ func Test_splitSmsCodeMap(t *testing.T) {
t.Fatal("invalid codeMap")
}
}

func TestGetTimeInUTCPlus8(t *testing.T) {
t1 := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
t2 := time.Date(2023, 1, 1, 1, 0, 0, 0, time.UTC)
t3 := time.Date(2023, 1, 1, 9, 0, 0, 0, time.UTC)
t4 := time.Date(2023, 1, 1, 11, 0, 0, 0, time.UTC)
t5 := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)
t6 := time.Date(2023, 1, 1, 13, 0, 0, 0, time.UTC)
t7 := time.Date(2023, 1, 1, 23, 0, 0, 0, time.UTC)
for _, _t := range []time.Time{t1, t2, t3, t4, t5, t6, t7} {
t.Logf("time: %v, timeInUTCPlus8: %v", _t, GetSendVmsTimeInUTCPlus8(_t))
}
}
35 changes: 35 additions & 0 deletions controllers/account/controllers/utils/email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright © 2024 sealos.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package utils

import "github.com/go-gomail/gomail"

type SMTPConfig struct {
ServerHost string
ServerPort int
FromEmail string
Passwd string
EmailTitle string
}

func (c *SMTPConfig) SendEmail(emailBody, to string) error {
m := gomail.NewMessage()
m.SetHeader("To", to)
m.SetAddressHeader("From", c.FromEmail, c.EmailTitle)
m.SetHeader("Subject", c.EmailTitle)
m.SetBody("text/html", emailBody)
d := gomail.NewDialer(c.ServerHost, c.ServerPort, c.FromEmail, c.Passwd)
return d.DialAndSend(m)
}
59 changes: 59 additions & 0 deletions controllers/account/controllers/utils/vms.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright © 2024 sealos.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package utils

import (
"fmt"
"time"

"github.com/astaxie/beego/logs"
"github.com/volcengine/volc-sdk-golang/service/vms"
)

func SendVms(phone, template, numberPollNo string, sendTime time.Time, forbidTimes []string) error {
var paramList []*vms.SingleParam
paramList = append(paramList, &vms.SingleParam{
Phone: phone,
Type: 1,
//RingAgainTimes: 1,
//RingAgainInterval: 5,
TriggerTime: &vms.JsonTime{Time: sendTime},
Resource: template,
NumberPoolNo: numberPollNo,
SingleOpenId: phone + "-" + sendTime.Format("2006-01-02"),
})
if len(forbidTimes) != 0 {
paramList[0].ForbidTimeList = []*vms.ForbidTimeItem{
{
Times: forbidTimes,
},
}
}
req := &vms.SingleAppendRequest{
List: paramList,
}
result, statusCode, err := vms.DefaultInstance.SingleBatchAppend(req)
if err != nil {
return fmt.Errorf("failed to SingleBatchAppend: %v", err)
}
if result.ResponseMetadata.Error != nil {
return fmt.Errorf("failed to send vms: %v", result.ResponseMetadata.Error)
}
logs.Info("send vms status code: %d, result: %#+v", statusCode, result.Result)
if statusCode != 200 {
return fmt.Errorf("failed to send vms, status code: %d, err : %v", statusCode, result)
}
return nil
}
44 changes: 44 additions & 0 deletions controllers/account/controllers/utils/vms_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright © 2024 sealos.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package utils

import (
"os"
"testing"
"time"

"github.com/volcengine/volc-sdk-golang/service/vms"
)

func TestSendVms(t *testing.T) {
vms.DefaultInstance.Client.SetAccessKey(os.Getenv("VMS_AK"))
vms.DefaultInstance.Client.SetSecretKey(os.Getenv("VMS_SK"))
var testData = struct {
phone string
template string
numberPollNo string
sendTime time.Time
}{
phone: "",
template: "",
numberPollNo: "",
sendTime: time.Now(),
}
err := SendVms(testData.phone, testData.template, testData.numberPollNo, testData.sendTime, []string{"10:00-20:00"})
if err != nil {
t.Fatal(err)
}
t.Log("SendVms success")
}
Loading

0 comments on commit 1ad3c71

Please sign in to comment.