Merge pull request #8011 from hashicorp/f-cnative-host

consul/connect: implement initial support for connect native
This commit is contained in:
Seth Hoenig
2020-06-24 10:33:12 -05:00
committed by GitHub
28 changed files with 1136 additions and 102 deletions

View File

@@ -110,6 +110,7 @@ type Service struct {
Connect *ConsulConnect
Meta map[string]string
CanaryMeta map[string]string
TaskName string `mapstructure:"task"`
}
// Canonicalize the Service by ensuring its name and address mode are set. Task

View File

@@ -69,7 +69,7 @@ func TestService_Connect_Canonicalize(t *testing.T) {
t.Run("empty connect", func(t *testing.T) {
cc := new(ConsulConnect)
cc.Canonicalize()
require.False(t, cc.Native)
require.Empty(t, cc.Native)
require.Nil(t, cc.SidecarService)
require.Nil(t, cc.SidecarTask)
})

View File

@@ -52,7 +52,7 @@ func (*consulSockHook) Name() string {
func (h *consulSockHook) shouldRun() bool {
tg := h.alloc.Job.LookupTaskGroup(h.alloc.TaskGroup)
for _, s := range tg.Services {
if s.Connect != nil {
if s.Connect.HasSidecar() {
return true
}
}

View File

@@ -0,0 +1,221 @@
package taskrunner
import (
"context"
"io"
"io/ioutil"
"os"
"path/filepath"
hclog "github.com/hashicorp/go-hclog"
ifs "github.com/hashicorp/nomad/client/allocrunner/interfaces"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
"github.com/pkg/errors"
)
const (
connectNativeHookName = "connect_native"
)
type connectNativeHookConfig struct {
consulShareTLS bool
consul consulTransportConfig
alloc *structs.Allocation
logger hclog.Logger
}
func newConnectNativeHookConfig(alloc *structs.Allocation, consul *config.ConsulConfig, logger hclog.Logger) *connectNativeHookConfig {
return &connectNativeHookConfig{
alloc: alloc,
logger: logger,
consulShareTLS: consul.ShareSSL == nil || *consul.ShareSSL, // default enabled
consul: newConsulTransportConfig(consul),
}
}
// connectNativeHook manages additional automagic configuration for a connect
// native task.
//
// If nomad client is configured to talk to Consul using TLS (or other special
// auth), the native task will inherit that configuration EXCEPT for the consul
// token.
//
// If consul is configured with ACLs enabled, a Service Identity token will be
// generated on behalf of the native service and supplied to the task.
type connectNativeHook struct {
// alloc is the allocation with the connect native task being run
alloc *structs.Allocation
// consulShareTLS is used to toggle whether the TLS configuration of the
// Nomad Client may be shared with Connect Native applications.
consulShareTLS bool
// consulConfig is used to enable the connect native enabled task to
// communicate with consul directly, as is necessary for the task to request
// its connect mTLS certificates.
consulConfig consulTransportConfig
// logger is used to log things
logger hclog.Logger
}
func newConnectNativeHook(c *connectNativeHookConfig) *connectNativeHook {
return &connectNativeHook{
alloc: c.alloc,
consulShareTLS: c.consulShareTLS,
consulConfig: c.consul,
logger: c.logger.Named(connectNativeHookName),
}
}
func (connectNativeHook) Name() string {
return connectNativeHookName
}
func (h *connectNativeHook) Prestart(
ctx context.Context,
request *ifs.TaskPrestartRequest,
response *ifs.TaskPrestartResponse) error {
if !request.Task.Kind.IsConnectNative() {
response.Done = true
return nil
}
if h.consulShareTLS {
// copy TLS certificates
if err := h.copyCertificates(h.consulConfig, request.TaskDir.SecretsDir); err != nil {
h.logger.Error("failed to copy Consul TLS certificates", "error", err)
return err
}
// set environment variables for communicating with Consul agent, but
// only if those environment variables are not already set
response.Env = h.tlsEnv(request.TaskEnv.EnvMap)
}
if err := h.maybeSetSITokenEnv(request.TaskDir.SecretsDir, request.Task.Name, response.Env); err != nil {
h.logger.Error("failed to load Consul Service Identity Token", "error", err, "task", request.Task.Name)
return err
}
// tls/acl setup for native task done
response.Done = true
return nil
}
const (
secretCAFilename = "consul_ca_file.pem"
secretCertfileFilename = "consul_cert_file.pem"
secretKeyfileFilename = "consul_key_file.pem"
)
func (h *connectNativeHook) copyCertificates(consulConfig consulTransportConfig, dir string) error {
if err := h.copyCertificate(consulConfig.CAFile, dir, secretCAFilename); err != nil {
return err
}
if err := h.copyCertificate(consulConfig.CertFile, dir, secretCertfileFilename); err != nil {
return err
}
if err := h.copyCertificate(consulConfig.KeyFile, dir, secretKeyfileFilename); err != nil {
return err
}
return nil
}
func (connectNativeHook) copyCertificate(source, dir, name string) error {
if source == "" {
return nil
}
original, err := os.Open(source)
if err != nil {
return errors.Wrap(err, "failed to open consul TLS certificate")
}
defer original.Close()
destination := filepath.Join(dir, name)
fd, err := os.Create(destination)
if err != nil {
return errors.Wrapf(err, "failed to create secrets/%s", name)
}
defer fd.Close()
if _, err := io.Copy(fd, original); err != nil {
return errors.Wrapf(err, "failed to copy certificate secrets/%s", name)
}
if err := fd.Sync(); err != nil {
return errors.Wrapf(err, "failed to write secrets/%s", name)
}
return nil
}
// tlsEnv creates a set of additional of environment variables to be used when launching
// the connect native task. This will enable the task to communicate with Consul
// if Consul has transport security turned on.
//
// We do NOT set CONSUL_HTTP_TOKEN from the nomad agent's consul config, as that
// is a separate security concern addressed by the service identity hook.
func (h *connectNativeHook) tlsEnv(env map[string]string) map[string]string {
m := make(map[string]string)
if _, exists := env["CONSUL_CACERT"]; !exists && h.consulConfig.CAFile != "" {
m["CONSUL_CACERT"] = filepath.Join("/secrets", secretCAFilename)
}
if _, exists := env["CONSUL_CLIENT_CERT"]; !exists && h.consulConfig.CertFile != "" {
m["CONSUL_CLIENT_CERT"] = filepath.Join("/secrets", secretCertfileFilename)
}
if _, exists := env["CONSUL_CLIENT_KEY"]; !exists && h.consulConfig.KeyFile != "" {
m["CONSUL_CLIENT_KEY"] = filepath.Join("/secrets", secretKeyfileFilename)
}
if _, exists := env["CONSUL_HTTP_SSL"]; !exists {
if v := h.consulConfig.SSL; v != "" {
m["CONSUL_HTTP_SSL"] = v
}
}
if _, exists := env["CONSUL_HTTP_SSL_VERIFY"]; !exists {
if v := h.consulConfig.VerifySSL; v != "" {
m["CONSUL_HTTP_SSL_VERIFY"] = v
}
}
return m
}
// maybeSetSITokenEnv will set the CONSUL_HTTP_TOKEN environment variable in
// the given env map, if the token is found to exist in the task's secrets
// directory AND the CONSUL_HTTP_TOKEN environment variable is not already set.
//
// Following the pattern of the envoy_bootstrap_hook, the Consul Service Identity
// ACL Token is generated prior to this hook, if Consul ACLs are enabled. This is
// done in the sids_hook, which places the token at secrets/si_token in the task
// workspace. The content of that file is the SI token specific to this task
// instance.
func (h *connectNativeHook) maybeSetSITokenEnv(dir, task string, env map[string]string) error {
if _, exists := env["CONSUL_HTTP_TOKEN"]; exists {
// Consul token was already set - typically by using the Vault integration
// and a template stanza to set the environment. Ignore the SI token as
// the configured token takes precedence.
return nil
}
token, err := ioutil.ReadFile(filepath.Join(dir, sidsTokenFile))
if err != nil {
if !os.IsNotExist(err) {
return errors.Wrapf(err, "failed to load SI token for native task %s", task)
}
h.logger.Trace("no SI token to load for native task", "task", task)
return nil // token file DNE; acls not enabled
}
h.logger.Trace("recovered pre-existing SI token for native task", "task", task)
env["CONSUL_HTTP_TOKEN"] = string(token)
return nil
}

View File

@@ -0,0 +1,545 @@
package taskrunner
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"testing"
consulapi "github.com/hashicorp/consul/api"
consultest "github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/allocrunner/interfaces"
"github.com/hashicorp/nomad/client/taskenv"
"github.com/hashicorp/nomad/client/testutil"
agentconsul "github.com/hashicorp/nomad/command/agent/consul"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/helper/testlog"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
"github.com/stretchr/testify/require"
)
func getTestConsul(t *testing.T) *consultest.TestServer {
testConsul, err := consultest.NewTestServerConfig(func(c *consultest.TestServerConfig) {
if !testing.Verbose() { // disable consul logging if -v not set
c.Stdout = ioutil.Discard
c.Stderr = ioutil.Discard
}
})
require.NoError(t, err, "failed to start test consul server")
return testConsul
}
func TestConnectNativeHook_Name(t *testing.T) {
t.Parallel()
name := new(connectNativeHook).Name()
require.Equal(t, "connect_native", name)
}
func setupCertDirs(t *testing.T) (string, string) {
fd, err := ioutil.TempFile("", "connect_native_testcert")
require.NoError(t, err)
_, err = fd.WriteString("ABCDEF")
require.NoError(t, err)
err = fd.Close()
require.NoError(t, err)
d, err := ioutil.TempDir("", "connect_native_testsecrets")
require.NoError(t, err)
return fd.Name(), d
}
func cleanupCertDirs(t *testing.T, original, secrets string) {
err := os.Remove(original)
require.NoError(t, err)
err = os.RemoveAll(secrets)
require.NoError(t, err)
}
func TestConnectNativeHook_copyCertificate(t *testing.T) {
t.Parallel()
f, d := setupCertDirs(t)
defer cleanupCertDirs(t, f, d)
t.Run("no source", func(t *testing.T) {
err := new(connectNativeHook).copyCertificate("", d, "out.pem")
require.NoError(t, err)
})
t.Run("normal", func(t *testing.T) {
err := new(connectNativeHook).copyCertificate(f, d, "out.pem")
require.NoError(t, err)
b, err := ioutil.ReadFile(filepath.Join(d, "out.pem"))
require.NoError(t, err)
require.Equal(t, "ABCDEF", string(b))
})
}
func TestConnectNativeHook_copyCertificates(t *testing.T) {
t.Parallel()
f, d := setupCertDirs(t)
defer cleanupCertDirs(t, f, d)
t.Run("normal", func(t *testing.T) {
err := new(connectNativeHook).copyCertificates(consulTransportConfig{
CAFile: f,
CertFile: f,
KeyFile: f,
}, d)
require.NoError(t, err)
ls, err := ioutil.ReadDir(d)
require.NoError(t, err)
require.Equal(t, 3, len(ls))
})
t.Run("no source", func(t *testing.T) {
err := new(connectNativeHook).copyCertificates(consulTransportConfig{
CAFile: "/does/not/exist.pem",
CertFile: "/does/not/exist.pem",
KeyFile: "/does/not/exist.pem",
}, d)
require.EqualError(t, err, "failed to open consul TLS certificate: open /does/not/exist.pem: no such file or directory")
})
}
func TestConnectNativeHook_tlsEnv(t *testing.T) {
t.Parallel()
// the hook config comes from client config
emptyHook := new(connectNativeHook)
fullHook := &connectNativeHook{
consulConfig: consulTransportConfig{
Auth: "user:password",
SSL: "true",
VerifySSL: "true",
CAFile: "/not/real/ca.pem",
CertFile: "/not/real/cert.pem",
KeyFile: "/not/real/key.pem",
},
}
// existing config from task env stanza
taskEnv := map[string]string{
"CONSUL_CACERT": "fakeCA.pem",
"CONSUL_CLIENT_CERT": "fakeCert.pem",
"CONSUL_CLIENT_KEY": "fakeKey.pem",
"CONSUL_HTTP_AUTH": "foo:bar",
"CONSUL_HTTP_SSL": "false",
"CONSUL_HTTP_SSL_VERIFY": "false",
}
t.Run("empty hook and empty task", func(t *testing.T) {
result := emptyHook.tlsEnv(nil)
require.Empty(t, result)
})
t.Run("empty hook and non-empty task", func(t *testing.T) {
result := emptyHook.tlsEnv(taskEnv)
require.Empty(t, result) // tlsEnv only overrides; task env is actually set elsewhere
})
t.Run("non-empty hook and empty task", func(t *testing.T) {
result := fullHook.tlsEnv(nil)
require.Equal(t, map[string]string{
// ca files are specifically copied into FS namespace
"CONSUL_CACERT": "/secrets/consul_ca_file.pem",
"CONSUL_CLIENT_CERT": "/secrets/consul_cert_file.pem",
"CONSUL_CLIENT_KEY": "/secrets/consul_key_file.pem",
"CONSUL_HTTP_SSL": "true",
"CONSUL_HTTP_SSL_VERIFY": "true",
}, result)
})
t.Run("non-empty hook and non-empty task", func(t *testing.T) {
result := fullHook.tlsEnv(taskEnv) // task env takes precedence, nothing gets set here
require.Empty(t, result)
})
}
func TestTaskRunner_ConnectNativeHook_Noop(t *testing.T) {
t.Parallel()
logger := testlog.HCLogger(t)
allocDir, cleanup := allocdir.TestAllocDir(t, logger, "ConnectNative")
defer cleanup()
alloc := mock.Alloc()
task := alloc.Job.LookupTaskGroup(alloc.TaskGroup).Tasks[0]
// run the connect native hook. use invalid consul address as it should not get hit
h := newConnectNativeHook(newConnectNativeHookConfig(alloc, &config.ConsulConfig{
Addr: "http://127.0.0.2:1",
}, logger))
request := &interfaces.TaskPrestartRequest{
Task: task,
TaskDir: allocDir.NewTaskDir(task.Name),
}
require.NoError(t, request.TaskDir.Build(false, nil))
response := new(interfaces.TaskPrestartResponse)
// Run the hook
require.NoError(t, h.Prestart(context.Background(), request, response))
// Assert the hook is Done
require.True(t, response.Done)
// Assert secrets dir is empty (no TLS config set)
checkFilesInDir(t, request.TaskDir.SecretsDir,
nil,
[]string{sidsTokenFile, secretCAFilename, secretCertfileFilename, secretKeyfileFilename},
)
}
func TestTaskRunner_ConnectNativeHook_Ok(t *testing.T) {
t.Parallel()
testutil.RequireConsul(t)
testConsul := getTestConsul(t)
defer testConsul.Stop()
alloc := mock.Alloc()
alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{{Mode: "host", IP: "1.1.1.1"}}
tg := alloc.Job.TaskGroups[0]
tg.Services = []*structs.Service{{
Name: "cn-service",
TaskName: tg.Tasks[0].Name,
Connect: &structs.ConsulConnect{
Native: true,
}},
}
tg.Tasks[0].Kind = structs.NewTaskKind("connect-native", "cn-service")
logger := testlog.HCLogger(t)
allocDir, cleanup := allocdir.TestAllocDir(t, logger, "ConnectNative")
defer cleanup()
// register group services
consulConfig := consulapi.DefaultConfig()
consulConfig.Address = testConsul.HTTPAddr
consulAPIClient, err := consulapi.NewClient(consulConfig)
require.NoError(t, err)
consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), logger, true)
go consulClient.Run()
defer consulClient.Shutdown()
require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter())))
// Run Connect Native hook
h := newConnectNativeHook(newConnectNativeHookConfig(alloc, &config.ConsulConfig{
Addr: consulConfig.Address,
}, logger))
request := &interfaces.TaskPrestartRequest{
Task: tg.Tasks[0],
TaskDir: allocDir.NewTaskDir(tg.Tasks[0].Name),
TaskEnv: taskenv.NewEmptyTaskEnv(),
}
require.NoError(t, request.TaskDir.Build(false, nil))
response := new(interfaces.TaskPrestartResponse)
// Run the Connect Native hook
require.NoError(t, h.Prestart(context.Background(), request, response))
// Assert the hook is Done
require.True(t, response.Done)
// Assert no environment variables configured to be set
require.Empty(t, response.Env)
// Assert no secrets were written
checkFilesInDir(t, request.TaskDir.SecretsDir,
nil,
[]string{sidsTokenFile, secretCAFilename, secretCertfileFilename, secretKeyfileFilename},
)
}
func TestTaskRunner_ConnectNativeHook_with_SI_token(t *testing.T) {
t.Parallel()
testutil.RequireConsul(t)
testConsul := getTestConsul(t)
defer testConsul.Stop()
alloc := mock.Alloc()
alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{{Mode: "host", IP: "1.1.1.1"}}
tg := alloc.Job.TaskGroups[0]
tg.Services = []*structs.Service{{
Name: "cn-service",
TaskName: tg.Tasks[0].Name,
Connect: &structs.ConsulConnect{
Native: true,
}},
}
tg.Tasks[0].Kind = structs.NewTaskKind("connect-native", "cn-service")
logger := testlog.HCLogger(t)
allocDir, cleanup := allocdir.TestAllocDir(t, logger, "ConnectNative")
defer cleanup()
// register group services
consulConfig := consulapi.DefaultConfig()
consulConfig.Address = testConsul.HTTPAddr
consulAPIClient, err := consulapi.NewClient(consulConfig)
require.NoError(t, err)
consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), logger, true)
go consulClient.Run()
defer consulClient.Shutdown()
require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter())))
// Run Connect Native hook
h := newConnectNativeHook(newConnectNativeHookConfig(alloc, &config.ConsulConfig{
Addr: consulConfig.Address,
}, logger))
request := &interfaces.TaskPrestartRequest{
Task: tg.Tasks[0],
TaskDir: allocDir.NewTaskDir(tg.Tasks[0].Name),
TaskEnv: taskenv.NewEmptyTaskEnv(),
}
require.NoError(t, request.TaskDir.Build(false, nil))
// Insert service identity token in the secrets directory
token := uuid.Generate()
siTokenFile := filepath.Join(request.TaskDir.SecretsDir, sidsTokenFile)
err = ioutil.WriteFile(siTokenFile, []byte(token), 0440)
require.NoError(t, err)
response := new(interfaces.TaskPrestartResponse)
response.Env = make(map[string]string)
// Run the Connect Native hook
require.NoError(t, h.Prestart(context.Background(), request, response))
// Assert the hook is Done
require.True(t, response.Done)
// Assert environment variable for token is set
require.NotEmpty(t, response.Env)
require.Equal(t, token, response.Env["CONSUL_HTTP_TOKEN"])
// Assert no additional secrets were written
checkFilesInDir(t, request.TaskDir.SecretsDir,
[]string{sidsTokenFile},
[]string{secretCAFilename, secretCertfileFilename, secretKeyfileFilename},
)
}
func TestTaskRunner_ConnectNativeHook_shareTLS(t *testing.T) {
t.Parallel()
testutil.RequireConsul(t)
try := func(t *testing.T, shareSSL *bool) {
fakeCert, fakeCertDir := setupCertDirs(t)
defer cleanupCertDirs(t, fakeCert, fakeCertDir)
testConsul := getTestConsul(t)
defer testConsul.Stop()
alloc := mock.Alloc()
alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{{Mode: "host", IP: "1.1.1.1"}}
tg := alloc.Job.TaskGroups[0]
tg.Services = []*structs.Service{{
Name: "cn-service",
TaskName: tg.Tasks[0].Name,
Connect: &structs.ConsulConnect{
Native: true,
}},
}
tg.Tasks[0].Kind = structs.NewTaskKind("connect-native", "cn-service")
logger := testlog.HCLogger(t)
allocDir, cleanup := allocdir.TestAllocDir(t, logger, "ConnectNative")
defer cleanup()
// register group services
consulConfig := consulapi.DefaultConfig()
consulConfig.Address = testConsul.HTTPAddr
consulAPIClient, err := consulapi.NewClient(consulConfig)
require.NoError(t, err)
consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), logger, true)
go consulClient.Run()
defer consulClient.Shutdown()
require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter())))
// Run Connect Native hook
h := newConnectNativeHook(newConnectNativeHookConfig(alloc, &config.ConsulConfig{
Addr: consulConfig.Address,
// TLS config consumed by native application
ShareSSL: shareSSL,
EnableSSL: helper.BoolToPtr(true),
VerifySSL: helper.BoolToPtr(true),
CAFile: fakeCert,
CertFile: fakeCert,
KeyFile: fakeCert,
Auth: "user:password",
Token: uuid.Generate(),
}, logger))
request := &interfaces.TaskPrestartRequest{
Task: tg.Tasks[0],
TaskDir: allocDir.NewTaskDir(tg.Tasks[0].Name),
TaskEnv: taskenv.NewEmptyTaskEnv(), // nothing set in env stanza
}
require.NoError(t, request.TaskDir.Build(false, nil))
response := new(interfaces.TaskPrestartResponse)
response.Env = make(map[string]string)
// Run the Connect Native hook
require.NoError(t, h.Prestart(context.Background(), request, response))
// Assert the hook is Done
require.True(t, response.Done)
// Assert environment variable for token is set
require.NotEmpty(t, response.Env)
require.Equal(t, map[string]string{
"CONSUL_CACERT": "/secrets/consul_ca_file.pem",
"CONSUL_CLIENT_CERT": "/secrets/consul_cert_file.pem",
"CONSUL_CLIENT_KEY": "/secrets/consul_key_file.pem",
"CONSUL_HTTP_SSL": "true",
"CONSUL_HTTP_SSL_VERIFY": "true",
}, response.Env)
require.NotContains(t, response.Env, "CONSUL_HTTP_AUTH") // explicitly not shared
require.NotContains(t, response.Env, "CONSUL_HTTP_TOKEN") // explicitly not shared
// Assert 3 pem files were written
checkFilesInDir(t, request.TaskDir.SecretsDir,
[]string{secretCAFilename, secretCertfileFilename, secretKeyfileFilename},
[]string{sidsTokenFile},
)
}
// The default behavior is that share_ssl is true (similar to allow_unauthenticated)
// so make sure an unset value turns the feature on.
t.Run("share_ssl is true", func(t *testing.T) {
try(t, helper.BoolToPtr(true))
})
t.Run("share_ssl is nil", func(t *testing.T) {
try(t, nil)
})
}
func checkFilesInDir(t *testing.T, dir string, includes, excludes []string) {
ls, err := ioutil.ReadDir(dir)
require.NoError(t, err)
var present []string
for _, fInfo := range ls {
present = append(present, fInfo.Name())
}
for _, filename := range includes {
require.Contains(t, present, filename)
}
for _, filename := range excludes {
require.NotContains(t, present, filename)
}
}
func TestTaskRunner_ConnectNativeHook_shareTLS_override(t *testing.T) {
t.Parallel()
testutil.RequireConsul(t)
fakeCert, fakeCertDir := setupCertDirs(t)
defer cleanupCertDirs(t, fakeCert, fakeCertDir)
testConsul := getTestConsul(t)
defer testConsul.Stop()
alloc := mock.Alloc()
alloc.AllocatedResources.Shared.Networks = []*structs.NetworkResource{{Mode: "host", IP: "1.1.1.1"}}
tg := alloc.Job.TaskGroups[0]
tg.Services = []*structs.Service{{
Name: "cn-service",
TaskName: tg.Tasks[0].Name,
Connect: &structs.ConsulConnect{
Native: true,
}},
}
tg.Tasks[0].Kind = structs.NewTaskKind("connect-native", "cn-service")
logger := testlog.HCLogger(t)
allocDir, cleanup := allocdir.TestAllocDir(t, logger, "ConnectNative")
defer cleanup()
// register group services
consulConfig := consulapi.DefaultConfig()
consulConfig.Address = testConsul.HTTPAddr
consulAPIClient, err := consulapi.NewClient(consulConfig)
require.NoError(t, err)
consulClient := agentconsul.NewServiceClient(consulAPIClient.Agent(), logger, true)
go consulClient.Run()
defer consulClient.Shutdown()
require.NoError(t, consulClient.RegisterWorkload(agentconsul.BuildAllocServices(mock.Node(), alloc, agentconsul.NoopRestarter())))
// Run Connect Native hook
h := newConnectNativeHook(newConnectNativeHookConfig(alloc, &config.ConsulConfig{
Addr: consulConfig.Address,
// TLS config consumed by native application
ShareSSL: helper.BoolToPtr(true),
EnableSSL: helper.BoolToPtr(true),
VerifySSL: helper.BoolToPtr(true),
CAFile: fakeCert,
CertFile: fakeCert,
KeyFile: fakeCert,
Auth: "user:password",
}, logger))
taskEnv := taskenv.NewEmptyTaskEnv()
taskEnv.EnvMap = map[string]string{
"CONSUL_CACERT": "/foo/ca.pem",
"CONSUL_CLIENT_CERT": "/foo/cert.pem",
"CONSUL_CLIENT_KEY": "/foo/key.pem",
"CONSUL_HTTP_AUTH": "foo:bar",
"CONSUL_HTTP_SSL_VERIFY": "false",
// CONSUL_HTTP_SSL (check the default value is assumed from client config)
}
request := &interfaces.TaskPrestartRequest{
Task: tg.Tasks[0],
TaskDir: allocDir.NewTaskDir(tg.Tasks[0].Name),
TaskEnv: taskEnv, // env stanza is configured w/ non-default tls configs
}
require.NoError(t, request.TaskDir.Build(false, nil))
response := new(interfaces.TaskPrestartResponse)
response.Env = make(map[string]string)
// Run the Connect Native hook
require.NoError(t, h.Prestart(context.Background(), request, response))
// Assert the hook is Done
require.True(t, response.Done)
// Assert environment variable for CONSUL_HTTP_SSL is set, because it was
// the only one not overridden by task env stanza config
require.NotEmpty(t, response.Env)
require.Equal(t, map[string]string{
"CONSUL_HTTP_SSL": "true",
}, response.Env)
// Assert 3 pem files were written (even though they will be ignored)
// as this is gated by share_tls, not the presense of ca environment variables.
checkFilesInDir(t, request.TaskDir.SecretsDir,
[]string{secretCAFilename, secretCertfileFilename, secretKeyfileFilename},
[]string{sidsTokenFile},
)
}

