From bb9e0089bc6670116eb963a81c5a042e4a457238 Mon Sep 17 00:00:00 2001 From: Aris Boutselis Date: Wed, 7 Jun 2023 20:02:59 +0100 Subject: [PATCH 01/10] feat: generic webhook Signed-off-by: Aris Boutselis --- api/v1alpha1/k8sgpt_types.go | 16 ++-- api/v1alpha1/result_types.go | 2 +- api/v1alpha1/zz_generated.deepcopy.go | 20 +++++ chart/operator/templates/deployment.yaml | 4 +- chart/operator/values.yaml | 1 + config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml | 7 ++ controllers/k8sgpt_controller.go | 28 +++++-- main.go | 15 ++++ pkg/resources/k8sgpt.go | 4 +- pkg/sinks/sinkreporter.go | 36 +++++++++ pkg/sinks/slack.go | 81 ++++++++++++++++++++ 11 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 pkg/sinks/sinkreporter.go create mode 100644 pkg/sinks/slack.go diff --git a/api/v1alpha1/k8sgpt_types.go b/api/v1alpha1/k8sgpt_types.go index c6995bfe..c4a37914 100644 --- a/api/v1alpha1/k8sgpt_types.go +++ b/api/v1alpha1/k8sgpt_types.go @@ -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"` @@ -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 diff --git a/api/v1alpha1/result_types.go b/api/v1alpha1/result_types.go index c5734c95..528baa50 100644 --- a/api/v1alpha1/result_types.go +++ b/api/v1alpha1/result_types.go @@ -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"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 660ac99f..1d307070 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -157,6 +157,11 @@ func (in *K8sGPTSpec) DeepCopyInto(out *K8sGPTSpec) { *out = new(ExtraOptionsRef) (*in).DeepCopyInto(*out) } + if in.Sink != nil { + in, out := &in.Sink, &out.Sink + *out = new(WebhookRef) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new K8sGPTSpec. @@ -309,3 +314,18 @@ func (in *Sensitive) DeepCopy() *Sensitive { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookRef) DeepCopyInto(out *WebhookRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookRef. +func (in *WebhookRef) DeepCopy() *WebhookRef { + if in == nil { + return nil + } + out := new(WebhookRef) + in.DeepCopyInto(out) + return out +} diff --git a/chart/operator/templates/deployment.yaml b/chart/operator/templates/deployment.yaml index 484fdd4d..b6575099 100644 --- a/chart/operator/templates/deployment.yaml +++ b/chart/operator/templates/deployment.yaml @@ -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: @@ -99,4 +101,4 @@ spec: securityContext: runAsNonRoot: true serviceAccountName: {{ include "chart.fullname" . }}-controller-manager - terminationGracePeriodSeconds: 10 \ No newline at end of file + terminationGracePeriodSeconds: 10 diff --git a/chart/operator/values.yaml b/chart/operator/values.yaml index 324c8a93..43ba2fab 100644 --- a/chart/operator/values.yaml +++ b/chart/operator/values.yaml @@ -29,6 +29,7 @@ controllerManager: cpu: 5m memory: 64Mi manager: + sinkWebhookTimeout: 30s containerSecurityContext: allowPrivilegeEscalation: false capabilities: diff --git a/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml b/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml index b67fce91..a9637d1a 100644 --- a/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml +++ b/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml @@ -72,6 +72,13 @@ spec: name: type: string type: object + sink: + properties: + type: + type: string + webhook: + type: string + type: object version: type: string required: diff --git a/controllers/k8sgpt_controller.go b/controllers/k8sgpt_controller.go index 64752205..f8b35022 100644 --- a/controllers/k8sgpt_controller.go +++ b/controllers/k8sgpt_controller.go @@ -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" @@ -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 @@ -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, @@ -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, @@ -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) } } } diff --git a/main.go b/main.go index 980e29ab..fdae8141 100644 --- a/main.go +++ b/main.go @@ -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. @@ -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" @@ -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) diff --git a/pkg/resources/k8sgpt.go b/pkg/resources/k8sgpt.go index 8d01c172..5dab90dd 100644 --- a/pkg/resources/k8sgpt.go +++ b/pkg/resources/k8sgpt.go @@ -262,7 +262,7 @@ 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, @@ -270,7 +270,7 @@ func GetDeployment(config v1alpha1.K8sGPT) (*appsv1.Deployment, error) { 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 diff --git a/pkg/sinks/sinkreporter.go b/pkg/sinks/sinkreporter.go new file mode 100644 index 00000000..33d41232 --- /dev/null +++ b/pkg/sinks/sinkreporter.go @@ -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, + } +} diff --git a/pkg/sinks/slack.go b/pkg/sinks/slack.go new file mode 100644 index 00000000..242ad3a6 --- /dev/null +++ b/pkg/sinks/slack.go @@ -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 +} From b39951bcfd3bce0faf588dab1baca0cdbb598f88 Mon Sep 17 00:00:00 2001 From: Aris Boutselis Date: Thu, 8 Jun 2023 17:19:03 +0100 Subject: [PATCH 02/10] chore: refactoring to catch diff use cases Signed-off-by: Aris Boutselis --- api/v1alpha1/k8sgpt_types.go | 1 + config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml | 2 ++ controllers/k8sgpt_controller.go | 20 +++++++++++++++++--- pkg/resources/k8sgpt.go | 4 ++-- pkg/sinks/slack.go | 14 +++----------- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/api/v1alpha1/k8sgpt_types.go b/api/v1alpha1/k8sgpt_types.go index c4a37914..3643a1d5 100644 --- a/api/v1alpha1/k8sgpt_types.go +++ b/api/v1alpha1/k8sgpt_types.go @@ -37,6 +37,7 @@ type ExtraOptionsRef struct { } type WebhookRef struct { + // +kubebuilder:validation:Enum=slack Type string `json:"type,omitempty"` Endpoint string `json:"webhook,omitempty"` } diff --git a/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml b/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml index a9637d1a..3104878a 100644 --- a/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml +++ b/config/crd/bases/core.k8sgpt.ai_k8sgpts.yaml @@ -75,6 +75,8 @@ spec: sink: properties: type: + enum: + - slack type: string webhook: type: string diff --git a/controllers/k8sgpt_controller.go b/controllers/k8sgpt_controller.go index f8b35022..b720c62c 100644 --- a/controllers/k8sgpt_controller.go +++ b/controllers/k8sgpt_controller.go @@ -130,7 +130,8 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // Configure sink Webhook var sinkType sinks.ISink - if k8sgptConfig.Spec.Sink != nil && k8sgptConfig.Spec.Sink.Type != "" { + sinkEnabled := k8sgptConfig.Spec.Sink != nil && k8sgptConfig.Spec.Sink.Type != "" && k8sgptConfig.Spec.Sink.Endpoint != "" + if sinkEnabled { sinkType = sinks.NewSink(k8sgptConfig.Spec.Sink.Type) sinkType.Configure(*k8sgptConfig, *r.SinkClient) } @@ -276,7 +277,14 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr k8sgptReconcileErrorCount.Inc() return r.finishReconcile(err, false) } else { - sinkType.Emit(result.Spec) + if sinkEnabled { + err := sinkType.Emit(result.Spec) + if err != nil { + k8sgptReconcileErrorCount.Inc() + return r.finishReconcile(err, false) + } + } + k8sgptNumberOfResultsByType.With(prometheus.Labels{ "kind": result.Spec.Kind, "name": result.Name, @@ -297,7 +305,13 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr k8sgptReconcileErrorCount.Inc() return r.finishReconcile(err, false) } - sinkType.Emit(existingResult.Spec) + if sinkEnabled { + err := sinkType.Emit(existingResult.Spec) + if err != nil { + k8sgptReconcileErrorCount.Inc() + return r.finishReconcile(err, false) + } + } } } } diff --git a/pkg/resources/k8sgpt.go b/pkg/resources/k8sgpt.go index 5dab90dd..8d01c172 100644 --- a/pkg/resources/k8sgpt.go +++ b/pkg/resources/k8sgpt.go @@ -262,7 +262,7 @@ 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 == "azureopenai" { + if config.Spec.Engine != "" && config.Spec.Backend == v1alpha1.AzureOpenAI { engine := v1.EnvVar{ Name: "K8SGPT_ENGINE", Value: config.Spec.Engine, @@ -270,7 +270,7 @@ func GetDeployment(config v1alpha1.K8sGPT) (*appsv1.Deployment, error) { deployment.Spec.Template.Spec.Containers[0].Env = append( deployment.Spec.Template.Spec.Containers[0].Env, engine, ) - } else if config.Spec.Engine != "" && config.Spec.Backend != "azureopenai" { + } else if config.Spec.Engine != "" && config.Spec.Backend != v1alpha1.AzureOpenAI { return &appsv1.Deployment{}, err.New("Engine is supported only by azureopenai provider.") } return &deployment, nil diff --git a/pkg/sinks/slack.go b/pkg/sinks/slack.go index 242ad3a6..210389f3 100644 --- a/pkg/sinks/slack.go +++ b/pkg/sinks/slack.go @@ -28,9 +28,9 @@ type Attachment struct { Title string `json:"title"` } -func buildSlackMessage(kind, name, details, backend string) SlackMessage { +func buildSlackMessage(kind, name, details string) SlackMessage { return SlackMessage{ - Text: fmt.Sprintf("`Analysis from %s of the %s %s`", backend, kind, name), + Text: fmt.Sprintf(">*K8sGPT analysis of the %s %s*", kind, name), Attachments: []Attachment{ Attachment{ Type: "mrkdwn", @@ -43,20 +43,12 @@ func buildSlackMessage(kind, name, details, backend string) SlackMessage { } 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) + message := buildSlackMessage(results.Kind, results.Name, results.Details) payload, err := json.Marshal(message) if err != nil { return err From fd165db2954deefd2ca3add64dc8207ceef04e21 Mon Sep 17 00:00:00 2001 From: Aris Boutselis Date: Sat, 10 Jun 2023 12:12:15 +0100 Subject: [PATCH 03/10] feat: decouple emit process and add Result statuses Signed-off-by: Aris Boutselis --- api/v1alpha1/result_types.go | 4 +- config/crd/bases/core.k8sgpt.ai_results.yaml | 5 + controllers/k8sgpt_controller.go | 119 +++++++------------ pkg/resources/results.go | 91 ++++++++++++++ 4 files changed, 141 insertions(+), 78 deletions(-) create mode 100644 pkg/resources/results.go diff --git a/api/v1alpha1/result_types.go b/api/v1alpha1/result_types.go index 528baa50..3d2bc3b7 100644 --- a/api/v1alpha1/result_types.go +++ b/api/v1alpha1/result_types.go @@ -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 + Type string `json:"type,omitempty"` + Sink string `json:"sink,omitempty"` } //+kubebuilder:object:root=true diff --git a/config/crd/bases/core.k8sgpt.ai_results.yaml b/config/crd/bases/core.k8sgpt.ai_results.yaml index 3e4a8ff6..79dffbea 100644 --- a/config/crd/bases/core.k8sgpt.ai_results.yaml +++ b/config/crd/bases/core.k8sgpt.ai_results.yaml @@ -80,6 +80,11 @@ spec: type: object status: description: ResultStatus defines the observed state of Result + properties: + sink: + type: string + type: + type: string type: object type: object served: true diff --git a/controllers/k8sgpt_controller.go b/controllers/k8sgpt_controller.go index b720c62c..6dbcfe16 100644 --- a/controllers/k8sgpt_controller.go +++ b/controllers/k8sgpt_controller.go @@ -32,8 +32,6 @@ import ( "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" @@ -128,14 +126,6 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return r.finishReconcile(nil, false) } - // Configure sink Webhook - var sinkType sinks.ISink - sinkEnabled := k8sgptConfig.Spec.Sink != nil && k8sgptConfig.Spec.Sink.Type != "" && k8sgptConfig.Spec.Sink.Endpoint != "" - if sinkEnabled { - 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, @@ -212,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 @@ -266,53 +238,48 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // 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, *k8sgptConfig) 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) - } else { - if sinkEnabled { - err := sinkType.Emit(result.Spec) - if err != nil { - k8sgptReconcileErrorCount.Inc() - return r.finishReconcile(err, false) - } - } + 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() + } + } - k8sgptNumberOfResultsByType.With(prometheus.Labels{ - "kind": result.Spec.Kind, - "name": result.Name, - }).Inc() - } - } else { - k8sgptReconcileErrorCount.Inc() - return r.finishReconcile(err, false) - } - } else { - // 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) - } - if sinkEnabled { - err := sinkType.Emit(existingResult.Spec) - if err != nil { - k8sgptReconcileErrorCount.Inc() - return r.finishReconcile(err, false) - } - } + } + + // At this stage we are ready to emit Results + // We emit when result type Status is created or updated + // and when user configures a sink for the first time + var sinkType sinks.ISink + sinkEnabled := k8sgptConfig.Spec.Sink != nil && k8sgptConfig.Spec.Sink.Type != "" && k8sgptConfig.Spec.Sink.Endpoint != "" + if sinkEnabled { + sinkType = sinks.NewSink(k8sgptConfig.Spec.Sink.Type) + sinkType.Configure(*k8sgptConfig, *r.SinkClient) + resultList := &corev1alpha1.ResultList{} + err = r.List(ctx, resultList) + if err != nil { + k8sgptReconcileErrorCount.Inc() + return r.finishReconcile(err, false) + } + if len(resultList.Items) > 0 { + for _, result := range resultList.Items { + if result.Status.Sink == "" { + _ = sinkType.Emit(result.Spec) + result.Status.Sink = k8sgptConfig.Spec.Sink.Type + _ = r.Status().Update(ctx, &result) + } else if result.Status.Type != resources.NoOpResult { + _ = sinkType.Emit(result.Spec) + result.Status.Sink = k8sgptConfig.Spec.Sink.Type + _ = r.Status().Update(ctx, &result) } + } } diff --git a/pkg/resources/results.go b/pkg/resources/results.go new file mode 100644 index 00000000..b4b0ec49 --- /dev/null +++ b/pkg/resources/results.go @@ -0,0 +1,91 @@ +package resources + +import ( + "context" + "strings" + + "github.com/k8sgpt-ai/k8sgpt-operator/api/v1alpha1" + "github.com/k8sgpt-ai/k8sgpt-operator/pkg/integrations" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + CreatedResult string = "created" + UpdatedResult = "updated" + NoOpResult = "historical" +) + +func MapResults(i integrations.Integrations, resultsSpec []v1alpha1.ResultSpec, config v1alpha1.K8sGPT) (map[string]v1alpha1.Result, error) { + namespace := config.Namespace + backend := config.Spec.Backend + backstageEnabled := config.Spec.ExtraOptions != nil && config.Spec.ExtraOptions.Backstage.Enabled + rawResults := make(map[string]v1alpha1.Result) + for _, resultSpec := range resultsSpec { + name := strings.ReplaceAll(resultSpec.Name, "-", "") + name = strings.ReplaceAll(name, "/", "") + result := GetResult(resultSpec, name, namespace, backend) + if backstageEnabled { + backstageLabel, err := i.BackstageLabel(resultSpec) + if err != nil { + return nil, err + } + // add Backstage label + result.ObjectMeta.Labels = backstageLabel + } + + rawResults[name] = result + } + return rawResults, nil +} + +func GetResult(resultSpec v1alpha1.ResultSpec, name, namespace, backend string) v1alpha1.Result { + resultSpec.Backend = backend + return v1alpha1.Result{ + Spec: resultSpec, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } +} + +func CreateOrUpdateResult(ctx context.Context, c client.Client, result v1alpha1.Result, config v1alpha1.K8sGPT) (string, error) { + // Check if the result already exists + var existingResult v1alpha1.Result + err := c.Get(ctx, client.ObjectKey{Namespace: config.Namespace, + Name: result.Name}, &existingResult) + if err != nil { + // if the result doesn't exist, we will create it + if errors.IsNotFound(err) { + err = c.Create(ctx, &result) + if err != nil { + return "", err + } + result.Status.Type = CreatedResult + _ = c.Status().Update(ctx, &result) + return CreatedResult, nil + + } else { + return "", err + } + } + + // If the result error and solution has changed, we will update CR + updateRequired := existingResult.Spec.Details != result.Spec.Details || existingResult.Spec.Name != result.Spec.Name || existingResult.Spec.Backend != result.Spec.Backend + if updateRequired { + existingResult.Spec = result.Spec + existingResult.Labels = result.Labels + err = c.Update(ctx, &existingResult) + if err != nil { + return "", err + } + existingResult.Status.Type = UpdatedResult + _ = c.Status().Update(ctx, &existingResult) + return UpdatedResult, err + } + existingResult.Status.Type = NoOpResult + _ = c.Status().Update(ctx, &existingResult) + return "", nil +} From 6d13bef69613d598a493883aabb5aed2ef679d82 Mon Sep 17 00:00:00 2001 From: Aris Boutselis Date: Sat, 10 Jun 2023 23:06:48 +0100 Subject: [PATCH 04/10] feat: add cleanup status process Signed-off-by: Aris Boutselis --- controllers/k8sgpt_controller.go | 76 ++++++++++++++++++++------------ pkg/resources/results.go | 20 +++++---- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/controllers/k8sgpt_controller.go b/controllers/k8sgpt_controller.go index 6dbcfe16..a71af88f 100644 --- a/controllers/k8sgpt_controller.go +++ b/controllers/k8sgpt_controller.go @@ -243,6 +243,7 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr k8sgptReconcileErrorCount.Inc() return r.finishReconcile(err, false) } + // Update metrics if operation == resources.CreatedResult { k8sgptNumberOfResultsByType.With(prometheus.Labels{ @@ -252,37 +253,58 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } } - } + // At this stage we are ready to emit Results + // We emit when result type Status is created or updated + // and when user configures a sink for the first time + latestResultList := &corev1alpha1.ResultList{} + err = r.List(ctx, latestResultList) + + var sinkType sinks.ISink + if len(latestResultList.Items) > 0 { + sinkEnabled := k8sgptConfig.Spec.Sink != nil && k8sgptConfig.Spec.Sink.Type != "" && k8sgptConfig.Spec.Sink.Endpoint != "" + if sinkEnabled { + sinkType = sinks.NewSink(k8sgptConfig.Spec.Sink.Type) + sinkType.Configure(*k8sgptConfig, *r.SinkClient) + } + for _, result := range latestResultList.Items { + var res corev1alpha1.Result + _ = r.Get(ctx, client.ObjectKey{Namespace: k8sgptConfig.Namespace, Name: result.Name}, &res) + if sinkEnabled { + if result.Status.Sink == "" { + err = sinkType.Emit(result.Spec) + if err != nil { + k8sgptReconcileErrorCount.Inc() + return r.finishReconcile(err, false) + } + + res.Status.Sink = k8sgptConfig.Spec.Sink.Type + res.Status.Type = resources.NoOpResult + } else if result.Status.Type != resources.NoOpResult { + err = sinkType.Emit(result.Spec) + if err != nil { + k8sgptReconcileErrorCount.Inc() + return r.finishReconcile(err, false) + } + res.Status.Sink = k8sgptConfig.Spec.Sink.Type + res.Status.Type = resources.NoOpResult + } + err = r.Status().Update(ctx, &res) + if err != nil { + k8sgptReconcileErrorCount.Inc() + return r.finishReconcile(err, false) + } + } else { + // remove the sink status from results + res.Status.Sink = "" + err = r.Status().Update(ctx, &res) + if err != nil { + k8sgptReconcileErrorCount.Inc() + return r.finishReconcile(err, false) - // At this stage we are ready to emit Results - // We emit when result type Status is created or updated - // and when user configures a sink for the first time - var sinkType sinks.ISink - sinkEnabled := k8sgptConfig.Spec.Sink != nil && k8sgptConfig.Spec.Sink.Type != "" && k8sgptConfig.Spec.Sink.Endpoint != "" - if sinkEnabled { - sinkType = sinks.NewSink(k8sgptConfig.Spec.Sink.Type) - sinkType.Configure(*k8sgptConfig, *r.SinkClient) - resultList := &corev1alpha1.ResultList{} - err = r.List(ctx, resultList) - if err != nil { - k8sgptReconcileErrorCount.Inc() - return r.finishReconcile(err, false) - } - if len(resultList.Items) > 0 { - for _, result := range resultList.Items { - if result.Status.Sink == "" { - _ = sinkType.Emit(result.Spec) - result.Status.Sink = k8sgptConfig.Spec.Sink.Type - _ = r.Status().Update(ctx, &result) - } else if result.Status.Type != resources.NoOpResult { - _ = sinkType.Emit(result.Spec) - result.Status.Sink = k8sgptConfig.Spec.Sink.Type - _ = r.Status().Update(ctx, &result) + } } - } } - } return r.finishReconcile(nil, false) diff --git a/pkg/resources/results.go b/pkg/resources/results.go index b4b0ec49..21f0f4f6 100644 --- a/pkg/resources/results.go +++ b/pkg/resources/results.go @@ -51,6 +51,7 @@ func GetResult(resultSpec v1alpha1.ResultSpec, name, namespace, backend string) } } +// Creates or Updates Result CRs and update their statuses func CreateOrUpdateResult(ctx context.Context, c client.Client, result v1alpha1.Result, config v1alpha1.K8sGPT) (string, error) { // Check if the result already exists var existingResult v1alpha1.Result @@ -61,14 +62,16 @@ func CreateOrUpdateResult(ctx context.Context, c client.Client, result v1alpha1. if errors.IsNotFound(err) { err = c.Create(ctx, &result) if err != nil { - return "", err + return NoOpResult, err } result.Status.Type = CreatedResult - _ = c.Status().Update(ctx, &result) + err = c.Status().Update(ctx, &result) + if err != nil { + return CreatedResult, err + } return CreatedResult, nil - } else { - return "", err + return NoOpResult, err } } @@ -77,15 +80,14 @@ func CreateOrUpdateResult(ctx context.Context, c client.Client, result v1alpha1. if updateRequired { existingResult.Spec = result.Spec existingResult.Labels = result.Labels - err = c.Update(ctx, &existingResult) if err != nil { - return "", err + return NoOpResult, err } existingResult.Status.Type = UpdatedResult - _ = c.Status().Update(ctx, &existingResult) + err = c.Status().Update(ctx, &existingResult) return UpdatedResult, err } existingResult.Status.Type = NoOpResult - _ = c.Status().Update(ctx, &existingResult) - return "", nil + err = c.Status().Update(ctx, &existingResult) + return NoOpResult, err } From 6aee55100651ca095fb5bf8d15ab018bf67ccc03 Mon Sep 17 00:00:00 2001 From: Aris Boutselis Date: Sun, 11 Jun 2023 11:52:19 +0100 Subject: [PATCH 05/10] chore: minor fix Signed-off-by: Aris Boutselis --- controllers/k8sgpt_controller.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/controllers/k8sgpt_controller.go b/controllers/k8sgpt_controller.go index a71af88f..8cb92bb1 100644 --- a/controllers/k8sgpt_controller.go +++ b/controllers/k8sgpt_controller.go @@ -277,17 +277,16 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return r.finishReconcile(err, false) } - res.Status.Sink = k8sgptConfig.Spec.Sink.Type - res.Status.Type = resources.NoOpResult } else if result.Status.Type != resources.NoOpResult { err = sinkType.Emit(result.Spec) if err != nil { k8sgptReconcileErrorCount.Inc() return r.finishReconcile(err, false) } - res.Status.Sink = k8sgptConfig.Spec.Sink.Type - res.Status.Type = resources.NoOpResult } + res.Status.Sink = k8sgptConfig.Spec.Sink.Type + res.Status.Type = resources.NoOpResult + err = r.Status().Update(ctx, &res) if err != nil { k8sgptReconcileErrorCount.Inc() From c6a150f9c655bba5a9f4170fbe3ba942de278b7b Mon Sep 17 00:00:00 2001 From: Aris Boutselis Date: Mon, 12 Jun 2023 01:07:28 +0100 Subject: [PATCH 06/10] fix: conditionals based on latest result object Signed-off-by: Aris Boutselis --- controllers/k8sgpt_controller.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/controllers/k8sgpt_controller.go b/controllers/k8sgpt_controller.go index 8cb92bb1..f760f6fc 100644 --- a/controllers/k8sgpt_controller.go +++ b/controllers/k8sgpt_controller.go @@ -270,22 +270,21 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr var res corev1alpha1.Result _ = r.Get(ctx, client.ObjectKey{Namespace: k8sgptConfig.Namespace, Name: result.Name}, &res) if sinkEnabled { - if result.Status.Sink == "" { - err = sinkType.Emit(result.Spec) + if res.Status.Sink == "" { + err = sinkType.Emit(res.Spec) if err != nil { k8sgptReconcileErrorCount.Inc() return r.finishReconcile(err, false) } - } else if result.Status.Type != resources.NoOpResult { - err = sinkType.Emit(result.Spec) + } else if res.Status.Type != resources.NoOpResult { + err = sinkType.Emit(res.Spec) if err != nil { k8sgptReconcileErrorCount.Inc() return r.finishReconcile(err, false) } } res.Status.Sink = k8sgptConfig.Spec.Sink.Type - res.Status.Type = resources.NoOpResult err = r.Status().Update(ctx, &res) if err != nil { From 085a8c84bd06825b12725ec3697bf2b65ca11725 Mon Sep 17 00:00:00 2001 From: Aris Boutselis Date: Mon, 12 Jun 2023 20:23:56 +0100 Subject: [PATCH 07/10] fix: missing client Update Signed-off-by: Aris Boutselis --- pkg/resources/results.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/resources/results.go b/pkg/resources/results.go index 21f0f4f6..ad9a0249 100644 --- a/pkg/resources/results.go +++ b/pkg/resources/results.go @@ -80,6 +80,7 @@ func CreateOrUpdateResult(ctx context.Context, c client.Client, result v1alpha1. if updateRequired { existingResult.Spec = result.Spec existingResult.Labels = result.Labels + err = c.Update(ctx, &existingResult) if err != nil { return NoOpResult, err } From 579ff653f38c4d24e261aab042ee239ff92fa533 Mon Sep 17 00:00:00 2001 From: Aris Boutselis Date: Thu, 15 Jun 2023 01:47:59 +0100 Subject: [PATCH 08/10] chore: refactor Results creation Signed-off-by: Aris Boutselis --- api/v1alpha1/result_types.go | 4 +- config/crd/bases/core.k8sgpt.ai_results.yaml | 4 +- controllers/k8sgpt_controller.go | 70 +++++++++----------- pkg/resources/results.go | 68 ++++++++----------- 4 files changed, 65 insertions(+), 81 deletions(-) diff --git a/api/v1alpha1/result_types.go b/api/v1alpha1/result_types.go index 3d2bc3b7..cfcdc5d7 100644 --- a/api/v1alpha1/result_types.go +++ b/api/v1alpha1/result_types.go @@ -42,8 +42,8 @@ type ResultSpec struct { // ResultStatus defines the observed state of Result type ResultStatus struct { - Type string `json:"type,omitempty"` - Sink string `json:"sink,omitempty"` + LifeCycle string `json:"lifecycle,omitempty"` + Sink string `json:"sink,omitempty"` } //+kubebuilder:object:root=true diff --git a/config/crd/bases/core.k8sgpt.ai_results.yaml b/config/crd/bases/core.k8sgpt.ai_results.yaml index 79dffbea..ca99b8e3 100644 --- a/config/crd/bases/core.k8sgpt.ai_results.yaml +++ b/config/crd/bases/core.k8sgpt.ai_results.yaml @@ -81,9 +81,9 @@ spec: status: description: ResultStatus defines the observed state of Result properties: - sink: + lifecycle: type: string - type: + sink: type: string type: object type: object diff --git a/controllers/k8sgpt_controller.go b/controllers/k8sgpt_controller.go index f760f6fc..74eeb068 100644 --- a/controllers/k8sgpt_controller.go +++ b/controllers/k8sgpt_controller.go @@ -237,11 +237,11 @@ 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 - operation, err := resources.CreateOrUpdateResult(ctx, r.Client, result, *k8sgptConfig) + operation, err := resources.CreateOrUpdateResult(ctx, r.Client, result) if err != nil { k8sgptReconcileErrorCount.Inc() return r.finishReconcile(err, false) + } // Update metrics @@ -250,57 +250,51 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr "kind": result.Spec.Kind, "name": result.Name, }).Inc() + } else if operation == resources.UpdatedResult { + fmt.Printf("Updated successfully %s \n", result.Name) } + } // At this stage we are ready to emit Results // We emit when result type Status is created or updated // and when user configures a sink for the first time latestResultList := &corev1alpha1.ResultList{} - err = r.List(ctx, latestResultList) + if err := r.List(ctx, latestResultList); err != nil { + return r.finishReconcile(err, false) + } + if len(latestResultList.Items) == 0 { + return r.finishReconcile(err, false) + } + sinkEnabled := k8sgptConfig.Spec.Sink != nil && k8sgptConfig.Spec.Sink.Type != "" && k8sgptConfig.Spec.Sink.Endpoint != "" var sinkType sinks.ISink - if len(latestResultList.Items) > 0 { - sinkEnabled := k8sgptConfig.Spec.Sink != nil && k8sgptConfig.Spec.Sink.Type != "" && k8sgptConfig.Spec.Sink.Endpoint != "" - if sinkEnabled { - sinkType = sinks.NewSink(k8sgptConfig.Spec.Sink.Type) - sinkType.Configure(*k8sgptConfig, *r.SinkClient) + 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) } - for _, result := range latestResultList.Items { - var res corev1alpha1.Result - _ = r.Get(ctx, client.ObjectKey{Namespace: k8sgptConfig.Namespace, Name: result.Name}, &res) - if sinkEnabled { - if res.Status.Sink == "" { - err = sinkType.Emit(res.Spec) - if err != nil { - k8sgptReconcileErrorCount.Inc() - return r.finishReconcile(err, false) - } - - } else if res.Status.Type != resources.NoOpResult { - err = sinkType.Emit(res.Spec) - if err != nil { - k8sgptReconcileErrorCount.Inc() - return r.finishReconcile(err, false) - } - } - res.Status.Sink = k8sgptConfig.Spec.Sink.Type - err = r.Status().Update(ctx, &res) - if err != nil { - k8sgptReconcileErrorCount.Inc() - return r.finishReconcile(err, false) - } - } else { - // remove the sink status from results - res.Status.Sink = "" - err = r.Status().Update(ctx, &res) - if err != nil { + if sinkEnabled { + if res.Status.LifeCycle != string(resources.NoOpResult) || res.Status.Sink == "" { + if err := sinkType.Emit(res.Spec); err != nil { k8sgptReconcileErrorCount.Inc() return r.finishReconcile(err, false) - } + res.Status.Sink = k8sgptConfig.Spec.Sink.Type } + } else { + // Remove the sink status from results + res.Status.Sink = "" + } + if err := r.Status().Update(ctx, &res); err != nil { + k8sgptReconcileErrorCount.Inc() + return r.finishReconcile(err, false) } } } diff --git a/pkg/resources/results.go b/pkg/resources/results.go index ad9a0249..97804538 100644 --- a/pkg/resources/results.go +++ b/pkg/resources/results.go @@ -2,6 +2,7 @@ package resources import ( "context" + "reflect" "strings" "github.com/k8sgpt-ai/k8sgpt-operator/api/v1alpha1" @@ -11,10 +12,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +type ResultOperation string + const ( - CreatedResult string = "created" - UpdatedResult = "updated" - NoOpResult = "historical" + CreatedResult ResultOperation = "created" + UpdatedResult = "updated" + NoOpResult = "historical" ) func MapResults(i integrations.Integrations, resultsSpec []v1alpha1.ResultSpec, config v1alpha1.K8sGPT) (map[string]v1alpha1.Result, error) { @@ -50,45 +53,32 @@ func GetResult(resultSpec v1alpha1.ResultSpec, name, namespace, backend string) }, } } - -// Creates or Updates Result CRs and update their statuses -func CreateOrUpdateResult(ctx context.Context, c client.Client, result v1alpha1.Result, config v1alpha1.K8sGPT) (string, error) { - // Check if the result already exists - var existingResult v1alpha1.Result - err := c.Get(ctx, client.ObjectKey{Namespace: config.Namespace, - Name: result.Name}, &existingResult) - if err != nil { - // if the result doesn't exist, we will create it - if errors.IsNotFound(err) { - err = c.Create(ctx, &result) - if err != nil { - return NoOpResult, err - } - result.Status.Type = CreatedResult - err = c.Status().Update(ctx, &result) - if err != nil { - return CreatedResult, err - } - return CreatedResult, nil - } else { +func CreateOrUpdateResult(ctx context.Context, c client.Client, res v1alpha1.Result) (ResultOperation, error) { + var existing v1alpha1.Result + if err := c.Get(ctx, client.ObjectKey{Namespace: res.Namespace, Name: res.Name}, &existing); err != nil { + if !errors.IsNotFound(err) { return NoOpResult, err } - } - - // If the result error and solution has changed, we will update CR - updateRequired := existingResult.Spec.Details != result.Spec.Details || existingResult.Spec.Name != result.Spec.Name || existingResult.Spec.Backend != result.Spec.Backend - if updateRequired { - existingResult.Spec = result.Spec - existingResult.Labels = result.Labels - err = c.Update(ctx, &existingResult) - if err != nil { + if err := c.Create(ctx, &res); err != nil { return NoOpResult, err } - existingResult.Status.Type = UpdatedResult - err = c.Status().Update(ctx, &existingResult) - return UpdatedResult, err + return CreatedResult, nil + } + if existing.Spec.Details == res.Spec.Details && reflect.DeepEqual(res.Labels, existing.Labels) { + existing.Status.LifeCycle = string(NoOpResult) + err := c.Status().Update(ctx, &existing) + return NoOpResult, err + } + + existing.Spec = res.Spec + existing.Labels = res.Labels + if err := c.Update(ctx, &existing); err != nil { + return NoOpResult, err + } + existing.Status.LifeCycle = string(UpdatedResult) + if err := c.Status().Update(ctx, &existing); err != nil { + return NoOpResult, err } - existingResult.Status.Type = NoOpResult - err = c.Status().Update(ctx, &existingResult) - return NoOpResult, err + + return UpdatedResult, nil } From 184b3b3bbb09142e4db961fdfd9f53dd73d3cc58 Mon Sep 17 00:00:00 2001 From: Aris Boutselis Date: Thu, 15 Jun 2023 02:11:24 +0100 Subject: [PATCH 09/10] chore: change status sink to webhook Signed-off-by: Aris Boutselis --- api/v1alpha1/result_types.go | 2 +- config/crd/bases/core.k8sgpt.ai_results.yaml | 2 +- controllers/k8sgpt_controller.go | 11 +++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/api/v1alpha1/result_types.go b/api/v1alpha1/result_types.go index cfcdc5d7..3201c94b 100644 --- a/api/v1alpha1/result_types.go +++ b/api/v1alpha1/result_types.go @@ -43,7 +43,7 @@ type ResultSpec struct { // ResultStatus defines the observed state of Result type ResultStatus struct { LifeCycle string `json:"lifecycle,omitempty"` - Sink string `json:"sink,omitempty"` + Webhook string `json:"webhook,omitempty"` } //+kubebuilder:object:root=true diff --git a/config/crd/bases/core.k8sgpt.ai_results.yaml b/config/crd/bases/core.k8sgpt.ai_results.yaml index ca99b8e3..9d938573 100644 --- a/config/crd/bases/core.k8sgpt.ai_results.yaml +++ b/config/crd/bases/core.k8sgpt.ai_results.yaml @@ -83,7 +83,7 @@ spec: properties: lifecycle: type: string - sink: + webhook: type: string type: object type: object diff --git a/controllers/k8sgpt_controller.go b/controllers/k8sgpt_controller.go index 74eeb068..761e48cb 100644 --- a/controllers/k8sgpt_controller.go +++ b/controllers/k8sgpt_controller.go @@ -256,8 +256,7 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } - // At this stage we are ready to emit Results - // We emit when result type Status is created or updated + // 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 { @@ -281,16 +280,16 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } if sinkEnabled { - if res.Status.LifeCycle != string(resources.NoOpResult) || res.Status.Sink == "" { + 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) } - res.Status.Sink = k8sgptConfig.Spec.Sink.Type + res.Status.Webhook = k8sgptConfig.Spec.Sink.Endpoint } } else { - // Remove the sink status from results - res.Status.Sink = "" + // Remove the Webhook status from results + res.Status.Webhook = "" } if err := r.Status().Update(ctx, &res); err != nil { k8sgptReconcileErrorCount.Inc() From e616f56da44103cefb79701537006ad4b6188ecd Mon Sep 17 00:00:00 2001 From: Aris Boutselis Date: Thu, 15 Jun 2023 09:48:05 +0100 Subject: [PATCH 10/10] fix: return from empty result list Signed-off-by: Aris Boutselis --- controllers/k8sgpt_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/k8sgpt_controller.go b/controllers/k8sgpt_controller.go index 761e48cb..47ec5f56 100644 --- a/controllers/k8sgpt_controller.go +++ b/controllers/k8sgpt_controller.go @@ -263,7 +263,7 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return r.finishReconcile(err, false) } if len(latestResultList.Items) == 0 { - return r.finishReconcile(err, false) + return r.finishReconcile(nil, false) } sinkEnabled := k8sgptConfig.Spec.Sink != nil && k8sgptConfig.Spec.Sink.Type != "" && k8sgptConfig.Spec.Sink.Endpoint != ""