agentikube/internal/kube/apply.go
2026-02-07 13:49:11 -05:00

92 lines
2.5 KiB
Go

package kube
import (
"bytes"
"context"
"fmt"
"io"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/cached/memory"
"k8s.io/client-go/restmapper"
yamlserializer "k8s.io/apimachinery/pkg/runtime/serializer/yaml"
)
// ServerSideApply splits a multi-document YAML into individual resources
// and applies each one using server-side apply with the "agentikube" field manager.
func (c *Client) ServerSideApply(ctx context.Context, manifests []byte) error {
decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(manifests), 4096)
discoveryClient, ok := c.Clientset().Discovery().(*discovery.DiscoveryClient)
if !ok {
return fmt.Errorf("failed to get discovery client")
}
cachedDiscovery := memory.NewMemCacheClient(discoveryClient)
mapper := restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscovery)
deserializer := yamlserializer.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
for {
var rawObj unstructured.Unstructured
if err := decoder.Decode(&rawObj); err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("decoding YAML document: %w", err)
}
// Skip empty documents
if len(rawObj.Object) == 0 {
continue
}
// Re-encode to JSON for the patch body
rawJSON, err := rawObj.MarshalJSON()
if err != nil {
return fmt.Errorf("marshaling to JSON: %w", err)
}
// Decode to get the GVK
obj := &unstructured.Unstructured{}
_, gvk, err := deserializer.Decode(rawJSON, nil, obj)
if err != nil {
return fmt.Errorf("deserializing object: %w", err)
}
// Map GVK to GVR using the REST mapper
mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return fmt.Errorf("mapping GVK %s to GVR: %w", gvk.String(), err)
}
gvr := mapping.Resource
name := obj.GetName()
namespace := obj.GetNamespace()
applyOpts := metav1.ApplyOptions{
FieldManager: "agentikube",
}
// Apply using the dynamic client - handle namespaced vs cluster-scoped
if namespace != "" {
_, err = c.Dynamic().Resource(gvr).Namespace(namespace).Patch(
ctx, name, types.ApplyPatchType, rawJSON, applyOpts.ToPatchOptions(),
)
} else {
_, err = c.Dynamic().Resource(gvr).Patch(
ctx, name, types.ApplyPatchType, rawJSON, applyOpts.ToPatchOptions(),
)
}
if err != nil {
return fmt.Errorf("applying %s/%s: %w", gvk.Kind, name, err)
}
fmt.Printf("applied %s/%s\n", gvk.Kind, name)
}
return nil
}