Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add tls support and tests to connect-init command #459

Merged
merged 7 commits into from
Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions subcommand/common/test_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ import (
// and a server certificate and saves them to temp files.
// It returns file names in this order:
// CA certificate, server certificate, and server key.
// Note that it's the responsibility of the caller to
// remove the temporary files created by this function.
func GenerateServerCerts(t *testing.T) (string, string, string, func()) {
func GenerateServerCerts(t *testing.T) (string, string, string) {
require := require.New(t)

caFile, err := ioutil.TempFile("", "ca")
Expand Down Expand Up @@ -46,12 +44,12 @@ func GenerateServerCerts(t *testing.T) (string, string, string, func()) {
_, err = certKeyFile.WriteString(keyPem)
require.NoError(err)

cleanupFunc := func() {
t.Cleanup(func() {
os.Remove(caFile.Name())
os.Remove(certFile.Name())
os.Remove(certKeyFile.Name())
Comment on lines +47 to 50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! I really like this pattern!

}
return caFile.Name(), certFile.Name(), certKeyFile.Name(), cleanupFunc
})
return caFile.Name(), certFile.Name(), certKeyFile.Name()
}

// WriteTempFile writes contents to a temporary file and returns the file
Expand Down
323 changes: 209 additions & 114 deletions subcommand/connect-init/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,140 +45,235 @@ func TestRun_FlagValidation(t *testing.T) {
}
}

// TestRun_ServicePollingWithACLs bootstraps and starts a consul server using a mock
// TestRun_ServicePollingWithACLsAndTLS bootstraps and starts a consul server using a mock
// kubernetes server to provide responses for setting up the consul AuthMethod
// then validates that the command runs end to end successfully.
func TestRun_ServicePollingWithACLs(t *testing.T) {
// then validates that the command runs end to end successfully. Also tests with TLS on/off.
func TestRun_ServicePollingWithACLsAndTLS(t *testing.T) {
t.Parallel()
bearerFile := common.WriteTempFile(t, serviceAccountJWTToken)
proxyFile := common.WriteTempFile(t, "")
tokenFile := common.WriteTempFile(t, "")

// Start Consul server with ACLs enabled and default deny policy.
var masterToken = "b78d37c7-0ca7-5f4d-99ee-6d9975ce4586"
server, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) {
c.ACL.Enabled = true
c.ACL.DefaultPolicy = "deny"
c.ACL.Tokens.Master = masterToken
})
defer server.Stop()
require.NoError(t, err)
server.WaitForLeader(t)
consulClient, err := api.NewClient(&api.Config{Address: server.HTTPAddr, Token: masterToken})
require.NoError(t, err)

// Start the mock k8s server.
k8sMockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
if r != nil && r.URL.Path == "/apis/authentication.k8s.io/v1/tokenreviews" && r.Method == "POST" {
w.Write([]byte(tokenReviewFoundResponse))
}
if r != nil && r.URL.Path == "/api/v1/namespaces/default/serviceaccounts/counting" && r.Method == "GET" {
w.Write([]byte(readServiceAccountFound))
}
}))
defer k8sMockServer.Close()

// Set up Consul's auth method.
authMethodTmpl := api.ACLAuthMethod{
Name: testAuthMethod,
Description: "Kubernetes Auth Method",
Type: "kubernetes",
Config: map[string]interface{}{
"Host": k8sMockServer.URL,
"CACert": serviceAccountCACert,
"ServiceAccountJWT": serviceAccountJWTToken,
cases := []struct {
name string
secure bool
kschoche marked this conversation as resolved.
Show resolved Hide resolved
}{
{
name: "ACLs enabled, not secure",
secure: false,
},
{
name: "ACLs enabled, secure",
secure: true,
},
}
_, _, err = consulClient.ACL().AuthMethodCreate(&authMethodTmpl, nil)
require.NoError(t, err)
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
bearerFile := common.WriteTempFile(t, serviceAccountJWTToken)
proxyFile := common.WriteTempFile(t, "")
tokenFile := common.WriteTempFile(t, "")

// Create the binding rule.
aclBindingRule := api.ACLBindingRule{
Description: "Kubernetes binding rule",
AuthMethod: testAuthMethod,
BindType: api.BindingRuleBindTypeService,
BindName: "${serviceaccount.name}",
Selector: "serviceaccount.name!=default",
}
_, _, err = consulClient.ACL().BindingRuleCreate(&aclBindingRule, nil)
require.NoError(t, err)
var caFile, certFile, keyFile string
// Start Consul server with ACLs enabled and default deny policy.
var masterToken = "b78d37c7-0ca7-5f4d-99ee-6d9975ce4586"
kschoche marked this conversation as resolved.
Show resolved Hide resolved
server, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) {
c.ACL.Enabled = true
c.ACL.DefaultPolicy = "deny"
c.ACL.Tokens.Master = masterToken
if test.secure {
caFile, certFile, keyFile = common.GenerateServerCerts(t)
c.CAFile = caFile
c.CertFile = certFile
c.KeyFile = keyFile
}
})
defer server.Stop()
require.NoError(t, err)
kschoche marked this conversation as resolved.
Show resolved Hide resolved
server.WaitForLeader(t)
cfg := &api.Config{
Scheme: "http",
Address: server.HTTPAddr,
Token: masterToken,
}
if test.secure {
cfg.Address = server.HTTPSAddr
cfg.Scheme = "https"
cfg.TLSConfig = api.TLSConfig{
// Address is taken from what was generated by common.GenerateServerCerts.
Address: "server.dc1.consul",
CAFile: caFile,
}
}
consulClient, err := api.NewClient(cfg)
require.NoError(t, err)

