diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d8d9bedc..5269bc1eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ BREAKING CHANGES: FEATURES: * CRDs: add new CRD `IngressGateway` for configuring Consul's [ingress-gateway](https://www.consul.io/docs/agent/config-entries/ingress-gateway) config entry. [[GH-714](https://github.com/hashicorp/consul-helm/pull/714)] +* CRDs: add new CRD `TerminatingGateway` for configuring Consul's [terminating-gateway](https://www.consul.io/docs/agent/config-entries/terminating-gateway) config entry. [[GH-715](https://github.com/hashicorp/consul-helm/pull/715)] IMPROVEMENTS: * Make `server.bootstrapExpect` optional. If not set, will now default to `server.replicas`. diff --git a/templates/controller-clusterrole.yaml b/templates/controller-clusterrole.yaml index 6d9d70726..30910a821 100644 --- a/templates/controller-clusterrole.yaml +++ b/templates/controller-clusterrole.yaml @@ -20,6 +20,7 @@ rules: - servicesplitters - serviceintentions - ingressgateways + - terminatinggateways verbs: - create - delete @@ -38,6 +39,7 @@ rules: - servicesplitters/status - serviceintentions/status - ingressgateways/status + - terminatinggateways/status verbs: - get - patch diff --git a/templates/controller-mutatingwebhookconfiguration.yaml b/templates/controller-mutatingwebhookconfiguration.yaml index 8024067ee..a3504f21d 100644 --- a/templates/controller-mutatingwebhookconfiguration.yaml +++ b/templates/controller-mutatingwebhookconfiguration.yaml @@ -168,4 +168,26 @@ webhooks: resources: - ingressgateways sideEffects: None +- clientConfig: + caBundle: Cg== + service: + name: {{ template "consul.fullname" . }}-controller-webhook + namespace: {{ .Release.Namespace }} + path: /mutate-v1alpha1-terminatinggateway + failurePolicy: Fail + admissionReviewVersions: + - "v1beta1" + - "v1" + name: mutate-terminatinggateway.consul.hashicorp.com + rules: + - apiGroups: + - consul.hashicorp.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - terminatinggateways + sideEffects: None {{- end }} diff --git a/templates/crd-terminatinggateways.yaml b/templates/crd-terminatinggateways.yaml new file mode 100644 index 000000000..a4b11e298 --- /dev/null +++ b/templates/crd-terminatinggateways.yaml @@ -0,0 +1,117 @@ +{{- if .Values.controller.enabled }} +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: terminatinggateways.consul.hashicorp.com + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: crd +spec: + additionalPrinterColumns: + - JSONPath: .status.conditions[?(@.type=="Synced")].status + description: The sync status of the resource with Consul + name: Synced + type: string + - JSONPath: .metadata.creationTimestamp + description: The age of the resource + name: Age + type: date + group: consul.hashicorp.com + names: + kind: TerminatingGateway + listKind: TerminatingGatewayList + plural: terminatinggateways + singular: terminatinggateway + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + description: TerminatingGateway is the Schema for the terminatinggateways 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: TerminatingGatewaySpec defines the desired state of TerminatingGateway + properties: + services: + description: Services is a list of service names represented by the terminating gateway. + items: + description: A LinkedService is a service represented by a terminating gateway + properties: + caFile: + description: CAFile is the optional path to a CA certificate to use for TLS connections from the gateway to the linked service. + type: string + certFile: + description: CertFile is the optional path to a client certificate to use for TLS connections from the gateway to the linked service. + type: string + keyFile: + description: KeyFile is the optional path to a private key to use for TLS connections from the gateway to the linked service. + type: string + name: + description: Name is the name of the service, as defined in Consul's catalog. + type: string + namespace: + description: The namespace the service is registered in. + type: string + sni: + description: SNI is the optional name to specify during the TLS handshake with a linked service. + 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 }} diff --git a/test/acceptance/tests/controller/controller_namespaces_test.go b/test/acceptance/tests/controller/controller_namespaces_test.go index 50bd6fc4d..9c4c07847 100644 --- a/test/acceptance/tests/controller/controller_namespaces_test.go +++ b/test/acceptance/tests/controller/controller_namespaces_test.go @@ -185,6 +185,17 @@ func TestControllerNamespaces(t *testing.T) { require.Len(r, ingressGatewayEntry.Listeners[0].Services, 1) require.Equal(r, "foo", ingressGatewayEntry.Listeners[0].Services[0].Name) + // terminating-gateway + entry, _, err = consulClient.ConfigEntries().Get(api.TerminatingGateway, "terminating-gateway", queryOpts) + require.NoError(r, err) + terminatingGatewayEntry, ok := entry.(*api.TerminatingGatewayConfigEntry) + require.True(r, ok, "could not cast to TerminatingGatewayConfigEntry") + require.Len(r, terminatingGatewayEntry.Services, 1) + require.Equal(r, "name", terminatingGatewayEntry.Services[0].Name) + require.Equal(r, "caFile", terminatingGatewayEntry.Services[0].CAFile) + require.Equal(r, "certFile", terminatingGatewayEntry.Services[0].CertFile) + require.Equal(r, "keyFile", terminatingGatewayEntry.Services[0].KeyFile) + require.Equal(r, "sni", terminatingGatewayEntry.Services[0].SNI) }) } @@ -216,6 +227,10 @@ func TestControllerNamespaces(t *testing.T) { patchPort := 9090 k8s.RunKubectl(t, ctx.KubectlOptions(t), "patch", "-n", KubeNS, "ingressgateway", "ingress-gateway", "-p", fmt.Sprintf(`{"spec": {"listeners": [{"port": %d, "protocol": "tcp", "services": [{"name": "foo"}]}]}}`, patchPort), "--type=merge") + logger.Log(t, "patching terminating-gateway custom resource") + patchSNI := "patch-sni" + k8s.RunKubectl(t, ctx.KubectlOptions(t), "patch", "-n", KubeNS, "terminatinggateway", "terminating-gateway", "-p", fmt.Sprintf(`{"spec": {"services": [{"name":"name","caFile":"caFile","certFile":"certFile","keyFile":"keyFile","sni":"%s"}]}}`, patchSNI), "--type=merge") + counter := &retry.Counter{Count: 10, Wait: 500 * time.Millisecond} retry.RunWith(counter, t, func(r *retry.R) { // service-defaults @@ -268,6 +283,13 @@ func TestControllerNamespaces(t *testing.T) { ingressGatewayEntry, ok := entry.(*api.IngressGatewayConfigEntry) require.True(r, ok, "could not cast to IngressGatewayConfigEntry") require.Equal(r, patchPort, ingressGatewayEntry.Listeners[0].Port) + + // terminating-gateway + entry, _, err = consulClient.ConfigEntries().Get(api.TerminatingGateway, "terminating-gateway", queryOpts) + require.NoError(r, err) + terminatingGatewayEntry, ok := entry.(*api.TerminatingGatewayConfigEntry) + require.True(r, ok, "could not cast to TerminatingGatewayConfigEntry") + require.Equal(r, patchSNI, terminatingGatewayEntry.Services[0].SNI) }) } @@ -294,6 +316,9 @@ func TestControllerNamespaces(t *testing.T) { logger.Log(t, "deleting ingress-gateway custom resource") k8s.RunKubectl(t, ctx.KubectlOptions(t), "delete", "-n", KubeNS, "ingressgateway", "ingress-gateway") + logger.Log(t, "deleting terminating-gateway custom resource") + k8s.RunKubectl(t, ctx.KubectlOptions(t), "delete", "-n", KubeNS, "terminatinggateway", "terminating-gateway") + counter := &retry.Counter{Count: 10, Wait: 500 * time.Millisecond} retry.RunWith(counter, t, func(r *retry.R) { // service-defaults @@ -330,6 +355,11 @@ func TestControllerNamespaces(t *testing.T) { _, _, err = consulClient.ConfigEntries().Get(api.IngressGateway, "ingress-gateway", queryOpts) require.Error(r, err) require.Contains(r, err.Error(), "404 (Config entry not found") + + // terminating-gateway + _, _, err = consulClient.ConfigEntries().Get(api.IngressGateway, "terminating-gateway", 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 e04f3f46b..a152295ad 100644 --- a/test/acceptance/tests/controller/controller_test.go +++ b/test/acceptance/tests/controller/controller_test.go @@ -125,6 +125,18 @@ func TestController(t *testing.T) { require.Equal(r, 8080, ingressGatewayEntry.Listeners[0].Port) require.Len(r, ingressGatewayEntry.Listeners[0].Services, 1) require.Equal(r, "foo", ingressGatewayEntry.Listeners[0].Services[0].Name) + + // terminating-gateway + entry, _, err = consulClient.ConfigEntries().Get(api.TerminatingGateway, "terminating-gateway", nil) + require.NoError(r, err) + terminatingGatewayEntry, ok := entry.(*api.TerminatingGatewayConfigEntry) + require.True(r, ok, "could not cast to TerminatingGatewayConfigEntry") + require.Len(r, terminatingGatewayEntry.Services, 1) + require.Equal(r, "name", terminatingGatewayEntry.Services[0].Name) + require.Equal(r, "caFile", terminatingGatewayEntry.Services[0].CAFile) + require.Equal(r, "certFile", terminatingGatewayEntry.Services[0].CertFile) + require.Equal(r, "keyFile", terminatingGatewayEntry.Services[0].KeyFile) + require.Equal(r, "sni", terminatingGatewayEntry.Services[0].SNI) }) } @@ -156,6 +168,10 @@ func TestController(t *testing.T) { patchPort := 9090 k8s.RunKubectl(t, ctx.KubectlOptions(t), "patch", "ingressgateway", "ingress-gateway", "-p", fmt.Sprintf(`{"spec": {"listeners": [{"port": %d, "protocol": "tcp", "services": [{"name": "foo"}]}]}}`, patchPort), "--type=merge") + logger.Log(t, "patching terminating-gateway custom resource") + patchSNI := "patch-sni" + k8s.RunKubectl(t, ctx.KubectlOptions(t), "patch", "terminatinggateway", "terminating-gateway", "-p", fmt.Sprintf(`{"spec": {"services": [{"name":"name","caFile":"caFile","certFile":"certFile","keyFile":"keyFile","sni":"%s"}]}}`, patchSNI), "--type=merge") + counter := &retry.Counter{Count: 10, Wait: 500 * time.Millisecond} retry.RunWith(counter, t, func(r *retry.R) { // service-defaults @@ -209,6 +225,13 @@ func TestController(t *testing.T) { ingressGatewayEntry, ok := entry.(*api.IngressGatewayConfigEntry) require.True(r, ok, "could not cast to IngressGatewayConfigEntry") require.Equal(r, patchPort, ingressGatewayEntry.Listeners[0].Port) + + // terminating-gateway + entry, _, err = consulClient.ConfigEntries().Get(api.TerminatingGateway, "terminating-gateway", nil) + require.NoError(r, err) + terminatingGatewayEntry, ok := entry.(*api.TerminatingGatewayConfigEntry) + require.True(r, ok, "could not cast to TerminatingGatewayConfigEntry") + require.Equal(r, patchSNI, terminatingGatewayEntry.Services[0].SNI) }) } @@ -235,6 +258,9 @@ func TestController(t *testing.T) { logger.Log(t, "deleting ingress-gateway custom resource") k8s.RunKubectl(t, ctx.KubectlOptions(t), "delete", "ingressgateway", "ingress-gateway") + logger.Log(t, "deleting terminating-gateway custom resource") + k8s.RunKubectl(t, ctx.KubectlOptions(t), "delete", "terminatinggateway", "terminating-gateway") + counter := &retry.Counter{Count: 10, Wait: 500 * time.Millisecond} retry.RunWith(counter, t, func(r *retry.R) { // service-defaults @@ -271,6 +297,11 @@ func TestController(t *testing.T) { _, _, err = consulClient.ConfigEntries().Get(api.IngressGateway, "ingress-gateway", nil) require.Error(r, err) require.Contains(r, err.Error(), "404 (Config entry not found") + + // terminating-gateway + _, _, err = consulClient.ConfigEntries().Get(api.IngressGateway, "terminating-gateway", nil) + require.Error(r, err) + require.Contains(r, err.Error(), "404 (Config entry not found") }) } }) diff --git a/test/acceptance/tests/fixtures/crds/terminatinggateway.yaml b/test/acceptance/tests/fixtures/crds/terminatinggateway.yaml new file mode 100644 index 000000000..77333b201 --- /dev/null +++ b/test/acceptance/tests/fixtures/crds/terminatinggateway.yaml @@ -0,0 +1,11 @@ +apiVersion: consul.hashicorp.com/v1alpha1 +kind: TerminatingGateway +metadata: + name: terminating-gateway +spec: + services: + - name: name + caFile: "caFile" + certFile: "certFile" + keyFile: "keyFile" + sni: "sni" diff --git a/test/unit/crd-terminatinggateway.bats b/test/unit/crd-terminatinggateway.bats new file mode 100644 index 000000000..84ed725c9 --- /dev/null +++ b/test/unit/crd-terminatinggateway.bats @@ -0,0 +1,24 @@ +#!/usr/bin/env bats + +load _helpers + +@test "terminatingGateway/CustomerResourceDefinition: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/crd-terminatinggateways.yaml \ + . +} + +@test "terminatingGateway/CustomerResourceDefinition: enabled with controller.enabled=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/crd-terminatinggateways.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" ] +}