Skip to content

Commit 9826bff

Browse files
authored
feat: added functionality for emitting results into the k8sgpt-operator log (#589)
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
1 parent d2b341b commit 9826bff

File tree

7 files changed

+158
-24
lines changed

7 files changed

+158
-24
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#metadata: Specifies the name and namespace of the custom resource.
2+
#spec.ai: Defines AI-related configurations, including backend, models, and optional parameters like anonymized data and retries.
3+
#spec.customAnalyzers: Configures additional custom analyzers, with URLs and ports for connections.
4+
#spec.extraOptions: Includes extra configurations like enabling Backstage integration.
5+
#spec.filters: Sets up filters for resource analysis.
6+
#spec.imagePullSecrets: References secrets for pulling images from private registries.
7+
#spec.integrations: Configures integrations such as Trivy.
8+
#spec.kubeconfig: Specifies a custom kubeconfig secret, if needed.
9+
#spec.noCache: Indicates whether caching is disabled.
10+
#spec.nodeSelector: Allows pod scheduling constraints based on node labels.
11+
#spec.remoteCache: Configures remote caching options like Azure, GCS, or S3.
12+
#spec.repository: Specifies the container image repository.
13+
#spec.sink: Configures notification sinks, e.g., Slack, with webhook and authentication details.
14+
#spec.targetNamespace: Target namespace for the resource.
15+
#spec.version: Version of K8sGPT to use.
16+
#status: Placeholder for status, typically managed by the operator.
17+
apiVersion: core.k8sgpt.ai/v1alpha1
18+
kind: K8sGPT
19+
metadata:
20+
name: example-k8sgpt
21+
namespace: default
22+
spec:
23+
ai:
24+
anonymized: false
25+
backOff:
26+
enabled: true
27+
maxRetries: 10
28+
backend: openai
29+
baseUrl: "https://api.openai.com"
30+
enabled: true
31+
engine: "davinci"
32+
language: "english"
33+
maxTokens: "4096"
34+
model: "gpt-4"
35+
providerId: "provider-123"
36+
proxyEndpoint: "http://proxy.example.com"
37+
region: "us-east-1"
38+
secret:
39+
name: openai-secret
40+
key: api-key
41+
topk: "100"
42+
customAnalyzers:
43+
- name: "custom-analyzer-1"
44+
connection:
45+
url: "http://analyzer-1.example.com"
46+
port: 8080
47+
- name: "custom-analyzer-2"
48+
connection:
49+
url: "http://analyzer-2.example.com"
50+
port: 9090
51+
extraOptions:
52+
backstage:
53+
enabled: true
54+
filters:
55+
- "PodNotReady"
56+
- "MemoryPressure"
57+
imagePullSecrets:
58+
- name: my-image-pull-secret
59+
integrations:
60+
trivy:
61+
enabled: true
62+
namespace: "trivy-namespace"
63+
skipInstall: false
64+
kubeconfig:
65+
name: kubeconfig-secret
66+
key: config
67+
noCache: true
68+
nodeSelector:
69+
disktype: ssd
70+
env: production
71+
remoteCache:
72+
azure:
73+
containerName: "azure-container"
74+
storageAccount: "azure-storage-account"
75+
credentials:
76+
name: "azure-credentials"
77+
gcs:
78+
bucketName: "gcs-bucket"
79+
projectId: "gcs-project-id"
80+
region: "us-central1"
81+
interplex:
82+
endpoint: "http://interplex.example.com"
83+
s3:
84+
bucketName: "s3-bucket"
85+
region: "us-west-2"
86+
repository: ghcr.io/k8sgpt-ai/k8sgpt
87+
sink:
88+
type: slack
89+
channel: "#alerts"
90+
username: "k8sgpt-bot"
91+
icon_url: "https://example.com/icon.png"
92+
webhook: "https://hooks.slack.com/services/..."
93+
secret:
94+
name: slack-webhook-secret
95+
key: webhook-url
96+
targetNamespace: "default"
97+
version: "latest"
98+
status: {}

config/samples/valid_k8sgpt.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ spec:
1515
# language: english
1616
noCache: false
1717
repository: ghcr.io/k8sgpt-ai/k8sgpt
18-
version: v0.3.8
18+
version: v0.3.48

controllers/analysis_step.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ limitations under the License.
1515
package controllers
1616

1717
import (
18+
"encoding/json"
1819
"fmt"
19-
20+
"github.com/go-logr/logr"
2021
corev1alpha1 "github.com/k8sgpt-ai/k8sgpt-operator/api/v1alpha1"
2122
"github.com/k8sgpt-ai/k8sgpt-operator/pkg/resources"
2223
ctrl "sigs.k8s.io/controller-runtime"
@@ -31,7 +32,15 @@ var (
3132
)
3233

3334
type AnalysisStep struct {
34-
next K8sGPT
35+
next K8sGPT
36+
enableResultLogging bool
37+
logger logr.Logger
38+
}
39+
40+
type AnalysisLogStatement struct {
41+
Name string
42+
Error string
43+
Details string
3544
}
3645

3746
func (step *AnalysisStep) execute(instance *K8sGPTInstance) (ctrl.Result, error) {
@@ -149,10 +158,30 @@ func (step *AnalysisStep) processRawResults(rawResults map[string]corev1alpha1.R
149158
numberOfResultsByType.Reset()
150159
}
151160
for _, result := range rawResults {
152-
err := resources.CreateOrUpdateResult(instance.ctx, instance.r.Client, result)
161+
result, err := resources.CreateOrUpdateResult(instance.ctx, instance.r.Client, result)
153162
if err != nil {
154163
return err
155164
}
165+
// Rather than using the raw corev1alpha.Result from the RPC, we log on the v1alpha.Result from KubeBuilder
166+
if step.enableResultLogging {
167+
168+
// check if result.spec.error is nil
169+
var errorString = ""
170+
if len(result.Spec.Error) > 0 {
171+
errorString = fmt.Sprintf("Error %s", result.Spec.Error)
172+
}
173+
logStatement := AnalysisLogStatement{
174+
Name: result.Spec.Name,
175+
Details: result.Spec.Details,
176+
Error: errorString,
177+
}
178+
// to json
179+
jsonBytes, err := json.Marshal(logStatement)
180+
if err != nil {
181+
step.logger.Error(err, "Error marshalling logStatement")
182+
}
183+
step.logger.Info(string(jsonBytes))
184+
}
156185
numberOfResultsByType.WithLabelValues(result.Spec.Kind, result.Spec.Name, instance.k8sgptConfig.Name).Inc()
157186
}
158187

controllers/k8sgpt_controller.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,12 @@ var (
4747
// K8sGPTReconciler reconciles a K8sGPT object
4848
type K8sGPTReconciler struct {
4949
client.Client
50-
Scheme *runtime.Scheme
51-
Integrations *integrations.Integrations
52-
SinkClient *sinks.Client
53-
K8sGPTClient *kclient.Client
54-
MetricsBuilder *metricspkg.MetricBuilder
50+
Scheme *runtime.Scheme
51+
Integrations *integrations.Integrations
52+
SinkClient *sinks.Client
53+
K8sGPTClient *kclient.Client
54+
MetricsBuilder *metricspkg.MetricBuilder
55+
EnableResultLogging bool
5556
}
5657

5758
type K8sGPTInstance struct {
@@ -90,7 +91,10 @@ func (r *K8sGPTReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
9091
finalizerStep := FinalizerStep{}
9192
configureStep := ConfigureStep{}
9293
preAnalysisStep := PreAnalysisStep{}
93-
analysisStep := AnalysisStep{}
94+
analysisStep := AnalysisStep{
95+
enableResultLogging: r.EnableResultLogging,
96+
logger: instance.logger.WithName("analysis"),
97+
}
9498
resultStatusStep := ResultStatusStep{}
9599

96100
initStep.setNext(&finalizerStep)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ require (
1313
github.com/prometheus/client_golang v1.20.5
1414
google.golang.org/grpc v1.69.0
1515
gopkg.in/yaml.v2 v2.4.0
16-
gopkg.in/yaml.v3 v3.0.1
1716
k8s.io/api v0.29.3
1817
k8s.io/apimachinery v0.29.3
1918
k8s.io/cli-runtime v0.29.3
@@ -99,6 +98,7 @@ require (
9998
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
10099
google.golang.org/protobuf v1.36.1 // indirect
101100
gopkg.in/inf.v0 v0.9.1 // indirect
101+
gopkg.in/yaml.v3 v3.0.1 // indirect
102102
k8s.io/apiextensions-apiserver v0.27.2 // indirect
103103
k8s.io/component-base v0.29.3 // indirect
104104
k8s.io/klog/v2 v2.110.1 // indirect

main.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ func main() {
5555
var metricsAddr string
5656
var enableLeaderElection bool
5757
var probeAddr string
58+
var enableResultLogging bool
5859
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
5960
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
61+
flag.BoolVar(&enableResultLogging, "enable-result-logging", false, "Whether to enable results logging")
6062
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
6163
"Enable leader election for controller manager. "+
6264
"Enabling this will ensure there is only one active controller manager.")
@@ -121,11 +123,12 @@ func main() {
121123
metricsBuilder := metrics.InitializeMetrics()
122124

123125
if err = (&controllers.K8sGPTReconciler{
124-
Client: mgr.GetClient(),
125-
Scheme: mgr.GetScheme(),
126-
Integrations: integration,
127-
SinkClient: sinkClient,
128-
MetricsBuilder: metricsBuilder,
126+
Client: mgr.GetClient(),
127+
Scheme: mgr.GetScheme(),
128+
Integrations: integration,
129+
SinkClient: sinkClient,
130+
MetricsBuilder: metricsBuilder,
131+
EnableResultLogging: enableResultLogging,
129132
}).SetupWithManager(mgr); err != nil {
130133
setupLog.Error(err, "unable to create controller", "controller", "K8sGPT")
131134
os.Exit(1)

pkg/resources/results.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,33 +62,33 @@ func GetResult(resultSpec v1alpha1.ResultSpec, name, namespace, backend string,
6262
},
6363
}
6464
}
65-
func CreateOrUpdateResult(ctx context.Context, c client.Client, res v1alpha1.Result) error {
65+
func CreateOrUpdateResult(ctx context.Context, c client.Client, res v1alpha1.Result) (*v1alpha1.Result, error) {
6666
var existing v1alpha1.Result
6767
if err := c.Get(ctx, client.ObjectKey{Namespace: res.Namespace, Name: res.Name}, &existing); err != nil {
6868
if !errors.IsNotFound(err) {
69-
return err
69+
return nil, err
7070
}
7171
if err := c.Create(ctx, &res); err != nil {
72-
return err
72+
return nil, err
7373
}
7474
fmt.Printf("Created result %s\n", res.Name)
75-
return nil
75+
return &existing, nil
7676
}
7777
if len(existing.Spec.Error) == len(res.Spec.Error) && reflect.DeepEqual(res.Labels, existing.Labels) {
7878
existing.Status.LifeCycle = string(NoOpResult)
7979
err := c.Status().Update(ctx, &existing)
80-
return err
80+
return &existing, err
8181
}
8282

8383
existing.Spec = res.Spec
8484
existing.Labels = res.Labels
8585
if err := c.Update(ctx, &existing); err != nil {
86-
return err
86+
return nil, err
8787
}
8888
existing.Status.LifeCycle = string(UpdatedResult)
8989
if err := c.Status().Update(ctx, &existing); err != nil {
90-
return err
90+
return nil, err
9191
}
9292
fmt.Printf("Updated result %s\n", res.Name)
93-
return nil
93+
return &existing, nil
9494
}

0 commit comments

Comments
 (0)