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 support for ACLs for sync catalog and partitions. #1180

Merged
merged 2 commits into from
Apr 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ BUG FIXES:
* Helm
* Update client-daemonset to include ca-cert volumeMount only when tls is enabled. [[GH-1194](https://github.com/hashicorp/consul-k8s/pull/1194)]
* Update create-federation-secret-job to look up the automatically generated gossip encryption key by the right name when global.name is unset or set to something other than consul. [[GH-1196](https://github.com/hashicorp/consul-k8s/pull/1196)]
* Add Admin Partitions support to Sync Catalog **(Consul Enterprise only)**. [[GH-1180](https://github.com/hashicorp/consul-k8s/pull/1190)]

## 0.43.0 (April 21, 2022)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const staticServerNamespace = "ns1"
const staticClientNamespace = "ns2"

// Test that Connect works in a default and ACLsAndAutoEncryptEnabled installations for X-Partition and in-partition networking.
func TestPartitions(t *testing.T) {
func TestPartitions_Connect(t *testing.T) {
env := suite.Environment()
cfg := suite.Config()

Expand Down Expand Up @@ -138,19 +138,19 @@ func TestPartitions(t *testing.T) {
caKeySecretName := fmt.Sprintf("%s-consul-ca-key", releaseName)

logger.Logf(t, "retrieving ca cert secret %s from the server cluster and applying to the client cluster", caCertSecretName)
moveSecret(t, serverClusterContext, clientClusterContext, caCertSecretName)
copySecret(t, serverClusterContext, clientClusterContext, caCertSecretName)

if !c.ACLsAndAutoEncryptEnabled {
// When auto-encrypt is disabled, we need both
// the CA cert and CA key to be available in the clients cluster to generate client certificates and keys.
logger.Logf(t, "retrieving ca key secret %s from the server cluster and applying to the client cluster", caKeySecretName)
moveSecret(t, serverClusterContext, clientClusterContext, caKeySecretName)
copySecret(t, serverClusterContext, clientClusterContext, caKeySecretName)
}

partitionToken := fmt.Sprintf("%s-consul-partitions-acl-token", releaseName)
if c.ACLsAndAutoEncryptEnabled {
logger.Logf(t, "retrieving partition token secret %s from the server cluster and applying to the client cluster", partitionToken)
moveSecret(t, serverClusterContext, clientClusterContext, partitionToken)
copySecret(t, serverClusterContext, clientClusterContext, partitionToken)
}

partitionServiceName := fmt.Sprintf("%s-consul-partition", releaseName)
Expand Down Expand Up @@ -630,7 +630,7 @@ func TestPartitions(t *testing.T) {
}
}

func moveSecret(t *testing.T, sourceContext, destContext environment.TestContext, secretName string) {
func copySecret(t *testing.T, sourceContext, destContext environment.TestContext, secretName string) {
t.Helper()

secret, err := sourceContext.KubernetesClient(t).CoreV1().Secrets(sourceContext.KubectlOptions(t).Namespace).Get(context.Background(), secretName, metav1.GetOptions{})
Expand Down
297 changes: 297 additions & 0 deletions acceptance/tests/partitions/partitions_sync_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
package partitions

import (
"context"
"fmt"
"strconv"
"testing"
"time"

terratestk8s "github.com/gruntwork-io/terratest/modules/k8s"
"github.com/hashicorp/consul-k8s/acceptance/framework/consul"
"github.com/hashicorp/consul-k8s/acceptance/framework/environment"
"github.com/hashicorp/consul-k8s/acceptance/framework/helpers"
"github.com/hashicorp/consul-k8s/acceptance/framework/k8s"
"github.com/hashicorp/consul-k8s/acceptance/framework/logger"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// Test that Sync Catalog works in a default and ACLsAndAutoEncryptEnabled installations for partitions.
func TestPartitions_Sync(t *testing.T) {
env := suite.Environment()
cfg := suite.Config()

if !cfg.EnableEnterprise {
t.Skipf("skipping this test because -enable-enterprise is not set")
}

const defaultPartition = "default"
const secondaryPartition = "secondary"
const defaultNamespace = "default"
cases := []struct {
name string
destinationNamespace string
mirrorK8S bool
ACLsAndAutoEncryptEnabled bool
}{
{
"default destination namespace",
defaultNamespace,
false,
false,
},
{
"default destination namespace; ACLs and auto-encrypt enabled",
defaultNamespace,
false,
true,
},
{
"single destination namespace",
staticServerNamespace,
false,
false,
},
{
"single destination namespace; ACLs and auto-encrypt enabled",
staticServerNamespace,
false,
true,
},
{
"mirror k8s namespaces",
staticServerNamespace,
true,
false,
},
{
"mirror k8s namespaces; ACLs and auto-encrypt enabled",
staticServerNamespace,
true,
true,
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
primaryClusterContext := env.DefaultContext(t)
secondaryClusterContext := env.Context(t, environment.SecondaryContextName)

ctx := context.Background()

commonHelmValues := map[string]string{
"global.adminPartitions.enabled": "true",

"global.enableConsulNamespaces": "true",

"global.tls.enabled": "true",
"global.tls.httpsOnly": strconv.FormatBool(c.ACLsAndAutoEncryptEnabled),
"global.tls.enableAutoEncrypt": strconv.FormatBool(c.ACLsAndAutoEncryptEnabled),

"global.acls.manageSystemACLs": strconv.FormatBool(c.ACLsAndAutoEncryptEnabled),

"syncCatalog.enabled": "true",
// When mirroringK8S is set, this setting is ignored.
"syncCatalog.consulNamespaces.consulDestinationNamespace": c.destinationNamespace,
"syncCatalog.consulNamespaces.mirroringK8S": strconv.FormatBool(c.mirrorK8S),
"syncCatalog.addK8SNamespaceSuffix": "false",

"controller.enabled": "true",

"dns.enabled": "true",
"dns.enableRedirection": strconv.FormatBool(cfg.EnableTransparentProxy),
}

serverHelmValues := map[string]string{
"server.exposeGossipAndRPCPorts": "true",
}

// On Kind, there are no load balancers but since all clusters
// share the same node network (docker bridge), we can use
// a NodePort service so that we can access node(s) in a different Kind cluster.
Comment on lines +112 to +114
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a great clarifying comment. 👍🏻

if cfg.UseKind {
serverHelmValues["global.adminPartitions.service.type"] = "NodePort"
serverHelmValues["global.adminPartitions.service.nodePort.https"] = "30000"
}

releaseName := helpers.RandomName()

helpers.MergeMaps(serverHelmValues, commonHelmValues)

// Install the consul cluster with servers in the default kubernetes context.
primaryConsulCluster := consul.NewHelmCluster(t, serverHelmValues, primaryClusterContext, cfg, releaseName)
primaryConsulCluster.Create(t)

// Get the TLS CA certificate and key secret from the server cluster and apply it to the client cluster.
caCertSecretName := fmt.Sprintf("%s-consul-ca-cert", releaseName)
caKeySecretName := fmt.Sprintf("%s-consul-ca-key", releaseName)

logger.Logf(t, "retrieving ca cert secret %s from the server cluster and applying to the client cluster", caCertSecretName)
copySecret(t, primaryClusterContext, secondaryClusterContext, caCertSecretName)

if !c.ACLsAndAutoEncryptEnabled {
// When auto-encrypt is disabled, we need both
// the CA cert and CA key to be available in the clients cluster to generate client certificates and keys.
logger.Logf(t, "retrieving ca key secret %s from the server cluster and applying to the client cluster", caKeySecretName)
copySecret(t, primaryClusterContext, secondaryClusterContext, caKeySecretName)
}

partitionToken := fmt.Sprintf("%s-consul-partitions-acl-token", releaseName)
if c.ACLsAndAutoEncryptEnabled {
logger.Logf(t, "retrieving partition token secret %s from the server cluster and applying to the client cluster", partitionToken)
copySecret(t, primaryClusterContext, secondaryClusterContext, partitionToken)
}

partitionServiceName := fmt.Sprintf("%s-consul-partition", releaseName)
partitionSvcAddress := k8s.ServiceHost(t, cfg, primaryClusterContext, partitionServiceName)

k8sAuthMethodHost := k8s.KubernetesAPIServerHost(t, cfg, secondaryClusterContext)

// Create client cluster.
clientHelmValues := map[string]string{
"global.enabled": "false",

"global.adminPartitions.name": secondaryPartition,

"global.tls.caCert.secretName": caCertSecretName,
"global.tls.caCert.secretKey": "tls.crt",

"externalServers.enabled": "true",
"externalServers.hosts[0]": partitionSvcAddress,
"externalServers.tlsServerName": "server.dc1.consul",

"client.enabled": "true",
"client.exposeGossipPorts": "true",
"client.join[0]": partitionSvcAddress,
}

if c.ACLsAndAutoEncryptEnabled {
// Setup partition token and auth method host if ACLs enabled.
clientHelmValues["global.acls.bootstrapToken.secretName"] = partitionToken
clientHelmValues["global.acls.bootstrapToken.secretKey"] = "token"
clientHelmValues["externalServers.k8sAuthMethodHost"] = k8sAuthMethodHost
} else {
// Provide CA key when auto-encrypt is disabled.
clientHelmValues["global.tls.caKey.secretName"] = caKeySecretName
clientHelmValues["global.tls.caKey.secretKey"] = "tls.key"
}

if cfg.UseKind {
clientHelmValues["externalServers.httpsPort"] = "30000"
}

helpers.MergeMaps(clientHelmValues, commonHelmValues)

// Install the consul cluster without servers in the client cluster kubernetes context.
secondaryConsulCluster := consul.NewHelmCluster(t, clientHelmValues, secondaryClusterContext, cfg, releaseName)
secondaryConsulCluster.Create(t)

// Ensure consul clients are created.
agentPodList, err := secondaryClusterContext.KubernetesClient(t).CoreV1().Pods(secondaryClusterContext.KubectlOptions(t).Namespace).List(ctx, metav1.ListOptions{LabelSelector: "app=consul,component=client"})
require.NoError(t, err)
require.NotEmpty(t, agentPodList.Items)

output, err := k8s.RunKubectlAndGetOutputE(t, secondaryClusterContext.KubectlOptions(t), "logs", agentPodList.Items[0].Name, "-n", secondaryClusterContext.KubectlOptions(t).Namespace)
require.NoError(t, err)
require.Contains(t, output, "Partition: 'secondary'")

primaryStaticServerOpts := &terratestk8s.KubectlOptions{
ContextName: primaryClusterContext.KubectlOptions(t).ContextName,
ConfigPath: primaryClusterContext.KubectlOptions(t).ConfigPath,
Namespace: staticServerNamespace,
}
secondaryStaticServerOpts := &terratestk8s.KubectlOptions{
ContextName: secondaryClusterContext.KubectlOptions(t).ContextName,
ConfigPath: secondaryClusterContext.KubectlOptions(t).ConfigPath,
Namespace: staticServerNamespace,
}

logger.Logf(t, "creating namespaces %s in servers cluster", staticServerNamespace)
k8s.RunKubectl(t, primaryClusterContext.KubectlOptions(t), "create", "ns", staticServerNamespace)
helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() {
k8s.RunKubectl(t, primaryClusterContext.KubectlOptions(t), "delete", "ns", staticServerNamespace)
})

logger.Logf(t, "creating namespaces %s in clients cluster", staticServerNamespace)
k8s.RunKubectl(t, secondaryClusterContext.KubectlOptions(t), "create", "ns", staticServerNamespace)
helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() {
k8s.RunKubectl(t, secondaryClusterContext.KubectlOptions(t), "delete", "ns", staticServerNamespace)
})

consulClient, _ := primaryConsulCluster.SetupConsulClient(t, c.ACLsAndAutoEncryptEnabled)

defaultPartitionQueryOpts := &api.QueryOptions{Namespace: staticServerNamespace, Partition: defaultPartition}
secondaryPartitionQueryOpts := &api.QueryOptions{Namespace: staticServerNamespace, Partition: secondaryPartition}

if !c.mirrorK8S {
defaultPartitionQueryOpts = &api.QueryOptions{Namespace: c.destinationNamespace, Partition: defaultPartition}
secondaryPartitionQueryOpts = &api.QueryOptions{Namespace: c.destinationNamespace, Partition: secondaryPartition}
}

// Check that the ACL token is deleted.
if c.ACLsAndAutoEncryptEnabled {
// We need to register the cleanup function before we create the deployments
// because golang will execute them in reverse order i.e. the last registered
// cleanup function will be executed first.
t.Cleanup(func() {
if c.ACLsAndAutoEncryptEnabled {
retry.Run(t, func(r *retry.R) {
tokens, _, err := consulClient.ACL().TokenList(defaultPartitionQueryOpts)
require.NoError(r, err)
for _, token := range tokens {
require.NotContains(r, token.Description, staticServerName)
}

tokens, _, err = consulClient.ACL().TokenList(secondaryPartitionQueryOpts)
require.NoError(r, err)
for _, token := range tokens {
require.NotContains(r, token.Description, staticServerName)
}
})
}
})
}

logger.Log(t, "creating a static-server with a service")
// create service in default partition.
k8s.DeployKustomize(t, primaryStaticServerOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/bases/static-server")
// create service in secondary partition.
k8s.DeployKustomize(t, secondaryStaticServerOpts, cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/bases/static-server")

logger.Log(t, "checking that the service has been synced to Consul")
var services map[string][]string
counter := &retry.Counter{Count: 20, Wait: 30 * time.Second}
retry.RunWith(counter, t, func(r *retry.R) {
var err error
// list services in default partition catalog.
services, _, err = consulClient.Catalog().Services(defaultPartitionQueryOpts)
require.NoError(r, err)
require.Contains(r, services, staticServerName)
if _, ok := services[staticServerName]; !ok {
r.Errorf("service '%s' is not in Consul's list of services %s in the default partition", staticServerName, services)
}
// list services in secondary partition catalog.
services, _, err = consulClient.Catalog().Services(secondaryPartitionQueryOpts)
require.NoError(r, err)
require.Contains(r, services, staticServerName)
if _, ok := services[staticServerName]; !ok {
r.Errorf("service '%s' is not in Consul's list of services %s in the secondary partition", staticServerName, services)
}
})
// validate service in the default partition.
service, _, err := consulClient.Catalog().Service(staticServerName, "", defaultPartitionQueryOpts)
require.NoError(t, err)
require.Equal(t, 1, len(service))
require.Equal(t, []string{"k8s"}, service[0].ServiceTags)
// validate service in the secondary partition.
service, _, err = consulClient.Catalog().Service(staticServerName, "", secondaryPartitionQueryOpts)
require.NoError(t, err)
require.Equal(t, 1, len(service))
require.Equal(t, []string{"k8s"}, service[0].ServiceTags)

})
}
}
2 changes: 1 addition & 1 deletion charts/consul/templates/server-acl-init-job.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,10 @@ spec:

{{- if (or (and (ne (.Values.connectInject.enabled | toString) "-") .Values.connectInject.enabled) (and (eq (.Values.connectInject.enabled | toString) "-") .Values.global.enabled)) }}
-connect-inject=true \
{{- end }}
{{- if and .Values.externalServers.enabled .Values.externalServers.k8sAuthMethodHost }}
-auth-method-host={{ .Values.externalServers.k8sAuthMethodHost }} \
{{- end }}
{{- end }}

{{- if .Values.global.federation.k8sAuthMethodHost }}
-auth-method-host={{ .Values.global.federation.k8sAuthMethodHost }} \
Expand Down
3 changes: 3 additions & 0 deletions charts/consul/templates/sync-catalog-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ spec:
{{- if .Values.syncCatalog.consulNodeName }}
-consul-node-name={{ .Values.syncCatalog.consulNodeName }} \
{{- end }}
{{- if .Values.global.adminPartitions.enabled }}
-partition={{ .Values.global.adminPartitions.name }} \
{{- end }}
{{- if .Values.syncCatalog.consulPrefix}}
-consul-service-prefix="{{ .Values.syncCatalog.consulPrefix}}" \
{{- end}}
Expand Down
18 changes: 18 additions & 0 deletions charts/consul/test/unit/sync-catalog-deployment.bats
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,24 @@ load _helpers
[ "${actual}" = "true" ]
}

@test "syncCatalog/Deployment: container is created when global.acls.manageSystemACLs=true and has correct command with Partitions enabled" {
cd `chart_dir`
local object=$(helm template \
-s templates/sync-catalog-deployment.yaml \
--set 'syncCatalog.enabled=true' \
--set 'global.tls.enabled=true' \
--set 'global.enableConsulNamespaces=true' \
--set 'global.adminPartitions.enabled=true' \
--set 'global.adminPartitions.name=default' \
--set 'global.acls.manageSystemACLs=true' \
. | tee /dev/stderr |
yq '.spec.template.spec.containers[] | select(.name == "sync-catalog")' | tee /dev/stderr)

local actual=$(echo $object |
yq -r '.command | any(contains("-partition=default"))' | tee /dev/stderr)
[ "${actual}" = "true" ]
}

@test "syncCatalog/Deployment: init container is created when global.acls.manageSystemACLs=true and has correct command and environment with tls enabled and autoencrypt enabled" {
cd `chart_dir`
local object=$(helm template \
Expand Down
Loading