// Register Consul services.
testConsulServices := []api.AgentServiceRegistration{consulCountingSvc, consulCountingSvcSidecar}
for _, svc := range testConsulServices {
require.NoError(t, consulClient.Agent().ServiceRegister(&svc))
}
// Start the mock k8s server.
k8sMockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
if r != nil && r.URL.Path == "/apis/authentication.k8s.io/v1/tokenreviews" && r.Method == "POST" {
w.Write([]byte(tokenReviewFoundResponse))
}
if r != nil && r.URL.Path == "/api/v1/namespaces/default/serviceaccounts/counting" && r.Method == "GET" {
w.Write([]byte(readServiceAccountFound))
}
}))
defer k8sMockServer.Close()

// Set up Consul's auth method.
authMethodTmpl := api.ACLAuthMethod{
Name: testAuthMethod,
Type: "kubernetes",
Description: "Kubernetes Auth Method",
Config: map[string]interface{}{
"Host": k8sMockServer.URL,
"CACert": serviceAccountCACert,
"ServiceAccountJWT": serviceAccountJWTToken,
},
}
_, _, err = consulClient.ACL().AuthMethodCreate(&authMethodTmpl, nil)
require.NoError(t, err)

ui := cli.NewMockUi()
cmd := Command{
UI: ui,
bearerTokenFile: bearerFile,
tokenSinkFile: tokenFile,
proxyIDFile: proxyFile,
serviceRegistrationPollingAttempts: 3,
}
flags := []string{"-pod-name", testPodName,
"-pod-namespace", testPodNamespace,
"-acl-auth-method", testAuthMethod,
"-http-addr", server.HTTPAddr,
"-skip-service-registration-polling=false"}
// Run the command.
code := cmd.Run(flags)
require.Equal(t, 0, code, ui.ErrorWriter.String())
// Create the binding rule.
aclBindingRule := api.ACLBindingRule{
Description: "Kubernetes binding rule",
AuthMethod: testAuthMethod,
BindType: api.BindingRuleBindTypeService,
BindName: "${serviceaccount.name}",
Selector: "serviceaccount.name!=default",
}
_, _, err = consulClient.ACL().BindingRuleCreate(&aclBindingRule, nil)
require.NoError(t, err)

// Validate the ACL token was written.
tokenData, err := ioutil.ReadFile(tokenFile)
require.NoError(t, err)
require.NotEmpty(t, tokenData)
// Register Consul services.
testConsulServices := []api.AgentServiceRegistration{consulCountingSvc, consulCountingSvcSidecar}
for _, svc := range testConsulServices {
require.NoError(t, consulClient.Agent().ServiceRegister(&svc))
}

// Check that the token has the metadata with pod name and pod namespace.
consulClient, err = api.NewClient(&api.Config{Address: server.HTTPAddr, Token: string(tokenData)})
require.NoError(t, err)
token, _, err := consulClient.ACL().TokenReadSelf(nil)
require.NoError(t, err)
require.Equal(t, token.Description, "token created via login: {\"pod\":\"default/counting\"}")
ui := cli.NewMockUi()
cmd := Command{
UI: ui,
bearerTokenFile: bearerFile,
tokenSinkFile: tokenFile,
proxyIDFile: proxyFile,
serviceRegistrationPollingAttempts: 3,
}
// We build the http-addr because normally it's defined by the init container setting
// CONSUL_HTTP_ADDR when it processes the command template.
flags := []string{"-pod-name", testPodName,
"-pod-namespace", testPodNamespace,
"-acl-auth-method", testAuthMethod,
"-http-addr", fmt.Sprintf("%s://%s", cfg.Scheme, cfg.Address),
"-skip-service-registration-polling=false"}
// Add the CA File if necessary since we're not setting CONSUL_CACERT in test ENV.
if test.secure {
flags = append(flags, "-ca-file", caFile)
}
// Run the command.
code := cmd.Run(flags)
require.Equal(t, 0, code, ui.ErrorWriter.String())

