internal/transport/wirecompat_test.go
Ref: Size: 12.5 KiB History
package transport
import (
"encoding/hex"
"fmt"
"testing"
"github.com/a73x/eitri/internal/pb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
)
// The renames in "wire: the spec/status vocabulary the loop always meant"
// (VMDesired -> VMSpec, ActualVM -> VMStatus, ExposureDesired/Actual ->
// ExposureSpec/Status, power -> power_state, and the two envelopes) rested on
// an argument: protobuf serialises field numbers, not names, so an agent built
// against the old .proto still talks to a server built against the new one.
// fieldnumbers_test.go locks the numbers, but both halves of every round-trip
// there are built from TODAY's descriptors — a coordinated rename plus renumber
// plus table edit would pass it, and every other test in this package, while
// breaking every fielded agent.
//
// These fixtures close that. They are real wire bytes, produced from the
// descriptor set of proto/eitri/v1/sync.proto AS IT STOOD AT TAG v0.0.7 — the
// last release before the rename — with every field in the tree populated, and
// asserted here to decode into today's generated types losing nothing.
//
// They are a CAPTURE, not a golden file: never regenerate them to make a test
// pass. A diff here is the wire moving under a fielded agent, which is the one
// thing this package exists to prevent. Adding a field is invisible to them by
// design — old bytes simply do not carry it — but moving or reusing a number is
// not, and neither is narrowing a type.
//
// Reproducing the capture, to audit it rather than to update it: build a
// FileDescriptorSet from `git show v0.0.7:proto/eitri/v1/sync.proto`, populate
// every field of eitri.v1.AgentMessage{report} / ServerMessage{snapshot}
// through dynamicpb with the values below (strings are the field's full name
// plus "#<index>", int64 is 1000+10*number, uint64 2000+10*number, doubles
// number+0.5, bytes the field's full name, bools true, repeated fields two
// elements), and marshal deterministically.
// v007ReportFrame is a v0.0.7 AgentMessage{ActualStateReport}, every field set.
const v007ReportFrame = "" +
"12F8080AF1010A1965697472692E76312E41637475616C564D2E766D5F696423" +
"30121965697472692E76312E41637475616C564D2E706F77657223301A196569" +
"7472692E76312E41637475616C564D2E70686173652330221665697472692E76" +
"312E41637475616C564D2E697023302A1E65697472692E76312E41637475616C" +
"564D2E6C6173745F6572726F722330322365697472692E76312E41637475616C" +
"564D2E7373685F686F73745F7075626B657923303A2165697472692E76312E41" +
"637475616C564D2E7374617475735F64657461696C2330421E65697472692E76" +
"312E41637475616C564D2E6E6574776F726B5F697023300AF1010A1965697472" +
"692E76312E41637475616C564D2E766D5F69642330121965697472692E76312E" +
"41637475616C564D2E706F77657223301A1965697472692E76312E4163747561" +
"6C564D2E70686173652330221665697472692E76312E41637475616C564D2E69" +
"7023302A1E65697472692E76312E41637475616C564D2E6C6173745F6572726F" +
"722330322365697472692E76312E41637475616C564D2E7373685F686F73745F" +
"7075626B657923303A2165697472692E76312E41637475616C564D2E73746174" +
"75735F64657461696C2330421E65697472692E76312E41637475616C564D2E6E" +
"6574776F726B5F69702330122665697472692E76312E41637475616C53746174" +
"655265706F72742E64657374726F7965642330122665697472692E76312E4163" +
"7475616C53746174655265706F72742E64657374726F79656423311A660A1E65" +
"697472692E76312E51756172616E74696E6564564D2E766D5F69642330121D65" +
"697472692E76312E51756172616E74696E6564564D2E6E616D6523301A226569" +
"7472692E76312E51756172616E74696E6564564D2E766D737065635F6A736F6E" +
"2090081A660A1E65697472692E76312E51756172616E74696E6564564D2E766D" +
"5F69642330121D65697472692E76312E51756172616E74696E6564564D2E6E61" +
"6D6523301A2265697472692E76312E51756172616E74696E6564564D2E766D73" +
"7065635F6A736F6E209008220908F20710FC071886082801308C103A2A08F207" +
"10FC07188608210000000000001240290000000000001640310000000000001A" +
"4038AE0840B808422765697472692E76312E41637475616C5374617465526570" +
"6F72742E67756573745F6369647223304A6C0A1C65697472692E76312E457870" +
"6F7375726541637475616C2E69642330121F65697472692E76312E4578706F73" +
"75726541637475616C2E737461746523301A2065697472692E76312E4578706F" +
"7375726541637475616C2E726561736F6E2330220908F20710FC071886084A6C" +
"0A1C65697472692E76312E4578706F7375726541637475616C2E69642330121F" +
"65697472692E76312E4578706F7375726541637475616C2E737461746523301A" +
"2065697472692E76312E4578706F7375726541637475616C2E726561736F6E23" +
"30220908F20710FC07188608522D65697472692E76312E41637475616C537461" +
"74655265706F72742E686F73745F75706C696E6B5F616464722330"
// v007SnapshotFrame is a v0.0.7 ServerMessage{DesiredStateSnapshot}, every
// field set.
const v007SnapshotFrame = "" +
"0A850908DA0F12A0030A1A65697472692E76312E564D446573697265642E766D" +
"5F69642330121965697472692E76312E564D446573697265642E6E616D652330" +
"1A1E65697472692E76312E564D446573697265642E696D6167655F75726C2330" +
"222165697472692E76312E564D446573697265642E696D6167655F7368613235" +
"3623302A1F65697472692E76312E564D446573697265642E636C6F75645F696E" +
"6974233030A40838AE0840B8084801522065697472692E76312E564D44657369" +
"7265642E706F7765725F737461746523305801622765697472692E76312E564D" +
"446573697265642E7373685F617574686F72697A65645F6B657923308A012265" +
"697472692E76312E564D446573697265642E7373685F686F73745F6365727423" +
"3092013065697472692E76312E564D446573697265642E7373685F757365725F" +
"63615F617574686F72697A65645F6B657973233092013065697472692E76312E" +
"564D446573697265642E7373685F757365725F63615F617574686F72697A6564" +
"5F6B6579732331980101A2011C65697472692E76312E564D446573697265642E" +
"6E6574776F726B233012A0030A1A65697472692E76312E564D44657369726564" +
"2E766D5F69642330121965697472692E76312E564D446573697265642E6E616D" +
"6523301A1E65697472692E76312E564D446573697265642E696D6167655F7572" +
"6C2330222165697472692E76312E564D446573697265642E696D6167655F7368" +
"6132353623302A1F65697472692E76312E564D446573697265642E636C6F7564" +
"5F696E6974233030A40838AE0840B8084801522065697472692E76312E564D44" +
"6573697265642E706F7765725F737461746523305801622765697472692E7631" +
"2E564D446573697265642E7373685F617574686F72697A65645F6B657923308A" +
"012265697472692E76312E564D446573697265642E7373685F686F73745F6365" +
"7274233092013065697472692E76312E564D446573697265642E7373685F7573" +
"65725F63615F617574686F72697A65645F6B657973233092013065697472692E" +
"76312E564D446573697265642E7373685F757365725F63615F617574686F7269" +
"7A65645F6B6579732331980101A2011C65697472692E76312E564D4465736972" +
"65642E6E6574776F726B23301A5E0A1F65697472692E76312E4167656E745570" +
"67726164652E76657273696F6E2330121B65697472692E76312E4167656E7455" +
"7067726164652E75726C23301A1E65697472692E76312E4167656E7455706772" +
"6164652E7368613235362330226C0A1D65697472692E76312E4578706F737572" +
"65446573697265642E69642330122065697472692E76312E4578706F73757265" +
"446573697265642E766D5F6964233018AE0320B8032A2365697472692E76312E" +
"4578706F73757265446573697265642E70726F746F636F6C2330226C0A1D6569" +
"7472692E76312E4578706F73757265446573697265642E696423301220656974" +
"72692E76312E4578706F73757265446573697265642E766D5F6964233018AE03" +
"20B8032A2365697472692E76312E4578706F73757265446573697265642E7072" +
"6F746F636F6C2330"
func TestV007AgentFrameDecodesIntoTodaysReport(t *testing.T) {
var m pb.AgentMessage
require.NoError(t, proto.Unmarshal(mustHex(t, v007ReportFrame), &m))
r := m.GetReport()
require.NotNil(t, r, "a v0.0.7 report must still arrive in the report branch of the oneof")
assertNoUnknownFields(t, m.ProtoReflect(), "AgentMessage")
// The renamed message: what v0.0.7 called ActualVM.power arrives verbatim in
// VMStatus.power_state, because both are field 2 of the same message.
require.Len(t, r.GetVms(), 2)
vm := r.GetVms()[0]
assert.Equal(t, "eitri.v1.ActualVM.vm_id#0", vm.GetVmId())
assert.Equal(t, "eitri.v1.ActualVM.power#0", vm.GetPowerState())
assert.Equal(t, "eitri.v1.ActualVM.phase#0", vm.GetPhase())
assert.Equal(t, "eitri.v1.ActualVM.ip#0", vm.GetIp())
assert.Equal(t, "eitri.v1.ActualVM.last_error#0", vm.GetLastError())
assert.Equal(t, "eitri.v1.ActualVM.ssh_host_pubkey#0", vm.GetSshHostPubkey())
assert.Equal(t, "eitri.v1.ActualVM.status_detail#0", vm.GetStatusDetail())
assert.Equal(t, "eitri.v1.ActualVM.network_ip#0", vm.GetNetworkIp())
// The renamed envelope's own fields.
assert.Equal(t, "eitri.v1.ActualStateReport.guest_cidr#0", r.GetGuestCidr())
assert.Equal(t, "eitri.v1.ActualStateReport.host_uplink_addr#0", r.GetHostUplinkAddr())
assert.True(t, r.GetFenceViolation())
assert.Equal(t, uint64(2060), r.GetLastSeenEpoch())
require.NotNil(t, r.GetCapacity())
assert.Equal(t, int64(1010), r.GetCapacity().GetVcpus())
require.NotNil(t, r.GetMetrics())
assert.InEpsilon(t, 4.5, r.GetMetrics().GetLoad1(), 1e-9)
require.Len(t, r.GetQuarantined(), 2)
assert.Equal(t, []byte("eitri.v1.QuarantinedVM.vmspec_json"), r.GetQuarantined()[0].GetVmspecJson())
assert.Len(t, r.GetDestroyed(), 2)
// ExposureActual -> ExposureStatus, sessions still a nested message.
require.Len(t, r.GetExposures(), 2)
ex := r.GetExposures()[0]
assert.Equal(t, "eitri.v1.ExposureActual.id#0", ex.GetId())
assert.Equal(t, "eitri.v1.ExposureActual.state#0", ex.GetState())
require.NotNil(t, ex.GetSessions(), "presence must survive: nil sessions means no counters, not zero counters")
assert.Equal(t, int64(1010), ex.GetSessions().GetActive())
}
func TestV007ServerFrameDecodesIntoTodaysSnapshot(t *testing.T) {
var m pb.ServerMessage
require.NoError(t, proto.Unmarshal(mustHex(t, v007SnapshotFrame), &m))
s := m.GetSnapshot()
require.NotNil(t, s, "a v0.0.7 snapshot must still arrive in the snapshot branch of the oneof")
assertNoUnknownFields(t, m.ProtoReflect(), "ServerMessage")
assert.Equal(t, uint64(2010), s.GetEpoch())
require.Len(t, s.GetVms(), 2)
vm := s.GetVms()[0]
// VMDesired.power_state was already spelled right; it is field 10 either way.
assert.Equal(t, "eitri.v1.VMDesired.vm_id#0", vm.GetVmId())
assert.Equal(t, "eitri.v1.VMDesired.power_state#0", vm.GetPowerState())
assert.Equal(t, "eitri.v1.VMDesired.network#0", vm.GetNetwork())
assert.Equal(t, "eitri.v1.VMDesired.ssh_host_cert#0", vm.GetSshHostCert())
assert.True(t, vm.GetHostCertRequired())
assert.True(t, vm.GetTombstoned())
assert.True(t, vm.GetPersistent(), "field 9 still means persistent on a v0.0.7 wire")
assert.Len(t, vm.GetSshUserCaAuthorizedKeys(), 2)
require.NotNil(t, s.GetAgentUpgrade())
require.Len(t, s.GetExposures(), 2)
assert.Equal(t, "eitri.v1.ExposureDesired.id#0", s.GetExposures()[0].GetId())
}
// A field the current schema does not claim lands in the unknown-fields set
// rather than failing to parse, so silence there is the real assertion: every
// number a v0.0.7 peer writes is still a number this build knows, at the same
// wire type. Recurses, because a renumber three messages deep parses fine at
// the top.
func assertNoUnknownFields(t *testing.T, m protoreflect.Message, path string) {
t.Helper()
if u := m.GetUnknown(); len(u) > 0 {
t.Errorf("%s carries %d bytes this build does not recognise (%x) — a field number moved or was reused", path, len(u), []byte(u))
}
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
switch {
case fd.IsMap():
case fd.Kind() != protoreflect.MessageKind && fd.Kind() != protoreflect.GroupKind:
case fd.IsList():
l := v.List()
for i := range l.Len() {
assertNoUnknownFields(t, l.Get(i).Message(), fmt.Sprintf("%s.%s[%d]", path, fd.Name(), i))
}
default:
assertNoUnknownFields(t, v.Message(), path+"."+string(fd.Name()))
}
return true
})
}
// Re-encoding what a v0.0.7 peer sent must reproduce it byte for byte. Field
// numbers, wire types, ordering and repeated-element count all have to match,
// which catches the losses the getters above are too specific to see: a field
// this build no longer defines, or one whose type changed under it.
func TestV007FramesReEncodeByteIdentically(t *testing.T) {
for _, tc := range []struct {
name string
hex string
msg proto.Message
}{
{"report", v007ReportFrame, &pb.AgentMessage{}},
{"snapshot", v007SnapshotFrame, &pb.ServerMessage{}},
} {
t.Run(tc.name, func(t *testing.T) {
want := mustHex(t, tc.hex)
require.NoError(t, proto.Unmarshal(want, tc.msg))
got, err := proto.MarshalOptions{Deterministic: true}.Marshal(tc.msg)
require.NoError(t, err)
assert.Equal(t, want, got, "re-encoding a v0.0.7 frame changed it")
})
}
}
func mustHex(t *testing.T, s string) []byte {
t.Helper()
b, err := hex.DecodeString(s)
require.NoError(t, err)
return b
}