From d7cbfca909a58bb62f754aa6d1b48bc66c72ca84 Mon Sep 17 00:00:00 2001 From: Xiao Liu <46879761+liangzai006@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:39:03 +0800 Subject: [PATCH] add email message send --- .github/workflows/ci.yml | 69 ------------ .github/workflows/job.yml | 2 +- chart/kubeeye/crds/inspectplan-crd.yaml | 13 ++- chart/kubeeye/crds/inspecttask-crd.yaml | 11 +- chart/kubeeye/templates/deployment.yaml | 7 +- chart/kubeeye/values.yaml | 18 +++- deploy/kubeeye_message_secret.yaml | 0 deploy/kubeeye_v1alpha2_inspectplan.yaml | 3 - pkg/conf/conf.go | 5 +- pkg/controllers/inspectplan_controller.go | 4 +- pkg/controllers/inspectresult_controller.go | 57 +++++----- pkg/message/alarm_message.go | 2 - pkg/message/email_message.go | 114 ++++++++++++++++++-- pkg/output/html.go | 6 +- pkg/rules/rules.go | 6 +- pkg/utils/utils.go | 12 ++- 16 files changed, 201 insertions(+), 128 deletions(-) delete mode 100644 .github/workflows/ci.yml delete mode 100644 deploy/kubeeye_message_secret.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 0e92bc83..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: CI - -on: - push: - branches: - - 'main' - pull_request: - branches: - - 'main' - -jobs: - build-test: - name: build-test - runs-on: ubuntu-latest - if: github.repository == 'kubesphere/kubeeye' - env: - GO111MODULE: on - steps: - - name: Set up Go 1.17 - uses: actions/setup-go@v2 - with: - go-version: 1.17 - - - name: Check out code into the Go module directory - uses: actions/checkout@v3 - with: - fetch-depth: 1 - - - name: Downloading go dependencies - run: go mod tidy - - - name: sync the vendor directory - run: go mod vendor - - - name: Build command-line tool - run: go install -v ./cmd/... - - - name: Creating kind cluster - uses: helm/kind-action@v1.2.0 - - - name: Run KubeEye - run: | - ke audit - - push-image-to-dockerhub: - runs-on: ubuntu-latest - needs: [build-test] - if: github.event_name == 'push' - - steps: - - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - with: - platforms: all - - - name: Set up Docker buildx - uses: docker/setup-buildx-action@v1 - - - name: Login to Docker Hub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - - name: Push image - run: | - REPO=kubesphere make container-cross-push diff --git a/.github/workflows/job.yml b/.github/workflows/job.yml index 01afa939..cca2f9e7 100644 --- a/.github/workflows/job.yml +++ b/.github/workflows/job.yml @@ -77,4 +77,4 @@ jobs: file: build/job/Dockerfile platforms: linux/amd64 push: true - tags: jw008/kubeeye:latest \ No newline at end of file + tags: jw008/kubeeye-job:latest \ No newline at end of file diff --git a/chart/kubeeye/crds/inspectplan-crd.yaml b/chart/kubeeye/crds/inspectplan-crd.yaml index a2ae78ab..e47cd7c0 100644 --- a/chart/kubeeye/crds/inspectplan-crd.yaml +++ b/chart/kubeeye/crds/inspectplan-crd.yaml @@ -50,11 +50,18 @@ spec: one: format: date-time type: string - ruleGroup: - type: string ruleNames: items: - type: string + properties: + name: + type: string + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + type: object type: array schedule: type: string diff --git a/chart/kubeeye/crds/inspecttask-crd.yaml b/chart/kubeeye/crds/inspecttask-crd.yaml index 639ff35e..78e5a620 100644 --- a/chart/kubeeye/crds/inspecttask-crd.yaml +++ b/chart/kubeeye/crds/inspecttask-crd.yaml @@ -47,7 +47,16 @@ spec: type: string ruleNames: items: - type: string + properties: + name: + type: string + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + type: object type: array timeout: type: string diff --git a/chart/kubeeye/templates/deployment.yaml b/chart/kubeeye/templates/deployment.yaml index 1639024b..ef1364cd 100644 --- a/chart/kubeeye/templates/deployment.yaml +++ b/chart/kubeeye/templates/deployment.yaml @@ -121,6 +121,8 @@ spec: volumeMounts: - mountPath: /hosts/result name: inspect-result + - mountPath: /etc/localtime + name: localtime securityContext: runAsNonRoot: true serviceAccountName: {{ include "kubeeye.fullname" . }}-controller-manager @@ -128,4 +130,7 @@ spec: volumes: - name: inspect-result persistentVolumeClaim: - claimName: {{ include "kubeeye.fullname" . }}-inspect-result \ No newline at end of file + claimName: {{ include "kubeeye.fullname" . }}-inspect-result + - hostPath: + path: /etc/localtime + name: localtime \ No newline at end of file diff --git a/chart/kubeeye/values.yaml b/chart/kubeeye/values.yaml index 47b019b7..7d6eb18d 100644 --- a/chart/kubeeye/values.yaml +++ b/chart/kubeeye/values.yaml @@ -33,10 +33,20 @@ config: requests: cpu: 50m memory: 256Mi - multiCluster: - member1: - image: kubespheredev/kubeeye-job:v1.0.0-beta.8 - imagePullPolicy: Always +# multiCluster: +# member1: +# image: kubespheredev/kubeeye-job:v1.0.0-beta.8 +# imagePullPolicy: Always +# message: +# enable: false +# mode: 发送模式(完成complete或异常abnormal) ,默认是异常 +# email: +# address: 邮箱服务器地址 +# port: 25 +# fo: 发送人邮箱 +# to: +# - 接受人邮箱 +# secretKey: 存储用户名(username)密码(password)的secret的Name controllerManager: kubeRbacProxy: args: diff --git a/deploy/kubeeye_message_secret.yaml b/deploy/kubeeye_message_secret.yaml deleted file mode 100644 index e69de29b..00000000 diff --git a/deploy/kubeeye_v1alpha2_inspectplan.yaml b/deploy/kubeeye_v1alpha2_inspectplan.yaml index 59f28cb1..f482ba7f 100644 --- a/deploy/kubeeye_v1alpha2_inspectplan.yaml +++ b/deploy/kubeeye_v1alpha2_inspectplan.yaml @@ -6,7 +6,4 @@ spec: maxTasks: 10 ruleNames: - name: inspect-rule-namespace - nodeSelector: - node-role.kubernetes.io/master: "" - name: inspect-rule-systemd - nodeName: node1 \ No newline at end of file diff --git a/pkg/conf/conf.go b/pkg/conf/conf.go index ec0d9fed..550d1a94 100644 --- a/pkg/conf/conf.go +++ b/pkg/conf/conf.go @@ -60,7 +60,7 @@ type MessageConfig struct { type EmailConfig struct { Address string `json:"address,omitempty"` Port int32 `json:"port,omitempty"` - Fo string `json:"form,omitempty"` + Fo string `json:"fo,omitempty"` To []string `json:"to,omitempty"` SecretKey string `json:"secretKey,omitempty"` } @@ -98,9 +98,8 @@ func (j *JobConfig) DeepCopy() *JobConfig { } type MessageEvent struct { + Title string Content []byte - Target string - Sender string Timestamp time.Time } diff --git a/pkg/controllers/inspectplan_controller.go b/pkg/controllers/inspectplan_controller.go index 9c5f7367..2259e55c 100644 --- a/pkg/controllers/inspectplan_controller.go +++ b/pkg/controllers/inspectplan_controller.go @@ -117,7 +117,7 @@ func (r *InspectPlanReconciler) Reconcile(ctx context.Context, req ctrl.Request) } if inspectPlan.Spec.Once != nil { - if !utils.IsEmptyString(inspectPlan.Status.LastTaskName) { + if !utils.IsEmptyValue(inspectPlan.Status.LastTaskName) { return ctrl.Result{}, nil } if !inspectPlan.Spec.Once.After(time.Now()) { @@ -139,7 +139,7 @@ func (r *InspectPlanReconciler) Reconcile(ctx context.Context, req ctrl.Request) } if inspectPlan.Spec.Schedule == nil { - if !utils.IsEmptyString(inspectPlan.Status.LastTaskName) { + if !utils.IsEmptyValue(inspectPlan.Status.LastTaskName) { return ctrl.Result{}, nil } taskName, err := r.createInspectTask(inspectPlan, ctx) diff --git a/pkg/controllers/inspectresult_controller.go b/pkg/controllers/inspectresult_controller.go index c73c4059..4e2ab6ae 100644 --- a/pkg/controllers/inspectresult_controller.go +++ b/pkg/controllers/inspectresult_controller.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" kubeeyev1alpha2 "github.com/kubesphere/kubeeye/apis/kubeeye/v1alpha2" "github.com/kubesphere/kubeeye/pkg/conf" "github.com/kubesphere/kubeeye/pkg/constant" @@ -139,8 +140,7 @@ func (r *InspectResultReconciler) Reconcile(ctx context.Context, req ctrl.Reques klog.Error("Failed to update inspect result status", err) return ctrl.Result{}, err } - - r.SendMessage(ctx, result.Name) + go r.SendMessage(ctx, result) return ctrl.Result{}, nil } @@ -201,22 +201,20 @@ func totalResultLevel(data interface{}, mapLevel map[kubeeyev1alpha2.Level]*int) } for _, m := range maps { _, exist := m["assert"] - if !exist { - continue - } - v, ok := m["level"] - if !ok { - mapLevel[kubeeyev1alpha2.DangerLevel] = Autoincrement(kubeeyev1alpha2.DangerLevel) - } else { - l := v.(string) - mapLevel[kubeeyev1alpha2.Level(l)] = Autoincrement(kubeeyev1alpha2.Level(l)) + if exist { + v, ok := m["level"] + if !ok { + mapLevel[kubeeyev1alpha2.DangerLevel] = Autoincrement(kubeeyev1alpha2.DangerLevel) + } else { + l := v.(string) + mapLevel[kubeeyev1alpha2.Level(l)] = Autoincrement(kubeeyev1alpha2.Level(l)) + } } - } - } -func (r *InspectResultReconciler) SendMessage(ctx context.Context, name string) { +func (r *InspectResultReconciler) SendMessage(ctx context.Context, result *kubeeyev1alpha2.InspectResult) { + kc, err := kube.GetKubeEyeConfig(ctx, r.Client) if err != nil { klog.Error("GetKubeEyeConfig error", err) @@ -226,12 +224,19 @@ func (r *InspectResultReconciler) SendMessage(ctx context.Context, name string) return } + n := GetIssuesNumber(result) + if kc.Message.Mode == "" || kc.Message.Mode == conf.AbnormalMode { + if n == 0 { + return + } + } + klog.Info("sending email") htmlTemplate, err := template.GetInspectResultHtmlTemplate() if err != nil { klog.Error("GetInspectResultHtmlTemplate error", err) return } - err, m := output.HtmlOut(name) + err, m := output.HtmlOut(result.Name) if err != nil { klog.Error("get html render data error", err) return @@ -242,17 +247,21 @@ func (r *InspectResultReconciler) SendMessage(ctx context.Context, name string) klog.Error("render html template error", err) return } - var messageHandler conf.EventHandler - switch kc.Message.Type { - case conf.EmailMessage: - messageHandler = message.NewEmailMessageOptions(&kc.Message.Email, r.Client) - default: - klog.Error("unable identify send message type") - return - } + messageHandler := message.NewEmailMessageOptions(&kc.Message.Email, r.Client) dispatcher := message.RegisterHandler(messageHandler) dispatcher.DispatchMessageEvent(&conf.MessageEvent{ - Content: data.Bytes(), + Title: fmt.Sprintf("巡检完成,共发现%d个问题", n), + Timestamp: time.Now(), + Content: data.Bytes(), }) } + +func GetIssuesNumber(result *kubeeyev1alpha2.InspectResult) (n int) { + for _, l := range result.Status.Level { + if l != nil { + n += *l + } + } + return n +} diff --git a/pkg/message/alarm_message.go b/pkg/message/alarm_message.go index a6f44dad..d93699fc 100644 --- a/pkg/message/alarm_message.go +++ b/pkg/message/alarm_message.go @@ -2,7 +2,6 @@ package message import ( "bytes" - "fmt" "github.com/kubesphere/kubeeye/pkg/conf" "io" @@ -19,7 +18,6 @@ func (h *AlarmMessageHandler) HandleMessageEvent(event *conf.MessageEvent) { // 执行消息发送操作 // 例如,发送消息给目标 - fmt.Printf("Message sent to %s by %s: %s\n", event.Target, event.Sender, event.Content) resp, err := http.Post(h.RequestUrl, "application/json", bytes.NewReader(event.Content)) if err != nil { klog.Error(err) diff --git a/pkg/message/email_message.go b/pkg/message/email_message.go index 99e77b9f..897af0e4 100644 --- a/pkg/message/email_message.go +++ b/pkg/message/email_message.go @@ -1,14 +1,21 @@ package message import ( + "bytes" "context" + "crypto/tls" + "errors" + "fmt" "github.com/kubesphere/kubeeye/pkg/conf" "github.com/kubesphere/kubeeye/pkg/constant" + "github.com/kubesphere/kubeeye/pkg/utils" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/klog/v2" + "mime" "net/smtp" "sigs.k8s.io/controller-runtime/pkg/client" + "time" ) type EmailMessageHandler struct { @@ -25,9 +32,9 @@ func NewEmailMessageOptions(event *conf.EmailConfig, c client.Client) *EmailMess } func (e *EmailMessageHandler) HandleMessageEvent(event *conf.MessageEvent) { - // 执行消息发送操作 - // 例如,发送消息给目标 - if e != nil { + + if err := e.Vail(); err != nil { + klog.Error("failed to vail params", err) return } var secret corev1.Secret @@ -35,11 +42,102 @@ func (e *EmailMessageHandler) HandleMessageEvent(event *conf.MessageEvent) { Namespace: constant.DefaultNamespace, Name: e.SecretKey, }, &secret) - u := secret.StringData["username"] - p := secret.StringData["password"] - auth := smtp.PlainAuth("", u, p, e.Address) - err = smtp.SendMail(e.Address, auth, e.Fo, e.To, event.Content) + + if err != nil { + klog.Error("failed to get secret", err) + return + } + + auth := smtp.PlainAuth("", string(secret.Data["username"]), string(secret.Data["password"]), e.Address) + err = e.SendMsg(auth, event) + if err != nil { + klog.Error("failed to send email, err: ", err) + return + } + klog.Info("send email success") +} + +func (e *EmailMessageHandler) setMsg(me *conf.MessageEvent, to string) []byte { + buffer := &bytes.Buffer{} + _, _ = fmt.Fprintf(buffer, "From: %s\r\n", mime.QEncoding.Encode("utf-8", e.Fo)) + _, _ = fmt.Fprintf(buffer, "To: %s\r\n", mime.QEncoding.Encode("utf-8", to)) + _, _ = fmt.Fprintf(buffer, "Subject: %s\r\n", mime.QEncoding.Encode("utf-8", me.Title)) + _, _ = fmt.Fprintf(buffer, "Message-Id: %s\r\n", fmt.Sprintf("<%d.@%s>", time.Now().UnixNano(), e.Address)) + _, _ = fmt.Fprintf(buffer, "Date: %s\r\n", me.Timestamp.Format(time.RFC1123Z)) + _, _ = fmt.Fprintf(buffer, "Content-Type: text/html; charset=UTF-8;") + _, _ = fmt.Fprintf(buffer, "MIME-Version: 1.0\r\n\r\n") + _, _ = fmt.Fprintf(buffer, "%s", me.Content) + return buffer.Bytes() +} + +func (e *EmailMessageHandler) SendMsg(a smtp.Auth, eve *conf.MessageEvent) error { + dial, err := smtp.Dial(fmt.Sprintf("%s:%d", e.Address, e.Port)) + if err != nil { + return err + } + defer dial.Close() + err = dial.Hello(e.Address) + if err != nil { + return err + } + if ok, _ := dial.Extension("STARTTLS"); ok { + config := &tls.Config{ServerName: e.Address} + if err = dial.StartTLS(config); err != nil { + return err + } + } + + if a != nil { + auth, _ := dial.Extension("AUTH") + if !auth { + return errors.New("smtp: server doesn't support AUTH") + } + if err = dial.Auth(a); err != nil { + return err + } + } + if err = dial.Mail(e.Fo); err != nil { + return err + } + for _, addr := range e.To { + if err = dial.Rcpt(addr); err != nil { + return err + } + } + w, err := dial.Data() + if err != nil { + return err + } + for _, s := range e.To { + _, err = w.Write(e.setMsg(eve, s)) + if err != nil { + klog.Errorf("unable send mail to %s", s) + } + } + + err = w.Close() if err != nil { - klog.Error("send email failed, err: ", err) + return err } + return dial.Quit() +} + +func (e *EmailMessageHandler) Vail() error { + if utils.IsEmptyValue(e.Address) { + return errors.New("address is empty") + } + if e.Port == 0 { + return errors.New("port error") + } + if utils.IsEmptyValue(e.Fo) { + return errors.New("fo is empty") + } + if utils.IsEmptyValue(e.To) { + return errors.New("to is empty") + } + if utils.IsEmptyValue(e.SecretKey) { + return errors.New("secretKey is empty") + } + + return nil } diff --git a/pkg/output/html.go b/pkg/output/html.go index fa4ae91c..fffaf0f2 100644 --- a/pkg/output/html.go +++ b/pkg/output/html.go @@ -183,8 +183,10 @@ func getComponent(component []v1alpha2.ComponentResultItem) []renderNode { villeinage = append(villeinage, header) for _, c := range component { - value := []renderNode{{Text: c.Name}, {Text: c.Namespace}, {Text: c.Endpoint}} - villeinage = append(villeinage, renderNode{Children: value}) + if c.Assert { + value := []renderNode{{Text: c.Name}, {Text: c.Namespace}, {Text: c.Endpoint}} + villeinage = append(villeinage, renderNode{Children: value}) + } } return villeinage diff --git a/pkg/rules/rules.go b/pkg/rules/rules.go index 873fdbe1..dc4cb534 100644 --- a/pkg/rules/rules.go +++ b/pkg/rules/rules.go @@ -344,7 +344,7 @@ func (e *ExecuteRule) SetRuleSchedule(rules []kubeeyev1alpha2.InspectRule) (newR return r.Name == m.Name }) if isExist { - if !utils.IsEmptyString(r.NodeName) || r.NodeSelector != nil { + if !utils.IsEmptyValue(r.NodeName) || r.NodeSelector != nil { toMap := utils.StructToMap(rule.Spec) if toMap != nil { for _, v := range toMap { @@ -373,9 +373,9 @@ func (e *ExecuteRule) SetRuleSchedule(rules []kubeeyev1alpha2.InspectRule) (newR func (e *ExecuteRule) SetPrometheusEndpoint(allRule []kubeeyev1alpha2.InspectRule) []kubeeyev1alpha2.InspectRule { for i := range allRule { - if !utils.IsEmptyString(allRule[i].Spec.PrometheusEndpoint) && allRule[i].Spec.Prometheus != nil { + if !utils.IsEmptyValue(allRule[i].Spec.PrometheusEndpoint) && allRule[i].Spec.Prometheus != nil { for p := range allRule[i].Spec.Prometheus { - if utils.IsEmptyString(allRule[i].Spec.Prometheus[p].Endpoint) { + if utils.IsEmptyValue(allRule[i].Spec.Prometheus[p].Endpoint) { allRule[i].Spec.Prometheus[p].Endpoint = allRule[i].Spec.PrometheusEndpoint } } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 63d1f079..57c5b063 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -72,8 +72,16 @@ func StringToBool(b string) bool { return b == "true" } -func IsEmptyString(s string) bool { - return len(strings.TrimSpace(s)) == 0 +func IsEmptyValue(val interface{}) bool { + switch v := val.(type) { + case string: + return len(strings.TrimSpace(v)) == 0 + case *string: + return v == nil + case []string: + return v == nil + } + return false } func ArrayStructToArrayMap(obj interface{}) ([]map[string]interface{}, error) {