// Validate contents of proxyFile.
data, err := ioutil.ReadFile(proxyFile)
require.NoError(t, err)
require.Contains(t, string(data), "counting-counting-sidecar-proxy")
// Validate the ACL token was written.
tokenData, err := ioutil.ReadFile(tokenFile)
require.NoError(t, err)
require.NotEmpty(t, tokenData)

// Check that the token has the metadata with pod name and pod namespace.
consulClient, err = api.NewClient(&api.Config{Address: server.HTTPAddr, Token: string(tokenData)})
require.NoError(t, err)
token, _, err := consulClient.ACL().TokenReadSelf(nil)
require.NoError(t, err)
require.Equal(t, token.Description, "token created via login: {\"pod\":\"default/counting\"}")

// Validate contents of proxyFile.
data, err := ioutil.ReadFile(proxyFile)
require.NoError(t, err)
require.Contains(t, string(data), "counting-counting-sidecar-proxy")
})
}
}

// This test validates service polling works in a happy case scenario.
func TestRun_ServicePollingOnly(t *testing.T) {
t.Parallel()
// This is the output file for the proxyid.
proxyFile := common.WriteTempFile(t, "")
cases := []struct {
name string
secure bool
}{
{
name: "ACLs enabled, not secure",
secure: false,
},
{
name: "ACLs enabled, secure",
secure: true,
},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
proxyFile := common.WriteTempFile(t, "")

// Start Consul server.
server, err := testutil.NewTestServerConfigT(t, nil)
defer server.Stop()
require.NoError(t, err)
server.WaitForLeader(t)
consulClient, err := api.NewClient(&api.Config{Address: server.HTTPAddr})
require.NoError(t, err)
var caFile, certFile, keyFile string
//defer cleanup()
kschoche marked this conversation as resolved.
Show resolved Hide resolved
// Start Consul server with TLS enabled if required.
server, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) {
if test.secure {
caFile, certFile, keyFile = common.GenerateServerCerts(t)
c.CAFile = caFile
c.CertFile = certFile
c.KeyFile = keyFile
}
})
// Start Consul server.
defer server.Stop()
require.NoError(t, err)
server.WaitForLeader(t)

// Register Consul services.
testConsulServices := []api.AgentServiceRegistration{consulCountingSvc, consulCountingSvcSidecar}
for _, svc := range testConsulServices {
require.NoError(t, consulClient.Agent().ServiceRegister(&svc))
}
// Get the Consul Client.
cfg := &api.Config{
Scheme: "http",
Address: server.HTTPAddr,
}
if test.secure {
cfg.Address = server.HTTPSAddr
cfg.Scheme = "https"
cfg.TLSConfig = api.TLSConfig{
// Address is taken from what was generated by common.GenerateServerCerts.
Address: "server.dc1.consul",
CAFile: caFile,
}
}
consulClient, err := api.NewClient(cfg)
require.NoError(t, err)

ui := cli.NewMockUi()
cmd := Command{
UI: ui,
proxyIDFile: proxyFile,
// Register Consul services.
testConsulServices := []api.AgentServiceRegistration{consulCountingSvc, consulCountingSvcSidecar}
for _, svc := range testConsulServices {
require.NoError(t, consulClient.Agent().ServiceRegister(&svc))
}

ui := cli.NewMockUi()
cmd := Command{
UI: ui,
proxyIDFile: proxyFile,
serviceRegistrationPollingAttempts: 3,
}
// We build the http-addr because normally it's defined by the init container setting
// CONSUL_HTTP_ADDR when it processes the command template.
flags := []string{"-http-addr", fmt.Sprintf("%s://%s", cfg.Scheme, cfg.Address)}
flags = append(flags, defaultTestFlags...)
// Add the CA File if necessary since we're not setting CONSUL_CACERT in test ENV.
if test.secure {
flags = append(flags, "-ca-file", caFile)
}

// Run the command.
code := cmd.Run(flags)
require.Equal(t, 0, code, ui.ErrorWriter.String())

// Validate contents of proxyFile.
data, err := ioutil.ReadFile(proxyFile)
require.NoError(t, err)
require.Contains(t, string(data), "counting-counting-sidecar-proxy")
})
}
flags := []string{"-http-addr", server.HTTPAddr}
flags = append(flags, defaultTestFlags...)
code := cmd.Run(flags)
require.Equal(t, 0, code)

// Validate contents of proxyFile.
data, err := ioutil.ReadFile(proxyFile)
require.NoError(t, err)
require.Contains(t, string(data), "counting-counting-sidecar-proxy")
}

// TestRun_ServicePollingErrors tests that when registered services could not be found,
Expand Down
Loading