Skip to content

Commit

Permalink
feat: generic webhook (#149)
Browse files Browse the repository at this point in the history
* feat: generic webhook

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* chore: refactoring to catch diff use cases

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* feat: decouple emit process and add Result statuses

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* feat: add cleanup status process

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* chore: minor fix

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* fix: conditionals based on latest result object

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* fix: missing client Update

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* chore: refactor Results creation

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* chore: change status sink to webhook

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* fix: return from empty result list

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

---------

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>
Co-authored-by: Aris Boutselis <arisboutselis08@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
  • Loading branch information
3 people committed Jun 15, 2023
1 parent b4b78ff commit 4880645
Show file tree
Hide file tree
Showing 12 changed files with 317 additions and 59 deletions.
17 changes: 11 additions & 6 deletions api/v1alpha1/k8sgpt_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,17 @@ type ExtraOptionsRef struct {
Backstage *Backstage `json:"backstage,omitempty"`
}

type WebhookRef struct {
// +kubebuilder:validation:Enum=slack
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 +57,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
6 changes: 3 additions & 3 deletions 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 All @@ -42,8 +42,8 @@ type ResultSpec struct {

// ResultStatus defines the observed state of Result
type ResultStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
LifeCycle string `json:"lifecycle,omitempty"`
Webhook string `json:"webhook,omitempty"`
}

//+kubebuilder:object:root=true
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
9 changes: 9 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,15 @@ spec:
name:
type: string
type: object
sink:
properties:
type:
enum:
- slack
type: string
webhook:
type: string
type: object
version:
type: string
required:
Expand Down
5 changes: 5 additions & 0 deletions config/crd/bases/core.k8sgpt.ai_results.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ spec:
type: object
status:
description: ResultStatus defines the observed state of Result
properties:
lifecycle:
type: string
webhook:
type: string
type: object
type: object
served: true
Expand Down
106 changes: 57 additions & 49 deletions controllers/k8sgpt_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,11 @@ 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"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -71,6 +70,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 @@ -202,29 +202,11 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
}
// Parse the k8sgpt-deployment response into a list of results
k8sgptNumberOfResults.Set(float64(len(response.Results)))
rawResults := make(map[string]corev1alpha1.Result)
for _, resultSpec := range response.Results {
resultSpec.Backend = k8sgptConfig.Spec.Backend
name := strings.ReplaceAll(resultSpec.Name, "-", "")
name = strings.ReplaceAll(name, "/", "")
result := corev1alpha1.Result{
Spec: resultSpec,
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: k8sgptConfig.Namespace,
},
}
if k8sgptConfig.Spec.ExtraOptions != nil && k8sgptConfig.Spec.ExtraOptions.Backstage.Enabled {
backstageLabel, err := r.Integrations.BackstageLabel(resultSpec)
if err != nil {
k8sgptReconcileErrorCount.Inc()
return r.finishReconcile(err, false)
}
result.ObjectMeta.Labels = backstageLabel
}
rawResults[name] = result
rawResults, err := resources.MapResults(*r.Integrations, response.Results, *k8sgptConfig)
if err != nil {
k8sgptReconcileErrorCount.Inc()
return r.finishReconcile(err, false)
}

// Prior to creating or updating any results we will delete any stale results that
// no longer are relevent, we can do this by using the resultSpec composed name against
// the custom resource name
Expand Down Expand Up @@ -255,39 +237,65 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
// At this point we are able to loop through our rawResults and create them or update
// them as needed
for _, result := range rawResults {
// Check if the result already exists
var existingResult corev1alpha1.Result
err = r.Get(ctx, client.ObjectKey{Namespace: k8sgptConfig.Namespace,
Name: result.Name}, &existingResult)
operation, err := resources.CreateOrUpdateResult(ctx, r.Client, result)
if err != nil {
// if the result doesn't exist, we will create it
if errors.IsNotFound(err) {
err = r.Create(ctx, &result)
if err != nil {
k8sgptReconcileErrorCount.Inc()
return r.finishReconcile(err, false)

}

// Update metrics
if operation == resources.CreatedResult {
k8sgptNumberOfResultsByType.With(prometheus.Labels{
"kind": result.Spec.Kind,
"name": result.Name,
}).Inc()
} else if operation == resources.UpdatedResult {
fmt.Printf("Updated successfully %s \n", result.Name)
}

}

// We emit when result Status is not historical
// and when user configures a sink for the first time
latestResultList := &corev1alpha1.ResultList{}
if err := r.List(ctx, latestResultList); err != nil {
return r.finishReconcile(err, false)
}
if len(latestResultList.Items) == 0 {
return r.finishReconcile(nil, false)
}
sinkEnabled := k8sgptConfig.Spec.Sink != nil && k8sgptConfig.Spec.Sink.Type != "" && k8sgptConfig.Spec.Sink.Endpoint != ""

var sinkType sinks.ISink
if sinkEnabled {
sinkType = sinks.NewSink(k8sgptConfig.Spec.Sink.Type)
sinkType.Configure(*k8sgptConfig, *r.SinkClient)
}

for _, result := range latestResultList.Items {
var res corev1alpha1.Result
if err := r.Get(ctx, client.ObjectKey{Namespace: result.Namespace, Name: result.Name}, &res); err != nil {
return r.finishReconcile(err, false)
}

if sinkEnabled {
if res.Status.LifeCycle != string(resources.NoOpResult) || res.Status.Webhook == "" {
if err := sinkType.Emit(res.Spec); err != nil {
k8sgptReconcileErrorCount.Inc()
return r.finishReconcile(err, false)
} else {
k8sgptNumberOfResultsByType.With(prometheus.Labels{
"kind": result.Spec.Kind,
"name": result.Name,
}).Inc()
}
} else {
k8sgptReconcileErrorCount.Inc()
return r.finishReconcile(err, false)
res.Status.Webhook = k8sgptConfig.Spec.Sink.Endpoint
}
} 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)
}
// Remove the Webhook status from results
res.Status.Webhook = ""
}
if err := r.Status().Update(ctx, &res); err != nil {
k8sgptReconcileErrorCount.Inc()
return r.finishReconcile(err, false)
}
}

}

return r.finishReconcile(nil, false)
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
Loading

0 comments on commit 4880645

Please sign in to comment.