diff --git a/cmd/beekeeper/cmd/cluster.go b/cmd/beekeeper/cmd/cluster.go index e5c87a1f8..4af4c9d01 100644 --- a/cmd/beekeeper/cmd/cluster.go +++ b/cmd/beekeeper/cmd/cluster.go @@ -247,9 +247,13 @@ func setupBootnodes(ctx context.Context, nodeName = node.Name } - bConfig.Bootnodes = fmt.Sprintf(node.Bootnodes, clusterConfig.GetNamespace()) // TODO: improve bootnode management, support more than 2 bootnodes - bootnodesOut = bConfig.Bootnodes - nodeOpts := setupNodeOptions(node, &bConfig) + // each node gets its own config copy, as the bootnode list is + // node-specific and nodes are set up concurrently + nodeConfig := bConfig + bootnodes := fmt.Sprintf(node.Bootnodes, clusterConfig.GetNamespace()) // TODO: improve bootnode management, support more than 2 bootnodes + nodeConfig.Bootnodes = &[]string{bootnodes} + bootnodesOut = bootnodes + nodeOpts := setupNodeOptions(node, &nodeConfig) nodeCount++ go setupOrAddNode(ctx, startCluster, inCluster, ng, nodeName, nodeOpts, nodeResultChan, orchestration.WithNoOptions()) } @@ -299,8 +303,8 @@ func setupNodes(ctx context.Context, } bConfig := beeConfig.Export() - if bConfig.Bootnodes == "" { - bConfig.Bootnodes = bootnodesIn + if (bConfig.Bootnodes == nil || len(*bConfig.Bootnodes) == 0) && bootnodesIn != "" { + bConfig.Bootnodes = &[]string{bootnodesIn} } ngOptions.BeeConfig = &bConfig diff --git a/config/config.yaml b/config/config.yaml index 6d90d4f23..4d4bd8f90 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -94,10 +94,10 @@ bee-configs: block-time: 1 blockchain-rpc-endpoint: "ws://geth-swap.bee-playground.svc.swarm1.local:8546" bootnode-mode: false - bootnodes: "" + bootnode: [] cache-capacity: 1000000 chequebook-enable: true - cors-allowed-origins: "" + cors-allowed-origins: [] data-dir: "/home/bee/.bee" db-block-cache-capacity: 33554432 db-disable-seeks-compaction: false @@ -117,19 +117,19 @@ bee-configs: postage-stamp-start-block: 1 price-oracle-address: "0x5aFE06fcC0855a76a15c3544b0886EDBE3294d62" redistribution-address: "0x09Ad42a7d020244920309FfA14EA376dd2D3b7d5" - resolver-options: "" + resolver-options: [] staking-address: "0xfc28330f1ecE0ef2371B724E0D19c1EE60B728b2" storage-incentives-enable: true swap-enable: true swap-factory-address: "0xdD661f2500bA5831e3d1FEbAc379Ea1bF80773Ac" swap-initial-deposit: 500000000000000000 - tracing-enabled: true + tracing-enable: true tracing-endpoint: "10.10.11.199:6831" tracing-service-name: "bee" verbosity: 5 # 1=error, 2=warn, 3=info, 4=debug, 5=trace warmup-time: 0s welcome-message: "Welcome to the Swarm, you are Bee-ing connected!" - withdrawal-addresses-whitelist: "0xec44cb15b1b033e74d55ac5d0e24d861bde54532" + withdrawal-addresses-whitelist: ["0xec44cb15b1b033e74d55ac5d0e24d861bde54532"] bootnode: _inherit: "default" bootnode-mode: true diff --git a/config/local.yaml b/config/local.yaml index 3b641bd7c..4d2e81f86 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -152,10 +152,10 @@ bee-configs: block-time: 1 blockchain-rpc-endpoint: "ws://geth-swap:8546" bootnode-mode: false - bootnodes: "" + bootnode: [] cache-capacity: 20000 chequebook-enable: true - cors-allowed-origins: "" + cors-allowed-origins: [] data-dir: "/home/bee/.bee" db-block-cache-capacity: 33554432 db-disable-seeks-compaction: false @@ -178,7 +178,7 @@ bee-configs: postage-stamp-start-block: 1 price-oracle-address: "0x5aFE06fcC0855a76a15c3544b0886EDBE3294d62" redistribution-address: "0x09Ad42a7d020244920309FfA14EA376dd2D3b7d5" - resolver-options: "" + resolver-options: [] staking-address: "0xfc28330f1ecE0ef2371B724E0D19c1EE60B728b2" storage-incentives-enable: true swap-enable: true @@ -187,14 +187,13 @@ bee-configs: verbosity: 5 warmup-time: 0s welcome-message: "Welcome to the Swarm, this is a local cluster!" - withdrawal-addresses-whitelist: "0xec44cb15b1b033e74d55ac5d0e24d861bde54532" + withdrawal-addresses-whitelist: ["0xec44cb15b1b033e74d55ac5d0e24d861bde54532"] bootnode-local-dns-autotls: _inherit: "bee-local-dns" bootnode-mode: true p2p-wss-enable: true bee-local-autotls: _inherit: "bee-local-dns" - bootnode: /dnsaddr/bootnode-0-headless.local.svc.cluster.local p2p-wss-enable: true bee-local-light-autotls: _inherit: "bee-local-light" @@ -209,14 +208,11 @@ bee-configs: bootnode-mode: true bee-local-dns: _inherit: "bee-local" - bootnode: /dnsaddr/localhost bootnode-local-dns: _inherit: "bee-local" - bootnode: /dnsaddr/localhost bootnode-mode: true bee-local-light: _inherit: "bee-local" - bootnode: /dnsaddr/localhost full-node: false bee-local-gc: _inherit: "bee-local" diff --git a/config/public-testnet.yaml b/config/public-testnet.yaml index ebd787358..05ca6c4d8 100644 --- a/config/public-testnet.yaml +++ b/config/public-testnet.yaml @@ -46,7 +46,7 @@ node-groups: bee-configs: sepolia: _inherit: "" - bootnodes: "/dnsaddr/testnet.ethswarm.org" + bootnode: ["/dnsaddr/testnet.ethswarm.org"] full-node: true checks: diff --git a/config/staging.yaml b/config/staging.yaml index 9345579ae..739b50939 100644 --- a/config/staging.yaml +++ b/config/staging.yaml @@ -18,7 +18,7 @@ clusters: mode: node bee-config: staging config: staging - count: 5 + count: 1 # node-groups defines node groups that can be registered in the cluster # node-groups may inherit it's configuration from already defined node-group and override specific fields from it @@ -34,14 +34,14 @@ bee-configs: _inherit: "" api-addr: ":1633" blockchain-rpc-endpoint: http://rpc-sepolia-haproxy.default.svc.swarm1.local - bootnodes: /dnsaddr/testnet.ethswarm.org + bootnode: ["/dnsaddr/testnet.ethswarm.org"] full-node: true mainnet: false network-id: 10 p2p-addr: ":1634" password: "beekeeper" swap-enable: true - tracing-enabled: true + tracing-enable: true tracing-endpoint: "10.10.11.199:6831" tracing-service-name: "bee" verbosity: 4 diff --git a/config/testnet-bee-playground.yaml b/config/testnet-bee-playground.yaml index 4ef4f645a..52d704b48 100644 --- a/config/testnet-bee-playground.yaml +++ b/config/testnet-bee-playground.yaml @@ -9,7 +9,7 @@ clusters: api-insecure-tls: true api-scheme: http funding: - eth: 0.1 + eth: 0.01 bzz: 3.0 node-groups: bootnode: @@ -44,33 +44,16 @@ bee-configs: api-addr: :1633 block-time: 12 blockchain-rpc-endpoint: http://rpc-sepolia-haproxy.default.svc.swarm1.local - bootnode-mode: false - bootnodes: # /dnsaddr/testnet.ethswarm.org - cache-capacity: 1000000 + # bootnode: [dnsaddr/testnet.ethswarm.org] chequebook-enable: true - cors-allowed-origins: "" data-dir: "/home/bee/.bee" - db-block-cache-capacity: 33554432 - db-disable-seeks-compaction: true - db-open-files-limit: 200 - db-write-buffer-size: 33554432 full-node: true mainnet: false - nat-addr: "" network-id: 5 p2p-addr: :1634 - p2p-ws-enable: true password: "beekeeper" - payment-early-percent: 50 - payment-threshold: 13500000 - payment-tolerance-percent: 25 - postage-stamp-start-block: 0 storage-incentives-enable: true swap-enable: true - swap-initial-deposit: 0 - tracing-enabled: false - tracing-endpoint: "10.10.11.199:6831" - tracing-service-name: "bee-playground" verbosity: 5 warmup-time: 5m0s welcome-message: Welcome to the bee-playground! diff --git a/pkg/check/autotls/autotls.go b/pkg/check/autotls/autotls.go index 2e30557d7..e8968150c 100644 --- a/pkg/check/autotls/autotls.go +++ b/pkg/check/autotls/autotls.go @@ -302,9 +302,10 @@ func (c *Check) forgeConfig(ctx context.Context, cluster orchestration.Cluster, continue } cfg := node.Config() - forgeDomain = cfg.AutoTLSDomain - if strings.Contains(cfg.AutoTLSCAEndpoint, "pebble") { - mgmtURL := pebbleMgmtURL(cfg.AutoTLSCAEndpoint) + forgeDomain = orchestration.Deref(cfg.AutoTLSDomain) + caEndpoint := orchestration.Deref(cfg.AutoTLSCAEndpoint) + if strings.Contains(caEndpoint, "pebble") { + mgmtURL := pebbleMgmtURL(caEndpoint) if pebbleMgmtURLOverride != "" { mgmtURL = pebbleMgmtURLOverride } diff --git a/pkg/config/bee.go b/pkg/config/bee.go index 343664794..ac4c66883 100644 --- a/pkg/config/bee.go +++ b/pkg/config/bee.go @@ -1,9 +1,6 @@ package config import ( - "reflect" - "time" - "github.com/ethersphere/beekeeper/pkg/orchestration" ) @@ -11,59 +8,17 @@ type Inheritable interface { GetParentName() string } -// BeeConfig represents Bee configuration +// BeeConfig represents Bee configuration as read from Beekeeper's YAML config. +// +// It embeds orchestration.Config (the Bee flag set), so the flag fields are +// defined in exactly one place and the same yaml tags — the Bee flag names — +// are used both for reading the config here and for rendering the node's +// .bee.yaml. The only thing BeeConfig adds is the config-file-only concern of +// inheritance (_inherit). Export returns just the embedded Config, so neither +// _inherit nor any other config-loading detail can leak into the rendered file. type BeeConfig struct { - // parent to inherit settings from - *Inherit `yaml:",inline"` - // Bee configuration - AllowPrivateCIDRs *bool `yaml:"allow-private-cidrs"` - APIAddr *string `yaml:"api-addr"` - AutoTLSCAEndpoint *string `yaml:"autotls-ca-endpoint"` - AutoTLSDomain *string `yaml:"autotls-domain"` - AutoTLSRegistrationEndpoint *string `yaml:"autotls-registration-endpoint"` - BlockchainRPCEndpoint *string `yaml:"blockchain-rpc-endpoint"` - BlockTime *uint64 `yaml:"block-time"` - BootnodeMode *bool `yaml:"bootnode-mode"` - Bootnodes *string `yaml:"bootnodes"` - CacheCapacity *uint64 `yaml:"cache-capacity"` - ChequebookEnable *bool `yaml:"chequebook-enable"` - CORSAllowedOrigins *string `yaml:"cors-allowed-origins"` - DataDir *string `yaml:"data-dir"` - DbBlockCacheCapacity *int `yaml:"db-block-cache-capacity"` - DbDisableSeeksCompaction *bool `yaml:"db-disable-seeks-compaction"` - DbOpenFilesLimit *int `yaml:"db-open-files-limit"` - DbWriteBufferSize *int `yaml:"db-write-buffer-size"` - FullNode *bool `yaml:"full-node"` - Mainnet *bool `yaml:"mainnet"` - NATAddr *string `yaml:"nat-addr"` - NATWSSAddr *string `yaml:"nat-wss-addr"` - NetworkID *uint64 `yaml:"network-id"` - P2PAddr *string `yaml:"p2p-addr"` - P2PWSEnable *bool `yaml:"p2p-ws-enable"` - P2PWSSAddr *string `yaml:"p2p-wss-addr"` - P2PWSSEnable *bool `yaml:"p2p-wss-enable"` - Password *string `yaml:"password"` - PaymentEarly *uint64 `yaml:"payment-early-percent"` - PaymentThreshold *uint64 `yaml:"payment-threshold"` - PaymentTolerance *uint64 `yaml:"payment-tolerance-percent"` - PostageContractStartBlock *uint64 `yaml:"postage-stamp-start-block"` - PostageStampAddress *string `yaml:"postage-stamp-address"` - PriceOracleAddress *string `yaml:"price-oracle-address"` - RedistributionAddress *string `yaml:"redistribution-address"` - ResolverOptions *string `yaml:"resolver-options"` - StakingAddress *string `yaml:"staking-address"` - StorageIncentivesEnable *string `yaml:"storage-incentives-enable"` - SwapEnable *bool `yaml:"swap-enable"` - SwapEndpoint *string `yaml:"swap-endpoint"` // deprecated: use blockchain-rpc-endpoint - SwapFactoryAddress *string `yaml:"swap-factory-address"` - SwapInitialDeposit *uint64 `yaml:"swap-initial-deposit"` - TracingEnabled *bool `yaml:"tracing-enabled"` - TracingEndpoint *string `yaml:"tracing-endpoint"` - TracingServiceName *string `yaml:"tracing-service-name"` - Verbosity *uint64 `yaml:"verbosity"` - WarmupTime *time.Duration `yaml:"warmup-time"` - WelcomeMessage *string `yaml:"welcome-message"` - WithdrawAddress *string `yaml:"withdrawal-addresses-whitelist"` + *Inherit `yaml:",inline"` + orchestration.Config `yaml:",inline"` } func (b BeeConfig) GetParentName() string { @@ -73,30 +28,9 @@ func (b BeeConfig) GetParentName() string { return "" } -// Export exports BeeConfig to orchestration.Config -func (b *BeeConfig) Export() (config orchestration.Config) { - localVal := reflect.ValueOf(b).Elem() - localType := reflect.TypeFor[BeeConfig]() - remoteVal := reflect.ValueOf(&config).Elem() - - for i := range localVal.NumField() { - localField := localVal.Field(i) - if localField.IsValid() && !localField.IsNil() { - localFieldVal := localVal.Field(i).Elem() - localFieldName := localType.Field(i).Name - - remoteFieldVal := remoteVal.FieldByName(localFieldName) - if remoteFieldVal.IsValid() && remoteFieldVal.Type() == localFieldVal.Type() { - remoteFieldVal.Set(localFieldVal) - } - } - } - - config = remoteVal.Interface().(orchestration.Config) - - if config.BlockchainRPCEndpoint == "" && b.SwapEndpoint != nil { - config.BlockchainRPCEndpoint = *b.SwapEndpoint - } - - return config +// Export returns the Bee flag configuration to be rendered into the node's +// .bee.yaml. Inheritance has already been resolved during config loading and is +// not part of the embedded Config. +func (b *BeeConfig) Export() orchestration.Config { + return b.Config } diff --git a/pkg/config/bee_test.go b/pkg/config/bee_test.go new file mode 100644 index 000000000..f429236a6 --- /dev/null +++ b/pkg/config/bee_test.go @@ -0,0 +1,112 @@ +package config_test + +import ( + "testing" + "time" + + "github.com/ethersphere/beekeeper/pkg/config" + "github.com/ethersphere/beekeeper/pkg/orchestration" + "gopkg.in/yaml.v3" +) + +func TestBeeConfigExport(t *testing.T) { + t.Parallel() + + // Export returns the embedded orchestration.Config verbatim: set fields + // (including explicit zero values) are preserved, unset ones stay nil. + in := config.BeeConfig{ + Config: orchestration.Config{ + APIAddr: new(":1633"), + FullNode: new(false), // explicit zero value must survive + WarmupTime: new(time.Duration(0)), // explicit 0s must survive + }, + } + + got := in.Export() + + if got.APIAddr == nil || *got.APIAddr != ":1633" { + t.Errorf("APIAddr = %v, want :1633", got.APIAddr) + } + if got.FullNode == nil || *got.FullNode { + t.Errorf("FullNode = %v, want explicit false preserved", got.FullNode) + } + if got.WarmupTime == nil || *got.WarmupTime != 0 { + t.Errorf("WarmupTime = %v, want 0s preserved", got.WarmupTime) + } + if got.P2PAddr != nil { + t.Errorf("P2PAddr = %v, want nil (unset)", *got.P2PAddr) + } +} + +// TestBeeConfigRender exercises the full pipeline a node config goes through: +// Beekeeper YAML -> config.BeeConfig -> Export -> yaml.Marshal -> .bee.yaml. +// The config keys are the Bee flag names, and _inherit is read but must never +// reach the rendered file. +func TestBeeConfigRender(t *testing.T) { + t.Parallel() + + const in = ` +_inherit: default +api-addr: ":1633" +p2p-addr: ":1634" +full-node: false +warmup-time: 0s +bootnode: ["/dnsaddr/localhost"] +payment-threshold: 13500000 +tracing-enable: false +` + var bc config.BeeConfig + if err := yaml.Unmarshal([]byte(in), &bc); err != nil { + t.Fatalf("unmarshal beekeeper config: %v", err) + } + if bc.GetParentName() != "default" { + t.Fatalf("GetParentName() = %q, want default (read from _inherit)", bc.GetParentName()) + } + + out, err := yaml.Marshal(bc.Export()) + if err != nil { + t.Fatalf("marshal bee config: %v", err) + } + + rendered := map[string]any{} + if err := yaml.Unmarshal(out, &rendered); err != nil { + t.Fatalf("unmarshal rendered config: %v", err) + } + + // Set keys are present, with explicit zero values preserved. Note + // payment-threshold: a numeric YAML scalar is read into bee's string-typed + // flag and rendered back as a string. + wantPresent := map[string]any{ + "api-addr": ":1633", + "p2p-addr": ":1634", + "full-node": false, + "warmup-time": "0s", + "payment-threshold": "13500000", + "tracing-enable": false, + } + for k, want := range wantPresent { + got, ok := rendered[k] + if !ok { + t.Errorf("rendered .bee.yaml is missing key %q", k) + continue + } + if got != want { + t.Errorf("rendered[%q] = %v (%T), want %v (%T)", k, got, got, want, want) + } + } + + // bootnode is a list flag in bee and must render as a YAML list. + if v, ok := rendered["bootnode"]; !ok { + t.Error("rendered .bee.yaml is missing key \"bootnode\"") + } else if l, isList := v.([]any); !isList || len(l) != 1 || l[0] != "/dnsaddr/localhost" { + t.Errorf(`bootnode = %v, want ["/dnsaddr/localhost"]`, v) + } + + // Unset flags are omitted so Bee applies its own defaults, and _inherit (a + // config-loading concern) never leaks into the rendered file. + for _, k := range []string{"verbosity", "mainnet", "swap-enable", "password", "_inherit"} { + if _, ok := rendered[k]; ok { + t.Errorf("rendered .bee.yaml should omit %q, but it is present", k) + } + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 487ee874e..c3089d41c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -98,8 +98,20 @@ func mergeConfigs[T any](configs map[string]T) (map[string]T, error) { p := reflect.ValueOf(&parentConfig).Elem() m := reflect.ValueOf(&v).Elem() for i := 0; i < m.NumField(); i++ { - if m.Field(i).IsNil() && !p.Field(i).IsNil() { - m.Field(i).Set(p.Field(i)) + mf, pf := m.Field(i), p.Field(i) + // Recurse one level into embedded structs (e.g. the + // orchestration.Config embedded in BeeConfig) so inheritance is + // applied per flag instead of all-or-nothing. + if mf.Kind() == reflect.Struct { + for j := 0; j < mf.NumField(); j++ { + if mf.Field(j).IsNil() && !pf.Field(j).IsNil() { + mf.Field(j).Set(pf.Field(j)) + } + } + continue + } + if mf.IsNil() && !pf.IsNil() { + mf.Set(pf) } } } diff --git a/pkg/config/inherit_test.go b/pkg/config/inherit_test.go new file mode 100644 index 000000000..20e37847e --- /dev/null +++ b/pkg/config/inherit_test.go @@ -0,0 +1,49 @@ +package config_test + +import ( + "io" + "testing" + + "github.com/ethersphere/beekeeper/pkg/config" + "github.com/ethersphere/beekeeper/pkg/logging" +) + +// TestBeeConfigInheritance verifies that _inherit still merges per flag (not +// all-or-nothing) now that the flags live in an embedded orchestration.Config: +// the child inherits the parent's unset flags while keeping its own overrides. +func TestBeeConfigInheritance(t *testing.T) { + t.Parallel() + + const in = ` +bee-configs: + default: + api-addr: ":1633" + full-node: true + verbosity: 5 + child: + _inherit: default + full-node: false +` + cfg, err := config.Read(logging.New(io.Discard, 0), []config.YamlFile{{Name: "test.yaml", Content: []byte(in)}}) + if err != nil { + t.Fatalf("read config: %v", err) + } + + child, ok := cfg.BeeConfigs["child"] + if !ok { + t.Fatal("child bee-config not found") + } + c := child.Export() + + // inherited from the parent (unset in the child) + if c.APIAddr == nil || *c.APIAddr != ":1633" { + t.Errorf("APIAddr = %v, want inherited :1633", c.APIAddr) + } + if c.Verbosity == nil || *c.Verbosity != "5" { + t.Errorf("Verbosity = %v, want inherited \"5\"", c.Verbosity) + } + // the child's own explicit value wins over the parent, per flag + if c.FullNode == nil || *c.FullNode { + t.Errorf("FullNode = %v, want child override false", c.FullNode) + } +} diff --git a/pkg/k8s/containers/containers_test.go b/pkg/k8s/containers/containers_test.go index 1a497f23d..a2d9d58c0 100644 --- a/pkg/k8s/containers/containers_test.go +++ b/pkg/k8s/containers/containers_test.go @@ -973,10 +973,7 @@ func newExpectedDefaultContainer() v1.Container { ReadOnlyRootFilesystem: new(bool), AllowPrivilegeEscalation: new(bool), RunAsGroup: new(int64), - ProcMount: func() *v1.ProcMountType { - procMountType := v1.ProcMountType("") - return &procMountType - }(), + // unset ProcMount stays nil; the API server rejects an empty value }, } } diff --git a/pkg/k8s/containers/ephermal_test.go b/pkg/k8s/containers/ephermal_test.go index f1fdf730c..d07bbb6e0 100644 --- a/pkg/k8s/containers/ephermal_test.go +++ b/pkg/k8s/containers/ephermal_test.go @@ -37,10 +37,7 @@ func Test_EphemeralContainers_ToK8S(t *testing.T) { ReadOnlyRootFilesystem: new(bool), AllowPrivilegeEscalation: new(bool), RunAsGroup: new(int64), - ProcMount: func() *v1.ProcMountType { - procMountType := v1.ProcMountType("") - return &procMountType - }(), + // unset ProcMount stays nil; the API server rejects an empty value }, }, }, diff --git a/pkg/k8s/containers/security.go b/pkg/k8s/containers/security.go index ed8535100..89e72ee7b 100644 --- a/pkg/k8s/containers/security.go +++ b/pkg/k8s/containers/security.go @@ -23,6 +23,10 @@ func (sc *SecurityContext) toK8S() *v1.SecurityContext { Capabilities: sc.Capabilities.toK8S(), Privileged: &sc.Privileged, ProcMount: func() *v1.ProcMountType { + // nil means "use the default"; + if sc.ProcMount == "" { + return nil + } p := v1.ProcMountType(sc.ProcMount) return &p }(), diff --git a/pkg/orchestration/config_test.go b/pkg/orchestration/config_test.go new file mode 100644 index 000000000..6da47e2cf --- /dev/null +++ b/pkg/orchestration/config_test.go @@ -0,0 +1,62 @@ +package orchestration_test + +import ( + "testing" + "time" + + "github.com/ethersphere/beekeeper/pkg/orchestration" + "gopkg.in/yaml.v3" +) + +// TestConfigMarshal documents the contract the rendered .bee.yaml relies on: +// a nil field is omitted (Bee uses its own default), a non-nil field is emitted +// even when it points to a zero value, and the Bee flag names are used. +func TestConfigMarshal(t *testing.T) { + t.Parallel() + + cfg := orchestration.Config{ + FullNode: new(false), // zero value, explicitly set -> emitted + WarmupTime: new(time.Duration(0)), // 0s, explicitly set -> emitted as "0s" + Bootnodes: &[]string{}, // empty list, explicitly set -> emitted (overrides bee's default bootnodes) + // all other fields nil -> omitted + } + + out, err := yaml.Marshal(cfg) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + m := map[string]any{} + if err := yaml.Unmarshal(out, &m); err != nil { + t.Fatalf("unmarshal: %v", err) + } + + if len(m) != 3 { + t.Fatalf("expected exactly the 3 set fields to be rendered, got %d: %v", len(m), m) + } + if v, ok := m["full-node"]; !ok || v != false { + t.Errorf("full-node = %v (present=%v), want false", v, ok) + } + if v, ok := m["warmup-time"]; !ok || v != "0s" { + t.Errorf(`warmup-time = %v (present=%v), want "0s"`, v, ok) + } + if v, ok := m["bootnode"]; !ok { + t.Error("bootnode missing, want explicit empty list to be emitted") + } else if l, isList := v.([]any); !isList || len(l) != 0 { + t.Errorf("bootnode = %v, want empty list", v) + } +} + +func TestDeref(t *testing.T) { + t.Parallel() + + if got := orchestration.Deref[string](nil); got != "" { + t.Errorf("Deref(nil) = %q, want empty string", got) + } + if got := orchestration.Deref(new(true)); !got { + t.Errorf("Deref(new(true)) = %v, want true", got) + } + if got := orchestration.Deref(new(false)); got { + t.Errorf("Deref(new(false)) = %v, want false", got) + } +} diff --git a/pkg/orchestration/k8s/cluster.go b/pkg/orchestration/k8s/cluster.go index 1fbffc9ce..cc689abac 100644 --- a/pkg/orchestration/k8s/cluster.go +++ b/pkg/orchestration/k8s/cluster.go @@ -226,7 +226,7 @@ func (c *Cluster) NodeNames() (names []string) { // LightNodeNames returns a list of light node names func (c *Cluster) LightNodeNames() (names []string) { for name, node := range c.Nodes() { - if !node.Config().FullNode { + if !orchestration.Deref(node.Config().FullNode) { names = append(names, name) } } @@ -237,7 +237,7 @@ func (c *Cluster) LightNodeNames() (names []string) { func (c *Cluster) FullNodeNames() (names []string) { for name, node := range c.Nodes() { cfg := node.Config() - if cfg.FullNode && !cfg.BootnodeMode { + if orchestration.Deref(cfg.FullNode) && !orchestration.Deref(cfg.BootnodeMode) { names = append(names, name) } } @@ -249,7 +249,7 @@ func (c *Cluster) ShuffledFullNodeClients(ctx context.Context, r *rand.Rand) (or var res orchestration.ClientList for _, node := range c.Nodes() { cfg := node.Config() - if cfg.FullNode && !cfg.BootnodeMode { + if orchestration.Deref(cfg.FullNode) && !orchestration.Deref(cfg.BootnodeMode) { res = append(res, node.Client()) } } @@ -464,7 +464,7 @@ func (c *Cluster) ClosestFullNodeClient(ctx context.Context, s *bee.Client) (*be } cfg := node.Config() // closet peer is not a full node. Check other peers in the same bin - if !cfg.FullNode || cfg.BootnodeMode { + if !orchestration.Deref(cfg.FullNode) || orchestration.Deref(cfg.BootnodeMode) { skipList = append(skipList, addr) b-- continue diff --git a/pkg/orchestration/k8s/helpers.go b/pkg/orchestration/k8s/helpers.go index e763be697..a309ec7a6 100644 --- a/pkg/orchestration/k8s/helpers.go +++ b/pkg/orchestration/k8s/helpers.go @@ -13,58 +13,6 @@ import ( "github.com/ethersphere/beekeeper/pkg/k8s/service" ) -const ( - configTemplate = ` -allow-private-cidrs: {{ .AllowPrivateCIDRs }} -api-addr: {{.APIAddr}} -autotls-ca-endpoint: {{.AutoTLSCAEndpoint}} -autotls-domain: {{.AutoTLSDomain}} -autotls-registration-endpoint: {{.AutoTLSRegistrationEndpoint}} -block-time: {{ .BlockTime }} -blockchain-rpc-endpoint: {{.BlockchainRPCEndpoint}} -bootnode-mode: {{.BootnodeMode}} -bootnode: {{.Bootnodes}} -cache-capacity: {{.CacheCapacity}} -chequebook-enable: {{.ChequebookEnable}} -cors-allowed-origins: {{.CORSAllowedOrigins}} -data-dir: {{.DataDir}} -db-block-cache-capacity: {{.DbBlockCacheCapacity}} -db-disable-seeks-compaction: {{.DbDisableSeeksCompaction}} -db-open-files-limit: {{.DbOpenFilesLimit}} -db-write-buffer-size: {{.DbWriteBufferSize}} -full-node: {{.FullNode}} -mainnet: {{.Mainnet}} -nat-addr: {{.NATAddr}} -nat-wss-addr: {{.NATWSSAddr}} -network-id: {{.NetworkID}} -p2p-addr: {{.P2PAddr}} -p2p-ws-enable: {{.P2PWSEnable}} -p2p-wss-addr: {{.P2PWSSAddr}} -p2p-wss-enable: {{.P2PWSSEnable}} -password: {{.Password}} -payment-early-percent: {{.PaymentEarly}} -payment-threshold: {{.PaymentThreshold}} -payment-tolerance-percent: {{.PaymentTolerance}} -postage-stamp-address: {{ .PostageStampAddress }} -postage-stamp-start-block: {{ .PostageContractStartBlock }} -price-oracle-address: {{ .PriceOracleAddress }} -redistribution-address: {{ .RedistributionAddress }} -resolver-options: {{.ResolverOptions}} -staking-address: {{ .StakingAddress }} -storage-incentives-enable: {{ .StorageIncentivesEnable }} -swap-enable: {{.SwapEnable}} -swap-factory-address: {{.SwapFactoryAddress}} -swap-initial-deposit: {{.SwapInitialDeposit}} -tracing-enable: {{.TracingEnabled}} -tracing-endpoint: {{.TracingEndpoint}} -tracing-service-name: {{.TracingServiceName}} -verbosity: {{.Verbosity}} -warmup-time: {{.WarmupTime}} -welcome-message: {{.WelcomeMessage}} -withdrawal-addresses-whitelist: {{.WithdrawAddress}} -` -) - type setInitContainersOptions struct { AutoTLSEnabled bool } diff --git a/pkg/orchestration/k8s/nodegroup.go b/pkg/orchestration/k8s/nodegroup.go index 219235864..f1d6a1921 100644 --- a/pkg/orchestration/k8s/nodegroup.go +++ b/pkg/orchestration/k8s/nodegroup.go @@ -672,11 +672,18 @@ func (g *NodeGroup) pregenerateSwarmKey(ctx context.Context, name string) (err e return err } - if !n.Config().SwapEnable || !n.Config().ChequebookEnable { + if !orchestration.Deref(n.Config().SwapEnable) || !orchestration.Deref(n.Config().ChequebookEnable) { var key *orchestration.EncryptedKey if n.SwarmKey() == nil { - key, err = orchestration.NewEncryptedKey(n.Config().Password) + // The pregenerated Swarm key is encrypted with this password and the + // same password must reach Bee (via .bee.yaml) so it can decrypt the + // key. If it is absent, Beekeeper must not guess Bee's default. + if n.Config().Password == nil { + return fmt.Errorf("password is required in bee config to pregenerate the Swarm key for node %s", name) + } + + key, err = orchestration.NewEncryptedKey(*n.Config().Password) if err != nil { return fmt.Errorf("create Swarm key for node %s: %w", name, err) } diff --git a/pkg/orchestration/k8s/orchestrator.go b/pkg/orchestration/k8s/orchestrator.go index c34207dd9..1f034dabe 100644 --- a/pkg/orchestration/k8s/orchestrator.go +++ b/pkg/orchestration/k8s/orchestrator.go @@ -1,10 +1,8 @@ package k8s import ( - "bytes" "context" "fmt" - "html/template" "github.com/ethersphere/beekeeper/pkg/k8s" "github.com/ethersphere/beekeeper/pkg/k8s/configmap" @@ -17,6 +15,7 @@ import ( "github.com/ethersphere/beekeeper/pkg/k8s/statefulset" "github.com/ethersphere/beekeeper/pkg/logging" "github.com/ethersphere/beekeeper/pkg/orchestration" + "gopkg.in/yaml.v3" ) var _ orchestration.NodeOrchestrator = (*nodeOrchestrator)(nil) @@ -54,10 +53,21 @@ func (n *nodeOrchestrator) StoppedNodes(ctx context.Context, namespace string) ( // Create func (n *nodeOrchestrator) Create(ctx context.Context, o orchestration.CreateOptions) (err error) { - // bee configuration - var config bytes.Buffer - if err := template.Must(template.New("").Parse(configTemplate)).Execute(&config, o.Config); err != nil { - return err + // The API and P2P listen addresses are required: the container ports and + // services built below need a concrete port number. + if o.Config.APIAddr == nil { + return fmt.Errorf("api-addr is required in bee config") + } + if o.Config.P2PAddr == nil { + return fmt.Errorf("p2p-addr is required in bee config") + } + + // bee configuration: only the flags set in Beekeeper's config are rendered + // (nil pointers are omitted via omitempty), so any flag absent here falls + // back to the Bee node's own built-in default. + config, err := yaml.Marshal(o.Config) + if err != nil { + return fmt.Errorf("marshalling bee config: %w", err) } configCM := o.Name @@ -65,7 +75,7 @@ func (n *nodeOrchestrator) Create(ctx context.Context, o orchestration.CreateOpt Annotations: o.Annotations, Labels: o.Labels, Data: map[string]string{ - ".bee.yaml": config.String(), + ".bee.yaml": string(config), }, }); err != nil { return fmt.Errorf("set configmap in namespace %s: %w", o.Namespace, err) @@ -106,7 +116,7 @@ func (n *nodeOrchestrator) Create(ctx context.Context, o orchestration.CreateOpt n.log.Infof("serviceaccount %s is set in namespace %s", svcAccount, o.Namespace) // api service - portAPI, err := parsePort(o.Config.APIAddr) + portAPI, err := parsePort(*o.Config.APIAddr) if err != nil { return fmt.Errorf("parsing API port from config: %w", err) } @@ -183,30 +193,30 @@ func (n *nodeOrchestrator) Create(ctx context.Context, o orchestration.CreateOpt } // p2p service - portP2P, err := parsePort(o.Config.P2PAddr) + portP2P, err := parsePort(*o.Config.P2PAddr) if err != nil { return fmt.Errorf("parsing P2P port from config: %w", err) } var nodePortP2P int32 - if len(o.Config.NATAddr) > 0 { - nodePortP2P, err = parsePort(o.Config.NATAddr) + if natAddr := orchestration.Deref(o.Config.NATAddr); len(natAddr) > 0 { + nodePortP2P, err = parsePort(natAddr) if err != nil { return fmt.Errorf("parsing NAT address from config: %w", err) } } var portP2PWSS int32 - if len(o.Config.P2PWSSAddr) > 0 { - portP2PWSS, err = parsePort(o.Config.P2PWSSAddr) + if p2pWSSAddr := orchestration.Deref(o.Config.P2PWSSAddr); len(p2pWSSAddr) > 0 { + portP2PWSS, err = parsePort(p2pWSSAddr) if err != nil { return fmt.Errorf("parsing P2P WSS port from config: %w", err) } } var nodePortP2PWSS int32 - if len(o.Config.NATWSSAddr) > 0 { - nodePortP2PWSS, err = parsePort(o.Config.NATWSSAddr) + if natWSSAddr := orchestration.Deref(o.Config.NATWSSAddr); len(natWSSAddr) > 0 { + nodePortP2PWSS, err = parsePort(natWSSAddr) if err != nil { return fmt.Errorf("parsing NAT WSS address from config: %w", err) } @@ -297,7 +307,7 @@ func (n *nodeOrchestrator) Create(ctx context.Context, o orchestration.CreateOpt sSet := o.Name libP2PEnabled := len(o.LibP2PKey) > 0 swarmEnabled := o.SwarmKey != nil - autoTLSEnabled := o.Config.P2PWSSEnable + autoTLSEnabled := orchestration.Deref(o.Config.P2PWSSEnable) if _, err := n.k8s.StatefulSet.Set(ctx, sSet, o.Namespace, statefulset.Options{ Annotations: o.Annotations, diff --git a/pkg/orchestration/node.go b/pkg/orchestration/node.go index 20bdce748..12a16569f 100644 --- a/pkg/orchestration/node.go +++ b/pkg/orchestration/node.go @@ -73,53 +73,102 @@ type CreateOptions struct { UpdateStrategy string } -// Config represents Bee configuration +// Config represents Bee configuration. +// +// Every field is a pointer so that a nil value means "not set in Beekeeper's +// YAML config". Such fields are omitted from the rendered .bee.yaml (see the +// yaml tags below), which lets the Bee node fall back to its own built-in +// default. A non-nil pointer is always rendered, even when it points to a zero +// value (e.g. full-node: false, warmup-time: 0s), so explicitly configured zero +// values still reach Bee. Beekeeper never hardcodes any of Bee's defaults. +// +// The yaml tags are the Bee flag names and are only used when marshalling this +// struct into the node's .bee.yaml. A few flag names differ from Beekeeper's +// config keys (e.g. bootnode, tracing-enable); those input keys live on +// config.BeeConfig and are unaffected. type Config struct { - AllowPrivateCIDRs bool // allow to advertise private CIDRs to the public network - APIAddr string // HTTP API listen address - AutoTLSCAEndpoint string // ACME CA endpoint - AutoTLSDomain string // domain for ACME - AutoTLSRegistrationEndpoint string // ACME registration endpoint - BlockchainRPCEndpoint string // blockchain RPC endpoint - BlockTime uint64 // chain block time - BootnodeMode bool // cause the node to always accept incoming connections - Bootnodes string // initial nodes to connect to - CacheCapacity uint64 // cache capacity in chunks, multiply by 4096 (MaxChunkSize) to get approximate capacity in bytes - ChequebookEnable bool // enable chequebook - CORSAllowedOrigins string // origins with CORS headers enabled - DataDir string // data directory - DbBlockCacheCapacity int // size of block cache of the database in bytes - DbDisableSeeksCompaction bool // disables DB compactions triggered by seeks - DbOpenFilesLimit int // number of open files allowed by database - DbWriteBufferSize int // size of the database write buffer in bytes - FullNode bool // cause the node to start in full mode - Mainnet bool // enable mainnet - NATAddr string // NAT exposed address - NATWSSAddr string // NAT exposed secure WebSocket address - NetworkID uint64 // ID of the Swarm network - P2PAddr string // P2P listen address - P2PWSEnable bool // enable P2P WebSocket transport - P2PWSSAddr string // P2P Secure WebSocket listen address - P2PWSSEnable bool // enable P2P Secure WebSocket transport - Password string // password for decrypting keys - PaymentEarly uint64 // amount in BZZ below the peers payment threshold when we initiate settlement - PaymentThreshold uint64 // threshold in BZZ where you expect to get paid from your peers - PaymentTolerance uint64 // excess debt above payment threshold in BZZ where you disconnect from your peer - PostageContractStartBlock uint64 // postage stamp address - PostageStampAddress string // postage stamp address - PriceOracleAddress string // price Oracle address - RedistributionAddress string // redistribution address - ResolverOptions string // ENS compatible API endpoint for a TLD and with contract address, can be repeated, format [tld:][contract-addr@]url - StakingAddress string // staking address - StorageIncentivesEnable string // storage incentives enable flag - SwapEnable bool // enable swap - SwapFactoryAddress string // swap factory address - SwapInitialDeposit uint64 // initial deposit if deploying a new chequebook - TracingEnabled bool // enable tracing - TracingEndpoint string // endpoint to send tracing data - TracingServiceName string // service name identifier for tracing - Verbosity uint64 // log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace - WarmupTime time.Duration // warmup time pull/pushsync protocols - WelcomeMessage string // send a welcome message string during handshakes - WithdrawAddress string // allowed addresses for wallet withdrawal + AllowPrivateCIDRs *bool `yaml:"allow-private-cidrs,omitempty"` // allow to advertise private CIDRs to the public network + APIAddr *string `yaml:"api-addr,omitempty"` // HTTP API listen address + AutoTLSCAEndpoint *string `yaml:"autotls-ca-endpoint,omitempty"` // autotls certificate authority endpoint + AutoTLSDomain *string `yaml:"autotls-domain,omitempty"` // autotls domain + AutoTLSRegistrationEndpoint *string `yaml:"autotls-registration-endpoint,omitempty"` // autotls registration endpoint + BlockchainRPCDialTimeout *time.Duration `yaml:"blockchain-rpc-dial-timeout,omitempty"` // blockchain rpc TCP dial timeout + BlockchainRPCEndpoint *string `yaml:"blockchain-rpc-endpoint,omitempty"` // rpc blockchain endpoint + BlockchainRPCIdleTimeout *time.Duration `yaml:"blockchain-rpc-idle-timeout,omitempty"` // blockchain rpc idle connection timeout + BlockchainRPCKeepalive *time.Duration `yaml:"blockchain-rpc-keepalive,omitempty"` // blockchain rpc TCP keepalive interval + BlockchainRPCTLSTimeout *time.Duration `yaml:"blockchain-rpc-tls-timeout,omitempty"` // blockchain rpc TLS handshake timeout + BlockSyncInterval *uint64 `yaml:"block-sync-interval,omitempty"` // block number cache sync interval in blocks + BlockTime *uint64 `yaml:"block-time,omitempty"` // chain block time + BootnodeMode *bool `yaml:"bootnode-mode,omitempty"` // cause the node to always accept incoming connections + Bootnodes *[]string `yaml:"bootnode,omitempty"` // initial nodes to connect to + CacheCapacity *uint64 `yaml:"cache-capacity,omitempty"` // cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes + CacheRetrieval *bool `yaml:"cache-retrieval,omitempty"` // enable forwarded content caching + ChequebookEnable *bool `yaml:"chequebook-enable,omitempty"` // enable chequebook + ChequebookMinBalance *string `yaml:"chequebook-min-balance,omitempty"` // minimum chequebook token balance required for verification, in token small units + ChequebookVerification *bool `yaml:"chequebook-verification,omitempty"` // reject full-node hive/handshake records that carry no chequebook address + CORSAllowedOrigins *[]string `yaml:"cors-allowed-origins,omitempty"` // origins with CORS headers enabled + DataDir *string `yaml:"data-dir,omitempty"` // data directory + DbBlockCacheCapacity *uint64 `yaml:"db-block-cache-capacity,omitempty"` // size of block cache of the database in bytes + DbDisableSeeksCompaction *bool `yaml:"db-disable-seeks-compaction,omitempty"` // disables db compactions triggered by seeks + DbOpenFilesLimit *uint64 `yaml:"db-open-files-limit,omitempty"` // number of open files allowed by database + DbWriteBufferSize *uint64 `yaml:"db-write-buffer-size,omitempty"` // size of the database write buffer in bytes + FullNode *bool `yaml:"full-node,omitempty"` // cause the node to start in full mode + GasLimitFallback *uint64 `yaml:"gas-limit-fallback,omitempty"` // gas limit fallback when estimation fails for contract transactions + Mainnet *bool `yaml:"mainnet,omitempty"` // triggers connect to main net bootnodes + MinimumGasTipCap *uint64 `yaml:"minimum-gas-tip-cap,omitempty"` // minimum gas tip cap in wei for transactions, 0 means use suggested gas tip cap + MinimumStorageRadius *uint `yaml:"minimum-storage-radius,omitempty"` // minimum radius storage threshold + NATAddr *string `yaml:"nat-addr,omitempty"` // NAT exposed address + NATWSSAddr *string `yaml:"nat-wss-addr,omitempty"` // WSS NAT exposed address + NeighborhoodSuggester *string `yaml:"neighborhood-suggester,omitempty"` // suggester for target neighborhood + NetworkID *uint64 `yaml:"network-id,omitempty"` // ID of the Swarm network + P2PAddr *string `yaml:"p2p-addr,omitempty"` // P2P listen address + P2PWSEnable *bool `yaml:"p2p-ws-enable,omitempty"` // enable P2P WebSocket transport + P2PWSSAddr *string `yaml:"p2p-wss-addr,omitempty"` // p2p wss address + P2PWSSEnable *bool `yaml:"p2p-wss-enable,omitempty"` // enable Secure WebSocket P2P connections + Password *string `yaml:"password,omitempty"` // password for decrypting keys + PasswordFile *string `yaml:"password-file,omitempty"` // path to a file that contains password for decrypting keys + PaymentEarly *int64 `yaml:"payment-early-percent,omitempty"` // percentage below the peers payment threshold when we initiate settlement + PaymentThreshold *string `yaml:"payment-threshold,omitempty"` // threshold in BZZ where you expect to get paid from your peers + PaymentTolerance *int64 `yaml:"payment-tolerance-percent,omitempty"` // excess debt above payment threshold in percentages where you disconnect from your peer + PostageContractStartBlock *uint64 `yaml:"postage-stamp-start-block,omitempty"` // postage stamp contract start block number + PostageStampAddress *string `yaml:"postage-stamp-address,omitempty"` // postage stamp contract address + PprofMutex *bool `yaml:"pprof-mutex,omitempty"` // enable pprof mutex profile + PprofProfile *bool `yaml:"pprof-profile,omitempty"` // enable pprof block profile + PriceOracleAddress *string `yaml:"price-oracle-address,omitempty"` // price oracle contract address + RedistributionAddress *string `yaml:"redistribution-address,omitempty"` // redistribution contract address + ReserveCapacityDoubling *int `yaml:"reserve-capacity-doubling,omitempty"` // reserve capacity doubling + ResolverOptions *[]string `yaml:"resolver-options,omitempty"` // ENS compatible API endpoint for a TLD and with contract address, can be repeated, format [tld:][contract-addr@]url + Resync *bool `yaml:"resync,omitempty"` // forces the node to resync postage contract data + SkipPostageSnapshot *bool `yaml:"skip-postage-snapshot,omitempty"` // skip postage snapshot + StakingAddress *string `yaml:"staking-address,omitempty"` // staking contract address + StatestoreCacheCapacity *uint64 `yaml:"statestore-cache-capacity,omitempty"` // lru memory caching capacity in number of statestore entries + StaticNodes *[]string `yaml:"static-nodes,omitempty"` // protect nodes from getting kicked out on bootnode + StorageIncentivesEnable *bool `yaml:"storage-incentives-enable,omitempty"` // enable storage incentives feature + SwapEnable *bool `yaml:"swap-enable,omitempty"` // enable swap + SwapFactoryAddress *string `yaml:"swap-factory-address,omitempty"` // swap factory addresses + SwapInitialDeposit *string `yaml:"swap-initial-deposit,omitempty"` // initial deposit if deploying a new chequebook + TargetNeighborhood *string `yaml:"target-neighborhood,omitempty"` // neighborhood to target in binary format (ex: 111111001) for mining the initial overlay + TracingEnabled *bool `yaml:"tracing-enable,omitempty"` // enable tracing + TracingEndpoint *string `yaml:"tracing-endpoint,omitempty"` // endpoint to send tracing data + TracingHost *string `yaml:"tracing-host,omitempty"` // host to send tracing data + TracingPort *string `yaml:"tracing-port,omitempty"` // port to send tracing data + TracingServiceName *string `yaml:"tracing-service-name,omitempty"` // service name identifier for tracing + TransactionDebugMode *bool `yaml:"transaction-debug-mode,omitempty"` // skips the gas estimate step for contract transactions + UseSIMD *bool `yaml:"use-simd,omitempty"` // use SIMD BMT hasher (available only on linux amd64 platforms) + Verbosity *string `yaml:"verbosity,omitempty"` // log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace + WarmupTime *time.Duration `yaml:"warmup-time,omitempty"` // maximum node warmup duration; proceeds when stable or after this time + WelcomeMessage *string `yaml:"welcome-message,omitempty"` // send a welcome message string during handshakes + WithdrawAddress *[]string `yaml:"withdrawal-addresses-whitelist,omitempty"` // withdrawal target addresses +} + +// Deref returns the value pointed to by p, or the zero value of T when p is nil. +// It is used for Beekeeper's own orchestration decisions on optional Config +// fields; it never substitutes any of Bee's defaults and is never written into +// the node's .bee.yaml. +func Deref[T any](p *T) T { + if p == nil { + var zero T + return zero + } + return *p }