Skip to content

Commit 52e92dc

Browse files
committed
add extension points for 3rd party integrations
Signed-off-by: Scott Weiss <sdw35@cornell.edu>
1 parent 7ad5567 commit 52e92dc

17 files changed

Lines changed: 398 additions & 133 deletions

go/cmd/controller/main.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,12 @@ import (
2929
func main() {
3030
authorizer := &auth.NoopAuthorizer{}
3131
authenticator := &auth.UnsecureAuthenticator{}
32-
app.Start(authenticator, authorizer)
32+
app.Start(func(bootstrap app.BootstrapConfig) (*app.ExtensionConfig, error) {
33+
return &app.ExtensionConfig{
34+
Authenticator: authenticator,
35+
Authorizer: authorizer,
36+
AgentPlugins: nil,
37+
MCPServerPlugins: nil,
38+
}, nil
39+
})
3340
}

go/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/gorilla/mux v1.8.1
1313
github.com/hashicorp/go-multierror v1.1.1
1414
github.com/jedib0t/go-pretty/v6 v6.6.8
15-
github.com/kagent-dev/kmcp v0.1.6
15+
github.com/kagent-dev/kmcp v0.1.7
1616
github.com/mark3labs/mcp-go v0.34.0
1717
github.com/prometheus/client_golang v1.23.0
1818
github.com/spf13/cobra v1.9.1
@@ -134,7 +134,7 @@ require (
134134
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
135135
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
136136
google.golang.org/grpc v1.73.0 // indirect
137-
google.golang.org/protobuf v1.36.6 // indirect
137+
google.golang.org/protobuf v1.36.6
138138
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
139139
gopkg.in/inf.v0 v0.9.1 // indirect
140140
gopkg.in/yaml.v3 v3.0.1 // indirect

go/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
128128
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
129129
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
130130
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
131-
github.com/kagent-dev/kmcp v0.1.6 h1:O+W2g1tV2GcBHl04Tzln/wbP+VsVV6Mp/E1qCjUjYPQ=
132-
github.com/kagent-dev/kmcp v0.1.6/go.mod h1:aPwM1QtoAackdbQqLrPaNSSg/KZa0fFjbTJyOZFJNK8=
131+
github.com/kagent-dev/kmcp v0.1.7 h1:laMTjesOxz+KVaMe0xHd6kxn50xi/PlfeelRM9tQtCs=
132+
github.com/kagent-dev/kmcp v0.1.7/go.mod h1:aPwM1QtoAackdbQqLrPaNSSg/KZa0fFjbTJyOZFJNK8=
133133
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
134134
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
135135
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=

go/internal/controller/reconciler/reconciler.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"sync"
99

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

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

475+
// modified version of controllerutil.CreateOrUpdate to support proto based objects like istio
476+
func createOrUpdate(ctx context.Context, c client.Client, obj client.Object, f controllerutil.MutateFn) (controllerutil.OperationResult, error) {
477+
key := client.ObjectKeyFromObject(obj)
478+
if err := c.Get(ctx, key, obj); err != nil {
479+
if !k8s_errors.IsNotFound(err) {
480+
return controllerutil.OperationResultNone, err
481+
}
482+
if f != nil {
483+
if err := mutate(f, key, obj); err != nil {
484+
return controllerutil.OperationResultNone, err
485+
}
486+
}
487+
488+
if err := c.Create(ctx, obj); err != nil {
489+
return controllerutil.OperationResultNone, err
490+
}
491+
return controllerutil.OperationResultCreated, nil
492+
}
493+
494+
existing := obj.DeepCopyObject()
495+
if f != nil {
496+
if err := mutate(f, key, obj); err != nil {
497+
return controllerutil.OperationResultNone, err
498+
}
499+
}
500+
501+
// special equality function to handle proto based crds
502+
if reconcilerutils.ObjectsEqual(existing, obj) {
503+
return controllerutil.OperationResultNone, nil
504+
}
505+
506+
if err := c.Update(ctx, obj); err != nil {
507+
return controllerutil.OperationResultNone, err
508+
}
509+
510+
return controllerutil.OperationResultUpdated, nil
511+
}
512+
513+
// mutate wraps a MutateFn and applies validation to its result.
514+
func mutate(f controllerutil.MutateFn, key client.ObjectKey, obj client.Object) error {
515+
if err := f(); err != nil {
516+
return err
517+
}
518+
if newKey := client.ObjectKeyFromObject(obj); key != newKey {
519+
return fmt.Errorf("MutateFn cannot mutate object name and/or object namespace")
520+
}
521+
return nil
522+
}
523+
474524
func (a *kagentReconciler) deleteObjects(ctx context.Context, objects map[types.UID]client.Object) error {
475525
// Pruning owned objects in the cluster which are not should not be present after the reconciliation.
476526
pruneErrs := []error{}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package utils
2+
3+
import (
4+
protoV2 "google.golang.org/protobuf/proto"
5+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
6+
"k8s.io/apimachinery/pkg/runtime"
7+
"reflect"
8+
)
9+
10+
// returns true if "relevant" parts of obj1 and obj2 have equal:
11+
// - labels,
12+
// - annotations,
13+
// - namespace+name,
14+
// - non-metadata, non-status fields
15+
// Note that Status fields are not compared.
16+
// To compare status fields, use ObjectStatusesEqual
17+
func ObjectsEqual(obj1, obj2 runtime.Object) bool {
18+
value1, value2 := reflect.ValueOf(obj1), reflect.ValueOf(obj2)
19+
20+
if value1.Type() != value2.Type() {
21+
return false
22+
}
23+
24+
if value1.Kind() == reflect.Ptr {
25+
value1 = value1.Elem()
26+
value2 = value2.Elem()
27+
}
28+
29+
if meta1, hasMeta := obj1.(metav1.Object); hasMeta {
30+
if !ObjectMetasEqual(meta1, obj2.(metav1.Object)) {
31+
return false
32+
}
33+
}
34+
35+
// recurse through fields of both, comparing each:
36+
for i := 0; i < value1.NumField(); i++ {
37+
field1Name := value1.Type().Field(i).Name
38+
if field1Name == "ObjectMeta" {
39+
// skip ObjectMeta field, as we already asserted relevant fields are equal
40+
continue
41+
}
42+
if field1Name == "TypeMeta" {
43+
// skip TypeMeta field, as it is set by the server and not relevant for object comparison
44+
continue
45+
}
46+
if field1Name == "Status" {
47+
// skip Status field, as it is considered a separate and not relevant for object comparison
48+
continue
49+
}
50+
51+
field1 := mkPointer(value1.Field(i))
52+
field2 := mkPointer(value2.Field(i))
53+
54+
// assert DeepEquality any other fields
55+
if !DeepEqual(field1, field2) {
56+
return false
57+
}
58+
}
59+
60+
return true
61+
}
62+
63+
// returns true if "relevant" parts of obj1 and obj2 have equal:
64+
// -labels
65+
// -annotations
66+
// -namespace+name
67+
// or if the objects are not metav1.Objects
68+
func ObjectMetasEqual(obj1, obj2 metav1.Object) bool {
69+
return obj1.GetNamespace() == obj2.GetNamespace() &&
70+
obj1.GetName() == obj2.GetName() &&
71+
mapStringEqual(obj1.GetLabels(), obj2.GetLabels()) &&
72+
mapStringEqual(obj1.GetAnnotations(), obj2.GetAnnotations())
73+
}
74+
75+
func mapStringEqual(map1, map2 map[string]string) bool {
76+
if map1 == nil && map2 == nil {
77+
return true
78+
}
79+
80+
if len(map1) != len(map2) {
81+
return false
82+
}
83+
84+
for key1, val1 := range map1 {
85+
val2, ok := map2[key1]
86+
if !ok {
87+
return false
88+
}
89+
if val1 != val2 {
90+
return false
91+
}
92+
}
93+
return true
94+
}
95+
96+
// if i is a pointer, just return the value.
97+
// if i is addressable, return that.
98+
// if i is a struct passed in by value, make a new instance of the type and copy the contents to that and return
99+
// the pointer to that.
100+
func mkPointer(val reflect.Value) interface{} {
101+
if val.Kind() == reflect.Ptr {
102+
return val.Interface()
103+
}
104+
if val.CanAddr() {
105+
return val.Addr().Interface()
106+
}
107+
if val.Kind() == reflect.Struct {
108+
nv := reflect.New(val.Type())
109+
nv.Elem().Set(val)
110+
return nv.Interface()
111+
}
112+
return val.Interface()
113+
}
114+
115+
// DeepEqual should be used in place of reflect.DeepEqual when the type of an object is unknown and may be a proto message.
116+
// see https://github.com/golang/protobuf/issues/1173 for details on why reflect.DeepEqual no longer works for proto messages
117+
func DeepEqual(val1, val2 interface{}) bool {
118+
protoVal1, isProto := val1.(protoV2.Message)
119+
if isProto {
120+
protoVal2, isProto := val2.(protoV2.Message)
121+
if !isProto {
122+
return false // different types
123+
}
124+
return protoV2.Equal(protoVal1, protoVal2)
125+
}
126+
return reflect.DeepEqual(val1, val2)
127+
}

0 commit comments

Comments
 (0)