Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generic webhook #149

Merged
merged 11 commits into from
Jun 15, 2023
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"`
arbreezy marked this conversation as resolved.
Show resolved Hide resolved
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",
AlexsJones marked this conversation as resolved.
Show resolved Hide resolved
Title: "Report",
},
},
}
}

func (s *SlackSink) Configure(config v1alpha1.K8sGPT, c Client) {
AlexsJones marked this conversation as resolved.
Show resolved Hide resolved
if config.Spec.Sink == nil {
s.Endpoint = ""
}
s.Endpoint = config.Spec.Sink.Endpoint
s.Client = c
}

func (s *SlackSink) Emit(results v1alpha1.ResultSpec) error {
AlexsJones marked this conversation as resolved.
Show resolved Hide resolved
if s.Endpoint == "" {
arbreezy marked this conversation as resolved.
Show resolved Hide resolved
// 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
}