diff --git a/.circleci/config.yml b/.circleci/config.yml index 6e87f011903d..8ce82f19c967 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -140,7 +140,7 @@ jobs: -kubeconfig="$primary_kubeconfig" \ -secondary-kubeconfig="$secondary_kubeconfig" \ -debug-directory="$TEST_RESULTS/debug" \ - -consul-k8s-image=hashicorpdev/consul-k8s:crd-controller-base-latest \ + -consul-k8s-image=hashicorpdev/consul-k8s:crd-controller-base-latest - store_test_results: path: /tmp/test-results diff --git a/templates/controller-clusterrole.yaml b/templates/controller-clusterrole.yaml index dbc10680d892..2e403d5fc69f 100644 --- a/templates/controller-clusterrole.yaml +++ b/templates/controller-clusterrole.yaml @@ -18,6 +18,7 @@ rules: - proxydefaults - servicerouters - servicesplitters + - serviceintentions verbs: - create - delete @@ -34,6 +35,7 @@ rules: - proxydefaults/status - servicerouters/status - servicesplitters/status + - serviceintentions/status verbs: - get - patch diff --git a/templates/controller-mutatingwebhookconfiguration.yaml b/templates/controller-mutatingwebhookconfiguration.yaml index e332f7fc54f9..b01a16f7b52c 100644 --- a/templates/controller-mutatingwebhookconfiguration.yaml +++ b/templates/controller-mutatingwebhookconfiguration.yaml @@ -105,4 +105,23 @@ webhooks: resources: - servicesplitters sideEffects: None +- clientConfig: + caBundle: Cg== + service: + name: {{ template "consul.fullname" . }}-controller-webhook + namespace: {{ .Release.Namespace }} + path: /mutate-v1alpha1-serviceintentions + failurePolicy: Fail + name: mutate-serviceintentions.consul.hashicorp.com + rules: + - apiGroups: + - consul.hashicorp.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - serviceintentions + sideEffects: None {{- end }} diff --git a/templates/crd-serviceintentions.yaml b/templates/crd-serviceintentions.yaml new file mode 100644 index 000000000000..b51fb3810934 --- /dev/null +++ b/templates/crd-serviceintentions.yaml @@ -0,0 +1,98 @@ +{{- if .Values.controller.enabled }} +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: serviceintentions.consul.hashicorp.com +spec: + group: consul.hashicorp.com + names: + kind: ServiceIntentions + listKind: ServiceIntentionsList + plural: serviceintentions + singular: serviceintentions + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + description: ServiceIntentions is the Schema for the serviceintentions API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ServiceIntentionsSpec defines the desired state of ServiceIntentions + properties: + destination: + properties: + name: + type: string + namespace: + type: string + type: object + sources: + items: + properties: + action: + description: IntentionAction is the action that the intention represents. This can be "allow" or "deny" to allowlist or denylist intentions. + type: string + description: + type: string + name: + type: string + namespace: + type: string + type: object + type: array + type: object + status: + properties: + conditions: + description: Conditions indicate the latest available observations of a resource's current state. + items: + description: 'Conditions define a readiness condition for a Consul resource. See: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties' + properties: + lastTransitionTime: + description: LastTransitionTime is the last time the condition transitioned from one status to another. + format: date-time + type: string + message: + description: A human readable message indicating details about the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition. + type: string + required: + - status + - type + type: object + type: array + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +{{- end }} \ No newline at end of file diff --git a/test/acceptance/go.mod b/test/acceptance/go.mod index c4d8419f2d20..dfe7cb9cc5fd 100644 --- a/test/acceptance/go.mod +++ b/test/acceptance/go.mod @@ -4,7 +4,7 @@ go 1.14 require ( github.com/gruntwork-io/terratest v0.29.0 - github.com/hashicorp/consul/api v1.6.0 + github.com/hashicorp/consul/api v1.4.1-0.20201006211533-eabba09b6633 github.com/hashicorp/consul/sdk v0.6.0 github.com/stretchr/testify v1.5.1 gopkg.in/yaml.v2 v2.2.8 diff --git a/test/acceptance/go.sum b/test/acceptance/go.sum index 26a102f1b554..1c6012d6e907 100644 --- a/test/acceptance/go.sum +++ b/test/acceptance/go.sum @@ -222,8 +222,8 @@ github.com/gruntwork-io/gruntwork-cli v0.5.1 h1:mVmVsFubUSLSCO8bGigI63HXzvzkC0uW github.com/gruntwork-io/gruntwork-cli v0.5.1/go.mod h1:IBX21bESC1/LGoV7jhXKUnTQTZgQ6dYRsoj/VqxUSZQ= github.com/gruntwork-io/terratest v0.29.0 h1:EyPLLxglZIHJ0jU1cbx2NJT7A3MVEmPle8ENWDDwVAA= github.com/gruntwork-io/terratest v0.29.0/go.mod h1:aVz7181EP4okz7LMx6BLpiF7bL8wkq+h57V6uicvoc0= -github.com/hashicorp/consul/api v1.6.0 h1:SZB2hQW8AcTOpfDmiVblQbijxzsRuiyy0JpHfabvHio= -github.com/hashicorp/consul/api v1.6.0/go.mod h1:1NSuaUUkFaJzMasbfq/11wKYWSR67Xn6r2DXKhuDNFg= +github.com/hashicorp/consul/api v1.4.1-0.20201006211533-eabba09b6633 h1:7Isk6LXOQLgzLc+77snDnR9jddDkTxbm4cAW1eCfc3c= +github.com/hashicorp/consul/api v1.4.1-0.20201006211533-eabba09b6633/go.mod h1:1NSuaUUkFaJzMasbfq/11wKYWSR67Xn6r2DXKhuDNFg= github.com/hashicorp/consul/sdk v0.6.0 h1:FfhMEkwvQl57CildXJyGHnwGGM4HMODGyfjGwNM1Vdw= github.com/hashicorp/consul/sdk v0.6.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= diff --git a/test/acceptance/tests/controller/controller_namespaces_test.go b/test/acceptance/tests/controller/controller_namespaces_test.go index cd11550be35f..1ccac1a0a952 100644 --- a/test/acceptance/tests/controller/controller_namespaces_test.go +++ b/test/acceptance/tests/controller/controller_namespaces_test.go @@ -18,6 +18,11 @@ const ( KubeNS = "ns1" ConsulDestNS = "from-k8s" DefaultConsulNamespace = "default" + + // The name of a service intention in consul is + // the name of the destination service and is not + // the same as the kube name of the resource. + IntentionName = "svc1" ) // Test that the controller works with Consul Enterprise namespaces. @@ -25,6 +30,7 @@ const ( // 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) { + t.Skip() cfg := suite.Config() if !cfg.EnableEnterprise { t.Skipf("skipping this test because -enable-enterprise is not set") @@ -162,6 +168,13 @@ func TestControllerNamespaces(t *testing.T) { svcSplitterEntry, ok := entry.(*api.ServiceSplitterConfigEntry) require.True(r, ok, "could not cast to ServiceSplitterConfigEntry") require.Equal(r, float32(100), svcSplitterEntry.Splits[0].Weight) + + // service-intentions + entry, _, err = consulClient.ConfigEntries().Get(api.ServiceIntentions, IntentionName, queryOpts) + require.NoError(r, err) + svcIntentions, ok := entry.(*api.ServiceIntentionsConfigEntry) + require.True(r, ok, "could not cast to ServiceSplitterConfigEntry") + require.Equal(r, api.IntentionActionAllow, svcIntentions.Sources[0].Action) }) } @@ -186,6 +199,9 @@ func TestControllerNamespaces(t *testing.T) { t.Log("patching service-splitter custom resource") helpers.RunKubectl(t, ctx.KubectlOptions(t), "patch", "-n", KubeNS, "servicesplitter", "splitter", "-p", `{"spec": {"splits": [{"weight": 50}, {"weight": 50, "service": "other-splitter"}]}}`, "--type=merge") + t.Log("patching service-intentions custom resource") + helpers.RunKubectl(t, ctx.KubectlOptions(t), "patch", "-n", KubeNS, "serviceintentions", "intentions", "-p", `{"spec": {"sources": [{"name": "svc2", "action": "deny"}]}}`, "--type=merge") + counter := &retry.Counter{Count: 10, Wait: 500 * time.Millisecond} retry.RunWith(counter, t, func(r *retry.R) { // service-defaults @@ -224,6 +240,13 @@ func TestControllerNamespaces(t *testing.T) { require.Equal(r, float32(50), svcSplitter.Splits[0].Weight) require.Equal(r, float32(50), svcSplitter.Splits[1].Weight) require.Equal(r, "other-splitter", svcSplitter.Splits[1].Service) + + // service-intentions + entry, _, err = consulClient.ConfigEntries().Get(api.ServiceIntentions, IntentionName, queryOpts) + require.NoError(r, err) + svcIntentions, ok := entry.(*api.ServiceIntentionsConfigEntry) + require.True(r, ok, "could not cast to ServiceIntentionsConfigEntry") + require.Equal(r, api.IntentionActionDeny, svcIntentions.Sources[0].Action) }) } @@ -244,6 +267,9 @@ func TestControllerNamespaces(t *testing.T) { t.Log("deleting service-splitter custom resource") helpers.RunKubectl(t, ctx.KubectlOptions(t), "delete", "-n", KubeNS, "servicesplitter", "splitter") + t.Log("deleting service-intentions custom resource") + helpers.RunKubectl(t, ctx.KubectlOptions(t), "delete", "-n", KubeNS, "serviceintentions", "intentions") + counter := &retry.Counter{Count: 10, Wait: 500 * time.Millisecond} retry.RunWith(counter, t, func(r *retry.R) { // service-defaults @@ -270,6 +296,11 @@ func TestControllerNamespaces(t *testing.T) { _, _, err = consulClient.ConfigEntries().Get(api.ServiceSplitter, "splitter", queryOpts) require.Error(r, err) require.Contains(r, err.Error(), "404 (Config entry not found") + + // service-intentions + _, _, err = consulClient.ConfigEntries().Get(api.ServiceIntentions, IntentionName, queryOpts) + require.Error(r, err) + require.Contains(r, err.Error(), "404 (Config entry not found") }) } }) diff --git a/test/acceptance/tests/controller/controller_test.go b/test/acceptance/tests/controller/controller_test.go index 45b6aea7faa6..0c7e45363493 100644 --- a/test/acceptance/tests/controller/controller_test.go +++ b/test/acceptance/tests/controller/controller_test.go @@ -25,6 +25,11 @@ func TestController(t *testing.T) { {true, true}, } + // The name of a service intention in consul is + // the name of the destination service and is not + // the same as the kube name of the resource. + const IntentionName = "svc1" + for _, c := range cases { name := fmt.Sprintf("secure: %t; auto-encrypt: %t", c.secure, c.autoEncrypt) t.Run(name, func(t *testing.T) { @@ -102,6 +107,13 @@ func TestController(t *testing.T) { svcSplitterEntry, ok := entry.(*api.ServiceSplitterConfigEntry) require.True(r, ok, "could not cast to ServiceSplitterConfigEntry") require.Equal(r, float32(100), svcSplitterEntry.Splits[0].Weight) + + // service-intentions + entry, _, err = consulClient.ConfigEntries().Get(api.ServiceIntentions, IntentionName, nil) + require.NoError(r, err) + svcIntentionsEntry, ok := entry.(*api.ServiceIntentionsConfigEntry) + require.True(r, ok, "could not cast to ServiceIntentionsConfigEntry") + require.Equal(r, api.IntentionActionAllow, svcIntentionsEntry.Sources[0].Action) }) } @@ -126,6 +138,9 @@ func TestController(t *testing.T) { t.Log("patching service-splitter custom resource") helpers.RunKubectl(t, ctx.KubectlOptions(t), "patch", "servicesplitter", "splitter", "-p", `{"spec": {"splits": [{"weight": 50}, {"weight": 50, "service": "other-splitter"}]}}`, "--type=merge") + t.Log("patching service-intentions custom resource") + helpers.RunKubectl(t, ctx.KubectlOptions(t), "patch", "serviceintentions", "intentions", "-p", `{"spec": {"sources": [{"name": "svc2", "action": "deny"}]}}`, "--type=merge") + counter := &retry.Counter{Count: 10, Wait: 500 * time.Millisecond} retry.RunWith(counter, t, func(r *retry.R) { // service-defaults @@ -164,6 +179,13 @@ func TestController(t *testing.T) { require.Equal(r, float32(50), svcSplitter.Splits[0].Weight) require.Equal(r, float32(50), svcSplitter.Splits[1].Weight) require.Equal(r, "other-splitter", svcSplitter.Splits[1].Service) + + // service-intentions + entry, _, err = consulClient.ConfigEntries().Get(api.ServiceIntentions, IntentionName, nil) + require.NoError(r, err) + svcIntentions, ok := entry.(*api.ServiceIntentionsConfigEntry) + require.True(r, ok, "could not cast to ServiceIntentionsConfigEntry") + require.Equal(r, api.IntentionActionDeny, svcIntentions.Sources[0].Action) }) } @@ -184,6 +206,9 @@ func TestController(t *testing.T) { t.Log("deleting service-splitter custom resource") helpers.RunKubectl(t, ctx.KubectlOptions(t), "delete", "servicesplitter", "splitter") + t.Log("deleting service-intentions custom resource") + helpers.RunKubectl(t, ctx.KubectlOptions(t), "delete", "serviceintentions", "intentions") + counter := &retry.Counter{Count: 10, Wait: 500 * time.Millisecond} retry.RunWith(counter, t, func(r *retry.R) { // service-defaults @@ -210,6 +235,11 @@ func TestController(t *testing.T) { _, _, err = consulClient.ConfigEntries().Get(api.ServiceSplitter, "splitter", nil) require.Error(r, err) require.Contains(r, err.Error(), "404 (Config entry not found") + + // service-intentions + _, _, err = consulClient.ConfigEntries().Get(api.ServiceIntentions, IntentionName, nil) + require.Error(r, err) + require.Contains(r, err.Error(), "404 (Config entry not found") }) } }) diff --git a/test/acceptance/tests/fixtures/crds/serviceintentions.yaml b/test/acceptance/tests/fixtures/crds/serviceintentions.yaml new file mode 100644 index 000000000000..ec98bee2af28 --- /dev/null +++ b/test/acceptance/tests/fixtures/crds/serviceintentions.yaml @@ -0,0 +1,20 @@ +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceDefaults +metadata: + name: svc1 +--- +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceDefaults +metadata: + name: svc2 +--- +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceIntentions +metadata: + name: intentions +spec: + destination: + name: svc1 + sources: + - name: svc2 + action: allow diff --git a/test/unit/crd-serviceintentions.bats b/test/unit/crd-serviceintentions.bats new file mode 100644 index 000000000000..8f699d165eb1 --- /dev/null +++ b/test/unit/crd-serviceintentions.bats @@ -0,0 +1,24 @@ +#!/usr/bin/env bats + +load _helpers + +@test "serviceintentions/CustomResourceDefinitions: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/crd-serviceintentions.yaml \ + . +} + +@test "serviceintentions/CustomResourceDefinitions: enabled with controller.enabled=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/crd-serviceintentions.yaml \ + --set 'controller.enabled=true' \ + . | tee /dev/stderr | + # The generated CRDs have "---" at the top which results in two objects + # being detected by yq, the first of which is null. We must therefore use + # yq -s so that length operates on both objects at once rather than + # individually, which would output false\ntrue and fail the test. + yq -s 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +}