HTTP agent registration allows proxy to be defined.

This commit is contained in:
Paul Banks 2018-04-17 13:29:02 +01:00 committed by Mitchell Hashimoto
parent 78e48fd547
commit c2266b134a
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
7 changed files with 182 additions and 67 deletions

View File

@ -2426,11 +2426,17 @@ func (a *Agent) unloadChecks() error {
// loadProxies will load connect proxy definitions from configuration and
// persisted definitions on disk, and load them into the local agent.
func (a *Agent) loadProxies(conf *config.RuntimeConfig) error {
for _, proxy := range conf.ConnectProxies {
for _, svc := range conf.Services {
if svc.Connect != nil {
proxy, err := svc.ConnectManagedProxy()
if err != nil {
return fmt.Errorf("failed adding proxy: %s", err)
}
if err := a.AddProxy(proxy, false); err != nil {
return fmt.Errorf("failed adding proxy: %s", err)
}
}
}
// TODO(banks): persist proxy state and re-load it here?
return nil

View File

@ -589,10 +589,24 @@ func (s *HTTPServer) AgentRegisterService(resp http.ResponseWriter, req *http.Re
return nil, err
}
// Get any proxy registrations
proxy, err := args.ConnectManagedProxy()
if err != nil {
resp.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(resp, err.Error())
return nil, nil
}
// Add the service.
if err := s.agent.AddService(ns, chkTypes, true, token); err != nil {
return nil, err
}
// Add proxy (which will add proxy service so do it before we trigger sync)
if proxy != nil {
if err := s.agent.AddProxy(proxy, true); err != nil {
return nil, err
}
}
s.syncChanges()
return nil, nil
}

View File

@ -26,6 +26,7 @@ import (
"github.com/hashicorp/serf/serf"
"github.com/pascaldekloe/goe/verify"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func makeReadOnlyAgentACL(t *testing.T, srv *HTTPServer) string {
@ -1369,10 +1370,78 @@ func TestAgent_RegisterService_InvalidAddress(t *testing.T) {
}
}
// This tests local agent service registration of a connect proxy. This
// verifies that it is put in the local state store properly for syncing
// later.
func TestAgent_RegisterService_ConnectProxy(t *testing.T) {
// This tests local agent service registration with a managed proxy.
func TestAgent_RegisterService_ManagedConnectProxy(t *testing.T) {
t.Parallel()
assert := assert.New(t)
require := require.New(t)
a := NewTestAgent(t.Name(), "")
defer a.Shutdown()
// Register a proxy. Note that the destination doesn't exist here on
// this agent or in the catalog at all. This is intended and part
// of the design.
args := &structs.ServiceDefinition{
Name: "web",
Port: 8000,
// This is needed just because empty check struct (not pointer) get json
// encoded as object with zero values and then decoded back to object with
// zero values _except that the header map is an empty map not a nil map_.
// So our check to see if s.Check.Empty() returns false since DeepEqual
// considers empty maps and nil maps to be different types. Then the request
// fails validation because the Check definition isn't valid... This is jank
// we should fix but it's another yak I don't want to shave right now.
Check: structs.CheckType{
TTL: 15 * time.Second,
},
Connect: &structs.ServiceDefinitionConnect{
Proxy: &structs.ServiceDefinitionConnectProxy{
ExecMode: "script",
Command: "proxy.sh",
Config: map[string]interface{}{
"foo": "bar",
},
},
},
}
req, _ := http.NewRequest("PUT", "/v1/agent/service/register?token=abc123", jsonReader(args))
resp := httptest.NewRecorder()
obj, err := a.srv.AgentRegisterService(resp, req)
assert.NoError(err)
assert.Nil(obj)
require.Equal(200, resp.Code, "request failed with body: %s",
resp.Body.String())
// Ensure the target service
_, ok := a.State.Services()["web"]
assert.True(ok, "has service")
// Ensure the proxy service was registered
proxySvc, ok := a.State.Services()["web-proxy"]
require.True(ok, "has proxy service")
assert.Equal(structs.ServiceKindConnectProxy, proxySvc.Kind)
assert.Equal("web", proxySvc.ProxyDestination)
assert.NotEmpty(proxySvc.Port, "a port should have been assigned")
// Ensure proxy itself was registered
proxy := a.State.Proxy("web-proxy")
require.NotNil(proxy)
assert.Equal(structs.ProxyExecModeScript, proxy.ExecMode)
assert.Equal("proxy.sh", proxy.Command)
assert.Equal(args.Connect.Proxy.Config, proxy.Config)
// Ensure the token was configured
assert.Equal("abc123", a.State.ServiceToken("web"))
assert.Equal("abc123", a.State.ServiceToken("web-proxy"))
}
// This tests local agent service registration of a unmanaged connect proxy.
// This verifies that it is put in the local state store properly for syncing
// later. Note that _managed_ connect proxies are registered as part of the
// target service's registration.
func TestAgent_RegisterService_UnmanagedConnectProxy(t *testing.T) {
t.Parallel()
assert := assert.New(t)
@ -1411,7 +1480,7 @@ func TestAgent_RegisterService_ConnectProxy(t *testing.T) {
// This tests that connect proxy validation is done for local agent
// registration. This doesn't need to test validation exhaustively since
// that is done via a table test in the structs package.
func TestAgent_RegisterService_ConnectProxyInvalid(t *testing.T) {
func TestAgent_RegisterService_UnmanagedConnectProxyInvalid(t *testing.T) {
t.Parallel()
assert := assert.New(t)

View File

@ -322,15 +322,8 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
}
var services []*structs.ServiceDefinition
var proxies []*structs.ConnectManagedProxy
for _, service := range c.Services {
services = append(services, b.serviceVal(&service))
// Register any connect proxies requested
if proxy := b.connectManagedProxyVal(&service); proxy != nil {
proxies = append(proxies, proxy)
}
// TODO(banks): support connect-native registrations (v.Connect.Enabled ==
// true)
}
if c.Service != nil {
services = append(services, b.serviceVal(c.Service))
@ -648,7 +641,6 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
CheckUpdateInterval: b.durationVal("check_update_interval", c.CheckUpdateInterval),
Checks: checks,
ClientAddrs: clientAddrs,
ConnectProxies: proxies,
ConnectProxyBindMinPort: proxyBindMinPort,
ConnectProxyBindMaxPort: proxyBindMaxPort,
DataDir: b.stringVal(c.DataDir),
@ -1020,46 +1012,26 @@ func (b *Builder) serviceVal(v *ServiceDefinition) *structs.ServiceDefinition {
Token: b.stringVal(v.Token),
EnableTagOverride: b.boolVal(v.EnableTagOverride),
Checks: checks,
Connect: b.serviceConnectVal(v.Connect),
}
}
func (b *Builder) connectManagedProxyVal(v *ServiceDefinition) *structs.ConnectManagedProxy {
if v.Connect == nil || v.Connect.Proxy == nil {
func (b *Builder) serviceConnectVal(v *ServiceConnect) *structs.ServiceDefinitionConnect {
if v == nil {
return nil
}
p := v.Connect.Proxy
targetID := b.stringVal(v.ID)
if targetID == "" {
targetID = b.stringVal(v.Name)
}
execMode := structs.ProxyExecModeDaemon
if p.ExecMode != nil {
switch *p.ExecMode {
case "daemon":
execMode = structs.ProxyExecModeDaemon
case "script":
execMode = structs.ProxyExecModeScript
default:
b.err = multierror.Append(fmt.Errorf(
"service[%s]: invalid connect proxy exec_mode: %s", targetID,
*p.ExecMode))
return nil
var proxy *structs.ServiceDefinitionConnectProxy
if v.Proxy != nil {
proxy = &structs.ServiceDefinitionConnectProxy{
ExecMode: b.stringVal(v.Proxy.ExecMode),
Command: b.stringVal(v.Proxy.Command),
Config: v.Proxy.Config,
}
}
return &structs.ConnectManagedProxy{
ExecMode: execMode,
Command: b.stringVal(p.Command),
Config: p.Config,
// ProxyService will be setup when the agent registers the configured
// proxies and starts them etc. We could do it here but we may need to do
// things like probe the OS for a free port etc. And we have enough info to
// resolve all this later.
ProxyService: nil,
TargetServiceID: targetID,
return &structs.ServiceDefinitionConnect{
Proxy: proxy,
}
}

View File

@ -621,10 +621,6 @@ type RuntimeConfig struct {
// that.
ConnectEnabled bool
// ConnectProxies is a list of configured proxies taken from the "connect"
// block of service registrations.
ConnectProxies []*structs.ConnectManagedProxy
// ConnectProxyBindMinPort is the inclusive start of the range of ports
// allocated to the agent for starting proxy listeners on where no explicit
// port is specified.

View File

@ -3405,19 +3405,6 @@ func TestFullConfig(t *testing.T) {
},
CheckUpdateInterval: 16507 * time.Second,
ClientAddrs: []*net.IPAddr{ipAddr("93.83.18.19")},
ConnectProxies: []*structs.ConnectManagedProxy{
{
ExecMode: structs.ProxyExecModeDaemon,
Command: "awesome-proxy",
Config: map[string]interface{}{
"foo": "qux", // Overriden by service
// Note globals are not merged here but on rendering to the proxy
// endpoint. That's because proxies can be added later too so merging
// at config time is redundant if we have to do it later anyway.
},
TargetServiceID: "MRHVMZuD",
},
},
ConnectProxyBindMinPort: 2000,
ConnectProxyBindMaxPort: 3000,
DNSAddrs: []net.Addr{tcpAddr("93.95.95.81:7001"), udpAddr("93.95.95.81:7001")},
@ -3592,6 +3579,15 @@ func TestFullConfig(t *testing.T) {
DeregisterCriticalServiceAfter: 68482 * time.Second,
},
},
Connect: &structs.ServiceDefinitionConnect{
Proxy: &structs.ServiceDefinitionConnectProxy{
ExecMode: "daemon",
Command: "awesome-proxy",
Config: map[string]interface{}{
"foo": "qux",
},
},
},
},
{
ID: "dLOXpSCI",
@ -4082,7 +4078,6 @@ func TestSanitize(t *testing.T) {
],
"ClientAddrs": [],
"ConnectEnabled": false,
"ConnectProxies": [],
"ConnectProxyBindMaxPort": 0,
"ConnectProxyBindMinPort": 0,
"ConnectProxyDefaultConfig": {},
@ -4219,6 +4214,7 @@ func TestSanitize(t *testing.T) {
"Timeout": "0s"
},
"Checks": [],
"Connect": null,
"EnableTagOverride": false,
"ID": "",
"Kind": "",

View File

@ -1,5 +1,9 @@
package structs
import (
"fmt"
)
// ServiceDefinition is used to JSON decode the Service definitions. For
// documentation on specific fields see NodeService which is better documented.
type ServiceDefinition struct {
@ -15,6 +19,7 @@ type ServiceDefinition struct {
Token string
EnableTagOverride bool
ProxyDestination string
Connect *ServiceDefinitionConnect
}
func (s *ServiceDefinition) NodeService() *NodeService {
@ -35,6 +40,45 @@ func (s *ServiceDefinition) NodeService() *NodeService {
return ns
}
// ConnectManagedProxy returns a ConnectManagedProxy from the ServiceDefinition
// if one is configured validly. Note that is may return nil if no proxy is
// configured and will also return nil error in this case too as it's an
// expected case. The error returned indicates that there was an attempt to
// configure a proxy made but that it was invalid input, e.g. invalid
// "exec_mode".
func (s *ServiceDefinition) ConnectManagedProxy() (*ConnectManagedProxy, error) {
if s.Connect == nil || s.Connect.Proxy == nil {
return nil, nil
}
// NodeService performs some simple normalization like copying ID from Name
// which we shouldn't hard code ourselves here...
ns := s.NodeService()
execMode := ProxyExecModeDaemon
switch s.Connect.Proxy.ExecMode {
case "":
execMode = ProxyExecModeDaemon
case "daemon":
execMode = ProxyExecModeDaemon
case "script":
execMode = ProxyExecModeScript
default:
return nil, fmt.Errorf("invalid exec mode: %s", s.Connect.Proxy.ExecMode)
}
p := &ConnectManagedProxy{
ExecMode: execMode,
Command: s.Connect.Proxy.Command,
Config: s.Connect.Proxy.Config,
// ProxyService will be setup when the agent registers the configured
// proxies and starts them etc.
TargetServiceID: ns.ID,
}
return p, nil
}
func (s *ServiceDefinition) CheckTypes() (checks CheckTypes, err error) {
if !s.Check.Empty() {
err := s.Check.Validate()
@ -51,3 +95,21 @@ func (s *ServiceDefinition) CheckTypes() (checks CheckTypes, err error) {
}
return checks, nil
}
// ServiceDefinitionConnect is the connect block within a service registration.
// Note this is duplicated in config.ServiceConnect and needs to be kept in
// sync.
type ServiceDefinitionConnect struct {
// TODO(banks) add way to specify that the app is connect-native
// Proxy configures a connect proxy instance for the service
Proxy *ServiceDefinitionConnectProxy `json:"proxy,omitempty" hcl:"proxy" mapstructure:"proxy"`
}
// ServiceDefinitionConnectProxy is the connect proxy config within a service
// registration. Note this is duplicated in config.ServiceConnectProxy and needs
// to be kept in sync.
type ServiceDefinitionConnectProxy struct {
Command string `json:"command,omitempty" hcl:"command" mapstructure:"command"`
ExecMode string `json:"exec_mode,omitempty" hcl:"exec_mode" mapstructure:"exec_mode"`
Config map[string]interface{} `json:"config,omitempty" hcl:"config" mapstructure:"config"`
}