hcs-1936: Prepare for adding license auto-retrieval to auto-config in enterprise
This commit is contained in:
parent
82f5cb3f08
commit
58b934133d
|
@ -0,0 +1,7 @@
|
||||||
|
```release-note:breaking-change
|
||||||
|
licensing: **(Enterprise Only)** Consul Enterprise has removed support for temporary licensing. All server agents must have a valid license at startup and client agents must have a license at startup or be able to retrieve one from the servers.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:breaking-change
|
||||||
|
licensing: **(Enterprise Only)** Consul Enterprise client agents now require a valid non-anonymous ACL token for retrieving their license from the servers. Additionally client agents rely on the value of the `start_join` and `retry_join` configurations for determining the servers to query for the license. Therefore one must be set to use license auto-retrieval.
|
||||||
|
```
|
|
@ -457,9 +457,7 @@ func (a *Agent) Start(ctx context.Context) error {
|
||||||
return fmt.Errorf("Failed to load TLS configurations after applying auto-config settings: %w", err)
|
return fmt.Errorf("Failed to load TLS configurations after applying auto-config settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// we cannot use the context passed into this method as that context will be cancelled after the
|
if err := a.startLicenseManager(ctx); err != nil {
|
||||||
// agent finishes starting up which would cause the license manager to stop
|
|
||||||
if err := a.startLicenseManager(&lib.StopChannelContext{StopCh: a.shutdownCh}); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,6 +92,10 @@ func New(config Config) (*AutoConfig, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := config.EnterpriseConfig.validateAndFinalize(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &AutoConfig{
|
return &AutoConfig{
|
||||||
acConfig: config,
|
acConfig: config,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
@ -126,13 +130,8 @@ func (ac *AutoConfig) ReadConfig() (*config.RuntimeConfig, error) {
|
||||||
// The context passed in can be used to cancel the retrieval of the initial configuration
|
// The context passed in can be used to cancel the retrieval of the initial configuration
|
||||||
// like when receiving a signal during startup.
|
// like when receiving a signal during startup.
|
||||||
func (ac *AutoConfig) InitialConfiguration(ctx context.Context) (*config.RuntimeConfig, error) {
|
func (ac *AutoConfig) InitialConfiguration(ctx context.Context) (*config.RuntimeConfig, error) {
|
||||||
if ac.config == nil {
|
if err := ac.maybeLoadConfig(); err != nil {
|
||||||
config, err := ac.ReadConfig()
|
return nil, err
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ac.config = config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
@ -180,6 +179,23 @@ func (ac *AutoConfig) InitialConfiguration(ctx context.Context) (*config.Runtime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maybeLoadConfig will read the Consul configuration using the
|
||||||
|
// provided config loader if and only if the config field of
|
||||||
|
// the struct is nil. When it does this it will fill in that
|
||||||
|
// field. If the config field already is non-nil then this
|
||||||
|
// is a noop.
|
||||||
|
func (ac *AutoConfig) maybeLoadConfig() error {
|
||||||
|
if ac.config == nil {
|
||||||
|
config, err := ac.ReadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ac.config = config
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// introToken is responsible for determining the correct intro token to use
|
// introToken is responsible for determining the correct intro token to use
|
||||||
// when making the initial AutoConfig.InitialConfiguration RPC request.
|
// when making the initial AutoConfig.InitialConfiguration RPC request.
|
||||||
func (ac *AutoConfig) introToken() (string, error) {
|
func (ac *AutoConfig) introToken() (string, error) {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
// +build !consulent
|
||||||
|
|
||||||
|
package autoconf
|
||||||
|
|
||||||
|
// AutoConfigEnterprise has no fields in OSS
|
||||||
|
type AutoConfigEnterprise struct{}
|
||||||
|
|
||||||
|
// newAutoConfigEnterprise initializes the enterprise AutoConfig struct
|
||||||
|
func newAutoConfigEnterprise(config Config) AutoConfigEnterprise {
|
||||||
|
return AutoConfigEnterprise{}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// +build !consulent
|
||||||
|
|
||||||
|
package autoconf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newEnterpriseConfig(t *testing.T) EnterpriseConfig {
|
||||||
|
return EnterpriseConfig{}
|
||||||
|
}
|
|
@ -136,11 +136,12 @@ func TestNew(t *testing.T) {
|
||||||
Loader: func(source config.Source) (result config.LoadResult, err error) {
|
Loader: func(source config.Source) (result config.LoadResult, err error) {
|
||||||
return config.LoadResult{}, nil
|
return config.LoadResult{}, nil
|
||||||
},
|
},
|
||||||
DirectRPC: newMockDirectRPC(t),
|
DirectRPC: newMockDirectRPC(t),
|
||||||
Tokens: newMockTokenStore(t),
|
Tokens: newMockTokenStore(t),
|
||||||
Cache: newMockCache(t),
|
Cache: newMockCache(t),
|
||||||
TLSConfigurator: newMockTLSConfigurator(t),
|
TLSConfigurator: newMockTLSConfigurator(t),
|
||||||
ServerProvider: newMockServerProvider(t),
|
ServerProvider: newMockServerProvider(t),
|
||||||
|
EnterpriseConfig: newEnterpriseConfig(t),
|
||||||
}
|
}
|
||||||
|
|
||||||
if tcase.modify != nil {
|
if tcase.modify != nil {
|
||||||
|
@ -211,18 +212,15 @@ func setupRuntimeConfig(t *testing.T) *configLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInitialConfiguration_disabled(t *testing.T) {
|
func TestInitialConfiguration_disabled(t *testing.T) {
|
||||||
loader := setupRuntimeConfig(t)
|
mcfg := newMockedConfig(t)
|
||||||
loader.addConfigHCL(`
|
mcfg.loader.addConfigHCL(`
|
||||||
primary_datacenter = "primary"
|
primary_datacenter = "primary"
|
||||||
auto_config = {
|
auto_config = {
|
||||||
enabled = false
|
enabled = false
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
conf := newMockedConfig(t).Config
|
ac, err := New(mcfg.Config)
|
||||||
conf.Loader = loader.Load
|
|
||||||
|
|
||||||
ac, err := New(conf)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, ac)
|
require.NotNil(t, ac)
|
||||||
|
|
||||||
|
@ -230,7 +228,7 @@ func TestInitialConfiguration_disabled(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, cfg)
|
require.NotNil(t, cfg)
|
||||||
require.Equal(t, "primary", cfg.PrimaryDatacenter)
|
require.Equal(t, "primary", cfg.PrimaryDatacenter)
|
||||||
require.NoFileExists(t, filepath.Join(*loader.opts.FlagValues.DataDir, autoConfigFileName))
|
require.NoFileExists(t, filepath.Join(*mcfg.loader.opts.FlagValues.DataDir, autoConfigFileName))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInitialConfiguration_cancelled(t *testing.T) {
|
func TestInitialConfiguration_cancelled(t *testing.T) {
|
||||||
|
|
|
@ -43,7 +43,7 @@ func (ac *AutoConfig) autoEncryptInitialCertsOnce(ctx context.Context, csr, key
|
||||||
}
|
}
|
||||||
var resp structs.SignedResponse
|
var resp structs.SignedResponse
|
||||||
|
|
||||||
servers, err := ac.autoEncryptHosts()
|
servers, err := ac.joinHosts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ func (ac *AutoConfig) autoEncryptInitialCertsOnce(ctx context.Context, csr, key
|
||||||
return nil, fmt.Errorf("No servers successfully responded to the auto-encrypt request")
|
return nil, fmt.Errorf("No servers successfully responded to the auto-encrypt request")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AutoConfig) autoEncryptHosts() ([]string, error) {
|
func (ac *AutoConfig) joinHosts() ([]string, error) {
|
||||||
// use servers known to gossip if there are any
|
// use servers known to gossip if there are any
|
||||||
if ac.acConfig.ServerProvider != nil {
|
if ac.acConfig.ServerProvider != nil {
|
||||||
if srv := ac.acConfig.ServerProvider.FindLANServer(); srv != nil {
|
if srv := ac.acConfig.ServerProvider.FindLANServer(); srv != nil {
|
||||||
|
|
|
@ -182,7 +182,7 @@ func TestAutoEncrypt_hosts(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
hosts, err := ac.autoEncryptHosts()
|
hosts, err := ac.joinHosts()
|
||||||
if tcase.err != "" {
|
if tcase.err != "" {
|
||||||
testutil.RequireErrorContains(t, err, tcase.err)
|
testutil.RequireErrorContains(t, err, tcase.err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -104,4 +104,7 @@ type Config struct {
|
||||||
// agent token as well as getting notifications when that token is updated.
|
// agent token as well as getting notifications when that token is updated.
|
||||||
// This field is required.
|
// This field is required.
|
||||||
Tokens TokenStore
|
Tokens TokenStore
|
||||||
|
|
||||||
|
// EnterpriseConfig is the embedded specific enterprise configurations
|
||||||
|
EnterpriseConfig
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
// +build !consulent
|
||||||
|
|
||||||
|
package autoconf
|
||||||
|
|
||||||
|
// EnterpriseConfig stub - only populated in Consul Enterprise
|
||||||
|
type EnterpriseConfig struct{}
|
||||||
|
|
||||||
|
// finalize is a noop for OSS
|
||||||
|
func (_ *EnterpriseConfig) validateAndFinalize() error {
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
// +build !consulent
|
||||||
|
|
||||||
|
package autoconf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockedEnterpriseConfig is pretty much just a stub in OSS
|
||||||
|
// It does contain an enterprise config for compatibility
|
||||||
|
// purposes but that in and of itself is just a stub.
|
||||||
|
type mockedEnterpriseConfig struct {
|
||||||
|
EnterpriseConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockedEnterpriseConfig(t *testing.T) *mockedEnterpriseConfig {
|
||||||
|
return &mockedEnterpriseConfig{}
|
||||||
|
}
|
|
@ -218,20 +218,25 @@ func (m *mockTokenStore) StopNotify(notifier token.Notifier) {
|
||||||
type mockedConfig struct {
|
type mockedConfig struct {
|
||||||
Config
|
Config
|
||||||
|
|
||||||
directRPC *mockDirectRPC
|
loader *configLoader
|
||||||
serverProvider *mockServerProvider
|
directRPC *mockDirectRPC
|
||||||
cache *mockCache
|
serverProvider *mockServerProvider
|
||||||
tokens *mockTokenStore
|
cache *mockCache
|
||||||
tlsCfg *mockTLSConfigurator
|
tokens *mockTokenStore
|
||||||
|
tlsCfg *mockTLSConfigurator
|
||||||
|
enterpriseConfig *mockedEnterpriseConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMockedConfig(t *testing.T) *mockedConfig {
|
func newMockedConfig(t *testing.T) *mockedConfig {
|
||||||
|
loader := setupRuntimeConfig(t)
|
||||||
directRPC := newMockDirectRPC(t)
|
directRPC := newMockDirectRPC(t)
|
||||||
serverProvider := newMockServerProvider(t)
|
serverProvider := newMockServerProvider(t)
|
||||||
mcache := newMockCache(t)
|
mcache := newMockCache(t)
|
||||||
tokens := newMockTokenStore(t)
|
tokens := newMockTokenStore(t)
|
||||||
tlsCfg := newMockTLSConfigurator(t)
|
tlsCfg := newMockTLSConfigurator(t)
|
||||||
|
|
||||||
|
entConfig := newMockedEnterpriseConfig(t)
|
||||||
|
|
||||||
// I am not sure it is well defined behavior but in testing it
|
// I am not sure it is well defined behavior but in testing it
|
||||||
// out it does appear like Cleanup functions can fail tests
|
// out it does appear like Cleanup functions can fail tests
|
||||||
// Adding in the mock expectations assertions here saves us
|
// Adding in the mock expectations assertions here saves us
|
||||||
|
@ -248,18 +253,23 @@ func newMockedConfig(t *testing.T) *mockedConfig {
|
||||||
|
|
||||||
return &mockedConfig{
|
return &mockedConfig{
|
||||||
Config: Config{
|
Config: Config{
|
||||||
DirectRPC: directRPC,
|
Loader: loader.Load,
|
||||||
ServerProvider: serverProvider,
|
DirectRPC: directRPC,
|
||||||
Cache: mcache,
|
ServerProvider: serverProvider,
|
||||||
Tokens: tokens,
|
Cache: mcache,
|
||||||
TLSConfigurator: tlsCfg,
|
Tokens: tokens,
|
||||||
Logger: testutil.Logger(t),
|
TLSConfigurator: tlsCfg,
|
||||||
|
Logger: testutil.Logger(t),
|
||||||
|
EnterpriseConfig: entConfig.EnterpriseConfig,
|
||||||
},
|
},
|
||||||
|
loader: loader,
|
||||||
directRPC: directRPC,
|
directRPC: directRPC,
|
||||||
serverProvider: serverProvider,
|
serverProvider: serverProvider,
|
||||||
cache: mcache,
|
cache: mcache,
|
||||||
tokens: tokens,
|
tokens: tokens,
|
||||||
tlsCfg: tlsCfg,
|
tlsCfg: tlsCfg,
|
||||||
|
|
||||||
|
enterpriseConfig: entConfig,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -159,11 +159,6 @@ func NewClient(config *Config, deps Deps) (*Client, error) {
|
||||||
go c.monitorACLMode()
|
go c.monitorACLMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.startEnterprise(); err != nil {
|
|
||||||
c.Shutdown()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -350,9 +350,6 @@ type Config struct {
|
||||||
// a Consul server is now up and known about.
|
// a Consul server is now up and known about.
|
||||||
ServerUp func()
|
ServerUp func()
|
||||||
|
|
||||||
// Shutdown callback is used to trigger a full Consul shutdown
|
|
||||||
Shutdown func()
|
|
||||||
|
|
||||||
// UserEventHandler callback can be used to handle incoming
|
// UserEventHandler callback can be used to handle incoming
|
||||||
// user events. This function should not block.
|
// user events. This function should not block.
|
||||||
UserEventHandler func(serf.UserEvent)
|
UserEventHandler func(serf.UserEvent)
|
||||||
|
|
|
@ -110,21 +110,30 @@ func NewBaseDeps(configLoader ConfigLoader, logOut io.Writer) (BaseDeps, error)
|
||||||
|
|
||||||
d.Router = router.NewRouter(d.Logger, cfg.Datacenter, fmt.Sprintf("%s.%s", cfg.NodeName, cfg.Datacenter), builder)
|
d.Router = router.NewRouter(d.Logger, cfg.Datacenter, fmt.Sprintf("%s.%s", cfg.NodeName, cfg.Datacenter), builder)
|
||||||
|
|
||||||
acConf := autoconf.Config{
|
// this needs to happen prior to creating auto-config as some of the dependencies
|
||||||
DirectRPC: d.ConnPool,
|
// must also be passed to auto-config
|
||||||
Logger: d.Logger,
|
d, err = initEnterpriseBaseDeps(d, cfg)
|
||||||
Loader: configLoader,
|
if err != nil {
|
||||||
ServerProvider: d.Router,
|
return d, err
|
||||||
TLSConfigurator: d.TLSConfigurator,
|
|
||||||
Cache: d.Cache,
|
|
||||||
Tokens: d.Tokens,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
acConf := autoconf.Config{
|
||||||
|
DirectRPC: d.ConnPool,
|
||||||
|
Logger: d.Logger,
|
||||||
|
Loader: configLoader,
|
||||||
|
ServerProvider: d.Router,
|
||||||
|
TLSConfigurator: d.TLSConfigurator,
|
||||||
|
Cache: d.Cache,
|
||||||
|
Tokens: d.Tokens,
|
||||||
|
EnterpriseConfig: initEnterpriseAutoConfig(d.EnterpriseDeps),
|
||||||
|
}
|
||||||
|
|
||||||
d.AutoConfig, err = autoconf.New(acConf)
|
d.AutoConfig, err = autoconf.New(acConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return d, err
|
return d, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return initEnterpriseBaseDeps(d, cfg)
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// grpcLogInitOnce because the test suite will call NewBaseDeps in many tests and
|
// grpcLogInitOnce because the test suite will call NewBaseDeps in many tests and
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
package agent
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
autoconf "github.com/hashicorp/consul/agent/auto-config"
|
||||||
"github.com/hashicorp/consul/agent/config"
|
"github.com/hashicorp/consul/agent/config"
|
||||||
|
"github.com/hashicorp/consul/agent/consul"
|
||||||
)
|
)
|
||||||
|
|
||||||
// initEnterpriseBaseDeps is responsible for initializing the enterprise dependencies that
|
// initEnterpriseBaseDeps is responsible for initializing the enterprise dependencies that
|
||||||
|
@ -11,3 +13,8 @@ import (
|
||||||
func initEnterpriseBaseDeps(d BaseDeps, _ *config.RuntimeConfig) (BaseDeps, error) {
|
func initEnterpriseBaseDeps(d BaseDeps, _ *config.RuntimeConfig) (BaseDeps, error) {
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initEnterpriseAutoConfig is responsible for setting up auto-config for enterprise
|
||||||
|
func initEnterpriseAutoConfig(_ consul.EnterpriseDeps) autoconf.EnterpriseConfig {
|
||||||
|
return autoconf.EnterpriseConfig{}
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ func LoggerWithOutput(t TestingTB, output io.Writer) hclog.InterceptLogger {
|
||||||
}
|
}
|
||||||
|
|
||||||
var sendTestLogsToStdout = os.Getenv("NOLOGBUFFER") == "1"
|
var sendTestLogsToStdout = os.Getenv("NOLOGBUFFER") == "1"
|
||||||
|
var testLogOnlyFailed = os.Getenv("TEST_LOGGING_ONLY_FAILED") == "1"
|
||||||
|
|
||||||
// NewLogBuffer returns an io.Writer which buffers all writes. When the test
|
// NewLogBuffer returns an io.Writer which buffers all writes. When the test
|
||||||
// ends, t.Failed is checked. If the test has failed or has been run in verbose
|
// ends, t.Failed is checked. If the test has failed or has been run in verbose
|
||||||
|
@ -30,13 +31,18 @@ var sendTestLogsToStdout = os.Getenv("NOLOGBUFFER") == "1"
|
||||||
//
|
//
|
||||||
// Set the env var NOLOGBUFFER=1 to disable buffering, resulting in all log
|
// Set the env var NOLOGBUFFER=1 to disable buffering, resulting in all log
|
||||||
// output being written immediately to stdout.
|
// output being written immediately to stdout.
|
||||||
|
//
|
||||||
|
// Typically log output is written either for failed tests or when go test
|
||||||
|
// is running with the verbose flag (-v) set. Setting TEST_LOGGING_ONLY_FAILED=1
|
||||||
|
// will prevent logs being output when the verbose flag is set if the test
|
||||||
|
// case is successful.
|
||||||
func NewLogBuffer(t TestingTB) io.Writer {
|
func NewLogBuffer(t TestingTB) io.Writer {
|
||||||
if sendTestLogsToStdout {
|
if sendTestLogsToStdout {
|
||||||
return os.Stdout
|
return os.Stdout
|
||||||
}
|
}
|
||||||
buf := &logBuffer{buf: new(bytes.Buffer)}
|
buf := &logBuffer{buf: new(bytes.Buffer)}
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
if t.Failed() || testing.Verbose() {
|
if t.Failed() || (!testLogOnlyFailed && testing.Verbose()) {
|
||||||
buf.Lock()
|
buf.Lock()
|
||||||
defer buf.Unlock()
|
defer buf.Unlock()
|
||||||
buf.buf.WriteTo(os.Stdout)
|
buf.buf.WriteTo(os.Stdout)
|
||||||
|
|
|
@ -142,6 +142,15 @@ function start_consul {
|
||||||
'-p=9502:8502'
|
'-p=9502:8502'
|
||||||
)
|
)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
license="${CONSUL_LICENSE:-}"
|
||||||
|
# load the consul license so we can pass it into the consul
|
||||||
|
# containers as an env var in the case that this is a consul
|
||||||
|
# enterprise test
|
||||||
|
if test -z "$license" -a -n "${CONSUL_LICENSE_PATH:-}"
|
||||||
|
then
|
||||||
|
license=$(cat $CONSUL_LICENSE_PATH)
|
||||||
|
fi
|
||||||
|
|
||||||
# Run consul and expose some ports to the host to make debugging locally a
|
# Run consul and expose some ports to the host to make debugging locally a
|
||||||
# bit easier.
|
# bit easier.
|
||||||
|
@ -151,6 +160,7 @@ function start_consul {
|
||||||
$WORKDIR_SNIPPET \
|
$WORKDIR_SNIPPET \
|
||||||
--hostname "consul-${DC}" \
|
--hostname "consul-${DC}" \
|
||||||
--network-alias "consul-${DC}" \
|
--network-alias "consul-${DC}" \
|
||||||
|
-e "CONSUL_LICENSE=$license" \
|
||||||
${ports[@]} \
|
${ports[@]} \
|
||||||
consul-dev \
|
consul-dev \
|
||||||
agent -dev -datacenter "${DC}" \
|
agent -dev -datacenter "${DC}" \
|
||||||
|
|
Loading…
Reference in New Issue