Skip to content

Commit

Permalink
Consul ent namespaces support for controller (hashicorp#601)
Browse files Browse the repository at this point in the history
* Consul ent namespaces support for controller
  • Loading branch information
lkysow authored Sep 16, 2020
1 parent 1954ea0 commit 1f3bf28
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 8 deletions.
3 changes: 2 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,11 @@ jobs:
gotestsum --junitfile "$TEST_RESULTS/gotestsum-report.xml" -- ./... -p 1 -timeout 20m -failfast \
-enable-enterprise \
-enable-multi-cluster \
-run TestControllerNamespaces\
-kubeconfig="$primary_kubeconfig" \
-secondary-kubeconfig="$secondary_kubeconfig" \
-debug-directory="$TEST_RESULTS/debug" \
-consul-k8s-image=hashicorpdev/consul-k8s:latest
-consul-k8s-image=hashicorpdev/consul-k8s:crd-controller-base-latest
- store_test_results:
path: /tmp/test-results
Expand Down
28 changes: 22 additions & 6 deletions templates/controller-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,28 @@ spec:
{{- end }}
{{- end }}
containers:
- args:
- controller
- --enable-leader-election
- --webhook-tls-cert-dir=/tmp/controller-webhook/certs
command:
- consul-k8s
- command:
- "/bin/sh"
- "-ec"
- |
consul-k8s controller \
--webhook-tls-cert-dir=/tmp/controller-webhook/certs \
--enable-leader-election \
{{- if .Values.global.enableConsulNamespaces }}
-enable-namespaces=true \
{{- if .Values.connectInject.consulNamespaces.consulDestinationNamespace }}
-consul-destination-namespace={{ .Values.connectInject.consulNamespaces.consulDestinationNamespace }} \
{{- end }}
{{- if .Values.connectInject.consulNamespaces.mirroringK8S }}
-enable-k8s-namespace-mirroring=true \
{{- if .Values.connectInject.consulNamespaces.mirroringK8SPrefix }}
-k8s-namespace-mirroring-prefix={{ .Values.connectInject.consulNamespaces.mirroringK8SPrefix }} \
{{- end }}
{{- end }}
{{- if .Values.global.acls.manageSystemACLs }}
-consul-cross-namespace-acl-policy=cross-namespace-policy \
{{- end }}
{{- end }}
env:
- name: HOST_IP
valueFrom:
Expand Down
12 changes: 11 additions & 1 deletion test/acceptance/helpers/kubectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func RunKubectlAndGetOutputWithLoggerE(t testing.TestingT, options *k8s.KubectlO
if options.ConfigPath != "" {
cmdArgs = append(cmdArgs, "--kubeconfig", options.ConfigPath)
}
if options.Namespace != "" {
if options.Namespace != "" && !sliceContains(args, "-n") && !sliceContains(args, "--namespace") {
cmdArgs = append(cmdArgs, "--namespace", options.Namespace)
}
cmdArgs = append(cmdArgs, args...)
Expand Down Expand Up @@ -80,3 +80,13 @@ func RunKubectl(t testing.TestingT, options *k8s.KubectlOptions, args ...string)
_, err := RunKubectlAndGetOutputE(t, options, args...)
require.NoError(t, err)
}

// sliceContains returns true if s contains target.
func sliceContains(s []string, target string) bool {
for _, elem := range s {
if elem == target {
return true
}
}
return false
}
162 changes: 162 additions & 0 deletions test/acceptance/tests/controller/controller_namespaces_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package controller

import (
"strconv"
"strings"
"testing"
"time"

"github.com/hashicorp/consul-helm/test/acceptance/framework"
"github.com/hashicorp/consul-helm/test/acceptance/helpers"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/stretchr/testify/require"
)

const (
KubeNS = "ns1"
ConsulDestNS = "from-k8s"
)

// Test that the controller works with Consul Enterprise namespaces.
// These tests currently only test non-secure and secure without auto-encrypt installations
// because in the case of namespaces there isn't a significant distinction in code between auto-encrypt
// and non-auto-encrypt secure installations, so testing just one is enough.
func TestControllerNamespaces(t *testing.T) {
cfg := suite.Config()
if !cfg.EnableEnterprise {
t.Skipf("skipping this test because -enable-enterprise is not set")
}

cases := []struct {
name string
destinationNamespace string
mirrorK8S bool
secure bool
}{
{
"single destination namespace (non-default)",
ConsulDestNS,
false,
false,
},
{
"single destination namespace (non-default); secure",
ConsulDestNS,
false,
true,
},
{
"mirror k8s namespaces",
KubeNS,
true,
false,
},
{
"mirror k8s namespaces; secure",
KubeNS,
true,
true,
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
ctx := suite.Environment().DefaultContext(t)

helmValues := map[string]string{
"global.enableConsulNamespaces": "true",
"controller.enabled": "true",
"connectInject.enabled": "true",

// When mirroringK8S is set, this setting is ignored.
"connectInject.consulNamespaces.consulDestinationNamespace": c.destinationNamespace,
"connectInject.consulNamespaces.mirroringK8S": strconv.FormatBool(c.mirrorK8S),

"global.acls.manageSystemACLs": strconv.FormatBool(c.secure),
"global.tls.enabled": strconv.FormatBool(c.secure),
}

releaseName := helpers.RandomName()
consulCluster := framework.NewHelmCluster(t, helmValues, ctx, cfg, releaseName)

consulCluster.Create(t)

t.Logf("creating namespace %q", KubeNS)
out, err := helpers.RunKubectlAndGetOutputE(t, ctx.KubectlOptions(), "create", "ns", KubeNS)
if err != nil && !strings.Contains(out, "(AlreadyExists)") {
require.NoError(t, err)
}
helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() {
helpers.RunKubectl(t, ctx.KubectlOptions(), "delete", "ns", KubeNS)
})

// Make sure that config entries are created in the correct namespace.
// If mirroring is enabled, we expect config entries to be created in the
// Consul namespace with the same name as their source
// Kubernetes namespace.
// If a single destination namespace is set, we expect all config entries
// to be created in that destination Consul namespace.
queryOpts := &api.QueryOptions{Namespace: KubeNS}
if !c.mirrorK8S {
queryOpts = &api.QueryOptions{Namespace: c.destinationNamespace}
}
consulClient := consulCluster.SetupConsulClient(t, c.secure)

// Test creation.
{
t.Log("creating service-defaults CRD")
retry.Run(t, func(r *retry.R) {
// Retry the kubectl apply because we've seen sporadic
// "connection refused" errors where the mutating webhook
// endpoint fails initially.
out, err := helpers.RunKubectlAndGetOutputE(t, ctx.KubectlOptions(), "apply", "-n", KubeNS, "-f", "../fixtures/crds")
require.NoError(r, err, out)
// NOTE: No need to clean up because the namespace will be deleted.
})

// On startup, the controller can take upwards of 6s to perform
// leader election so we may need to wait a long time for
// the reconcile loop to run (hence the 20s timeout here).
counter := &retry.Counter{Count: 20, Wait: 1 * time.Second}
retry.RunWith(counter, t, func(r *retry.R) {
entry, _, err := consulClient.ConfigEntries().Get(api.ServiceDefaults, "foo", queryOpts)
require.NoError(r, err, "ns: %s", queryOpts.Namespace)

svcDefaultEntry, ok := entry.(*api.ServiceConfigEntry)
require.True(r, ok, "could not cast to ServiceConfigEntry")
require.Equal(r, "http", svcDefaultEntry.Protocol)
})
}

// Test an update.
{
t.Log("patching service-defaults CRD")
helpers.RunKubectl(t, ctx.KubectlOptions(), "patch", "-n", KubeNS, "servicedefaults", "foo", "-p", `{"spec":{"protocol":"tcp"}}`, "--type=merge")

counter := &retry.Counter{Count: 10, Wait: 500 * time.Millisecond}
retry.RunWith(counter, t, func(r *retry.R) {
entry, _, err := consulClient.ConfigEntries().Get(api.ServiceDefaults, "foo", queryOpts)
require.NoError(r, err, "ns: %s", queryOpts.Namespace)

svcDefaultEntry, ok := entry.(*api.ServiceConfigEntry)
require.True(r, ok, "could not cast to ServiceConfigEntry")
require.Equal(r, "tcp", svcDefaultEntry.Protocol)
})
}

// Test a delete.
{
t.Log("deleting service-defaults CRD")
helpers.RunKubectl(t, ctx.KubectlOptions(), "delete", "-n", KubeNS, "servicedefaults", "foo")

counter := &retry.Counter{Count: 10, Wait: 500 * time.Millisecond}
retry.RunWith(counter, t, func(r *retry.R) {
_, _, err := consulClient.ConfigEntries().Get(api.ServiceDefaults, "foo", queryOpts)
require.Error(r, err)
require.Contains(r, err.Error(), "404 (Config entry not found")
})
}
})
}
}
15 changes: 15 additions & 0 deletions test/acceptance/tests/controller/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package controller

import (
"os"
"testing"

"github.com/hashicorp/consul-helm/test/acceptance/framework"
)

var suite framework.Suite

func TestMain(m *testing.M) {
suite = framework.NewSuite(m)
os.Exit(suite.Run())
}
1 change: 1 addition & 0 deletions test/acceptance/tests/example/example_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Rename package to your test package.
package example

import (
Expand Down
1 change: 1 addition & 0 deletions test/acceptance/tests/example/main_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Rename package to your test package.
package example

import (
Expand Down
6 changes: 6 additions & 0 deletions test/acceptance/tests/fixtures/crds/servicedefaults.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: consul.hashicorp.com/v1alpha1
kind: ServiceDefaults
metadata:
name: foo
spec:
protocol: "http"
Loading

0 comments on commit 1f3bf28

Please sign in to comment.