Parse KVM domain metadata by local name: libvirt returns it without a namespace

Run 3 on the real host got past Create and failed in Get: libvirt 11.6
selects the <instance> element by our URI in DomainGetMetadata but hands
it back with no namespace declaration, and the parser required the URI
on the element. The lookup already guarantees the namespace, so match on
the local name only. The fake now strips the declaration the way libvirt
does, and a new test pins the bare form. Execution log updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 19:13:51 +00:00
co-authored by Claude Fable 5.1
parent cd51886139
commit ffb328661d
4 changed files with 50 additions and 8 deletions
+5 -1
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"slices"
"strings"
"sync"
libvirt "github.com/digitalocean/go-libvirt"
@@ -151,8 +152,11 @@ func (f *fakeAPI) DomainDefineXML(raw string) (libvirt.Domain, error) {
if err != nil {
return libvirt.Domain{}, lvError(libvirt.ErrXMLError, "%v", err)
}
// libvirt hands the element back without its namespace declaration
// (seen on 11.6); mimic that so Get/ListByTag parse the real shape.
stored := strings.Replace(string(meta), ` xmlns="`+metadataURI+`"`, "", 1)
existing := f.domains[d.Name]
nd := &fakeDomain{name: d.Name, xml: raw, metadata: string(meta)}
nd := &fakeDomain{name: d.Name, xml: raw, metadata: stored}
if existing != nil {
nd.running = existing.running
nd.addrs = existing.addrs
+12 -6
View File
@@ -31,15 +31,21 @@ func newInstanceMetadata(uid string, createdAt time.Time) instanceMetadata {
}
// parseInstanceMetadata decodes the element as DomainGetMetadata returns it.
// libvirt may re-serialise it with a namespace prefix rather than the default
// namespace it was defined with; encoding/xml matches on namespace URI, so
// both spellings decode.
// libvirt selects the element by namespace URI but serialises it without the
// declaration (observed on libvirt 11.6: a bare <instance>), and other
// versions may keep a default or prefixed form. The URI is already checked
// by the lookup, so only the local name is matched here.
func parseInstanceMetadata(s string) (instanceMetadata, error) {
var m instanceMetadata
if err := xml.Unmarshal([]byte(s), &m); err != nil {
var raw struct {
XMLName xml.Name `xml:"instance"`
Managed string `xml:"managed"`
UID string `xml:"uid"`
CreatedAt string `xml:"createdAt"`
}
if err := xml.Unmarshal([]byte(s), &raw); err != nil {
return instanceMetadata{}, fmt.Errorf("parsing domain metadata: %w", err)
}
return m, nil
return instanceMetadata{Managed: raw.Managed, UID: raw.UID, CreatedAt: raw.CreatedAt}, nil
}
func (m instanceMetadata) managed() bool {
+15
View File
@@ -45,6 +45,21 @@ func TestParseInstanceMetadata_libvirtPrefixedForm(t *testing.T) {
}
}
func TestParseInstanceMetadata_libvirtBareForm(t *testing.T) {
t.Parallel()
// What libvirt 11.6 actually returns from DomainGetMetadata: the element
// selected by our URI, serialised without any namespace declaration.
in := `<instance><managed>true</managed><uid>uid-3</uid>` +
`<createdAt>2026-09-10T07:30:00Z</createdAt></instance>`
m, err := parseInstanceMetadata(in)
if err != nil {
t.Fatalf("parseInstanceMetadata() error = %v", err)
}
if !m.managed() || m.UID != "uid-3" || m.createdAt().Minute() != 30 {
t.Errorf("parsed = %+v", m)
}
}
func TestParseInstanceMetadata_notManaged(t *testing.T) {
t.Parallel()
tests := []struct {