Skip to content

Commit

Permalink
feat: generic webhook
Browse files Browse the repository at this point in the history
Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>
  • Loading branch information
arbreezy committed Jun 8, 2023
1 parent 7141cc1 commit bb9e008
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 17 deletions.
16 changes: 10 additions & 6 deletions api/v1alpha1/k8sgpt_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ type ExtraOptionsRef struct {
Backstage *Backstage `json:"backstage,omitempty"`
}

type WebhookRef struct {
Type string `json:"type,omitempty"`
Endpoint string `json:"webhook,omitempty"`
}

// K8sGPTSpec defines the desired state of K8sGPT
type K8sGPTSpec struct {
// +kubebuilder:default:=openai
// +kubebuilder:validation:Enum=openai;localai;azureopenai
Backend `json:"backend"`
Backend string `json:"backend"`
BaseUrl string `json:"baseUrl,omitempty"`
// +kubebuilder:default:=gpt-3.5-turbo
Model string `json:"model,omitempty"`
Expand All @@ -51,14 +56,13 @@ type K8sGPTSpec struct {
NoCache bool `json:"noCache,omitempty"`
Filters []string `json:"filters,omitempty"`
ExtraOptions *ExtraOptionsRef `json:"extraOptions,omitempty"`
Sink *WebhookRef `json:"sink,omitempty"`
}

type Backend string

const (
OpenAI Backend = "openai"
AzureOpenAI Backend = "azureopenai"
LocalAI Backend = "localai"
OpenAI = "openai"
AzureOpenAI = "azureopenai"
LocalAI = "localai"
)

// K8sGPTStatus defines the observed state of K8sGPT
Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha1/result_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type Sensitive struct {

// ResultSpec defines the desired state of Result
type ResultSpec struct {
Backend `json:"backend"`
Backend string `json:"backend"`
Kind string `json:"kind"`
Name string `json:"name"`
Error []Failure `json:"error"`
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

4 changes: 3 additions & 1 deletion chart/operator/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ spec:
env:
- name: KUBERNETES_CLUSTER_DOMAIN
value: {{ quote .Values.kubernetesClusterDomain }}
- name: OPERATOR_SINK_WEBHOOK_TIMEOUT_SECONDS
value: {{ quote .Values.controllerManager.manager.sinkWebhookTimeout }}
image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag
| default .Chart.AppVersion }}
livenessProbe:
Expand All @@ -99,4 +101,4 @@ spec:
securityContext:
runAsNonRoot: true
serviceAccountName: {{ include "chart.fullname" . }}-controller-manager
terminationGracePeriodSeconds: 10
terminationGracePeriodSeconds: 10
1 change: 1 addition & 0 deletions chart/operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ controllerManager:
cpu: 5m
memory: 64Mi
manager:
sinkWebhookTimeout: 30s
containerSecurityContext:
allowPrivilegeEscalation: false
capabilities:
Expand Down
7 changes: 7 additions & 0 deletions config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ spec:
name:
type: string
type: object
sink:
properties:
type:
type: string
webhook:
type: string
type: object
version:
type: string
required:
Expand Down
28 changes: 21 additions & 7 deletions controllers/k8sgpt_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
kclient "github.com/k8sgpt-ai/k8sgpt-operator/pkg/client"
"github.com/k8sgpt-ai/k8sgpt-operator/pkg/integrations"
"github.com/k8sgpt-ai/k8sgpt-operator/pkg/resources"
"github.com/k8sgpt-ai/k8sgpt-operator/pkg/sinks"
"github.com/k8sgpt-ai/k8sgpt-operator/pkg/utils"
"github.com/prometheus/client_golang/prometheus"
v1 "k8s.io/api/apps/v1"
Expand Down Expand Up @@ -71,6 +72,7 @@ type K8sGPTReconciler struct {
client.Client
Scheme *runtime.Scheme
Integrations *integrations.Integrations
SinkClient *sinks.Client
K8sGPTClient *kclient.Client
// This is a map of clients for each deployment
k8sGPTClients map[string]*kclient.Client
Expand Down Expand Up @@ -126,6 +128,13 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return r.finishReconcile(nil, false)
}

// Configure sink Webhook
var sinkType sinks.ISink
if k8sgptConfig.Spec.Sink != nil && k8sgptConfig.Spec.Sink.Type != "" {
sinkType = sinks.NewSink(k8sgptConfig.Spec.Sink.Type)
sinkType.Configure(*k8sgptConfig, *r.SinkClient)
}

// Check and see if the instance is new or has a K8sGPT deployment in flight
deployment := v1.Deployment{}
err = r.Get(ctx, client.ObjectKey{Namespace: k8sgptConfig.Namespace,
Expand Down Expand Up @@ -267,6 +276,7 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
k8sgptReconcileErrorCount.Inc()
return r.finishReconcile(err, false)
} else {
sinkType.Emit(result.Spec)
k8sgptNumberOfResultsByType.With(prometheus.Labels{
"kind": result.Spec.Kind,
"name": result.Name,
Expand All @@ -277,13 +287,17 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return r.finishReconcile(err, false)
}
} else {
// If the result already exists we will update it
existingResult.Spec = result.Spec
existingResult.Labels = result.Labels
err = r.Update(ctx, &existingResult)
if err != nil {
k8sgptReconcileErrorCount.Inc()
return r.finishReconcile(err, false)
// If the result error and solution has changed, we will update CR
updateResult := existingResult.Spec.Details != result.Spec.Details || existingResult.Spec.Name != result.Spec.Name || existingResult.Spec.Backend != result.Spec.Backend
if updateResult {
existingResult.Spec = result.Spec
existingResult.Labels = result.Labels
err = r.Update(ctx, &existingResult)
if err != nil {
k8sgptReconcileErrorCount.Inc()
return r.finishReconcile(err, false)
}
sinkType.Emit(existingResult.Spec)
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"context"
"flag"
"os"
"time"

// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
Expand All @@ -25,6 +26,7 @@ import (
corev1alpha1 "github.com/k8sgpt-ai/k8sgpt-operator/api/v1alpha1"
"github.com/k8sgpt-ai/k8sgpt-operator/controllers"
"github.com/k8sgpt-ai/k8sgpt-operator/pkg/integrations"
"github.com/k8sgpt-ai/k8sgpt-operator/pkg/sinks"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
Expand Down Expand Up @@ -93,10 +95,23 @@ func main() {
os.Exit(1)
}

timeout, exists := os.LookupEnv("OPERATOR_SINK_WEBHOOK_TIMEOUT_SECONDS")
if !exists {
timeout = "35s"
}

sinkTimeout, err := time.ParseDuration(timeout)
if err != nil {
setupLog.Error(err, "unable to read webhook timeout value")
os.Exit(1)
}
sinkClient := sinks.NewClient(sinkTimeout)

if err = (&controllers.K8sGPTReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Integrations: integration,
SinkClient: sinkClient,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "K8sGPT")
os.Exit(1)
Expand Down
4 changes: 2 additions & 2 deletions pkg/resources/k8sgpt.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,15 +262,15 @@ func GetDeployment(config v1alpha1.K8sGPT) (*appsv1.Deployment, error) {
)
}
// Engine is required only when azureopenai is the ai backend
if config.Spec.Engine != "" && config.Spec.Backend == v1alpha1.AzureOpenAI {
if config.Spec.Engine != "" && config.Spec.Backend == "azureopenai" {
engine := v1.EnvVar{
Name: "K8SGPT_ENGINE",
Value: config.Spec.Engine,
}
deployment.Spec.Template.Spec.Containers[0].Env = append(
deployment.Spec.Template.Spec.Containers[0].Env, engine,
)
} else if config.Spec.Engine != "" && config.Spec.Backend != v1alpha1.AzureOpenAI {
} else if config.Spec.Engine != "" && config.Spec.Backend != "azureopenai" {
return &appsv1.Deployment{}, err.New("Engine is supported only by azureopenai provider.")
}
return &deployment, nil
Expand Down
36 changes: 36 additions & 0 deletions pkg/sinks/sinkreporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package sinks

import (
"net/http"
"time"

"github.com/k8sgpt-ai/k8sgpt-operator/api/v1alpha1"
)

type ISink interface {
Configure(config v1alpha1.K8sGPT, c Client)
Emit(results v1alpha1.ResultSpec) error
}

func NewSink(sinkType string) ISink {
switch sinkType {
case "slack":
return &SlackSink{}
//Introduce more Sink Providers
default:
return &SlackSink{}
}
}

type Client struct {
hclient *http.Client
}

func NewClient(timeout time.Duration) *Client {
client := &http.Client{
Timeout: timeout,
}
return &Client{
hclient: client,
}
}
81 changes: 81 additions & 0 deletions pkg/sinks/slack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package sinks

import (
"bytes"
"encoding/json"
"fmt"
"net/http"

"github.com/k8sgpt-ai/k8sgpt-operator/api/v1alpha1"
)

var _ ISink = (*SlackSink)(nil)

type SlackSink struct {
Endpoint string
Client Client
}

type SlackMessage struct {
Text string `json:"text"`
Attachments []Attachment `json:"attachments"`
}

type Attachment struct {
Type string `json:"type"`
Text string `json:"text"`
Color string `json:"color"`
Title string `json:"title"`
}

func buildSlackMessage(kind, name, details, backend string) SlackMessage {
return SlackMessage{
Text: fmt.Sprintf("`Analysis from %s of the %s %s`", backend, kind, name),
Attachments: []Attachment{
Attachment{
Type: "mrkdwn",
Text: details,
Color: "danger",
Title: "Report",
},
},
}
}

func (s *SlackSink) Configure(config v1alpha1.K8sGPT, c Client) {
if config.Spec.Sink == nil {
s.Endpoint = ""
}
s.Endpoint = config.Spec.Sink.Endpoint
s.Client = c
}

func (s *SlackSink) Emit(results v1alpha1.ResultSpec) error {
if s.Endpoint == "" {
// emit nothing
return nil
}

message := buildSlackMessage(results.Kind, results.Name, results.Details, results.Backend)
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
}

0 comments on commit bb9e008

Please sign in to comment.