diff --git a/agent/agent.go b/agent/agent.go index 4b4377a810..36ca1a2958 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1136,6 +1136,8 @@ func (a *Agent) consulConfig() (*consul.Config, error) { return nil, fmt.Errorf("Failed to configure keyring: %v", err) } + base.ConfigEntryBootstrap = a.config.ConfigEntryBootstrap + return base, nil } @@ -3616,6 +3618,12 @@ func (a *Agent) ReloadConfig(newCfg *config.RuntimeConfig) error { } } + // this only gets used by the consulConfig function and since + // that is only ever done during init and reload here then + // an in place modification is safe as reloads cannot be + // concurrent due to both gaing a full lock on the stateLock + a.config.ConfigEntryBootstrap = newCfg.ConfigEntryBootstrap + // create the config for the rpc server/client consulCfg, err := a.consulConfig() if err != nil { diff --git a/agent/config/builder.go b/agent/config/builder.go index 18706ffe9d..5ef7e35044 100644 --- a/agent/config/builder.go +++ b/agent/config/builder.go @@ -633,6 +633,22 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) { verifyOutgoing = true } + var configEntries []structs.ConfigEntry + + if len(c.ConfigEntries.Bootstrap.ProxyDefaults) > 0 { + for name, config := range c.ConfigEntries.Bootstrap.ProxyDefaults { + if name != structs.ProxyConfigGlobal { + return RuntimeConfig{}, fmt.Errorf("invalid config.proxy_defaults name (%q), only %q is supported", name, structs.ProxyConfigGlobal) + } + + configEntries = append(configEntries, &structs.ProxyConfigEntry{ + Kind: structs.ProxyDefaults, + Name: structs.ProxyConfigGlobal, + Config: config, + }) + } + } + // ---------------------------------------------------------------- // build runtime config // @@ -767,6 +783,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) { CheckUpdateInterval: b.durationVal("check_update_interval", c.CheckUpdateInterval), Checks: checks, ClientAddrs: clientAddrs, + ConfigEntryBootstrap: configEntries, ConnectEnabled: connectEnabled, ConnectCAProvider: connectCAProvider, ConnectCAConfig: connectCAConfig, diff --git a/agent/config/config.go b/agent/config/config.go index 855ce91948..958f53f26c 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -184,6 +184,7 @@ type Config struct { CheckUpdateInterval *string `json:"check_update_interval,omitempty" hcl:"check_update_interval" mapstructure:"check_update_interval"` Checks []CheckDefinition `json:"checks,omitempty" hcl:"checks" mapstructure:"checks"` ClientAddr *string `json:"client_addr,omitempty" hcl:"client_addr" mapstructure:"client_addr"` + ConfigEntries ConfigEntries `json:"config_entries,omitempty" hcl:"config_entries" mapstructure:"config_entries"` Connect Connect `json:"connect,omitempty" hcl:"connect" mapstructure:"connect"` DNS DNS `json:"dns_config,omitempty" hcl:"dns_config" mapstructure:"dns_config"` DNSDomain *string `json:"domain,omitempty" hcl:"domain" mapstructure:"domain"` @@ -650,3 +651,11 @@ type Tokens struct { Default *string `json:"default,omitempty" hcl:"default" mapstructure:"default"` Agent *string `json:"agent,omitempty" hcl:"agent" mapstructure:"agent"` } + +type ConfigEntries struct { + Bootstrap ConfigEntriesBootstrap `json:"bootstrap,omitempty" hcl:"bootstrap" mapstructure:"bootstrap"` +} + +type ConfigEntriesBootstrap struct { + ProxyDefaults map[string]map[string]interface{} `json:"proxy_defaults,omitempty" hcl:"proxy_defaults" mapstructure:"proxy_defaults"` +} diff --git a/agent/config/runtime.go b/agent/config/runtime.go index ba3689a2cd..96bbe1e3e4 100644 --- a/agent/config/runtime.go +++ b/agent/config/runtime.go @@ -496,6 +496,10 @@ type RuntimeConfig struct { // flag: -client string ClientAddrs []*net.IPAddr + // ConfigEntryBootstrap contains a list of ConfigEntries to ensure are created + // If entries of the same Kind/Name exist already these will not update them. + ConfigEntryBootstrap []structs.ConfigEntry + // ConnectEnabled opts the agent into connect. It should be set on all clients // and servers in a cluster for correct connect operation. ConnectEnabled bool diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go index 59ab47de70..0e22b95d73 100644 --- a/agent/config/runtime_test.go +++ b/agent/config/runtime_test.go @@ -3007,6 +3007,16 @@ func TestFullConfig(t *testing.T) { ], "check_update_interval": "16507s", "client_addr": "93.83.18.19", + "config_entries": { + "bootstrap": { + "proxy_defaults": { + "global": { + "foo": "bar", + "bar": 1.0 + } + } + } + }, "connect": { "ca_provider": "consul", "ca_config": { @@ -3560,6 +3570,12 @@ func TestFullConfig(t *testing.T) { ] check_update_interval = "16507s" client_addr = "93.83.18.19" + config_entries { + bootstrap proxy_defaults global { + foo = "bar" + bar = 1.0 + } + } connect { ca_provider = "consul" ca_config { @@ -4217,8 +4233,19 @@ func TestFullConfig(t *testing.T) { DeregisterCriticalServiceAfter: 13209 * time.Second, }, }, - CheckUpdateInterval: 16507 * time.Second, - ClientAddrs: []*net.IPAddr{ipAddr("93.83.18.19")}, + CheckUpdateInterval: 16507 * time.Second, + ClientAddrs: []*net.IPAddr{ipAddr("93.83.18.19")}, + ConfigEntryBootstrap: []structs.ConfigEntry{ + &structs.ProxyConfigEntry{ + Kind: structs.ProxyDefaults, + Name: structs.ProxyConfigGlobal, + Config: map[string]interface{}{ + "foo": "bar", + // has to be a float due to being a map[string]interface + "bar": float64(1), + }, + }, + }, ConnectEnabled: true, ConnectProxyBindMinPort: 2000, ConnectProxyBindMaxPort: 3000, @@ -4996,6 +5023,7 @@ func TestSanitize(t *testing.T) { "Token": "hidden" }], "ClientAddrs": [], + "ConfigEntryBootstrap": [], "ConnectCAConfig": {}, "ConnectCAProvider": "", "ConnectEnabled": false, diff --git a/agent/consul/config.go b/agent/consul/config.go index ff2b008377..baf9e63a29 100644 --- a/agent/consul/config.go +++ b/agent/consul/config.go @@ -391,6 +391,10 @@ type Config struct { // CAConfig is used to apply the initial Connect CA configuration when // bootstrapping. CAConfig *structs.CAConfiguration + + // ConfigEntryBootstrap contains a list of ConfigEntries to ensure are created + // If entries of the same Kind/Name exist already these will not update them. + ConfigEntryBootstrap []structs.ConfigEntry } func (c *Config) ToTLSUtilConfig() tlsutil.Config { diff --git a/agent/consul/leader.go b/agent/consul/leader.go index 5abf12d607..3264892095 100644 --- a/agent/consul/leader.go +++ b/agent/consul/leader.go @@ -40,6 +40,10 @@ var ( // minAutopilotVersion is the minimum Consul version in which Autopilot features // are supported. minAutopilotVersion = version.Must(version.NewVersion("0.8.0")) + + // minCentralizedConfigVersion is the minimum Consul version in which centralized + // config is supported + minCentralizedConfigVersion = version.Must(version.NewVersion("1.5.0")) ) // monitorLeadership is used to monitor if we acquire or lose our role @@ -261,6 +265,11 @@ func (s *Server) establishLeadership() error { return err } + // attempt to bootstrap config entries + if err := s.bootstrapConfigEntries(s.config.ConfigEntryBootstrap); err != nil { + return err + } + s.getOrCreateAutopilotConfig() s.autopilot.Start() @@ -893,6 +902,44 @@ func (s *Server) getOrCreateAutopilotConfig() *autopilot.Config { return config } +func (s *Server) bootstrapConfigEntries(entries []structs.ConfigEntry) error { + if s.config.PrimaryDatacenter != "" && s.config.PrimaryDatacenter != s.config.Datacenter { + // only bootstrap in the primary datacenter + return nil + } + + if len(entries) < 1 { + // nothing to initialize + return nil + } + + if !ServersMeetMinimumVersion(s.LANMembers(), minCentralizedConfigVersion) { + s.logger.Printf("[WARN] centralized config: can't initialize until all servers >= %s", minCentralizedConfigVersion.String()) + return nil + } + + state := s.fsm.State() + for _, entry := range entries { + _, existing, err := state.ConfigEntry(nil, entry.GetKind(), entry.GetName()) + if err != nil { + return fmt.Errorf("Failed to determine whether the configuration for %q / %q already exists: %v", entry.GetKind(), entry.GetName(), err) + } + + if existing == nil { + req := structs.ConfigEntryRequest{ + Op: structs.ConfigEntryUpsert, + Datacenter: s.config.Datacenter, + Entry: entry, + } + + if _, err = s.raftApply(structs.ConfigEntryRequestType, &req); err != nil { + return fmt.Errorf("Failed to apply configuration entry %q / %q: %v", entry.GetKind(), entry.GetName(), err) + } + } + } + return nil +} + // initializeCAConfig is used to initialize the CA config if necessary // when setting up the CA during establishLeadership func (s *Server) initializeCAConfig() (*structs.CAConfiguration, error) { diff --git a/agent/consul/leader_test.go b/agent/consul/leader_test.go index b6dc58bce0..c000a37ed8 100644 --- a/agent/consul/leader_test.go +++ b/agent/consul/leader_test.go @@ -1206,3 +1206,38 @@ func TestLeader_ACLUpgrade(t *testing.T) { require.Equal(t, client.ACL.Rules, token.Rules) }) } + +func TestLeader_ConfigEntryBootstrap(t *testing.T) { + t.Parallel() + global_entry_init := &structs.ProxyConfigEntry{ + Kind: structs.ProxyDefaults, + Name: structs.ProxyConfigGlobal, + Config: map[string]interface{}{ + // these are made a []uint8 and a int64 to allow the Equals test to pass + // otherwise it will fail complaining about data types + "foo": []uint8("bar"), + "bar": int64(1), + }, + } + + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.Build = "1.5.0" + c.ConfigEntryBootstrap = []structs.ConfigEntry{ + global_entry_init, + } + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + testrpc.WaitForTestAgent(t, s1.RPC, "dc1") + + retry.Run(t, func(t *retry.R) { + _, entry, err := s1.fsm.State().ConfigEntry(nil, structs.ProxyDefaults, structs.ProxyConfigGlobal) + require.NoError(t, err) + require.NotNil(t, entry) + global, ok := entry.(*structs.ProxyConfigEntry) + require.True(t, ok) + require.Equal(t, global_entry_init.Kind, global.Kind) + require.Equal(t, global_entry_init.Name, global.Name) + require.Equal(t, global_entry_init.Config, global.Config) + }) +} diff --git a/agent/consul/server.go b/agent/consul/server.go index b1596a82ec..f19a907455 100644 --- a/agent/consul/server.go +++ b/agent/consul/server.go @@ -1132,6 +1132,11 @@ func (s *Server) GetLANCoordinate() (lib.CoordinateSet, error) { // ReloadConfig is used to have the Server do an online reload of // relevant configuration information func (s *Server) ReloadConfig(config *Config) error { + if s.IsLeader() { + // only bootstrap the config entries if we are the leader + // this will error if we lose leadership while bootstrapping here. + return s.bootstrapConfigEntries(config.ConfigEntryBootstrap) + } return nil } diff --git a/agent/consul/server_test.go b/agent/consul/server_test.go index 86505b906a..4f809b664c 100644 --- a/agent/consul/server_test.go +++ b/agent/consul/server_test.go @@ -970,3 +970,41 @@ func TestServer_RevokeLeadershipIdempotent(t *testing.T) { t.Fatal(err) } } + +func TestServer_Reload(t *testing.T) { + t.Parallel() + + global_entry_init := &structs.ProxyConfigEntry{ + Kind: structs.ProxyDefaults, + Name: structs.ProxyConfigGlobal, + Config: map[string]interface{}{ + // these are made a []uint8 and a int64 to allow the Equals test to pass + // otherwise it will fail complaining about data types + "foo": []uint8("bar"), + "bar": int64(1), + }, + } + + dir1, s := testServerWithConfig(t, func(c *Config) { + c.Build = "1.5.0" + }) + defer os.RemoveAll(dir1) + defer s.Shutdown() + + testrpc.WaitForTestAgent(t, s.RPC, "dc1") + + s.config.ConfigEntryBootstrap = []structs.ConfigEntry{ + global_entry_init, + } + + s.ReloadConfig(s.config) + + _, entry, err := s.fsm.State().ConfigEntry(nil, structs.ProxyDefaults, structs.ProxyConfigGlobal) + require.NoError(t, err) + require.NotNil(t, entry) + global, ok := entry.(*structs.ProxyConfigEntry) + require.True(t, ok) + require.Equal(t, global_entry_init.Kind, global.Kind) + require.Equal(t, global_entry_init.Name, global.Name) + require.Equal(t, global_entry_init.Config, global.Config) +} diff --git a/website/source/docs/agent/options.html.md b/website/source/docs/agent/options.html.md index e15f59b362..2aaf9cb834 100644 --- a/website/source/docs/agent/options.html.md +++ b/website/source/docs/agent/options.html.md @@ -807,6 +807,21 @@ default will automatically work with some tooling. * `client_addr` Equivalent to the [`-client` command-line flag](#_client). +* `config_entries` + This object allows setting options for centralized config entries. + + The following sub-keys are available: + + * `bootstrap` + This object allows configuring centralized config entries to be bootstrapped + by the leader. These entries will be reloaded during an agent config reload. + + The following sub-keys are available: + + * `proxy_defaults` + This object should contain a mapping of config entry names to an opaque proxy configuration mapping. + Currently the only supported name is `global` + * `connect` This object allows setting options for the Connect feature.