diff --git a/config/samples/exhaustive_sample.yaml b/config/samples/exhaustive_sample.yaml new file mode 100644 index 00000000..f1c2101b --- /dev/null +++ b/config/samples/exhaustive_sample.yaml @@ -0,0 +1,98 @@ +#metadata: Specifies the name and namespace of the custom resource. +#spec.ai: Defines AI-related configurations, including backend, models, and optional parameters like anonymized data and retries. +#spec.customAnalyzers: Configures additional custom analyzers, with URLs and ports for connections. +#spec.extraOptions: Includes extra configurations like enabling Backstage integration. +#spec.filters: Sets up filters for resource analysis. +#spec.imagePullSecrets: References secrets for pulling images from private registries. +#spec.integrations: Configures integrations such as Trivy. +#spec.kubeconfig: Specifies a custom kubeconfig secret, if needed. +#spec.noCache: Indicates whether caching is disabled. +#spec.nodeSelector: Allows pod scheduling constraints based on node labels. +#spec.remoteCache: Configures remote caching options like Azure, GCS, or S3. +#spec.repository: Specifies the container image repository. +#spec.sink: Configures notification sinks, e.g., Slack, with webhook and authentication details. +#spec.targetNamespace: Target namespace for the resource. +#spec.version: Version of K8sGPT to use. +#status: Placeholder for status, typically managed by the operator. +apiVersion: core.k8sgpt.ai/v1alpha1 +kind: K8sGPT +metadata: + name: example-k8sgpt + namespace: default +spec: + ai: + anonymized: false + backOff: + enabled: true + maxRetries: 10 + backend: openai + baseUrl: "https://api.openai.com" + enabled: true + engine: "davinci" + language: "english" + maxTokens: "4096" + model: "gpt-4" + providerId: "provider-123" + proxyEndpoint: "http://proxy.example.com" + region: "us-east-1" + secret: + name: openai-secret + key: api-key + topk: "100" + customAnalyzers: + - name: "custom-analyzer-1" + connection: + url: "http://analyzer-1.example.com" + port: 8080 + - name: "custom-analyzer-2" + connection: + url: "http://analyzer-2.example.com" + port: 9090 + extraOptions: + backstage: + enabled: true + filters: + - "PodNotReady" + - "MemoryPressure" + imagePullSecrets: + - name: my-image-pull-secret + integrations: + trivy: + enabled: true + namespace: "trivy-namespace" + skipInstall: false + kubeconfig: + name: kubeconfig-secret + key: config + noCache: true + nodeSelector: + disktype: ssd + env: production + remoteCache: + azure: + containerName: "azure-container" + storageAccount: "azure-storage-account" + credentials: + name: "azure-credentials" + gcs: + bucketName: "gcs-bucket" + projectId: "gcs-project-id" + region: "us-central1" + interplex: + endpoint: "http://interplex.example.com" + s3: + bucketName: "s3-bucket" + region: "us-west-2" + repository: ghcr.io/k8sgpt-ai/k8sgpt + sink: + type: slack + channel: "#alerts" + username: "k8sgpt-bot" + icon_url: "https://example.com/icon.png" + webhook: "https://hooks.slack.com/services/..." + secret: + name: slack-webhook-secret + key: webhook-url + targetNamespace: "default" + version: "latest" +status: {} \ No newline at end of file diff --git a/config/samples/valid_k8sgpt.yaml b/config/samples/valid_k8sgpt.yaml index 1c938f63..dd4c9b8a 100644 --- a/config/samples/valid_k8sgpt.yaml +++ b/config/samples/valid_k8sgpt.yaml @@ -15,4 +15,4 @@ spec: # language: english noCache: false repository: ghcr.io/k8sgpt-ai/k8sgpt - version: v0.3.8 \ No newline at end of file + version: v0.3.48 \ No newline at end of file diff --git a/controllers/analysis_step.go b/controllers/analysis_step.go index 5ec63764..a6ba5110 100644 --- a/controllers/analysis_step.go +++ b/controllers/analysis_step.go @@ -15,8 +15,9 @@ limitations under the License. package controllers import ( + "encoding/json" "fmt" - + "github.com/go-logr/logr" corev1alpha1 "github.com/k8sgpt-ai/k8sgpt-operator/api/v1alpha1" "github.com/k8sgpt-ai/k8sgpt-operator/pkg/resources" ctrl "sigs.k8s.io/controller-runtime" @@ -31,7 +32,15 @@ var ( ) type AnalysisStep struct { - next K8sGPT + next K8sGPT + enableResultLogging bool + logger logr.Logger +} + +type AnalysisLogStatement struct { + Name string + Error string + Details string } func (step *AnalysisStep) execute(instance *K8sGPTInstance) (ctrl.Result, error) { @@ -149,10 +158,30 @@ func (step *AnalysisStep) processRawResults(rawResults map[string]corev1alpha1.R numberOfResultsByType.Reset() } for _, result := range rawResults { - err := resources.CreateOrUpdateResult(instance.ctx, instance.r.Client, result) + result, err := resources.CreateOrUpdateResult(instance.ctx, instance.r.Client, result) if err != nil { return err } + // Rather than using the raw corev1alpha.Result from the RPC, we log on the v1alpha.Result from KubeBuilder + if step.enableResultLogging { + + // check if result.spec.error is nil + var errorString = "" + if len(result.Spec.Error) > 0 { + errorString = fmt.Sprintf("Error %s", result.Spec.Error) + } + logStatement := AnalysisLogStatement{ + Name: result.Spec.Name, + Details: result.Spec.Details, + Error: errorString, + } + // to json + jsonBytes, err := json.Marshal(logStatement) + if err != nil { + step.logger.Error(err, "Error marshalling logStatement") + } + step.logger.Info(string(jsonBytes)) + } numberOfResultsByType.WithLabelValues(result.Spec.Kind, result.Spec.Name, instance.k8sgptConfig.Name).Inc() } diff --git a/controllers/k8sgpt_controller.go b/controllers/k8sgpt_controller.go index 99b57a36..56536ea9 100644 --- a/controllers/k8sgpt_controller.go +++ b/controllers/k8sgpt_controller.go @@ -47,11 +47,12 @@ var ( // K8sGPTReconciler reconciles a K8sGPT object type K8sGPTReconciler struct { client.Client - Scheme *runtime.Scheme - Integrations *integrations.Integrations - SinkClient *sinks.Client - K8sGPTClient *kclient.Client - MetricsBuilder *metricspkg.MetricBuilder + Scheme *runtime.Scheme + Integrations *integrations.Integrations + SinkClient *sinks.Client + K8sGPTClient *kclient.Client + MetricsBuilder *metricspkg.MetricBuilder + EnableResultLogging bool } type K8sGPTInstance struct { @@ -90,7 +91,10 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr finalizerStep := FinalizerStep{} configureStep := ConfigureStep{} preAnalysisStep := PreAnalysisStep{} - analysisStep := AnalysisStep{} + analysisStep := AnalysisStep{ + enableResultLogging: r.EnableResultLogging, + logger: instance.logger.WithName("analysis"), + } resultStatusStep := ResultStatusStep{} initStep.setNext(&finalizerStep) diff --git a/go.mod b/go.mod index 15487d6a..d4b85506 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/prometheus/client_golang v1.20.5 google.golang.org/grpc v1.69.0 gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.29.3 k8s.io/apimachinery v0.29.3 k8s.io/cli-runtime v0.29.3 @@ -99,6 +98,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect google.golang.org/protobuf v1.36.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.27.2 // indirect k8s.io/component-base v0.29.3 // indirect k8s.io/klog/v2 v2.110.1 // indirect diff --git a/main.go b/main.go index 338e137a..99326d82 100644 --- a/main.go +++ b/main.go @@ -55,8 +55,10 @@ func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string + var enableResultLogging bool flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableResultLogging, "enable-result-logging", false, "Whether to enable results logging") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") @@ -121,11 +123,12 @@ func main() { metricsBuilder := metrics.InitializeMetrics() if err = (&controllers.K8sGPTReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Integrations: integration, - SinkClient: sinkClient, - MetricsBuilder: metricsBuilder, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Integrations: integration, + SinkClient: sinkClient, + MetricsBuilder: metricsBuilder, + EnableResultLogging: enableResultLogging, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "K8sGPT") os.Exit(1) diff --git a/pkg/resources/results.go b/pkg/resources/results.go index 339ba76d..b8d6dc9a 100644 --- a/pkg/resources/results.go +++ b/pkg/resources/results.go @@ -62,33 +62,33 @@ func GetResult(resultSpec v1alpha1.ResultSpec, name, namespace, backend string, }, } } -func CreateOrUpdateResult(ctx context.Context, c client.Client, res v1alpha1.Result) error { +func CreateOrUpdateResult(ctx context.Context, c client.Client, res v1alpha1.Result) (*v1alpha1.Result, 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 err + return nil, err } if err := c.Create(ctx, &res); err != nil { - return err + return nil, err } fmt.Printf("Created result %s\n", res.Name) - return nil + return &existing, nil } if len(existing.Spec.Error) == len(res.Spec.Error) && reflect.DeepEqual(res.Labels, existing.Labels) { existing.Status.LifeCycle = string(NoOpResult) err := c.Status().Update(ctx, &existing) - return err + return &existing, err } existing.Spec = res.Spec existing.Labels = res.Labels if err := c.Update(ctx, &existing); err != nil { - return err + return nil, err } existing.Status.LifeCycle = string(UpdatedResult) if err := c.Status().Update(ctx, &existing); err != nil { - return err + return nil, err } fmt.Printf("Updated result %s\n", res.Name) - return nil + return &existing, nil }