mirror of
https://github.com/status-im/consul.git
synced 2025-01-15 16:26:06 +00:00
5fb9df1640
* Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
477 lines
14 KiB
Go
477 lines
14 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/consul/agent/config"
|
|
"github.com/hashicorp/consul/agent/hcp"
|
|
hcpclient "github.com/hashicorp/consul/agent/hcp/client"
|
|
"github.com/hashicorp/consul/lib"
|
|
"github.com/hashicorp/consul/tlsutil"
|
|
"github.com/hashicorp/go-uuid"
|
|
"github.com/mitchellh/cli"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestBootstrapConfigLoader(t *testing.T) {
|
|
baseLoader := func(source config.Source) (config.LoadResult, error) {
|
|
return config.Load(config.LoadOpts{
|
|
DefaultConfig: source,
|
|
HCL: []string{
|
|
`server = true`,
|
|
`bind_addr = "127.0.0.1"`,
|
|
`data_dir = "/tmp/consul-data"`,
|
|
},
|
|
})
|
|
}
|
|
|
|
bootstrapLoader := func(source config.Source) (config.LoadResult, error) {
|
|
return bootstrapConfigLoader(baseLoader, &RawBootstrapConfig{
|
|
ConfigJSON: `{"bootstrap_expect": 8}`,
|
|
ManagementToken: "test-token",
|
|
})(source)
|
|
}
|
|
|
|
result, err := bootstrapLoader(nil)
|
|
require.NoError(t, err)
|
|
|
|
// bootstrap_expect and management token are injected from bootstrap config received from HCP.
|
|
require.Equal(t, 8, result.RuntimeConfig.BootstrapExpect)
|
|
require.Equal(t, "test-token", result.RuntimeConfig.Cloud.ManagementToken)
|
|
|
|
// Response header is always injected from a constant.
|
|
require.Equal(t, "x-consul-default-acl-policy", result.RuntimeConfig.HTTPResponseHeaders[accessControlHeaderName])
|
|
}
|
|
|
|
func Test_finalizeRuntimeConfig(t *testing.T) {
|
|
type testCase struct {
|
|
rc *config.RuntimeConfig
|
|
cfg *RawBootstrapConfig
|
|
verifyFn func(t *testing.T, rc *config.RuntimeConfig)
|
|
}
|
|
run := func(t *testing.T, tc testCase) {
|
|
finalizeRuntimeConfig(tc.rc, tc.cfg)
|
|
tc.verifyFn(t, tc.rc)
|
|
}
|
|
|
|
tt := map[string]testCase{
|
|
"set header if not present": {
|
|
rc: &config.RuntimeConfig{},
|
|
cfg: &RawBootstrapConfig{
|
|
ManagementToken: "test-token",
|
|
},
|
|
verifyFn: func(t *testing.T, rc *config.RuntimeConfig) {
|
|
require.Equal(t, "test-token", rc.Cloud.ManagementToken)
|
|
require.Equal(t, "x-consul-default-acl-policy", rc.HTTPResponseHeaders[accessControlHeaderName])
|
|
},
|
|
},
|
|
"append to header if present": {
|
|
rc: &config.RuntimeConfig{
|
|
HTTPResponseHeaders: map[string]string{
|
|
accessControlHeaderName: "Content-Encoding",
|
|
},
|
|
},
|
|
cfg: &RawBootstrapConfig{
|
|
ManagementToken: "test-token",
|
|
},
|
|
verifyFn: func(t *testing.T, rc *config.RuntimeConfig) {
|
|
require.Equal(t, "test-token", rc.Cloud.ManagementToken)
|
|
require.Equal(t, "Content-Encoding,x-consul-default-acl-policy", rc.HTTPResponseHeaders[accessControlHeaderName])
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range tt {
|
|
t.Run(name, func(t *testing.T) {
|
|
run(t, tc)
|
|
})
|
|
}
|
|
}
|
|
|
|
func boolPtr(value bool) *bool {
|
|
return &value
|
|
}
|
|
|
|
func TestLoadConfig_Persistence(t *testing.T) {
|
|
type testCase struct {
|
|
// resourceID is the HCP resource ID. If set, a server is considered to be cloud-enabled.
|
|
resourceID string
|
|
|
|
// devMode indicates whether the loader should not have a data directory.
|
|
devMode bool
|
|
|
|
// verifyFn issues case-specific assertions.
|
|
verifyFn func(t *testing.T, rc *config.RuntimeConfig)
|
|
}
|
|
|
|
run := func(t *testing.T, tc testCase) {
|
|
dir, err := os.MkdirTemp(os.TempDir(), "bootstrap-test-")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { os.RemoveAll(dir) })
|
|
|
|
s := hcp.NewMockHCPServer()
|
|
s.AddEndpoint(TestEndpoint())
|
|
|
|
// Use an HTTPS server since that's what the HCP SDK expects for auth.
|
|
srv := httptest.NewTLSServer(s)
|
|
defer srv.Close()
|
|
|
|
caCert, err := x509.ParseCertificate(srv.TLS.Certificates[0].Certificate[0])
|
|
require.NoError(t, err)
|
|
|
|
pool := x509.NewCertPool()
|
|
pool.AddCert(caCert)
|
|
clientTLS := &tls.Config{RootCAs: pool}
|
|
|
|
baseOpts := config.LoadOpts{
|
|
HCL: []string{
|
|
`server = true`,
|
|
`bind_addr = "127.0.0.1"`,
|
|
fmt.Sprintf(`http_config = { response_headers = { %s = "Content-Encoding" } }`, accessControlHeaderName),
|
|
fmt.Sprintf(`cloud { client_id="test" client_secret="test" hostname=%q auth_url=%q resource_id=%q }`,
|
|
srv.Listener.Addr().String(), srv.URL, tc.resourceID),
|
|
},
|
|
}
|
|
if tc.devMode {
|
|
baseOpts.DevMode = boolPtr(true)
|
|
} else {
|
|
baseOpts.HCL = append(baseOpts.HCL, fmt.Sprintf(`data_dir = %q`, dir))
|
|
}
|
|
|
|
baseLoader := func(source config.Source) (config.LoadResult, error) {
|
|
baseOpts.DefaultConfig = source
|
|
return config.Load(baseOpts)
|
|
}
|
|
|
|
ui := cli.NewMockUi()
|
|
|
|
// Load initial config to check whether bootstrapping from HCP is enabled.
|
|
initial, err := baseLoader(nil)
|
|
require.NoError(t, err)
|
|
|
|
// Override the client TLS config so that the test server can be trusted.
|
|
initial.RuntimeConfig.Cloud.WithTLSConfig(clientTLS)
|
|
client, err := hcpclient.NewClient(initial.RuntimeConfig.Cloud)
|
|
require.NoError(t, err)
|
|
|
|
loader, err := LoadConfig(context.Background(), client, initial.RuntimeConfig.DataDir, baseLoader, ui)
|
|
require.NoError(t, err)
|
|
|
|
// Load the agent config with the potentially wrapped loader.
|
|
fromRemote, err := loader(nil)
|
|
require.NoError(t, err)
|
|
|
|
// HCP-enabled cases should fetch from HCP on the first run of LoadConfig.
|
|
require.Contains(t, ui.OutputWriter.String(), "Fetching configuration from HCP")
|
|
|
|
// Run case-specific verification.
|
|
tc.verifyFn(t, fromRemote.RuntimeConfig)
|
|
|
|
require.Empty(t, fromRemote.RuntimeConfig.ACLInitialManagementToken,
|
|
"initial_management token should have been sanitized")
|
|
|
|
if tc.devMode {
|
|
// Re-running the bootstrap func below isn't relevant to dev mode
|
|
// since they don't have a data directory to load data from.
|
|
return
|
|
}
|
|
|
|
// Run LoadConfig again to exercise the logic of loading config from disk.
|
|
loader, err = LoadConfig(context.Background(), client, initial.RuntimeConfig.DataDir, baseLoader, ui)
|
|
require.NoError(t, err)
|
|
|
|
fromDisk, err := loader(nil)
|
|
require.NoError(t, err)
|
|
|
|
// HCP-enabled cases should fetch from disk on the second run.
|
|
require.Contains(t, ui.OutputWriter.String(), "Loaded HCP configuration from local disk")
|
|
|
|
// Config loaded from disk should be the same as the one that was initially fetched from the HCP servers.
|
|
require.Equal(t, fromRemote.RuntimeConfig, fromDisk.RuntimeConfig)
|
|
}
|
|
|
|
tt := map[string]testCase{
|
|
"dev mode": {
|
|
devMode: true,
|
|
|
|
resourceID: "organization/0b9de9a3-8403-4ca6-aba8-fca752f42100/" +
|
|
"project/0b9de9a3-8403-4ca6-aba8-fca752f42100/" +
|
|
"consul.cluster/new-cluster-id",
|
|
|
|
verifyFn: func(t *testing.T, rc *config.RuntimeConfig) {
|
|
require.Empty(t, rc.DataDir)
|
|
|
|
// Dev mode should have persisted certs since they can't be inlined.
|
|
require.NotEmpty(t, rc.TLS.HTTPS.CertFile)
|
|
require.NotEmpty(t, rc.TLS.HTTPS.KeyFile)
|
|
require.NotEmpty(t, rc.TLS.HTTPS.CAFile)
|
|
|
|
// Find the temporary directory they got stored in.
|
|
dir := filepath.Dir(rc.TLS.HTTPS.CertFile)
|
|
|
|
// Ensure we only stored the TLS materials.
|
|
entries, err := os.ReadDir(dir)
|
|
require.NoError(t, err)
|
|
require.Len(t, entries, 3)
|
|
|
|
haveFiles := make([]string, 3)
|
|
for i, entry := range entries {
|
|
haveFiles[i] = entry.Name()
|
|
}
|
|
|
|
wantFiles := []string{caFileName, certFileName, keyFileName}
|
|
require.ElementsMatch(t, wantFiles, haveFiles)
|
|
},
|
|
},
|
|
"new cluster": {
|
|
resourceID: "organization/0b9de9a3-8403-4ca6-aba8-fca752f42100/" +
|
|
"project/0b9de9a3-8403-4ca6-aba8-fca752f42100/" +
|
|
"consul.cluster/new-cluster-id",
|
|
|
|
// New clusters should have received and persisted the whole suite of config.
|
|
verifyFn: func(t *testing.T, rc *config.RuntimeConfig) {
|
|
dir := filepath.Join(rc.DataDir, subDir)
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
require.NoError(t, err)
|
|
require.Len(t, entries, 6)
|
|
|
|
files := []string{
|
|
filepath.Join(dir, configFileName),
|
|
filepath.Join(dir, caFileName),
|
|
filepath.Join(dir, certFileName),
|
|
filepath.Join(dir, keyFileName),
|
|
filepath.Join(dir, tokenFileName),
|
|
filepath.Join(dir, successFileName),
|
|
}
|
|
for _, name := range files {
|
|
_, err := os.Stat(name)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
require.Equal(t, filepath.Join(dir, certFileName), rc.TLS.HTTPS.CertFile)
|
|
require.Equal(t, filepath.Join(dir, keyFileName), rc.TLS.HTTPS.KeyFile)
|
|
require.Equal(t, filepath.Join(dir, caFileName), rc.TLS.HTTPS.CAFile)
|
|
|
|
cert, key, caCerts, err := loadCerts(dir)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, validateTLSCerts(cert, key, caCerts))
|
|
},
|
|
},
|
|
"existing cluster": {
|
|
resourceID: "organization/0b9de9a3-8403-4ca6-aba8-fca752f42100/" +
|
|
"project/0b9de9a3-8403-4ca6-aba8-fca752f42100/" +
|
|
"consul.cluster/" + TestExistingClusterID,
|
|
|
|
// Existing clusters should have only received and persisted the management token.
|
|
verifyFn: func(t *testing.T, rc *config.RuntimeConfig) {
|
|
dir := filepath.Join(rc.DataDir, subDir)
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
require.NoError(t, err)
|
|
require.Len(t, entries, 3)
|
|
|
|
files := []string{
|
|
filepath.Join(dir, tokenFileName),
|
|
filepath.Join(dir, successFileName),
|
|
filepath.Join(dir, configFileName),
|
|
}
|
|
for _, name := range files {
|
|
_, err := os.Stat(name)
|
|
require.NoError(t, err)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range tt {
|
|
t.Run(name, func(t *testing.T) {
|
|
run(t, tc)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_loadPersistedBootstrapConfig(t *testing.T) {
|
|
type expect struct {
|
|
loaded bool
|
|
warning string
|
|
}
|
|
type testCase struct {
|
|
existingCluster bool
|
|
disableManagementToken bool
|
|
mutateFn func(t *testing.T, dir string)
|
|
expect expect
|
|
}
|
|
|
|
run := func(t *testing.T, tc testCase) {
|
|
dataDir, err := os.MkdirTemp(os.TempDir(), "load-bootstrap-test-")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { os.RemoveAll(dataDir) })
|
|
|
|
dir := filepath.Join(dataDir, subDir)
|
|
|
|
// Do some common setup as if we received config from HCP and persisted it to disk.
|
|
require.NoError(t, lib.EnsurePath(dir, true))
|
|
require.NoError(t, persistSuccessMarker(dir))
|
|
|
|
if !tc.existingCluster {
|
|
caCert, caKey, err := tlsutil.GenerateCA(tlsutil.CAOpts{})
|
|
require.NoError(t, err)
|
|
|
|
serverCert, serverKey, err := testLeaf(caCert, caKey)
|
|
require.NoError(t, err)
|
|
require.NoError(t, persistTLSCerts(dir, serverCert, serverKey, []string{caCert}))
|
|
|
|
cfgJSON := `{"bootstrap_expect": 8}`
|
|
require.NoError(t, persistBootstrapConfig(dir, cfgJSON))
|
|
}
|
|
|
|
var token string
|
|
if !tc.disableManagementToken {
|
|
token, err = uuid.GenerateUUID()
|
|
require.NoError(t, err)
|
|
require.NoError(t, persistManagementToken(dir, token))
|
|
}
|
|
|
|
// Optionally mutate the persisted data to trigger errors while loading.
|
|
if tc.mutateFn != nil {
|
|
tc.mutateFn(t, dir)
|
|
}
|
|
|
|
ui := cli.NewMockUi()
|
|
cfg, loaded := loadPersistedBootstrapConfig(dataDir, ui)
|
|
require.Equal(t, tc.expect.loaded, loaded, ui.ErrorWriter.String())
|
|
if loaded {
|
|
require.Equal(t, token, cfg.ManagementToken)
|
|
require.Empty(t, ui.ErrorWriter.String())
|
|
} else {
|
|
require.Nil(t, cfg)
|
|
require.Contains(t, ui.ErrorWriter.String(), tc.expect.warning)
|
|
}
|
|
}
|
|
|
|
tt := map[string]testCase{
|
|
"existing cluster with valid files": {
|
|
existingCluster: true,
|
|
// Don't mutate, files from setup are valid.
|
|
mutateFn: nil,
|
|
expect: expect{
|
|
loaded: true,
|
|
warning: "",
|
|
},
|
|
},
|
|
"existing cluster no token": {
|
|
existingCluster: true,
|
|
disableManagementToken: true,
|
|
expect: expect{
|
|
loaded: false,
|
|
},
|
|
},
|
|
"existing cluster no files": {
|
|
existingCluster: true,
|
|
mutateFn: func(t *testing.T, dir string) {
|
|
// Remove all files
|
|
require.NoError(t, os.RemoveAll(dir))
|
|
},
|
|
expect: expect{
|
|
loaded: false,
|
|
// No warnings since we assume we need to fetch config from HCP for the first time.
|
|
warning: "",
|
|
},
|
|
},
|
|
"new cluster with valid files": {
|
|
// Don't mutate, files from setup are valid.
|
|
mutateFn: nil,
|
|
expect: expect{
|
|
loaded: true,
|
|
warning: "",
|
|
},
|
|
},
|
|
"new cluster with no token": {
|
|
disableManagementToken: true,
|
|
expect: expect{
|
|
loaded: false,
|
|
},
|
|
},
|
|
"new cluster some files": {
|
|
mutateFn: func(t *testing.T, dir string) {
|
|
// Remove one of the required files
|
|
require.NoError(t, os.Remove(filepath.Join(dir, certFileName)))
|
|
},
|
|
expect: expect{
|
|
loaded: false,
|
|
warning: "configuration files on disk are incomplete",
|
|
},
|
|
},
|
|
"new cluster no files": {
|
|
mutateFn: func(t *testing.T, dir string) {
|
|
// Remove all files
|
|
require.NoError(t, os.RemoveAll(dir))
|
|
},
|
|
expect: expect{
|
|
loaded: false,
|
|
// No warnings since we assume we need to fetch config from HCP for the first time.
|
|
warning: "",
|
|
},
|
|
},
|
|
"new cluster invalid cert": {
|
|
mutateFn: func(t *testing.T, dir string) {
|
|
name := filepath.Join(dir, certFileName)
|
|
require.NoError(t, os.WriteFile(name, []byte("not-a-cert"), 0600))
|
|
},
|
|
expect: expect{
|
|
loaded: false,
|
|
warning: "invalid server certificate",
|
|
},
|
|
},
|
|
"new cluster invalid CA": {
|
|
mutateFn: func(t *testing.T, dir string) {
|
|
name := filepath.Join(dir, caFileName)
|
|
require.NoError(t, os.WriteFile(name, []byte("not-a-ca-cert"), 0600))
|
|
},
|
|
expect: expect{
|
|
loaded: false,
|
|
warning: "invalid CA certificate",
|
|
},
|
|
},
|
|
"new cluster invalid config flag": {
|
|
mutateFn: func(t *testing.T, dir string) {
|
|
name := filepath.Join(dir, configFileName)
|
|
require.NoError(t, os.WriteFile(name, []byte(`{"not_a_consul_agent_config_field" = "zap"}`), 0600))
|
|
},
|
|
expect: expect{
|
|
loaded: false,
|
|
warning: "failed to parse local bootstrap config",
|
|
},
|
|
},
|
|
"existing cluster invalid token": {
|
|
existingCluster: true,
|
|
mutateFn: func(t *testing.T, dir string) {
|
|
name := filepath.Join(dir, tokenFileName)
|
|
require.NoError(t, os.WriteFile(name, []byte("not-a-uuid"), 0600))
|
|
},
|
|
expect: expect{
|
|
loaded: false,
|
|
warning: "is not a valid UUID",
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range tt {
|
|
t.Run(name, func(t *testing.T) {
|
|
run(t, tc)
|
|
})
|
|
}
|
|
}
|