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
17 changes: 16 additions & 1 deletion internal/agentcrd/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/ObolNetwork/obol-stack/internal/config"
"github.com/ObolNetwork/obol-stack/internal/kubectl"
"github.com/ObolNetwork/obol-stack/internal/stackbackup"
"github.com/ObolNetwork/obol-stack/internal/ui"
"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -89,14 +90,28 @@ func ResumeAll(cfg *config.Config, u *ui.UI) {
u.Warnf("Could not read recorded agent %s: %v", name, err)
continue
}
// Persisted manifests may include server-managed metadata (resourceVersion,
// uid, managedFields, ...) captured after apply. Strip them so kubectl
// apply does not fail with "resourceVersion: Invalid value: 0".
var doc map[string]any
if err := yaml.Unmarshal(data, &doc); err != nil {
u.Warnf("Could not parse recorded agent %s: %v", name, err)
continue
}
stackbackup.StripServerManagedMetadata(doc)
stripped, err := yaml.Marshal(doc)
if err != nil {
u.Warnf("Could not re-marshal recorded agent %s: %v", name, err)
continue
}
warnIfWalletWouldRegenerate(cfg, name, data, u)
nsErr := kubectl.PipeCommands(bin, kubeconfig,
[]string{"create", "namespace", Namespace(name), "--dry-run=client", "-o", "yaml"},
[]string{"apply", "-f", "-"})
if nsErr != nil {
u.Warnf("Could not ensure namespace for agent %s: %v", name, nsErr)
}
if err := kubectl.Apply(bin, kubeconfig, data); err != nil {
if err := kubectl.Apply(bin, kubeconfig, stripped); err != nil {
u.Warnf("Could not re-apply agent %s (run 'obol agent new %s' to recreate): %v", name, name, err)
continue
}
Expand Down
53 changes: 53 additions & 0 deletions internal/agentcrd/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/ObolNetwork/obol-stack/internal/config"
"github.com/ObolNetwork/obol-stack/internal/stackbackup"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -64,3 +65,55 @@ func TestManifestStoreRoundTrip(t *testing.T) {
t.Fatalf("after remove: %v", names)
}
}

// TestStripServerManagedMetadataOnAgentManifest pins the ResumeAll strip path:
// persisted Agent YAML may carry server-managed metadata; stripping must drop
// those fields while keeping name/namespace and spec intact.
func TestStripServerManagedMetadataOnAgentManifest(t *testing.T) {
manifest := map[string]any{
"apiVersion": "obol.org/v1alpha1",
"kind": "Agent",
"metadata": map[string]any{
"name": "quant",
"namespace": "agent-quant",
"resourceVersion": "12345",
"uid": "abc-123",
"creationTimestamp": "2024-01-01T00:00:00Z",
"managedFields": []any{map[string]any{"manager": "kubectl"}},
},
"spec": map[string]any{
"model": "qwen3.5:9b",
"skills": []any{"gas"},
"wallet": map[string]any{"create": true},
},
}
data, err := yaml.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
var doc map[string]any
if err := yaml.Unmarshal(data, &doc); err != nil {
t.Fatal(err)
}
stackbackup.StripServerManagedMetadata(doc)

meta, ok := doc["metadata"].(map[string]any)
if !ok {
t.Fatalf("metadata missing or wrong type: %T", doc["metadata"])
}
for _, k := range []string{"resourceVersion", "uid", "creationTimestamp", "managedFields"} {
if _, present := meta[k]; present {
t.Errorf("server-managed field %q still present after strip", k)
}
}
if meta["name"] != "quant" || meta["namespace"] != "agent-quant" {
t.Fatalf("identity fields altered: %v", meta)
}
spec, ok := doc["spec"].(map[string]any)
if !ok {
t.Fatalf("spec missing or wrong type: %T", doc["spec"])
}
if spec["model"] != "qwen3.5:9b" {
t.Fatalf("spec.model altered: %v", spec["model"])
}
}
10 changes: 7 additions & 3 deletions internal/stackbackup/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,16 +211,20 @@ func StripK8sJSON(data []byte) ([]byte, error) {
}
for _, it := range items {
if obj, ok := it.(map[string]any); ok {
stripObject(obj)
StripServerManagedMetadata(obj)
}
}
} else {
stripObject(doc)
StripServerManagedMetadata(doc)
}
return json.MarshalIndent(doc, "", " ")
}

func stripObject(obj map[string]any) {
// StripServerManagedMetadata strips server-managed fields from one decoded
// Kubernetes object (map from json.Unmarshal or yaml.Unmarshal) so it can be
// re-applied. Used by StripK8sJSON (export/import dumps) and by
// internal/agentcrd.ResumeAll (persisted Agent manifests on stack up).
func StripServerManagedMetadata(obj map[string]any) {
delete(obj, "status")
meta, ok := obj["metadata"].(map[string]any)
if !ok {
Expand Down
Loading