diff --git a/README.md b/README.md index be8e15dd..c0386021 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,19 @@ EOF +
+sink (integrations) + +Optional parameters available for sink. +('type', 'webhook' are required parameters.) + +| tool | channel | icon_url | username | +|------------|---------|----------|----------| +| Slack | | | | +| Mattermost | ✔️ | ✔️ | ✔️ | + +
+ ## Helm values For details please see [here](chart/operator/values.yaml) diff --git a/api/v1alpha1/k8sgpt_types.go b/api/v1alpha1/k8sgpt_types.go index 3c4a1ba0..d74065e6 100644 --- a/api/v1alpha1/k8sgpt_types.go +++ b/api/v1alpha1/k8sgpt_types.go @@ -64,9 +64,12 @@ type GCSBackend struct { } type WebhookRef struct { - // +kubebuilder:validation:Enum=slack + // +kubebuilder:validation:Enum=slack;mattermost Type string `json:"type,omitempty"` Endpoint string `json:"webhook,omitempty"` + Channel string `json:"channel,omitempty"` + UserName string `json:"username,omitempty"` + IconURL string `json:"icon_url,omitempty"` } type AISpec struct { diff --git a/chart/operator/templates/k8sgpt-crd.yaml b/chart/operator/templates/k8sgpt-crd.yaml index 673f660b..3b2664f9 100644 --- a/chart/operator/templates/k8sgpt-crd.yaml +++ b/chart/operator/templates/k8sgpt-crd.yaml @@ -145,9 +145,16 @@ spec: type: string sink: properties: + channel: + type: string + icon_url: + type: string type: enum: - slack + - mattermost + type: string + username: type: string webhook: type: string diff --git a/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml b/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml index 3af7f828..f5bd1da7 100644 --- a/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml +++ b/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml @@ -145,9 +145,16 @@ spec: type: string sink: properties: + channel: + type: string + icon_url: + type: string type: enum: - slack + - mattermost + type: string + username: type: string webhook: type: string diff --git a/pkg/sinks/mattermost.go b/pkg/sinks/mattermost.go new file mode 100644 index 00000000..5cc6d5d1 --- /dev/null +++ b/pkg/sinks/mattermost.go @@ -0,0 +1,107 @@ +package sinks + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "github.com/k8sgpt-ai/k8sgpt-operator/api/v1alpha1" +) + +var _ ISink = (*MattermostSink)(nil) + +type MattermostSink struct { + Endpoint string + K8sGPT string + Client Client + Channel string + UserName string + IconURL string +} + +type MattermostMessage struct { + Text string `json:"text"` + Channel string `json:"channel,omitempty"` + UserName string `json:"username,omitempty"` + IconURL string `json:"icon_url,omitempty"` + Attachments []attachment `json:"attachments"` +} + +type attachment struct { + Text string `json:"text"` + Color string `json:"color"` + Title string `json:"title"` +} + +func buildMattermostMessage(kind, name, details, k8sgptCR, channel, username, iconURL string) MattermostMessage { + return MattermostMessage{ + Text: fmt.Sprintf(">*[%s] K8sGPT analysis of the %s %s*", k8sgptCR, kind, name), + Channel: channel, + UserName: username, + IconURL: iconURL, + Attachments: []attachment{ + attachment{ + Text: details, + Color: "danger", + Title: "Report", + }, + }, + } +} + +func (s *MattermostSink) Configure(config v1alpha1.K8sGPT, c Client) { + s.Endpoint = config.Spec.Sink.Endpoint + // If no value is given, the default value of the webhook is used + if config.Spec.Sink.Channel != "" { + s.Channel = config.Spec.Sink.Channel + } + // If no value is given, the default value of the webhook is used + if config.Spec.Sink.UserName != "" { + s.UserName = config.Spec.Sink.UserName + } + // If no value is given, the default value of the webhook is used + if config.Spec.Sink.IconURL != "" { + s.IconURL = config.Spec.Sink.IconURL + } + s.Client = c + // take the name of the K8sGPT Custom Resource + s.K8sGPT = config.Name +} + +func (s *MattermostSink) Emit(results v1alpha1.ResultSpec) error { + details := "" + // If AI is set to False, Details will not have a value, so if it is empty, use the Error text. + if results.Details == "" && len(results.Error) > 0 { + for i, v := range results.Error { + details += fmt.Sprintf("%d. %s\n", i+1, v.Text) + } + } else { + details = results.Details + } + message := buildMattermostMessage( + results.Kind, results.Name, details, s.K8sGPT, + s.Channel, s.UserName, s.IconURL, + ) + payload, err := json.Marshal(message) + if err != nil { + return err + } + req, err := http.NewRequest(http.MethodPost, s.Endpoint, bytes.NewBuffer(payload)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + resp, err := s.Client.hclient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to send report: %s", resp.Status) + } + + return nil +} diff --git a/pkg/sinks/sinkreporter.go b/pkg/sinks/sinkreporter.go index 33d41232..2331b7e0 100644 --- a/pkg/sinks/sinkreporter.go +++ b/pkg/sinks/sinkreporter.go @@ -16,7 +16,9 @@ func NewSink(sinkType string) ISink { switch sinkType { case "slack": return &SlackSink{} - //Introduce more Sink Providers + //Introduce more Sink Providers + case "mattermost": + return &MattermostSink{} default: return &SlackSink{} }