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
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"`
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 +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