View File

@@ -22,7 +22,7 @@ import (
const envoyBootstrapHookName = "envoy_bootstrap"
type envoyBootstrapConsulConfig struct {
type consulTransportConfig struct {
HTTPAddr string // required
Auth string // optional, env CONSUL_HTTP_AUTH
SSL string // optional, env CONSUL_HTTP_SSL
@@ -33,8 +33,20 @@ type envoyBootstrapConsulConfig struct {
// CAPath (dir) not supported by Nomad's config object
}
func newConsulTransportConfig(consul *config.ConsulConfig) consulTransportConfig {
return consulTransportConfig{
HTTPAddr: consul.Addr,
Auth: consul.Auth,
SSL: decodeTriState(consul.EnableSSL),
VerifySSL: decodeTriState(consul.VerifySSL),
CAFile: consul.CAFile,
CertFile: consul.CertFile,
KeyFile: consul.KeyFile,
}
}
type envoyBootstrapHookConfig struct {
consul envoyBootstrapConsulConfig
consul consulTransportConfig
alloc *structs.Allocation
logger hclog.Logger
}
@@ -54,15 +66,7 @@ func newEnvoyBootstrapHookConfig(alloc *structs.Allocation, consul *config.Consu
return &envoyBootstrapHookConfig{
alloc: alloc,
logger: logger,
consul: envoyBootstrapConsulConfig{
HTTPAddr: consul.Addr,
Auth: consul.Auth,
SSL: decodeTriState(consul.EnableSSL),
VerifySSL: decodeTriState(consul.VerifySSL),
CAFile: consul.CAFile,
CertFile: consul.CertFile,
KeyFile: consul.KeyFile,
},
consul: newConsulTransportConfig(consul),
}
}
@@ -81,7 +85,7 @@ type envoyBootstrapHook struct {
// the bootstrap.json config. Runtime Envoy configuration is done via
// Consul's gRPC endpoint. There are many security parameters to configure
// before contacting Consul.
consulConfig envoyBootstrapConsulConfig
consulConfig consulTransportConfig
// logger is used to log things
logger hclog.Logger
@@ -269,7 +273,7 @@ func (h *envoyBootstrapHook) execute(cmd *exec.Cmd) (string, error) {
// along to the exec invocation of consul which will then generate the bootstrap
// configuration file for envoy.
type envoyBootstrapArgs struct {
consulConfig envoyBootstrapConsulConfig
consulConfig consulTransportConfig
sidecarFor string
grpcAddr string
envoyAdminBind string

View File

@@ -13,7 +13,6 @@ import (
"testing"
consulapi "github.com/hashicorp/consul/api"
consultest "github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/allocrunner/interfaces"
"github.com/hashicorp/nomad/client/taskenv"
@@ -93,11 +92,11 @@ func TestEnvoyBootstrapHook_decodeTriState(t *testing.T) {
}
var (
consulPlainConfig = envoyBootstrapConsulConfig{
consulPlainConfig = consulTransportConfig{
HTTPAddr: "2.2.2.2",
}
consulTLSConfig = envoyBootstrapConsulConfig{
consulTLSConfig = consulTransportConfig{
HTTPAddr: "2.2.2.2", // arg
Auth: "user:password", // env
SSL: "true", // env
@@ -220,16 +219,7 @@ func TestEnvoyBootstrapHook_with_SI_token(t *testing.T) {
t.Parallel()
testutil.RequireConsul(t)
testconsul, err := consultest.NewTestServerConfig(func(c *consultest.TestServerConfig) {
// If -v wasn't specified squelch consul logging
if !testing.Verbose() {
c.Stdout = ioutil.Discard
c.Stderr = ioutil.Discard
}
})
if err != nil {
t.Fatalf("error starting test consul server: %v", err)
}
testconsul := getTestConsul(t)
defer testconsul.Stop()
alloc := mock.Alloc()
@@ -328,16 +318,7 @@ func TestTaskRunner_EnvoyBootstrapHook_Ok(t *testing.T) {
t.Parallel()
testutil.RequireConsul(t)
testconsul, err := consultest.NewTestServerConfig(func(c *consultest.TestServerConfig) {
// If -v wasn't specified squelch consul logging
if !testing.Verbose() {
c.Stdout = ioutil.Discard
c.Stderr = ioutil.Discard
}
})
if err != nil {
t.Fatalf("error starting test consul server: %v", err)
}
testconsul := getTestConsul(t)
defer testconsul.Stop()
alloc := mock.Alloc()
@@ -470,16 +451,7 @@ func TestTaskRunner_EnvoyBootstrapHook_RecoverableError(t *testing.T) {
t.Parallel()
testutil.RequireConsul(t)
testconsul, err := consultest.NewTestServerConfig(func(c *consultest.TestServerConfig) {
// If -v wasn't specified squelch consul logging
if !testing.Verbose() {
c.Stdout = ioutil.Discard
c.Stderr = ioutil.Discard
}
})
if err != nil {
t.Fatalf("error starting test consul server: %v", err)
}
testconsul := getTestConsul(t)
defer testconsul.Stop()
alloc := mock.Alloc()
@@ -534,7 +506,7 @@ func TestTaskRunner_EnvoyBootstrapHook_RecoverableError(t *testing.T) {
resp := &interfaces.TaskPrestartResponse{}
// Run the hook
err = h.Prestart(context.Background(), req, resp)
err := h.Prestart(context.Background(), req, resp)
require.EqualError(t, err, "error creating bootstrap configuration for Connect proxy sidecar: exit status 1")
require.True(t, structs.IsRecoverable(err))

View File

@@ -127,10 +127,15 @@ func (tr *TaskRunner) initHooks() {
}))
}
// envoy bootstrap must execute after sidsHook maybe sets SI token
tr.runnerHooks = append(tr.runnerHooks, newEnvoyBootstrapHook(
newEnvoyBootstrapHookConfig(alloc, tr.clientConfig.ConsulConfig, hookLogger),
))
if task.Kind.IsConnectProxy() {
tr.runnerHooks = append(tr.runnerHooks, newEnvoyBootstrapHook(
newEnvoyBootstrapHookConfig(alloc, tr.clientConfig.ConsulConfig, hookLogger),
))
} else if task.Kind.IsConnectNative() {
tr.runnerHooks = append(tr.runnerHooks, newConnectNativeHook(
newConnectNativeHookConfig(alloc, tr.clientConfig.ConsulConfig, hookLogger),
))
}
}
// If there are any script checks, add the hook

View File

@@ -17,7 +17,7 @@ func newConnect(serviceName string, nc *structs.ConsulConnect, networks structs.
return nil, nil
}
if nc.Native {
if nc.IsNative() {
return &api.AgentServiceConnect{Native: true}, nil
}

View File

@@ -1177,6 +1177,7 @@ func ApiServicesToStructs(in []*api.Service) []*structs.Service {
out[i] = &structs.Service{
Name: s.Name,
PortLabel: s.PortLabel,
TaskName: s.TaskName,
Tags: s.Tags,
CanaryTags: s.CanaryTags,
EnableTagOverride: s.EnableTagOverride,

View File

@@ -2763,7 +2763,7 @@ func TestConversion_apiConnectSidecarServiceToStructs(t *testing.T) {
}))
}
func TestConversion_ApiConsulConnectToStructs(t *testing.T) {
func TestConversion_ApiConsulConnectToStructs_legacy(t *testing.T) {
t.Parallel()
require.Nil(t, ApiConsulConnectToStructs(nil))
require.Equal(t, &structs.ConsulConnect{
@@ -2776,3 +2776,13 @@ func TestConversion_ApiConsulConnectToStructs(t *testing.T) {
SidecarTask: &api.SidecarTask{Name: "task"},
}))
}
func TestConversion_ApiConsulConnectToStructs_native(t *testing.T) {
t.Parallel()
require.Nil(t, ApiConsulConnectToStructs(nil))
require.Equal(t, &structs.ConsulConnect{
Native: true,
}, ApiConsulConnectToStructs(&api.ConsulConnect{
Native: true,
}))
}

View File

@@ -47,6 +47,7 @@ func parseService(o *ast.ObjectItem) (*api.Service, error) {
"address_mode",
"check_restart",
"connect",
"task",
"meta",
"canary_meta",
}

View File

@@ -1276,6 +1276,24 @@ func TestParse(t *testing.T) {
},
false,
},
{
"tg-service-connect-native.hcl",
&api.Job{
ID: helper.StringToPtr("connect_native_service"),
Name: helper.StringToPtr("connect_native_service"),
TaskGroups: []*api.TaskGroup{{
Name: helper.StringToPtr("group"),
Services: []*api.Service{{
Name: "example",
TaskName: "task1",
Connect: &api.ConsulConnect{
Native: true,
},
}},
}},
},
false,
},
{
"tg-service-enable-tag-override.hcl",
&api.Job{

View File

@@ -0,0 +1,12 @@
job "connect_native_service" {
group "group" {
service {
name = "example"
task = "task1"
connect {
native = true
}
}
}
}

View File

@@ -97,6 +97,15 @@ func isSidecarForService(t *structs.Task, svc string) bool {
return t.Kind == structs.TaskKind(fmt.Sprintf("%s:%s", structs.ConnectProxyPrefix, svc))
}
func getNamedTaskForNativeService(tg *structs.TaskGroup, taskName string) *structs.Task {
for _, t := range tg.Tasks {
if t.Name == taskName {
return t
}
}
return nil
}
// probably need to hack this up to look for checks on the service, and if they
// qualify, configure a port for envoy to use to expose their paths.
func groupConnectHook(job *structs.Job, g *structs.TaskGroup) error {
@@ -145,10 +154,15 @@ func groupConnectHook(job *structs.Job, g *structs.TaskGroup) error {
// create a port for the sidecar task's proxy port
makePort(fmt.Sprintf("%s-%s", structs.ConnectProxyPrefix, service.Name))
// todo(shoenig) magic port for 'expose.checks'
} else if service.Connect.IsNative() {
nativeTaskName := service.TaskName
if t := getNamedTaskForNativeService(g, nativeTaskName); t != nil {
t.Kind = structs.NewTaskKind(structs.ConnectNativePrefix, service.Name)
} else {
return fmt.Errorf("native task %s named by %s->%s does not exist", nativeTaskName, g.Name, service.Name)
}
}
}
return nil
}
@@ -180,18 +194,42 @@ func newConnectTask(serviceName string) *structs.Task {
func groupConnectValidate(g *structs.TaskGroup) (warnings []error, err error) {
for _, s := range g.Services {
if s.Connect.HasSidecar() {
if n := len(g.Networks); n != 1 {
return nil, fmt.Errorf("Consul Connect sidecars require exactly 1 network, found %d in group %q", n, g.Name)
if err := groupConnectSidecarValidate(g); err != nil {
return nil, err
}
if g.Networks[0].Mode != "bridge" {
return nil, fmt.Errorf("Consul Connect sidecar requires bridge network, found %q in group %q", g.Networks[0].Mode, g.Name)
} else if s.Connect.IsNative() {
if err := groupConnectNativeValidate(g, s); err != nil {
return nil, err
}
// Stopping loop, only need to do the validation once
break
}
}
return nil, nil
}
func groupConnectSidecarValidate(g *structs.TaskGroup) error {
if n := len(g.Networks); n != 1 {
return fmt.Errorf("Consul Connect sidecars require exactly 1 network, found %d in group %q", n, g.Name)
}
if g.Networks[0].Mode != "bridge" {
return fmt.Errorf("Consul Connect sidecar requires bridge network, found %q in group %q", g.Networks[0].Mode, g.Name)
}
return nil
}
func groupConnectNativeValidate(g *structs.TaskGroup, s *structs.Service) error {
// note that network mode is not enforced for connect native services
// a native service must have the task identified in the service definition.
if len(s.TaskName) == 0 {
return fmt.Errorf("Consul Connect Native service %q requires task name", s.Name)
}
// also make sure that task actually exists
for _, task := range g.Tasks {
if s.TaskName == task.Name {
return nil
}
}
return fmt.Errorf("Consul Connect Native service %q requires undefined task %q in group %q", s.Name, s.TaskName, g.Name)
}

View File

@@ -10,7 +10,9 @@ import (
"github.com/stretchr/testify/require"
)
func Test_isSidecarForService(t *testing.T) {
func TestJobEndpointConnect_isSidecarForService(t *testing.T) {
t.Parallel()
cases := []struct {
t *structs.Task // task
s string // service
@@ -49,7 +51,9 @@ func Test_isSidecarForService(t *testing.T) {
}
}
func Test_groupConnectHook(t *testing.T) {
func TestJobEndpointConnect_groupConnectHook(t *testing.T) {
t.Parallel()
// Test that connect-proxy task is inserted for backend service
job := mock.Job()
job.TaskGroups[0] = &structs.TaskGroup{
@@ -110,7 +114,7 @@ func Test_groupConnectHook(t *testing.T) {
// the service name is interpolated *before the task is created.
//
// See https://github.com/hashicorp/nomad/issues/6853
func TestJobEndpoint_ConnectInterpolation(t *testing.T) {
func TestJobEndpointConnect_ConnectInterpolation(t *testing.T) {
t.Parallel()
server := &Server{logger: testlog.HCLogger(t)}
@@ -125,3 +129,60 @@ func TestJobEndpoint_ConnectInterpolation(t *testing.T) {
require.Len(t, j.TaskGroups[0].Tasks, 2)
require.Equal(t, "connect-proxy-my-job-api", j.TaskGroups[0].Tasks[1].Name)
}
func TestJobEndpointConnect_groupConnectSidecarValidate(t *testing.T) {
t.Run("sidecar 0 networks", func(t *testing.T) {
require.EqualError(t, groupConnectSidecarValidate(&structs.TaskGroup{
Name: "g1",
Networks: nil,
}), `Consul Connect sidecars require exactly 1 network, found 0 in group "g1"`)
})
t.Run("sidecar non bridge", func(t *testing.T) {
require.EqualError(t, groupConnectSidecarValidate(&structs.TaskGroup{
Name: "g2",
Networks: structs.Networks{{
Mode: "host",
}},
}), `Consul Connect sidecar requires bridge network, found "host" in group "g2"`)
})
t.Run("sidecar okay", func(t *testing.T) {
require.NoError(t, groupConnectSidecarValidate(&structs.TaskGroup{
Name: "g3",
Networks: structs.Networks{{
Mode: "bridge",
}},
}))
})
}
func TestJobEndpointConnect_groupConnectNativeValidate(t *testing.T) {
t.Run("no task in service", func(t *testing.T) {
require.EqualError(t, groupConnectNativeValidate(&structs.TaskGroup{
Name: "g1",
}, &structs.Service{
Name: "s1",
TaskName: "",
}), `Consul Connect Native service "s1" requires task name`)
})
t.Run("no task for service", func(t *testing.T) {
require.EqualError(t, groupConnectNativeValidate(&structs.TaskGroup{
Name: "g2",
}, &structs.Service{
Name: "s2",
TaskName: "t1",
}), `Consul Connect Native service "s2" requires undefined task "t1" in group "g2"`)
})
t.Run("native okay", func(t *testing.T) {
require.NoError(t, groupConnectNativeValidate(&structs.TaskGroup{
Name: "g2",
Tasks: []*structs.Task{{Name: "t0"}, {Name: "t1"}, {Name: "t3"}},
}, &structs.Service{
Name: "s2",
TaskName: "t1",
}))
})
}

View File

@@ -1889,8 +1889,7 @@ func taskUsesConnect(task *structs.Task) bool {
// not even in the task group
return false
}
return task.Kind.IsConnectProxy() || task.Kind.IsConnectNative()
return task.UsesConnect()
}
func (n *Node) EmitEvents(args *structs.EmitNodeEventsRequest, reply *structs.EmitNodeEventsResponse) error {

View File

@@ -85,6 +85,12 @@ type ConsulConfig struct {
// Uses Consul's default and env var.
EnableSSL *bool `hcl:"ssl"`
// ShareSSL enables Consul Connect Native applications to use the TLS
// configuration of the Nomad Client for establishing connections to Consul.
//
// Does not include sharing of ACL tokens.
ShareSSL *bool `hcl:"share_ssl"`
// VerifySSL enables or disables SSL verification when the transport scheme
// for the consul api client is https
//
@@ -200,6 +206,9 @@ func (c *ConsulConfig) Merge(b *ConsulConfig) *ConsulConfig {
if b.VerifySSL != nil {
result.VerifySSL = helper.BoolToPtr(*b.VerifySSL)
}
if b.ShareSSL != nil {
result.ShareSSL = helper.BoolToPtr(*b.ShareSSL)
}
if b.CAFile != "" {
result.CAFile = b.CAFile
}
@@ -301,6 +310,9 @@ func (c *ConsulConfig) Copy() *ConsulConfig {
if nc.VerifySSL != nil {
nc.VerifySSL = helper.BoolToPtr(*nc.VerifySSL)
}
if nc.ShareSSL != nil {
nc.ShareSSL = helper.BoolToPtr(*nc.ShareSSL)
}
if nc.ServerAutoJoin != nil {
nc.ServerAutoJoin = helper.BoolToPtr(*nc.ServerAutoJoin)
}

View File

@@ -2558,6 +2558,7 @@ func TestTaskGroupDiff(t *testing.T) {
Services: []*Service{
{
Name: "foo",
TaskName: "task1",
EnableTagOverride: false,
Checks: []*ServiceCheck{
{
@@ -2573,6 +2574,7 @@ func TestTaskGroupDiff(t *testing.T) {
},
},
Connect: &ConsulConnect{
Native: false,
SidecarTask: &SidecarTask{
Name: "sidecar",
Driver: "docker",
@@ -2592,6 +2594,7 @@ func TestTaskGroupDiff(t *testing.T) {
Services: []*Service{
{
Name: "foo",
TaskName: "task2",
EnableTagOverride: true,
Checks: []*ServiceCheck{
{
@@ -2609,6 +2612,7 @@ func TestTaskGroupDiff(t *testing.T) {
},
},
Connect: &ConsulConnect{
Native: true,
SidecarService: &ConsulSidecarService{
Port: "http",
Proxy: &ConsulProxy{
@@ -2661,6 +2665,12 @@ func TestTaskGroupDiff(t *testing.T) {
Old: "",
New: "",
},
{
Type: DiffTypeEdited,
Name: "TaskName",
Old: "task1",
New: "task2",
},
},
Objects: []*ObjectDiff{
{
@@ -2786,10 +2796,10 @@ func TestTaskGroupDiff(t *testing.T) {
Name: "ConsulConnect",
Fields: []*FieldDiff{
{
Type: DiffTypeNone,
Type: DiffTypeEdited,
Name: "Native",
Old: "false",
New: "false",
New: "true",
},
},
Objects: []*ObjectDiff{
@@ -4726,6 +4736,7 @@ func TestTaskDiff(t *testing.T) {
Name: "foo",
PortLabel: "bar",
AddressMode: "driver",
TaskName: "task1",
},
},
},
@@ -4760,6 +4771,12 @@ func TestTaskDiff(t *testing.T) {
Old: "foo",
New: "bar",
},
{
Type: DiffTypeAdded,
Name: "TaskName",
Old: "",
New: "task1",
},
},
},
},
@@ -4890,6 +4907,10 @@ func TestTaskDiff(t *testing.T) {
Type: DiffTypeNone,
Name: "PortLabel",
},
{
Type: DiffTypeNone,
Name: "TaskName",
},
},
},
},
@@ -4946,6 +4967,59 @@ func TestTaskDiff(t *testing.T) {
},
},
{
Name: "Service with Connect Native",
Old: &Task{
Services: []*Service{
{
Name: "foo",
},
},
},
New: &Task{
Services: []*Service{
{
Name: "foo",
TaskName: "task1",
Connect: &ConsulConnect{
Native: true,
},
},
},
},
Expected: &TaskDiff{
Type: DiffTypeEdited,
Objects: []*ObjectDiff{
{
Type: DiffTypeEdited,
Name: "Service",
Fields: []*FieldDiff{
{
Type: DiffTypeAdded,
Name: "TaskName",
Old: "",
New: "task1",
},
},
Objects: []*ObjectDiff{
{
Type: DiffTypeAdded,
Name: "ConsulConnect",
Fields: []*FieldDiff{
{
Type: DiffTypeAdded,
Name: "Native",
Old: "",
New: "true",
},
},
},
},
},
},
},
},
{
Name: "Service Checks edited",
Old: &Task{
@@ -5297,6 +5371,12 @@ func TestTaskDiff(t *testing.T) {
Old: "",
New: "",
},
{
Type: DiffTypeNone,
Name: "TaskName",
Old: "",
New: "",
},
},
Objects: []*ObjectDiff{
{

View File

@@ -351,6 +351,12 @@ type Service struct {
// as one of the seed values when generating a Consul ServiceID.
Name string
// Name of the Task associated with this service.
//
// Currently only used to identify the implementing task of a Consul
// Connect Native enabled service.
TaskName string
// PortLabel is either the numeric port number or the `host:port`.
// To specify the port number using the host's Consul Advertise
// address, specify an empty host in the PortLabel (e.g. `:port`).
@@ -422,8 +428,7 @@ func (s *Service) Canonicalize(job string, taskGroup string, task string) {
"TASKGROUP": taskGroup,
"TASK": task,
"BASE": fmt.Sprintf("%s-%s-%s", job, taskGroup, task),
},
)
})
for _, check := range s.Checks {
check.Canonicalize(s.Name)
@@ -450,6 +455,7 @@ func (s *Service) Validate() error {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Service address_mode must be %q, %q, or %q; not %q", AddressModeAuto, AddressModeHost, AddressModeDriver, s.AddressMode))
}
// check checks
for _, c := range s.Checks {
if s.PortLabel == "" && c.PortLabel == "" && c.RequiresPort() {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Check %s invalid: check requires a port but neither check nor service %+q have a port", c.Name, s.Name))
@@ -469,10 +475,16 @@ func (s *Service) Validate() error {
}
}
// check connect
if s.Connect != nil {
if err := s.Connect.Validate(); err != nil {
mErr.Errors = append(mErr.Errors, err)
}
// if service is connect native, service task must be set
if s.Connect.IsNative() && len(s.TaskName) == 0 {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Service %s is Connect Native and requires setting the task", s.Name))
}
}
return mErr.ErrorOrNil()
@@ -620,8 +632,7 @@ OUTER:
// ConsulConnect represents a Consul Connect jobspec stanza.
type ConsulConnect struct {
// Native is true if a service implements Connect directly and does not
// need a sidecar.
// Native indicates whether the service is Consul Connect Native enabled.
Native bool
// SidecarService is non-nil if a service requires a sidecar.
@@ -662,17 +673,21 @@ func (c *ConsulConnect) HasSidecar() bool {
return c != nil && c.SidecarService != nil
}
func (c *ConsulConnect) IsNative() bool {
return c != nil && c.Native
}
// Validate that the Connect stanza has exactly one of Native or sidecar.
func (c *ConsulConnect) Validate() error {
if c == nil {
return nil
}
if c.Native && c.SidecarService != nil {
if c.IsNative() && c.HasSidecar() {
return fmt.Errorf("Consul Connect must be native or use a sidecar service; not both")
}
if !c.Native && c.SidecarService == nil {
if !c.IsNative() && !c.HasSidecar() {
return fmt.Errorf("Consul Connect must be native or use a sidecar service")
}

View File

@@ -123,7 +123,6 @@ func TestConsulConnect_Validate(t *testing.T) {
// An empty Connect stanza is invalid
require.Error(t, c.Validate())
// Native=true is valid
c.Native = true
require.NoError(t, c.Validate())
@@ -131,7 +130,6 @@ func TestConsulConnect_Validate(t *testing.T) {
c.SidecarService = &ConsulSidecarService{}
require.Error(t, c.Validate())
// Native=false + Sidecar!=nil is valid
c.Native = false
require.NoError(t, c.Validate())
}

View File

@@ -5979,7 +5979,7 @@ func (tg *TaskGroup) LookupTask(name string) *Task {
func (tg *TaskGroup) UsesConnect() bool {
for _, service := range tg.Services {
if service.Connect != nil {
if service.Connect.Native || service.Connect.SidecarService != nil {
if service.Connect.IsNative() || service.Connect.HasSidecar() {
return true
}
}
@@ -6179,18 +6179,10 @@ type Task struct {
// UsesConnect is for conveniently detecting if the Task is able to make use
// of Consul Connect features. This will be indicated in the TaskKind of the
// Task, which exports known types of Tasks.
//
// Currently only Consul Connect Proxy tasks are known.
// (Consul Connect Native tasks will be supported soon).
// Task, which exports known types of Tasks. UsesConnect will be true if the
// task is a connect proxy, or if the task is connect native.
func (t *Task) UsesConnect() bool {
// todo(shoenig): native tasks
switch {
case t.Kind.IsConnectProxy():
return true
default:
return false
}
return t.Kind.IsConnectProxy() || t.Kind.IsConnectNative()
}
func (t *Task) Copy() *Task {

View File

@@ -814,13 +814,20 @@ func TestTask_UsesConnect(t *testing.T) {
t.Run("sidecar proxy", func(t *testing.T) {
task := &Task{
Name: "connect-proxy-task1",
Kind: "connect-proxy:task1",
Kind: NewTaskKind(ConnectProxyPrefix, "task1"),
}
usesConnect := task.UsesConnect()
require.True(t, usesConnect)
})
// todo(shoenig): add native case
t.Run("native task", func(t *testing.T) {
task := &Task{
Name: "task1",
Kind: NewTaskKind(ConnectNativePrefix, "task1"),
}
usesConnect := task.UsesConnect()
require.True(t, usesConnect)
})
}
func TestTaskGroup_UsesConnect(t *testing.T) {
@@ -2707,10 +2714,14 @@ func TestService_Validate(t *testing.T) {
// Base service should be valid
require.NoError(t, s.Validate())
// Native Connect should be valid
// Native Connect requires task name on service
s.Connect = &ConsulConnect{
Native: true,
}
require.Error(t, s.Validate())
// Native Connect should work with task name on service set
s.TaskName = "testtask"
require.NoError(t, s.Validate())
// Native Connect + Sidecar should be invalid

View File

@@ -110,6 +110,7 @@ type Service struct {
Connect *ConsulConnect
Meta map[string]string
CanaryMeta map[string]string
TaskName string `mapstructure:"task"`
}
// Canonicalize the Service by ensuring its name and address mode are set. Task

View File

@@ -104,6 +104,12 @@ configuring Nomad to talk to Consul via DNS such as consul.service.consul
Consul service name defined in the `server_service_name` option. This search
only happens if the server does not have a leader.
- `share_ssl` `(bool: true)` - Specifies whether the Nomad client should share
its Consul SSL configuration with Connect Native applications. Includes values
of `ca_file`, `cert_file`, `key_file`, `ssl`, and `verify_ssl`. Does not include
the values for the ACL `token` or `auth`. This option should be disabled in
environments where Consul ACLs are not enabled.
- `ssl` `(bool: false)` - Specifies if the transport scheme should use HTTPS to
communicate with the Consul agent. Will default to the `CONSUL_HTTP_SSL`
environment variable if set.

View File

@@ -328,7 +328,7 @@ dashes (`-`) are converted to underscores (`_`) in environment variables so
- The `consul` binary must be present in Nomad's `$PATH` to run the Envoy
proxy sidecar on client nodes.
- Consul Connect Native is not yet supported ([#6083][gh6083]).
- Consul Connect Native requires host networking.
- Only the Docker, `exec`, `raw_exec`, and `java` drivers support network namespaces
and Connect.
- Changes to the `connect` stanza may not properly trigger a job update
@@ -337,7 +337,6 @@ dashes (`-`) are converted to underscores (`_`) in environment variables so
- Consul Connect and network namespaces are only supported on Linux.
[count-dashboard]: /img/count-dashboard.png
[gh6083]: https://github.com/hashicorp/nomad/issues/6083
[gh6120]: https://github.com/hashicorp/nomad/issues/6120
[gh6701]: https://github.com/hashicorp/nomad/issues/6701
[gh6459]: https://github.com/hashicorp/nomad/issues/6459

View File

@@ -47,14 +47,22 @@ job "countdash" {
## `connect` Parameters
- `native` - `(bool: false)` - This is used to configure the service as supporting
[Connect Native](https://www.consul.io/docs/connect/native) applications. If set,
the service definition must provide the name of the implementing task in the
[task][service_task] field.
Incompatible with `sidecar_service` and `sidecar_task`.
- `sidecar_service` - <code>([sidecar_service][]: nil)</code> - This is used to configure the sidecar
service injected by Nomad for Consul Connect.
service injected by Nomad for Consul Connect. Incompatible with `native`.
- `sidecar_task` - <code>([sidecar_task][]:nil)</code> - This modifies the configuration of the Envoy
proxy task.
proxy task. Incompatible with `native`.
## `connect` Examples
### Using Sidecar Service
The following example is a minimal connect stanza with defaults and is
sufficient to start an Envoy proxy sidecar for allowing incoming connections
via Consul Connect.
@@ -161,11 +169,29 @@ job "countdash" {
}
```
### Using Connect Native
The following example is a minimal service stanza for a
[Consul Connect Native](https://www.consul.io/docs/connect/native)
application implemented by a task named `generate`.
```hcl
service {
name = "uuid-api"
port = "${NOMAD_PORT_api}"
task = "generate"
connect {
native = true
}
}
```
### Limitations
[Consul Connect Native services][native] and [Nomad variable
interpolation][interpolation] are _not_ yet supported.
[Nomad variable interpolation][interpolation] is _not_ yet supported ([gh-7221]).
[gh-7221]: https://github.com/hashicorp/nomad/issues/7221
[job]: /docs/job-specification/job 'Nomad job Job Specification'
[group]: /docs/job-specification/group 'Nomad group Job Specification'
[task]: /docs/job-specification/task 'Nomad task Job Specification'
@@ -174,3 +200,4 @@ interpolation][interpolation] are _not_ yet supported.
[sidecar_task]: /docs/job-specification/sidecar_task 'Nomad sidecar task config Specification'
[upstreams]: /docs/job-specification/upstreams 'Nomad sidecar service upstreams Specification'
[native]: https://www.consul.io/docs/connect/native.html
[service_task]: /docs/job-specification/service#task-1 'Nomad service task'

View File

@@ -147,6 +147,11 @@ Connect][connect] integration.
- `host` - Use the host IP and port.
- `task` `(string: "")` - Specifies the name of the Nomad Task associated with
this service definition. Only available on group services. Must be set if this
service definition represents a Consul Connect Native service. The Nomad Task
must exist in the same Task Group.
- `meta` <code>([Meta][]: nil)</code> - Specifies a key-value map that annotates
the Consul service with user-defined metadata.