Skip to content
Draft
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
10 changes: 8 additions & 2 deletions admin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ const (
DefaultAddress = "127.0.0.1:9095"
)

// The keys this package's reader resolves.
const (
flagAdminEnabled = "admin_server.admin_enabled"
flagAdminAddress = "admin_server.admin_address"
)

// Config defines configuration for the admin gRPC server.
type Config struct {
// Enabled controls whether the admin gRPC server starts.
Expand All @@ -29,10 +35,10 @@ var DefaultConfig = Config{
// ReadConfig reads admin config from app options (Viper-backed).
func ReadConfig(opts servertypes.AppOptions) (Config, error) {
cfg := DefaultConfig
if v := opts.Get("admin_server.admin_enabled"); v != nil {
if v := opts.Get(flagAdminEnabled); v != nil {
cfg.Enabled = cast.ToBool(v)
}
if v := opts.Get("admin_server.admin_address"); v != nil {
if v := opts.Get(flagAdminAddress); v != nil {
if s := cast.ToString(v); s != "" {
cfg.Address = s
}
Expand Down
19 changes: 19 additions & 0 deletions admin/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package admin

import (
"github.com/sei-protocol/sei-chain/config/registry"
)

// SectionName is this section's name in the configuration key space.
const SectionName = "admin_server"

// Registration puts this section in the configuration registry.
//
// The keys derive from the mapstructure tags, and the reader resolves the same strings through the
// constants beside it, so a rename moves one occurrence and the test holds the two together.
func init() {
registry.RegisterSection(SectionName, &Config{}, defaults)
}

// defaults is what this section resolves to for a node that has written nothing.
func defaults(registry.Mode) any { return DefaultConfig }
44 changes: 44 additions & 0 deletions admin/register_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package admin

import (
"reflect"
"sort"
"testing"

"github.com/sei-protocol/sei-chain/config/registry"
)

// TestTheDeclaredKeysAreTheKeysThisReaderResolves holds the declaration against the reader.
//
// The tags derive the keys and the constants below are what the reader passes to Get, so this compares
// two statements that are edited for different reasons rather than one written out twice.
func TestTheDeclaredKeysAreTheKeysThisReaderResolves(t *testing.T) {
for _, defect := range registry.Defects() {
if defect.Section == SectionName {
t.Fatalf("%s was refused: %v", SectionName, defect.Err)
}
}
section, ok := registry.Lookup(SectionName)
if !ok {
t.Fatalf("%s is not registered, so nothing resolves its keys", SectionName)
}

want := []string{flagAdminAddress, flagAdminEnabled}
sort.Strings(want)
if got := section.Keys; !reflect.DeepEqual(got, want) {
t.Errorf("declared keys are %v, want %v", got, want)
}
}

// TestTheDefaultsAreWhatTheNodeAlreadyRuns keeps the section from restating the values by hand.
func TestTheDefaultsAreWhatTheNodeAlreadyRuns(t *testing.T) {
for _, mode := range registry.Modes() {
got, ok := defaults(mode).(Config)
if !ok {
t.Fatalf("mode %q: defaults returned %T, want Config", mode, defaults(mode))
}
if got != DefaultConfig {
t.Errorf("mode %q resolves to %+v, want the package default %+v", mode, got, DefaultConfig)
}
}
}
51 changes: 51 additions & 0 deletions cmd/seid/cmd/registry_sections_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cmd

import (
"testing"

"github.com/sei-protocol/sei-chain/config/registry"
)

// TestEverySectionThisBinaryDeclaresIsUsable is the check no single section can make.
//
// A section registers during its own package's initialisation, and a registration the registry cannot use
// is recorded rather than panicked, so a section that failed to register is absent rather than loud. Two
// of the refusals depend on what else has registered: two sections declaring one key, and two keys that
// collapse onto one environment variable. Neither is visible from inside either section, and the section
// that loses is dropped whole, with every key it declared.
//
// This package links every section a node's configuration reaches, so asking here is asking about the set
// a node actually gets. Nothing is enumerated, so a section added later is covered without this file
// changing.
func TestEverySectionThisBinaryDeclaresIsUsable(t *testing.T) {
for _, defect := range registry.Defects() {
t.Errorf("%s was refused, so none of its keys is declared: %v", defect.Section, defect.Err)
}
if len(registry.Sections()) == 0 {
t.Fatal("no section registered, so the checks above hold for an empty set. This package links " +
"the packages that register, and one of those imports has gone")
}
}

// TestEveryDeclaredKeyResolvesForEveryMode covers the half of a registration a section's own test cannot.
//
// Registering validates the struct a section declares against. Whether its defaults can state one value
// for every key it declared is checked when something resolves them, and until now nothing did outside the
// registry's own tests. A default that arrives short is refused rather than filled, so the failure is an
// error here instead of a key resolving to a zero nobody chose.
func TestEveryDeclaredKeyResolvesForEveryMode(t *testing.T) {
for _, mode := range registry.Modes() {
resolved, err := registry.Resolve(mode, registry.Sources{})
if err != nil {
t.Errorf("mode %q does not resolve: %v", mode, err)
continue
}
for _, section := range registry.Sections() {
for _, key := range section.Keys {
if _, ok := resolved.Values[key]; !ok {
t.Errorf("mode %q: %s declares %s and it did not resolve", mode, section.Name, key)
}
}
}
}
}
6 changes: 6 additions & 0 deletions config/registry/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@
// tag, an unexported field carrying a tag, two fields declaring one path, a struct that declares no
// key, a struct that contains itself, and two keys that collapse onto one environment variable.
//
// A field tagged "-" is the deliberate opposite and is not a defect. That tag excludes a field from
// configuration, so the field declares no key at all rather than one resolving to a default. The
// distinction matters because a missing tag and a "-" tag look alike in a diff: one is a key nothing
// names reaching a field, and the other is a field nothing configures. It is meaningful only on an
// exported field, since an unexported one carrying any tag is refused before the tag is read.
//
// A key segment is also refused if it is upper-case, or if it carries a dot or a space. That rule
// holds for the section name and for a field's tag alike, since both become segments of the same
// dotted key and answer to the same sources.
Expand Down
34 changes: 24 additions & 10 deletions config/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,13 @@ func walk(t reflect.Type, prefix string, keys *[]string, open map[reflect.Type]b
continue
}

tag, squash, err := tagOf(f, prefix)
tag, squash, skip, err := tagOf(f, prefix)
if err != nil {
return err
}
if skip {
continue
}

ft := f.Type
for ft.Kind() == reflect.Ptr {
Expand Down Expand Up @@ -289,10 +292,10 @@ func walkSubtree(t reflect.Type, path, field string, keys *[]string, open map[re
}

// tagOf returns a field's mapstructure name, or reports that the field cannot be addressed.
func tagOf(f reflect.StructField, prefix string) (name string, squash bool, err error) {
func tagOf(f reflect.StructField, prefix string) (name string, squash, skip bool, err error) {
tag, ok := f.Tag.Lookup("mapstructure")
if !ok {
return "", false, fmt.Errorf("%s.%s has no mapstructure tag; a key derived from a field "+
return "", false, false, fmt.Errorf("%s.%s has no mapstructure tag; a key derived from a field "+
"name is a key no operator writes, which is how ninety-two legacy keys became "+
"unreachable through their tags", prefix, f.Name)
}
Expand All @@ -306,25 +309,36 @@ func tagOf(f reflect.StructField, prefix string) (name string, squash bool, err
}
if squash {
if name != "" {
return "", false, fmt.Errorf("%s.%s is squashed and also names %q; one or the other",
return "", false, false, fmt.Errorf("%s.%s is squashed and also names %q; one or the other",
prefix, f.Name, name)
}
return "", true, nil
return "", true, false, nil
}
if name == "-" {
// The tag that excludes a field from configuration. Something else in the program assigns the
// field, so no reader resolves a key for it.
//
// It declares no key. Declaring one would put a key in the space that reaches no field, which an
// operator can write and nothing answers, and that is what every other refusal here exists to
// prevent. A field with no tag stays a defect for the same reason read from the other end: it
// would declare a key derived from a field name, which is a key no operator writes. The two look
// alike in a diff and mean opposite things.
return "", false, true, nil
}
if name == "" || name == "-" {
return "", false, fmt.Errorf("%s.%s has an empty mapstructure name", prefix, f.Name)
if name == "" {
return "", false, false, fmt.Errorf("%s.%s has an empty mapstructure name", prefix, f.Name)
}
if bad, found := unaddressableChar(name); found {
return "", false, fmt.Errorf("%s.%s names %q, which carries %q. A dot makes the field claim a "+
return "", false, false, fmt.Errorf("%s.%s names %q, which carries %q. A dot makes the field claim a "+
"subtree the struct does not have, and neither a dot nor a space survives a round trip "+
"through a configuration source", prefix, f.Name, name, bad)
}
if name != strings.ToLower(name) {
return "", false, fmt.Errorf("%s.%s names %q, which is not lower case; a configuration "+
return "", false, false, fmt.Errorf("%s.%s names %q, which is not lower case; a configuration "+
"source enumerates lower-cased, so this key would never match a written one",
prefix, f.Name, name)
}
return name, false, nil
return name, false, false, nil
}

// unaddressableChar returns the first character in a key segment that no configuration source can
Expand Down
7 changes: 6 additions & 1 deletion config/registry/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,15 @@ func walkValues(v reflect.Value, prefix string, out map[string]any) error {
}
continue
}
tag, squash, err := tagOf(f, prefix)
tag, squash, skip, err := tagOf(f, prefix)
if err != nil {
return err
}
if skip {
// Skipped on the type side too, so the declared keys and the rendered defaults describe the
// same set of fields and matchesDeclaration has nothing to disagree about.
continue
}

fv := v.Field(i)
for fv.Kind() == reflect.Ptr {
Expand Down
65 changes: 59 additions & 6 deletions config/registry/spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ import (
// authoring check, and it reads its order from Source's declaration rather than from its caller's
// argument order, so no caller can reorder its way to a different answer.

// gigaSection mirrors what the giga executor's own package would register. The struct under test
// is the real one, so the key comparison below measures the live reader rather than a copy of it.
// gigaSection re-registers the giga executor's section with a default that varies by mode, so the
// mode property below has something to measure. The struct is the real one, so the key comparison
// measures the live reader rather than a copy of it.
const gigaSection = "giga_executor"

func registerGiga(t *testing.T) {
Expand Down Expand Up @@ -758,9 +759,6 @@ func TestEveryRefusalIsReportedAsADefect(t *testing.T) {
type emptyName struct {
N string `mapstructure:""`
}
type dashName struct {
N string `mapstructure:"-"`
}
type upperName struct {
N string `mapstructure:"N"`
}
Expand Down Expand Up @@ -812,7 +810,6 @@ func TestEveryRefusalIsReportedAsADefect(t *testing.T) {
}{
{"a squashed field that also names a segment", "s", &squashNamed{}, anyDefault, "one or the other"},
{"an empty mapstructure name", "s", &emptyName{}, anyDefault, "empty mapstructure name"},
{"a dash mapstructure name", "s", &dashName{}, anyDefault, "empty mapstructure name"},
{"an upper-case key", "s", &upperName{}, anyDefault, "not lower case"},
{"a squashed scalar", "s", &squashScalar{}, anyDefault, "not a struct"},
{"a struct declaring nothing", "s", &noKeys{}, anyDefault, "declares no keys"},
Expand Down Expand Up @@ -1353,3 +1350,59 @@ func TestARefusalInsideASquashedBaseIsReported(t *testing.T) {
t.Errorf("the refusal reads %q; a squashed field's path is the section's own", msg)
}
}

// TestAFieldExcludedFromConfigDeclaresNoKey covers the tag that means "not from configuration".
//
// mapstructure reads "-" as skip this field, and a configuration struct uses it for a field something else
// in the program assigns.
//
// Such a field declares no key. Declaring one that resolved to a default would put a key in the space that
// reaches no field: an operator could write it and the assignment would discard whatever they wrote.
//
// An untagged field stays a defect. The two look alike in a diff and mean opposite things: one is a field
// the author excluded from configuration, the other is a field configuration cannot reach because nothing
// names it.
func TestAFieldExcludedFromConfigDeclaresNoKey(t *testing.T) {
type excluded struct {
Kept string `mapstructure:"kept"`
Assigned int `mapstructure:"-"`
}

registry.Reset()
registry.RegisterSection("probe", &excluded{}, func(registry.Mode) any {
return &excluded{Kept: "x", Assigned: 42}
})
for _, d := range registry.Defects() {
t.Fatalf("a field excluded from configuration was reported as a defect: %v", d.Err)
}

if got, want := registry.Keys(), []string{"probe.kept"}; !reflect.DeepEqual(got, want) {
t.Fatalf("declared keys are %v, want %v. A key for an excluded field is one an operator can write "+
"that whatever assigns the field then discards", got, want)
}

resolved, err := registry.Resolve(registry.ModeValidator, registry.Sources{})
if err != nil {
t.Fatalf("resolving a section with an excluded field: %v", err)
}
if _, present := resolved.Values["probe.assigned"]; present {
t.Error("the excluded field resolved to a value, so an operator could write a key that reaches no " +
"field")
}

// An untagged field means the opposite and stays a defect.
type untagged struct {
Kept string `mapstructure:"kept"`
Forgotten int
}
registry.Reset()
registry.RegisterSection("probe", &untagged{}, func(registry.Mode) any { return &untagged{} })
defects := registry.Defects()
if len(defects) == 0 {
t.Fatal("a field with no tag at all registered cleanly, so a key nothing names reaches no field")
}
// Named, so this cannot pass on a refusal raised for some other reason.
if got := defects[0].Err.Error(); !strings.Contains(got, "Forgotten") {
t.Errorf("the refusal reads %q and does not name the untagged field", got)
}
}
27 changes: 27 additions & 0 deletions giga/executor/config/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package config

import (
"github.com/sei-protocol/sei-chain/config/registry"
)

// SectionName is this section's name in the configuration key space.
//
// The same prefix the flag constants already use, so the derived keys are the keys this package's reader
// resolves rather than a second spelling of them.
const SectionName = "giga_executor"

// Registration puts this section in the configuration registry.
//
// The owning package registers its own section, so the struct, the values and the keys come from one place
// and cannot drift apart. The dotted keys derive from the mapstructure tags, which is what makes the
// registry's spelling and this package's flag constants the same strings.
func init() {
registry.RegisterSection(SectionName, &Config{}, defaults)
}

// defaults is what this section resolves to for a node that has written nothing.
//
// The same values for every mode. Nothing in the binary makes either setting follow from what kind of
// node is asking, so a default that varied here would be this section inventing a rule rather than
// stating one.
func defaults(registry.Mode) any { return DefaultConfig }
Loading
Loading