fix: consume ignored entries in CE downgrade via Ent snapshot (#20977)

This operation would previously fail due to unconsumed bytes in the
decoder buffer when reading the Ent snapshot (the first byte of the
record would be misinterpreted as a type indicator, and the remaining
bytes would fail to be deserialized or read as invalid data).

Ensure restore succeeds by decoding the ignored record as an
interface{}, which will consume the record bytes without requiring a
concrete target struct, then moving on to the next record.
This commit is contained in:
Michael Zalimeni 2024-04-11 17:08:44 -04:00 committed by GitHub
parent e231f0ee9b
commit a8d08e759f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 106 additions and 3 deletions

3
.changelog/20977.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
server: fix Ent snapshot restore on CE when CE downgrade is enabled
```

View File

@ -196,7 +196,7 @@ func (c *FSM) Apply(log *raft.Log) interface{} {
return nil
}
if structs.CEDowngrade && msgType >= 64 {
c.logger.Warn("ignoring enterprise message, for downgrading to oss", "type", msgType)
c.logger.Warn("ignoring enterprise message as part of downgrade to CE", "type", msgType)
return nil
}
panic(fmt.Errorf("failed to apply request: %#v", buf))
@ -268,8 +268,9 @@ func (c *FSM) Restore(old io.ReadCloser) error {
}
default:
if structs.CEDowngrade && msg >= 64 {
c.logger.Warn("ignoring enterprise message , for downgrading to oss", "type", msg)
return nil
c.logger.Warn("ignoring enterprise message as part of downgrade to CE", "type", msg)
var ignore interface{}
return dec.Decode(&ignore)
} else if msg >= 64 {
return fmt.Errorf("msg type <%d> is a Consul Enterprise log entry. Consul CE cannot restore it", msg)
} else {

View File

@ -15,6 +15,7 @@ import (
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib/stringslice"
"github.com/hashicorp/consul/sdk/testutil"
)
@ -60,3 +61,101 @@ func TestRestoreFromEnterprise(t *testing.T) {
require.EqualError(t, fsm.Restore(sink), "msg type <65> is a Consul Enterprise log entry. Consul CE cannot restore it")
sink.Cancel()
}
func TestRestoreFromEnterprise_CEDowngrade(t *testing.T) {
logger := testutil.Logger(t)
handle := &testRaftHandle{}
storageBackend := newStorageBackend(t, handle)
handle.apply = func(buf []byte) (any, error) { return storageBackend.Apply(buf, 123), nil }
fsm := NewFromDeps(Deps{
Logger: logger,
NewStateStore: func() *state.Store {
return state.NewStateStore(nil)
},
StorageBackend: storageBackend,
})
// To verify if a proper message is displayed when Consul CE tries to
// unsuccessfully restore entries from a Consul Ent snapshot.
buf := bytes.NewBuffer(nil)
sink := &MockSink{buf, false}
type EntMock struct {
ID int
Type string
}
entMockEntry := EntMock{
ID: 65,
Type: "A Consul Ent Log Type",
}
// Create one entry to exercise the Go struct marshaller, and one to exercise the
// Binary Marshaller interface. This verifies that regardless of whether the struct gets
// encoded as a msgpack byte string (binary marshaller) or msgpack map (other struct),
// it will still be skipped over correctly.
registerEntry := structs.RegisterRequest{
Datacenter: "dc1",
Node: "foo",
Address: "127.0.0.1",
Service: &structs.NodeService{
ID: "db",
Service: "db",
Tags: []string{"primary"},
Port: 8000,
},
}
proxyDefaultsEntry := &structs.ConfigEntryRequest{
Op: structs.ConfigEntryUpsert,
Entry: &structs.ProxyConfigEntry{
Kind: structs.ProxyDefaults,
Name: "global",
Config: map[string]interface{}{
"foo": "bar",
},
EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
},
}
// Write the header and records.
header := SnapshotHeader{
LastIndex: 0,
}
encoder := codec.NewEncoder(sink, structs.MsgpackHandle)
encoder.Encode(&header)
sink.Write([]byte{byte(structs.MessageType(entMockEntry.ID))})
encoder.Encode(entMockEntry)
sink.Write([]byte{byte(structs.RegisterRequestType)})
encoder.Encode(registerEntry)
sink.Write([]byte{byte(structs.ConfigEntryRequestType)})
encoder.Encode(proxyDefaultsEntry)
defer func() {
structs.CEDowngrade = false
}()
structs.CEDowngrade = true
require.NoError(t, fsm.Restore(sink), "failed to decode Ent snapshot to CE")
// Verify the register request
_, nodes, err := fsm.state.Nodes(nil, nil, "")
require.NoError(t, err)
require.Len(t, nodes, 1, "incorrect number of nodes: %v", nodes)
require.Equal(t, "foo", nodes[0].Node)
require.Equal(t, "dc1", nodes[0].Datacenter)
require.Equal(t, "127.0.0.1", nodes[0].Address)
_, fooSrv, err := fsm.state.NodeServices(nil, "foo", nil, "")
require.NoError(t, err)
require.Len(t, fooSrv.Services, 1)
require.Contains(t, fooSrv.Services["db"].Tags, "primary")
require.True(t, stringslice.Contains(fooSrv.Services["db"].Tags, "primary"))
require.Equal(t, 8000, fooSrv.Services["db"].Port)
// Verify the proxy defaults request
_, configEntry, err := fsm.state.ConfigEntry(nil, structs.ProxyDefaults, "global", structs.DefaultEnterpriseMetaInDefaultPartition())
require.NoError(t, err)
configEntry.SetHash(proxyDefaultsEntry.Entry.GetHash())
require.Equal(t, proxyDefaultsEntry.Entry, configEntry)
}