Skip to content

Commit

Permalink
feat: added similarity score and retified some object update issues a…
Browse files Browse the repository at this point in the history
…cross phases

Signed-off-by: AlexsJones <[email protected]>
  • Loading branch information
AlexsJones committed Jan 17, 2025
1 parent f4983cb commit 002a6cb
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 35 deletions.
9 changes: 5 additions & 4 deletions api/v1alpha1/mutation_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
type MutationSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
SimilarityScore string `json:"similarityScore,omitempty"`
Resource corev1.ObjectReference `json:"resource,omitempty"`
Result Result `json:"result,omitempty"`
OriginConfiguration string `json:"originConfiguration,omitempty"`
Expand All @@ -44,15 +45,15 @@ type MutationStatus struct {
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Display in wide format the autoremediationphase status
// Display in wide format the autoremediationphase status and similarity score
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="The phase of the autoremediation"
// +kubebuilder:printcolumn:name="Similarity Score",type="string",JSONPath=".spec.similarityScore",description="The similarity score of the autoremediation"
// Mutation is the Schema for the mutations API.
type Mutation struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec MutationSpec `json:"spec,omitempty"`
Status MutationStatus `json:"status,omitempty"`
Spec MutationSpec `json:"spec,omitempty"`
Status MutationStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
35 changes: 28 additions & 7 deletions config/crd/bases/core.k8sgpt.ai_mutations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ spec:
jsonPath: .status.phase
name: Phase
type: string
- description: The similarity score of the autoremediation
jsonPath: .spec.similarityScore
name: Similarity Score
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: |-
Display in wide format the autoremediationphase status
Mutation is the Schema for the mutations API.
description: Mutation is the Schema for the mutations API.
properties:
apiVersion:
description: |-
Expand All @@ -50,8 +52,23 @@ spec:
type: string
resource:
description: |-
INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
Important: Run "make" to regenerate code after modifying this file
ObjectReference contains enough information to let you inspect or modify the referred object.
---
New uses of this type are discouraged because of difficulty describing its usage when embedded in APIs.
1. Ignored fields. It includes many fields which are not generally honored. For instance, ResourceVersion and FieldPath are both very rarely valid in actual usage.
2. Invalid usage help. It is impossible to add specific help for individual usage. In most embedded usages, there are particular
restrictions like, "must refer only to types A and B" or "UID not honored" or "name must be restricted".
Those cannot be well described when embedded.
3. Inconsistent validation. Because the usages are different, the validation rules are different by usage, which makes it hard for users to predict what will happen.
4. The fields are both imprecise and overly precise. Kind is not a precise mapping to a URL. This can produce ambiguity
during interpretation and require a REST mapping. In most cases, the dependency is on the group,resource tuple
and the version of the actual struct is irrelevant.
5. We cannot easily change it. Because this type is embedded in many locations, updates to this type
will affect numerous schemas. Don't make new APIs embed an underspecified API type they do not control.
Instead of using this type, create a locally provided and used type that is well-focused on your reference.
For example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 .
properties:
apiVersion:
description: API version of the referent.
Expand Down Expand Up @@ -167,6 +184,11 @@ spec:
type: string
type: object
type: object
similarityScore:
description: |-
INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
Important: Run "make" to regenerate code after modifying this file
type: string
targetConfiguration:
type: string
type: object
Expand All @@ -182,5 +204,4 @@ spec:
type: object
served: true
storage: true
subresources:
status: {}
subresources: {}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ toolchain go1.23.4
require (
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.5.1-20240920204244-7a91c8620515.1
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.35.2-20241118152629-1379a5a1889d.1
github.com/agnivade/levenshtein v1.2.0
github.com/go-logr/logr v1.4.2
github.com/onsi/ginkgo/v2 v2.22.1
github.com/onsi/gomega v1.36.2
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
Expand Down Expand Up @@ -98,7 +100,6 @@ 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
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg6
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=
github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
Expand All @@ -30,6 +34,8 @@ github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
Expand Down
34 changes: 24 additions & 10 deletions internal/controller/mutation/mutation_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/go-logr/logr"
corev1alpha1 "github.com/k8sgpt-ai/k8sgpt-operator/api/v1alpha1"
"github.com/k8sgpt-ai/k8sgpt-operator/internal/controller/channel_types"
"github.com/k8sgpt-ai/k8sgpt-operator/internal/controller/util"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand Down Expand Up @@ -95,19 +96,26 @@ func (r *MutationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
mutationControllerLog.Error(err, "unable to query K8sGPT")
return ctrl.Result{}, err
}
mutationControllerLog.Info("Got mutation targetConfiguration for", "mutation", mutation.Name)
// compute similarity score
score := util.SimilarityScore(mutation.Spec.OriginConfiguration, queryResponse.GetResponse())
mutationControllerLog.Info("Similarity score", "score", score)

mutation.Status.Phase = corev1alpha1.AutoRemediationPhaseInProgress
if err := r.Client.Status().Update(ctx, &mutation); err != nil {
mutationControllerLog.Error(err, "unable to update mutation status")
return ctrl.Result{}, err
}
mutationControllerLog.Info("Got mutation targetConfiguration for", "mutation", mutation.Name)
mutation.Spec.TargetConfiguration = queryResponse.GetResponse()
mutation.Spec.SimilarityScore = fmt.Sprintf("%f", score)
// Update the spec (if needed)
if err := r.Client.Update(ctx, &mutation); err != nil {
mutationControllerLog.Error(err, "unable to update mutation")
return ctrl.Result{}, err
}
mutationControllerLog.Info("Updated mutation with targetConfiguration", "mutation", mutation.Name)
// Update the status
mutation.Status.Phase = corev1alpha1.AutoRemediationPhaseInProgress
if err := r.Client.Status().Update(ctx, &mutation); err != nil {
mutationControllerLog.Error(err, "unable to update mutation status")
return ctrl.Result{}, err
}
mutationControllerLog.Info("Updated mutation status to InProgress", "mutation", mutation.Name)

break
case corev1alpha1.AutoRemediationPhaseInProgress:
Expand Down Expand Up @@ -144,15 +152,21 @@ func (r *MutationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
// 4. Set the object's name and namespace (important for updates!)
obj.SetName(mutation.Spec.Resource.Name)
obj.SetNamespace(mutation.Spec.Resource.Namespace)

// 5. Apply the update using Patch
patch := client.MergeFrom(obj) // Create a patch based on the current state of the object
// print out the patch
mutationControllerLog.Info("Patch", "patch", patch)
if err := r.Client.Patch(ctx, obj, patch); err != nil {
mutationControllerLog.Error(err, "unable to patch object", "object", obj.GetName())
return ctrl.Result{RequeueAfter: 60 * time.Second}, err
}
mutationControllerLog.Info("Successfully patched object", "object", obj.GetName())
// update status with the crazy process again
// Fetch the mutation again, because otherwise we can get into async updates across the list
// I don't know, this just seems to fix it
if err := r.Client.Get(ctx, client.ObjectKey{Namespace: mutation.Namespace, Name: mutation.Name}, &mutation); err != nil {
mutationControllerLog.Error(err, "unable to get mutation")
return ctrl.Result{RequeueAfter: 60 * time.Second}, err
}
mutation.Status.Phase = corev1alpha1.AutoRemediationPhaseCompleted
if err := r.Client.Status().Update(ctx, &mutation); err != nil {
mutationControllerLog.Error(err, "unable to update mutation status")
Expand Down Expand Up @@ -181,10 +195,10 @@ func (r *MutationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
//}
//result.Annotations["mutation-timestamp"] = time.Now().String()
//mutationControllerLog.Info("Annotated result with mutation timestamp", "result", result.Name)
break
return ctrl.Result{RequeueAfter: time.Second * 30}, nil
case corev1alpha1.AutoRemediationPhaseFailed:
// This phase will occur when a result does not expire after phase completed
break
return ctrl.Result{RequeueAfter: time.Second * 120}, nil
}
}

Expand Down
73 changes: 60 additions & 13 deletions internal/controller/mutation/mutation_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,31 @@ limitations under the License.
package mutation

import (
rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc"
"context"
"github.com/k8sgpt-ai/k8sgpt-operator/internal/controller/channel_types"
v1 "k8s.io/api/core/v1"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

corev1alpha1 "github.com/k8sgpt-ai/k8sgpt-operator/api/v1alpha1"
)

var _ = Describe("Mutation Controller", func() {
Context("When reconciling a resource", func() {
const resourceName = "test-resource"
Context("When reconciling a resource with targetConfiguration not set", func() {
const resourceName = "test-mutation-no-targetconfig"

ctx := context.Background()

typeNamespacedName := types.NamespacedName{
Name: resourceName,
Namespace: "default", // TODO(user):Modify as needed
Namespace: "default",
}
mutation := &corev1alpha1.Mutation{}
reconciler := &MutationReconciler{}
Expand All @@ -51,14 +54,34 @@ var _ = Describe("Mutation Controller", func() {
Name: resourceName,
Namespace: "default",
},
// TODO(user): Specify other spec details if needed.
Spec: corev1alpha1.MutationSpec{
Resource: v1.ObjectReference{
Kind: "Service",
Name: "my-service",
Namespace: "default",
},
OriginConfiguration: `
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
app: my-app
type: LoadBalancer
`,
TargetConfiguration: "", // Empty targetConfiguration
},
}
Expect(reconciler.Client.Create(ctx, resource)).To(Succeed())
}
})

AfterEach(func() {
// TODO(user): Cleanup logic after each test, like removing the resource instance.
reconciler := &MutationReconciler{}
resource := &corev1alpha1.Mutation{}
err := reconciler.Client.Get(ctx, typeNamespacedName, resource)
Expand All @@ -67,19 +90,43 @@ var _ = Describe("Mutation Controller", func() {
By("Cleanup the specific resource instance Mutation")
Expect(reconciler.Client.Delete(ctx, resource)).To(Succeed())
})
It("should successfully reconcile the resource", func() {
It("should requeue the resource and not update the status", func() {
By("Reconciling the created resource")
controllerReconciler := &MutationReconciler{
Client: reconciler.Client,
Scheme: reconciler.Client.Scheme(),
Client: reconciler.Client,
Scheme: reconciler.Client.Scheme(),
ServerQueryClient: nil,
Signal: make(chan channel_types.InterControllerSignal),
RemoteBackend: "test-backend",
}
controllerReconciler.Signal <- channel_types.InterControllerSignal{
K8sGPTClient: nil,
Backend: "test-backend",
}
go func() {
controllerReconciler.Signal <- channel_types.InterControllerSignal{
K8sGPTClient: nil,
Backend: "test-backend",
}
}()
*controllerReconciler.ServerQueryClient = rpc.NewServerQueryServiceClient(nil)

_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
result, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.

// Assertions
Expect(result.Requeue).To(BeTrue()) // Expect requeue
Expect(result.RequeueAfter).To(Equal(60 * time.Second)) // Expect requeue after 60 seconds

// Fetch the updated Mutation object
updatedMutation := &corev1alpha1.Mutation{}
err = reconciler.Client.Get(ctx, typeNamespacedName, updatedMutation)
Expect(err).NotTo(HaveOccurred())

// Verify the status phase remains unchanged (still InProgress)
Expect(updatedMutation.Status.Phase).To(Equal(corev1alpha1.AutoRemediationPhaseInProgress))
})
})
})
24 changes: 24 additions & 0 deletions internal/controller/util/compare.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package util

import "github.com/agnivade/levenshtein"

func SimilarityScore(text1 string, text2 string) float64 {
// Calculate the Levenshtein distance.
distance := levenshtein.ComputeDistance(text1, text2)

// Calculate the maximum length between the two strings.
maxLength := max(len(text1), len(text2))

// Calculate the similarity score as a percentage.
similarity := (1.0 - float64(distance)/float64(maxLength)) * 100.0

return similarity
}

// max returns the maximum of two integers.
func max(a, b int) int {
if a > b {
return a
}
return b
}

0 comments on commit 002a6cb

Please sign in to comment.