diff --git a/command/agent/command.go b/command/agent/command.go index 6d72dc79d4d..6d1c2cf9a4e 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -67,9 +67,10 @@ func (c *Command) readConfig() *Config { // Make a new, empty config. cmdConfig := &Config{ - Client: &ClientConfig{}, - Consul: &config.ConsulConfig{}, - Ports: &Ports{}, + APISocket: &SocketConfig{}, + Client: &ClientConfig{}, + Consul: &config.ConsulConfig{}, + Ports: &Ports{}, Server: &ServerConfig{ ServerJoin: &ServerJoin{}, }, @@ -122,6 +123,12 @@ func (c *Command) readConfig() *Config { flags.BoolVar(&cmdConfig.LogJson, "log-json", false, "") flags.StringVar(&cmdConfig.NodeName, "node", "", "") + // API Socket Config options + flags.StringVar(&cmdConfig.APISocket.Path, "api-socket-path", "", "") + flags.StringVar(&cmdConfig.APISocket.User, "api-socket-user", "", "") + flags.StringVar(&cmdConfig.APISocket.Group, "api-socket-group", "", "") + flags.StringVar(&cmdConfig.APISocket.Mode, "api-socket-mode", "", "") + // Consul options flags.StringVar(&cmdConfig.Consul.Auth, "consul-auth", "", "") flags.Var((flaghelper.FuncBoolVar)(func(b bool) error { @@ -349,10 +356,11 @@ func (c *Command) IsValidConfig(config, cmdConfig *Config) bool { // Verify the paths are absolute. dirs := map[string]string{ - "data-dir": config.DataDir, - "plugin-dir": config.PluginDir, - "alloc-dir": config.Client.AllocDir, - "state-dir": config.Client.StateDir, + "data-dir": config.DataDir, + "plugin-dir": config.PluginDir, + "alloc-dir": config.Client.AllocDir, + "state-dir": config.Client.StateDir, + "api-socket-path": config.APISocket.Path, } for k, dir := range dirs { if dir == "" { diff --git a/command/agent/config.go b/command/agent/config.go index e02a7ca0f3d..3396b2bd0a3 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -92,6 +92,10 @@ type Config struct { // Use normalizedAddrs if you need the host+port to bind to. Addresses *Addresses `hcl:"addresses"` + // APISocket is used to configure a unix domain socket listener for the + // HTTP API + APISocket *SocketConfig `hcl:"api_socket"` + // normalizedAddr is set to the Address+Port by normalizeAddrs() normalizedAddrs *NormalizedAddrs @@ -1089,6 +1093,57 @@ func (n *NormalizedAddrs) Copy() *NormalizedAddrs { return &nn } +// SocketConfig contains the path and configuration for a unix domain socket +// listener. +type SocketConfig struct { + Path string `hcl:"path"` + User string `hcl:"user"` + Mode string `hcl:"mode"` + Group string `hcl:"group"` +} + +func (s *SocketConfig) Copy() *SocketConfig { + if s == nil { + return nil + } + ns := *s + return &ns +} + +// Merge merges two SocketConfigs together, preferring values from the argument. +func (s *SocketConfig) Merge(b *SocketConfig) *SocketConfig { + if b == nil { + return s + } + var result SocketConfig + if s != nil { + result = *s + } + if b.Path != "" { + result.Path = b.Path + } + if b.User != "" { + result.User = b.User + } + if b.Group != "" { + result.Group = b.Group + } + if b.Mode != "" { + result.Mode = b.Mode + } + return &result +} + +// UnixSocketsConfig extracts user, group, and mode into a UnixSocketsConfig +// suitable for listenerutil.UnixSocketListener +func (s *SocketConfig) UnixSocketsConfig() *listenerutil.UnixSocketsConfig { + return &listenerutil.UnixSocketsConfig{ + User: s.User, + Group: s.Group, + Mode: s.Mode, + } +} + // AdvertiseAddrs is used to control the addresses we advertise out for // different network services. All are optional and default to BindAddr and // their default Port. @@ -1264,6 +1319,7 @@ func DefaultConfig() *Config { }, Addresses: &Addresses{}, AdvertiseAddrs: &AdvertiseAddrs{}, + APISocket: &SocketConfig{}, Consul: config.DefaultConsulConfig(), Vault: config.DefaultVaultConfig(), UI: config.DefaultUIConfig(), @@ -1501,6 +1557,13 @@ func (c *Config) Merge(b *Config) *Config { result.AdvertiseAddrs = result.AdvertiseAddrs.Merge(b.AdvertiseAddrs) } + // Apply the api_socket config + if result.APISocket == nil && b.APISocket != nil { + result.APISocket = b.APISocket.Copy() + } else if b.APISocket != nil { + result.APISocket = result.APISocket.Merge(b.APISocket) + } + // Apply the Consul Configuration if result.Consul == nil && b.Consul != nil { result.Consul = b.Consul.Copy() @@ -1576,6 +1639,7 @@ func (c *Config) Copy() *Config { nc.Addresses = c.Addresses.Copy() nc.normalizedAddrs = c.normalizedAddrs.Copy() nc.AdvertiseAddrs = c.AdvertiseAddrs.Copy() + nc.APISocket = c.APISocket.Copy() nc.Client = c.Client.Copy() nc.Server = c.Server.Copy() nc.ACL = c.ACL.Copy() diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index 8a45d4dd3d9..19103f4175b 100644 --- a/command/agent/config_parse.go +++ b/command/agent/config_parse.go @@ -37,6 +37,7 @@ func ParseConfigFile(path string) (*Config, error) { // parse c := &Config{ + APISocket: &SocketConfig{}, Client: &ClientConfig{ ServerJoin: &ServerJoin{}, TemplateConfig: &client.ClientTemplateConfig{ diff --git a/command/agent/config_parse_test.go b/command/agent/config_parse_test.go index d023a90c08d..b527df2d43a 100644 --- a/command/agent/config_parse_test.go +++ b/command/agent/config_parse_test.go @@ -42,6 +42,12 @@ var basicConfig = &Config{ RPC: "127.0.0.3", Serf: "127.0.0.4", }, + APISocket: &SocketConfig{ + Path: "/var/run/nomad.sock", + User: "nomad", + Group: "nomad", + Mode: "0600", + }, Client: &ClientConfig{ Enabled: true, StateDir: "/tmp/client-state", @@ -219,33 +225,33 @@ var basicConfig = &Config{ ClientServiceName: "nomad-client", ClientHTTPCheckName: "nomad-client-http-health-check", Addr: "127.0.0.1:9500", - AllowUnauthenticated: &trueValue, + AllowUnauthenticated: pointer.Of(true), Token: "token1", Auth: "username:pass", - EnableSSL: &trueValue, - VerifySSL: &trueValue, + EnableSSL: pointer.Of(true), + VerifySSL: pointer.Of(true), CAFile: "/path/to/ca/file", CertFile: "/path/to/cert/file", KeyFile: "/path/to/key/file", - ServerAutoJoin: &trueValue, - ClientAutoJoin: &trueValue, - AutoAdvertise: &trueValue, - ChecksUseAdvertise: &trueValue, + ServerAutoJoin: pointer.Of(true), + ClientAutoJoin: pointer.Of(true), + AutoAdvertise: pointer.Of(true), + ChecksUseAdvertise: pointer.Of(true), Timeout: 5 * time.Second, TimeoutHCL: "5s", }, Vault: &config.VaultConfig{ Addr: "127.0.0.1:9500", - AllowUnauthenticated: &trueValue, + AllowUnauthenticated: pointer.Of(true), ConnectionRetryIntv: config.DefaultVaultConnectRetryIntv, - Enabled: &falseValue, + Enabled: pointer.Of(false), Role: "test_role", TLSCaFile: "/path/to/ca/file", TLSCaPath: "/path/to/ca", TLSCertFile: "/path/to/cert/file", TLSKeyFile: "/path/to/key/file", TLSServerName: "foobar", - TLSSkipVerify: &trueValue, + TLSSkipVerify: pointer.Of(true), TaskTokenTTL: "1s", Token: "12345", }, @@ -280,16 +286,16 @@ var basicConfig = &Config{ }, }, Autopilot: &config.AutopilotConfig{ - CleanupDeadServers: &trueValue, + CleanupDeadServers: pointer.Of(true), ServerStabilizationTime: 23057 * time.Second, ServerStabilizationTimeHCL: "23057s", LastContactThreshold: 12705 * time.Second, LastContactThresholdHCL: "12705s", MaxTrailingLogs: 17849, MinQuorum: 3, - EnableRedundancyZones: &trueValue, - DisableUpgradeMigration: &trueValue, - EnableCustomUpgrades: &trueValue, + EnableRedundancyZones: pointer.Of(true), + DisableUpgradeMigration: pointer.Of(true), + EnableCustomUpgrades: pointer.Of(true), }, Plugins: []*config.PluginConfig{ { @@ -532,6 +538,9 @@ func removeHelperAttributes(c *Config) *Config { } func (c *Config) addDefaults() { + if c.APISocket == nil { + c.APISocket = &SocketConfig{} + } if c.Client == nil { c.Client = &ClientConfig{} } @@ -635,7 +644,8 @@ var sample0 = &Config{ RPC: "host.example.com", Serf: "host.example.com", }, - Client: &ClientConfig{ServerJoin: &ServerJoin{}}, + APISocket: &SocketConfig{}, + Client: &ClientConfig{ServerJoin: &ServerJoin{}}, Server: &ServerConfig{ Enabled: true, BootstrapExpect: 3, @@ -730,7 +740,8 @@ var sample1 = &Config{ RPC: "host.example.com", Serf: "host.example.com", }, - Client: &ClientConfig{ServerJoin: &ServerJoin{}}, + APISocket: &SocketConfig{}, + Client: &ClientConfig{ServerJoin: &ServerJoin{}}, Server: &ServerConfig{ Enabled: true, BootstrapExpect: 3, diff --git a/command/agent/config_test.go b/command/agent/config_test.go index e82dca1bd58..4304dc09adf 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/nomad/helper/pointer" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs/config" + "github.com/shoenig/test/must" "github.com/stretchr/testify/require" ) @@ -44,6 +45,7 @@ func TestConfig_Merge(t *testing.T) { Ports: &Ports{}, Addresses: &Addresses{}, AdvertiseAddrs: &AdvertiseAddrs{}, + APISocket: &SocketConfig{}, Vault: &config.VaultConfig{}, Consul: &config.ConsulConfig{}, Sentinel: &config.SentinelConfig{}, @@ -386,6 +388,12 @@ func TestConfig_Merge(t *testing.T) { RPC: "127.0.0.2", Serf: "127.0.0.2", }, + APISocket: &SocketConfig{ + Path: "/tmp/run/nomad.sock", + User: "nomad", + Group: "nomad", + Mode: "0600", + }, HTTPAPIResponseHeaders: map[string]string{ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", @@ -1578,3 +1586,67 @@ func TestParseMultipleIPTemplates(t *testing.T) { }) } } + +func TestConfig_SocketConfig_Copy(t *testing.T) { + ci.Parallel(t) + + a := &SocketConfig{ + Path: "/var/run/nomad.sock", + User: "nomad", + Group: "wheel", + Mode: "0660", + } + b := a.Copy() + must.Eq(t, a, b) +} + +func TestConfig_SocketConfig_Merge(t *testing.T) { + ci.Parallel(t) + + sc1 := &SocketConfig{ + Path: "/tmp/sc1.sock", + User: "nomad", + Group: "wheel", + Mode: "660", + } + tcs := []struct { + name string + a *SocketConfig + b *SocketConfig + expect *SocketConfig + }{ + { + name: "nils", + expect: nil, + }, + { + name: "nil_b", + a: sc1, + expect: sc1, + }, + { + name: "nil_a", + b: sc1, + expect: sc1, + }, + { + name: "overwrite_a.Path", + a: sc1, + b: &SocketConfig{Path: "/tc/sc2"}, + expect: &SocketConfig{ + Path: "/tc/sc2", + User: "nomad", + Group: "wheel", + Mode: "660", + }, + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + tc := tc + ci.Parallel(t) + c := tc.a.Merge(tc.b) + must.Eq(t, tc.expect, c) + }) + } +} diff --git a/command/agent/http.go b/command/agent/http.go index 5ccba35d69e..3dafc413cdc 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -27,6 +27,7 @@ import ( log "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-msgpack/codec" multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-secure-stdlib/listenerutil" "github.com/rs/cors" "golang.org/x/time/rate" @@ -193,6 +194,58 @@ func NewHTTPServers(agent *Agent, config *Config) ([]*HTTPServer, error) { srvs = append(srvs, srv) } + // Initialize the API socket if configured + startAPISocket := func() { + if config.APISocket.Path == "" { + return + } + + const ( + MaxSocketPathLength = 104 + msgPrefix = "error starting api socket:" + ) + + if len(config.APISocket.Path) > MaxSocketPathLength { + serverInitializationErrors = multierror.Append(serverInitializationErrors, + fmt.Errorf("%s path must be %v characters or less", msgPrefix, MaxSocketPathLength)) + } + + sl, err := listenerutil.UnixSocketListener(config.APISocket.Path, config.APISocket.UnixSocketsConfig()) + if err != nil { + serverInitializationErrors = multierror.Append(serverInitializationErrors, + fmt.Errorf("%s %v", msgPrefix, err)) + return + } + + // Create the server + srv := &HTTPServer{ + agent: agent, + eventAuditor: agent.auditor, + mux: http.NewServeMux(), + listener: sl, + listenerCh: make(chan struct{}), + logger: agent.httpLogger, + Addr: sl.Addr().String(), + wsUpgrader: wsUpgrader, + } + srv.registerHandlers(config.EnableDebug) + + // Create HTTP server with timeouts + httpServer := http.Server{ + Addr: srv.Addr, + Handler: handlers.CompressHandler(srv.mux), + ConnState: makeConnState(config.TLSConfig.EnableHTTP, handshakeTimeout, maxConns, srv.logger), + ErrorLog: newHTTPServerLogger(srv.logger), + } + + go func() { + defer close(srv.listenerCh) + httpServer.Serve(sl) + }() + srvs = append(srvs, srv) + } + startAPISocket() + // Return early on errors if serverInitializationErrors != nil { for _, srv := range srvs { diff --git a/command/agent/testdata/basic.hcl b/command/agent/testdata/basic.hcl index c3637c68030..267824dc28f 100644 --- a/command/agent/testdata/basic.hcl +++ b/command/agent/testdata/basic.hcl @@ -39,6 +39,13 @@ advertise { serf = "127.0.0.4" } +api_socket { + path = "/var/run/nomad.sock" + user = "nomad" + group = "nomad" + mode = "0600" +} + client { enabled = true state_dir = "/tmp/client-state" diff --git a/command/agent/testdata/basic.json b/command/agent/testdata/basic.json index 653c1b4d581..a0c8006c168 100644 --- a/command/agent/testdata/basic.json +++ b/command/agent/testdata/basic.json @@ -51,6 +51,14 @@ "serf": "127.0.0.4" } ], + "api_socket": [ + { + "path": "/var/run/nomad.sock", + "user": "nomad", + "group": "nomad", + "mode": "0600" + } + ], "autopilot": [ { "cleanup_dead_servers": true,