Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion go/cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,12 @@ import (
func main() {
authorizer := &auth.NoopAuthorizer{}
authenticator := &auth.UnsecureAuthenticator{}
app.Start(authenticator, authorizer)
app.Start(func(bootstrap app.BootstrapConfig) (*app.ExtensionConfig, error) {
return &app.ExtensionConfig{
Authenticator: authenticator,
Authorizer: authorizer,
AgentPlugins: nil,
MCPServerPlugins: nil,
}, nil
})
}
4 changes: 2 additions & 2 deletions go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/hashicorp/go-multierror v1.1.1
github.com/jedib0t/go-pretty/v6 v6.6.8
github.com/kagent-dev/kmcp v0.1.6
github.com/kagent-dev/kmcp v0.1.7
github.com/mark3labs/mcp-go v0.34.0
github.com/prometheus/client_golang v1.23.0
github.com/spf13/cobra v1.9.1
Expand Down Expand Up @@ -134,7 +134,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
google.golang.org/protobuf v1.36.6
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kagent-dev/kmcp v0.1.6 h1:O+W2g1tV2GcBHl04Tzln/wbP+VsVV6Mp/E1qCjUjYPQ=
github.com/kagent-dev/kmcp v0.1.6/go.mod h1:aPwM1QtoAackdbQqLrPaNSSg/KZa0fFjbTJyOZFJNK8=
github.com/kagent-dev/kmcp v0.1.7 h1:laMTjesOxz+KVaMe0xHd6kxn50xi/PlfeelRM9tQtCs=
github.com/kagent-dev/kmcp v0.1.7/go.mod h1:aPwM1QtoAackdbQqLrPaNSSg/KZa0fFjbTJyOZFJNK8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
Expand Down
52 changes: 51 additions & 1 deletion go/internal/controller/reconciler/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync"

"github.com/hashicorp/go-multierror"
reconcilerutils "github.com/kagent-dev/kagent/go/internal/controller/reconciler/utils"
appsv1 "k8s.io/api/apps/v1"
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
Expand Down Expand Up @@ -446,7 +447,7 @@ func (a *kagentReconciler) reconcileDesiredObjects(ctx context.Context, owner me
mutateFn := translator.MutateFuncFor(existing, desired)

if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
_, createOrUpdateErr := controllerutil.CreateOrUpdate(ctx, a.kube, existing, mutateFn)
_, createOrUpdateErr := createOrUpdate(ctx, a.kube, existing, mutateFn)
return createOrUpdateErr
}); err != nil {
l.Error(err, "failed to configure desired")
Expand All @@ -471,6 +472,55 @@ func (a *kagentReconciler) reconcileDesiredObjects(ctx context.Context, owner me
return nil
}

// modified version of controllerutil.CreateOrUpdate to support proto based objects like istio
func createOrUpdate(ctx context.Context, c client.Client, obj client.Object, f controllerutil.MutateFn) (controllerutil.OperationResult, error) {
key := client.ObjectKeyFromObject(obj)
if err := c.Get(ctx, key, obj); err != nil {
if !k8s_errors.IsNotFound(err) {
return controllerutil.OperationResultNone, err
}
if f != nil {
if err := mutate(f, key, obj); err != nil {
return controllerutil.OperationResultNone, err
}
}

if err := c.Create(ctx, obj); err != nil {
return controllerutil.OperationResultNone, err
}
return controllerutil.OperationResultCreated, nil
}

existing := obj.DeepCopyObject()
if f != nil {
if err := mutate(f, key, obj); err != nil {
return controllerutil.OperationResultNone, err
}
}

// special equality function to handle proto based crds
if reconcilerutils.ObjectsEqual(existing, obj) {
return controllerutil.OperationResultNone, nil
}

if err := c.Update(ctx, obj); err != nil {
return controllerutil.OperationResultNone, err
}

return controllerutil.OperationResultUpdated, nil
}

// mutate wraps a MutateFn and applies validation to its result.
func mutate(f controllerutil.MutateFn, key client.ObjectKey, obj client.Object) error {
if err := f(); err != nil {
return err
}
if newKey := client.ObjectKeyFromObject(obj); key != newKey {
return fmt.Errorf("MutateFn cannot mutate object name and/or object namespace")
}
return nil
}

func (a *kagentReconciler) deleteObjects(ctx context.Context, objects map[types.UID]client.Object) error {
// Pruning owned objects in the cluster which are not should not be present after the reconciliation.
pruneErrs := []error{}
Expand Down
127 changes: 127 additions & 0 deletions go/internal/controller/reconciler/utils/reconciler_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package utils

import (
protoV2 "google.golang.org/protobuf/proto"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"reflect"
)

// returns true if "relevant" parts of obj1 and obj2 have equal:
// - labels,
// - annotations,
// - namespace+name,
// - non-metadata, non-status fields
// Note that Status fields are not compared.
// To compare status fields, use ObjectStatusesEqual
func ObjectsEqual(obj1, obj2 runtime.Object) bool {
value1, value2 := reflect.ValueOf(obj1), reflect.ValueOf(obj2)

if value1.Type() != value2.Type() {
return false
}

if value1.Kind() == reflect.Ptr {
value1 = value1.Elem()
value2 = value2.Elem()
}

if meta1, hasMeta := obj1.(metav1.Object); hasMeta {
if !ObjectMetasEqual(meta1, obj2.(metav1.Object)) {
return false
}
}

// recurse through fields of both, comparing each:
for i := 0; i < value1.NumField(); i++ {
field1Name := value1.Type().Field(i).Name
if field1Name == "ObjectMeta" {
// skip ObjectMeta field, as we already asserted relevant fields are equal
continue
}
if field1Name == "TypeMeta" {
// skip TypeMeta field, as it is set by the server and not relevant for object comparison
continue
}
if field1Name == "Status" {
// skip Status field, as it is considered a separate and not relevant for object comparison
continue
}

field1 := mkPointer(value1.Field(i))
field2 := mkPointer(value2.Field(i))

// assert DeepEquality any other fields
if !DeepEqual(field1, field2) {
return false
}
}

return true
}

// returns true if "relevant" parts of obj1 and obj2 have equal:
// -labels
// -annotations
// -namespace+name
// or if the objects are not metav1.Objects
func ObjectMetasEqual(obj1, obj2 metav1.Object) bool {
return obj1.GetNamespace() == obj2.GetNamespace() &&
obj1.GetName() == obj2.GetName() &&
mapStringEqual(obj1.GetLabels(), obj2.GetLabels()) &&
mapStringEqual(obj1.GetAnnotations(), obj2.GetAnnotations())
}

func mapStringEqual(map1, map2 map[string]string) bool {
if map1 == nil && map2 == nil {
return true
}

if len(map1) != len(map2) {
return false
}

for key1, val1 := range map1 {
val2, ok := map2[key1]
if !ok {
return false
}
if val1 != val2 {
return false
}
}
return true
}

// if i is a pointer, just return the value.
// if i is addressable, return that.
// if i is a struct passed in by value, make a new instance of the type and copy the contents to that and return
// the pointer to that.
func mkPointer(val reflect.Value) interface{} {
if val.Kind() == reflect.Ptr {
return val.Interface()
}
if val.CanAddr() {
return val.Addr().Interface()
}
if val.Kind() == reflect.Struct {
nv := reflect.New(val.Type())
nv.Elem().Set(val)
return nv.Interface()
}
return val.Interface()
}

// DeepEqual should be used in place of reflect.DeepEqual when the type of an object is unknown and may be a proto message.
// see https://github.com/golang/protobuf/issues/1173 for details on why reflect.DeepEqual no longer works for proto messages
func DeepEqual(val1, val2 interface{}) bool {
protoVal1, isProto := val1.(protoV2.Message)
if isProto {
protoVal2, isProto := val2.(protoV2.Message)
if !isProto {
return false // different types
}
return protoV2.Equal(protoVal1, protoVal2)
}
return reflect.DeepEqual(val1, val2)
}
Loading