From 52a1d78e3962015d57e5e245a1925fde33dee09b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 15 Jan 2021 18:28:32 -0500 Subject: [PATCH] state: add a regression test for state store schema To allow the index to be refactored without accidental changes. To update the expected value run: 'go test ./agent/consul/state -update' --- agent/consul/state/schema_oss_test.go | 5 + agent/consul/state/schema_test.go | 93 ++++++++- .../testdata/TestStateStoreSchema.golden | 188 ++++++++++++++++++ internal/testing/golden/golden.go | 36 ++++ 4 files changed, 317 insertions(+), 5 deletions(-) create mode 100644 agent/consul/state/schema_oss_test.go create mode 100644 agent/consul/state/testdata/TestStateStoreSchema.golden create mode 100644 internal/testing/golden/golden.go diff --git a/agent/consul/state/schema_oss_test.go b/agent/consul/state/schema_oss_test.go new file mode 100644 index 000000000..120d49603 --- /dev/null +++ b/agent/consul/state/schema_oss_test.go @@ -0,0 +1,5 @@ +// +build !consulent + +package state + +var stateStoreSchemaExpected = "TestStateStoreSchema.golden" diff --git a/agent/consul/state/schema_test.go b/agent/consul/state/schema_test.go index b96a027c2..0d9a47838 100644 --- a/agent/consul/state/schema_test.go +++ b/agent/consul/state/schema_test.go @@ -1,17 +1,100 @@ package state import ( + "bytes" + "fmt" + "reflect" + "runtime" + "sort" "testing" "github.com/hashicorp/go-memdb" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/internal/testing/golden" ) -func TestStateStore_Schema(t *testing.T) { - // First call the schema creation +func TestStateStoreSchema(t *testing.T) { schema := stateStoreSchema() + require.NoError(t, schema.Validate()) - // Try to initialize a new memdb using the schema - if _, err := memdb.NewMemDB(schema); err != nil { - t.Fatalf("err: %s", err) + _, err := memdb.NewMemDB(schema) + require.NoError(t, err) + + actual, err := repr(schema) + require.NoError(t, err) + + expected := golden.Get(t, actual, stateStoreSchemaExpected) + require.Equal(t, expected, actual) +} + +func repr(schema *memdb.DBSchema) (string, error) { + tables := make([]string, 0, len(schema.Tables)) + for name := range schema.Tables { + tables = append(tables, name) + } + sort.Strings(tables) + + buf := new(bytes.Buffer) + for _, name := range tables { + fmt.Fprintf(buf, "table=%v\n", name) + + indexes := indexNames(schema.Tables[name]) + for _, i := range indexes { + index := schema.Tables[name].Indexes[i] + fmt.Fprintf(buf, " index=%v", i) + if index.Unique { + buf.WriteString(" unique") + } + if index.AllowMissing { + buf.WriteString(" allow-missing") + } + buf.WriteString("\n") + buf.WriteString(" indexer=") + formatIndexer(buf, index.Indexer) + buf.WriteString("\n") + } + buf.WriteString("\n") + } + + return buf.String(), nil +} + +func formatIndexer(buf *bytes.Buffer, indexer memdb.Indexer) { + v := reflect.Indirect(reflect.ValueOf(indexer)) + typ := v.Type() + buf.WriteString(typ.PkgPath() + "." + typ.Name()) + for i := 0; i < typ.NumField(); i++ { + fmt.Fprintf(buf, " %v=", typ.Field(i).Name) + + field := v.Field(i) + switch typ.Field(i).Type.Kind() { + case reflect.Slice: + buf.WriteString("[") + for j := 0; j < field.Len(); j++ { + if j != 0 { + buf.WriteString(", ") + } + // TODO: handle other types of slices + formatIndexer(buf, v.Field(i).Index(j).Interface().(memdb.Indexer)) + } + buf.WriteString("]") + case reflect.Func: + // Functions are printed as pointer addresses, which change frequently. + // Instead use the name. + buf.WriteString(runtime.FuncForPC(field.Pointer()).Name()) + default: + fmt.Fprintf(buf, "%v", field) + } } } + +func indexNames(table *memdb.TableSchema) []string { + indexes := make([]string, 0, len(table.Indexes)) + for name := range table.Indexes { + indexes = append(indexes, name) + } + + sort.Strings(indexes) + return indexes +} diff --git a/agent/consul/state/testdata/TestStateStoreSchema.golden b/agent/consul/state/testdata/TestStateStoreSchema.golden new file mode 100644 index 000000000..2b817d029 --- /dev/null +++ b/agent/consul/state/testdata/TestStateStoreSchema.golden @@ -0,0 +1,188 @@ +table=acl-auth-methods + index=id unique + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Name Lowercase=true + +table=acl-binding-rules + index=authmethod + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=AuthMethod Lowercase=true + index=id unique + indexer=github.com/hashicorp/go-memdb.UUIDFieldIndex Field=ID + +table=acl-policies + index=id unique + indexer=github.com/hashicorp/go-memdb.UUIDFieldIndex Field=ID + index=name unique + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Name Lowercase=true + +table=acl-roles + index=id unique + indexer=github.com/hashicorp/go-memdb.UUIDFieldIndex Field=ID + index=name unique + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Name Lowercase=true + index=policies allow-missing + indexer=github.com/hashicorp/consul/agent/consul/state.RolePoliciesIndex + +table=acl-tokens + index=accessor unique allow-missing + indexer=github.com/hashicorp/go-memdb.UUIDFieldIndex Field=AccessorID + index=authmethod allow-missing + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=AuthMethod Lowercase=false + index=expires-global allow-missing + indexer=github.com/hashicorp/consul/agent/consul/state.TokenExpirationIndex LocalFilter=false + index=expires-local allow-missing + indexer=github.com/hashicorp/consul/agent/consul/state.TokenExpirationIndex LocalFilter=true + index=id unique + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=SecretID Lowercase=false + index=local + indexer=github.com/hashicorp/go-memdb.ConditionalIndex Conditional=github.com/hashicorp/consul/agent/consul/state.tokensTableSchema.func1 + index=needs-upgrade + indexer=github.com/hashicorp/go-memdb.ConditionalIndex Conditional=github.com/hashicorp/consul/agent/consul/state.tokensTableSchema.func2 + index=policies allow-missing + indexer=github.com/hashicorp/consul/agent/consul/state.TokenPoliciesIndex + index=roles allow-missing + indexer=github.com/hashicorp/consul/agent/consul/state.TokenRolesIndex + +table=autopilot-config + index=id unique allow-missing + indexer=github.com/hashicorp/go-memdb.ConditionalIndex Conditional=github.com/hashicorp/consul/agent/consul/state.autopilotConfigTableSchema.func1 + +table=checks + index=id unique + indexer=github.com/hashicorp/go-memdb.CompoundIndex Indexes=[github.com/hashicorp/go-memdb.StringFieldIndex Field=Node Lowercase=true, github.com/hashicorp/go-memdb.StringFieldIndex Field=CheckID Lowercase=true] AllowMissing=false + index=node allow-missing + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Node Lowercase=true + index=node_service allow-missing + indexer=github.com/hashicorp/go-memdb.CompoundIndex Indexes=[github.com/hashicorp/go-memdb.StringFieldIndex Field=Node Lowercase=true, github.com/hashicorp/go-memdb.StringFieldIndex Field=ServiceID Lowercase=true] AllowMissing=false + index=node_service_check allow-missing + indexer=github.com/hashicorp/go-memdb.CompoundIndex Indexes=[github.com/hashicorp/go-memdb.StringFieldIndex Field=Node Lowercase=true, github.com/hashicorp/go-memdb.FieldSetIndex Field=ServiceID] AllowMissing=false + index=service allow-missing + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=ServiceName Lowercase=true + index=status + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Status Lowercase=false + +table=config-entries + index=id unique + indexer=github.com/hashicorp/go-memdb.CompoundIndex Indexes=[github.com/hashicorp/go-memdb.StringFieldIndex Field=Kind Lowercase=true, github.com/hashicorp/go-memdb.StringFieldIndex Field=Name Lowercase=true] AllowMissing=false + index=intention-legacy-id unique allow-missing + indexer=github.com/hashicorp/consul/agent/consul/state.ServiceIntentionLegacyIDIndex uuidFieldIndex={} + index=intention-source allow-missing + indexer=github.com/hashicorp/consul/agent/consul/state.ServiceIntentionSourceIndex + index=kind + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Kind Lowercase=true + index=link allow-missing + indexer=github.com/hashicorp/consul/agent/consul/state.ConfigEntryLinkIndex + +table=connect-ca-builtin + index=id unique + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=ID Lowercase=false + +table=connect-ca-config + index=id unique allow-missing + indexer=github.com/hashicorp/go-memdb.ConditionalIndex Conditional=github.com/hashicorp/consul/agent/consul/state.caConfigTableSchema.func1 + +table=connect-ca-roots + index=id unique + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=ID Lowercase=false + +table=connect-intentions + index=destination allow-missing + indexer=github.com/hashicorp/go-memdb.CompoundIndex Indexes=[github.com/hashicorp/go-memdb.StringFieldIndex Field=DestinationNS Lowercase=true, github.com/hashicorp/go-memdb.StringFieldIndex Field=DestinationName Lowercase=true] AllowMissing=false + index=id unique + indexer=github.com/hashicorp/go-memdb.UUIDFieldIndex Field=ID + index=source allow-missing + indexer=github.com/hashicorp/go-memdb.CompoundIndex Indexes=[github.com/hashicorp/go-memdb.StringFieldIndex Field=SourceNS Lowercase=true, github.com/hashicorp/go-memdb.StringFieldIndex Field=SourceName Lowercase=true] AllowMissing=false + index=source_destination unique allow-missing + indexer=github.com/hashicorp/go-memdb.CompoundIndex Indexes=[github.com/hashicorp/go-memdb.StringFieldIndex Field=SourceNS Lowercase=true, github.com/hashicorp/go-memdb.StringFieldIndex Field=SourceName Lowercase=true, github.com/hashicorp/go-memdb.StringFieldIndex Field=DestinationNS Lowercase=true, github.com/hashicorp/go-memdb.StringFieldIndex Field=DestinationName Lowercase=true] AllowMissing=false + +table=coordinates + index=id unique + indexer=github.com/hashicorp/go-memdb.CompoundIndex Indexes=[github.com/hashicorp/go-memdb.StringFieldIndex Field=Node Lowercase=true, github.com/hashicorp/go-memdb.StringFieldIndex Field=Segment Lowercase=true] AllowMissing=true + index=node + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Node Lowercase=true + +table=federation-states + index=id unique + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Datacenter Lowercase=true + +table=gateway-services + index=gateway + indexer=github.com/hashicorp/consul/agent/consul/state.ServiceNameIndex Field=Gateway + index=id unique + indexer=github.com/hashicorp/go-memdb.CompoundIndex Indexes=[github.com/hashicorp/consul/agent/consul/state.ServiceNameIndex Field=Gateway, github.com/hashicorp/consul/agent/consul/state.ServiceNameIndex Field=Service, github.com/hashicorp/go-memdb.IntFieldIndex Field=Port] AllowMissing=false + index=service allow-missing + indexer=github.com/hashicorp/consul/agent/consul/state.ServiceNameIndex Field=Service + +table=index + index=id unique + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Key Lowercase=true + +table=kvs + index=id unique + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Key Lowercase=false + index=session allow-missing + indexer=github.com/hashicorp/go-memdb.UUIDFieldIndex Field=Session + +table=mesh-topology + index=downstream + indexer=github.com/hashicorp/consul/agent/consul/state.ServiceNameIndex Field=Downstream + index=id unique + indexer=github.com/hashicorp/go-memdb.CompoundIndex Indexes=[github.com/hashicorp/consul/agent/consul/state.ServiceNameIndex Field=Upstream, github.com/hashicorp/consul/agent/consul/state.ServiceNameIndex Field=Downstream] AllowMissing=false + index=upstream allow-missing + indexer=github.com/hashicorp/consul/agent/consul/state.ServiceNameIndex Field=Upstream + +table=nodes + index=id unique + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Node Lowercase=true + index=meta allow-missing + indexer=github.com/hashicorp/go-memdb.StringMapFieldIndex Field=Meta Lowercase=false + index=uuid unique allow-missing + indexer=github.com/hashicorp/go-memdb.UUIDFieldIndex Field=ID + +table=prepared-queries + index=id unique + indexer=github.com/hashicorp/go-memdb.UUIDFieldIndex Field=ID + index=name unique allow-missing + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Name Lowercase=true + index=session allow-missing + indexer=github.com/hashicorp/go-memdb.UUIDFieldIndex Field=Session + index=template unique allow-missing + indexer=github.com/hashicorp/consul/agent/consul/state.PreparedQueryIndex + +table=services + index=connect allow-missing + indexer=github.com/hashicorp/consul/agent/consul/state.IndexConnectService + index=id unique + indexer=github.com/hashicorp/go-memdb.CompoundIndex Indexes=[github.com/hashicorp/go-memdb.StringFieldIndex Field=Node Lowercase=true, github.com/hashicorp/go-memdb.StringFieldIndex Field=ServiceID Lowercase=true] AllowMissing=false + index=kind + indexer=github.com/hashicorp/consul/agent/consul/state.IndexServiceKind + index=node + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Node Lowercase=true + index=service allow-missing + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=ServiceName Lowercase=true + +table=session_checks + index=id unique + indexer=github.com/hashicorp/go-memdb.CompoundIndex Indexes=[github.com/hashicorp/go-memdb.StringFieldIndex Field=Node Lowercase=true, github.com/hashicorp/consul/agent/consul/state.CheckIDIndex, github.com/hashicorp/go-memdb.UUIDFieldIndex Field=Session] AllowMissing=false + index=node_check + indexer=github.com/hashicorp/go-memdb.CompoundIndex Indexes=[github.com/hashicorp/go-memdb.StringFieldIndex Field=Node Lowercase=true, github.com/hashicorp/consul/agent/consul/state.CheckIDIndex] AllowMissing=false + index=session + indexer=github.com/hashicorp/go-memdb.UUIDFieldIndex Field=Session + +table=sessions + index=id unique + indexer=github.com/hashicorp/go-memdb.UUIDFieldIndex Field=ID + index=node + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Node Lowercase=true + +table=system-metadata + index=id unique + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Key Lowercase=true + +table=tombstones + index=id unique + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=Key Lowercase=false + +table=usage + index=id unique + indexer=github.com/hashicorp/go-memdb.StringFieldIndex Field=ID Lowercase=true + diff --git a/internal/testing/golden/golden.go b/internal/testing/golden/golden.go new file mode 100644 index 000000000..66ec2a0ff --- /dev/null +++ b/internal/testing/golden/golden.go @@ -0,0 +1,36 @@ +package golden + +import ( + "flag" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +// update allows golden files to be updated based on the current output. +var update = flag.Bool("update", false, "update golden files") + +// Get reads the expected value from the file at filename and returns the value. +// filename is relative to the ./testdata directory. +// +// If the `-update` flag is used with `go test`, the golden file will be updated +// to the value of actual. +func Get(t *testing.T, actual, filename string) string { + t.Helper() + + path := filepath.Join("testdata", filename) + if *update { + if dir := filepath.Dir(path); dir != "." { + require.NoError(t, os.MkdirAll(dir, 0755)) + } + err := ioutil.WriteFile(path, []byte(actual), 0644) + require.NoError(t, err) + } + + expected, err := ioutil.ReadFile(path) + require.NoError(t, err) + return string(expected) +}