diff --git a/charts/consul/templates/api-gateway-controller-clusterrole.yaml b/charts/consul/templates/api-gateway-controller-clusterrole.yaml new file mode 100644 index 0000000000..c6a69106c8 --- /dev/null +++ b/charts/consul/templates/api-gateway-controller-clusterrole.yaml @@ -0,0 +1,211 @@ +{{- if .Values.apiGateway.enabled }} +# The ClusterRole to enable the API Gateway controller to access required api endpoints. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ template "consul.fullname" . }}-api-gateway-controller + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: api-gateway-controller +rules: +- apiGroups: + - api-gateway.consul.hashicorp.com + resources: + - gatewayclassconfigs + verbs: + - get + - list + - update + - watch +- apiGroups: + - api-gateway.consul.hashicorp.com + resources: + - gatewayclassconfigs/finalizers + verbs: + - update +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - get + - list + - watch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - list + - update +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - patch + - update +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - pods + verbs: + - list + - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - services + verbs: + - create + - get + - list + - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - get + - list + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - gatewayclasses + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - gatewayclasses/finalizers + verbs: + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - gatewayclasses/status + verbs: + - get + - patch + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - gateways + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - gateways/finalizers + verbs: + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - gateways/status + verbs: + - get + - patch + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - httproutes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - httproutes/finalizers + verbs: + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - httproutes/status + verbs: + - get + - patch + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - tcproutes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - tcproutes/finalizers + verbs: + - update +- apiGroups: + - gateway.networking.k8s.io + resources: + - tcproutes/status + verbs: + - get + - patch + - update +{{- end }} diff --git a/charts/consul/templates/api-gateway-controller-clusterrolebinding.yaml b/charts/consul/templates/api-gateway-controller-clusterrolebinding.yaml new file mode 100644 index 0000000000..d083a08129 --- /dev/null +++ b/charts/consul/templates/api-gateway-controller-clusterrolebinding.yaml @@ -0,0 +1,20 @@ +{{- if .Values.apiGateway.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ template "consul.fullname" . }}-api-gateway-controller + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: api-gateway-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ template "consul.fullname" . }}-api-gateway-controller +subjects: +- kind: ServiceAccount + name: {{ template "consul.fullname" . }}-api-gateway-controller + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/charts/consul/templates/api-gateway-controller-deployment.yaml b/charts/consul/templates/api-gateway-controller-deployment.yaml new file mode 100644 index 0000000000..aee171e6da --- /dev/null +++ b/charts/consul/templates/api-gateway-controller-deployment.yaml @@ -0,0 +1,148 @@ +{{- if .Values.apiGateway.enabled }} +{{- if not .Values.client.grpc }}{{ fail "client.grpc must be true for api gateway" }}{{ end }} +{{- if not .Values.apiGateway.image}}{{ fail "apiGateway.image must be set to enable api gateway" }}{{ end }} +{{- if and .Values.global.adminPartitions.enabled (not .Values.global.enableConsulNamespaces) }}{{ fail "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" }}{{ end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "consul.fullname" . }}-api-gateway-controller + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: api-gateway-controller +spec: + replicas: {{ .Values.apiGateway.controller.replicas }} + selector: + matchLabels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + release: {{ .Release.Name }} + component: api-gateway-controller + template: + metadata: + annotations: + consul.hashicorp.com/connect-inject: "false" + {{- if (and .Values.global.secretsBackend.vault.enabled .Values.global.tls.enabled) }} + "vault.hashicorp.com/agent-init-first": "true" + "vault.hashicorp.com/agent-inject": "true" + "vault.hashicorp.com/role": {{ .Values.global.secretsBackend.vault.consulCARole }} + "vault.hashicorp.com/agent-inject-secret-serverca.crt": {{ .Values.global.tls.caCert.secretName }} + "vault.hashicorp.com/agent-inject-template-serverca.crt": {{ template "consul.serverTLSCATemplate" . }} + {{- if and .Values.global.secretsBackend.vault.ca.secretName .Values.global.secretsBackend.vault.ca.secretKey }} + "vault.hashicorp.com/agent-extra-secret": "{{ .Values.global.secretsBackend.vault.ca.secretName }}" + "vault.hashicorp.com/ca-cert": "/vault/custom/{{ .Values.global.secretsBackend.vault.ca.secretKey }}" + {{- end }} + {{- end }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + release: {{ .Release.Name }} + component: api-gateway-controller + spec: + serviceAccountName: {{ template "consul.fullname" . }}-api-gateway-controller + containers: + - name: api-gateway-controller + image: {{ .Values.apiGateway.image }} + ports: + - containerPort: 9090 + name: sds + protocol: TCP + env: + {{- if .Values.global.tls.enabled }} + - name: CONSUL_CACERT + value: /consul/tls/ca/tls.crt + {{- end }} + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: IP + valueFrom: + fieldRef: + fieldPath: status.podIP + {{- if .Values.global.acls.manageSystemACLs }} + - name: CONSUL_HTTP_TOKEN + valueFrom: + secretKeyRef: + name: "{{ template "consul.fullname" . }}-api-gateway-controller-acl-token" + key: "token" + {{- end }} + - name: CONSUL_HTTP_ADDR + {{- if .Values.global.tls.enabled }} + value: https://$(HOST_IP):8501 + {{- else }} + value: http://$(HOST_IP):8500 + {{- end }} + command: + - "/bin/sh" + - "-ec" + - | + consul-api-gateway server \ + -sds-server-host $(IP) \ + -k8s-namespace {{ .Release.Namespace }} \ + -log-level {{ default .Values.global.logLevel .Values.apiGateway.logLevel }} \ + volumeMounts: + {{- if .Values.global.tls.enabled }} + {{- if .Values.global.tls.enableAutoEncrypt }} + - name: consul-auto-encrypt-ca-cert + {{- else }} + - name: consul-ca-cert + {{- end }} + mountPath: /consul/tls/ca + readOnly: true + {{- end }} + volumes: + {{- if .Values.global.tls.enabled }} + {{- if not (and .Values.externalServers.enabled .Values.externalServers.useSystemRoots) }} + - name: consul-ca-cert + secret: + {{- if .Values.global.tls.caCert.secretName }} + secretName: {{ .Values.global.tls.caCert.secretName }} + {{- else }} + secretName: {{ template "consul.fullname" . }}-ca-cert + {{- end }} + items: + - key: {{ default "tls.crt" .Values.global.tls.caCert.secretKey }} + path: tls.crt + {{- end }} + {{- if .Values.global.tls.enableAutoEncrypt }} + - name: consul-auto-encrypt-ca-cert + emptyDir: + medium: "Memory" + {{- end }} + {{- end }} + {{- if or (and .Values.global.acls.manageSystemACLs) (and .Values.global.tls.enabled .Values.global.tls.enableAutoEncrypt) }} + initContainers: + {{- if .Values.global.acls.manageSystemACLs }} + - name: api-gateway-controller-acl-init + image: {{ .Values.global.imageK8S }} + command: + - "/bin/sh" + - "-ec" + - | + consul-k8s-control-plane acl-init \ + -secret-name="{{ template "consul.fullname" . }}-api-gateway-controller-acl-token" \ + -k8s-namespace={{ .Release.Namespace }} + resources: + requests: + memory: "25Mi" + cpu: "50m" + limits: + memory: "25Mi" + cpu: "50m" + {{- end }} + {{- if (and .Values.global.tls.enabled .Values.global.tls.enableAutoEncrypt) }} + {{- include "consul.getAutoEncryptClientCA" . | nindent 6 }} + {{- end }} + {{- end }} + {{- if .Values.apiGateway.controller.priorityClassName }} + priorityClassName: {{ .Values.apiGateway.controller.priorityClassName | quote }} + {{- end }} + {{- if .Values.apiGateway.controller.nodeSelector }} + nodeSelector: + {{ tpl .Values.apiGateway.controller.nodeSelector . | indent 8 | trim }} + {{- end }} +{{- end }} diff --git a/charts/consul/templates/api-gateway-controller-service.yaml b/charts/consul/templates/api-gateway-controller-service.yaml new file mode 100644 index 0000000000..aa79ff9fc3 --- /dev/null +++ b/charts/consul/templates/api-gateway-controller-service.yaml @@ -0,0 +1,27 @@ +{{- if .Values.apiGateway.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "consul.fullname" . }}-api-gateway-controller + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: api-gateway-controller + annotations: + {{- if .Values.apiGateway.controller.service.annotations }} + {{ tpl .Values.apiGateway.controller.service.annotations . | nindent 4 | trim }} + {{- end }} +spec: + ports: + - name: sds + port: 9090 + protocol: TCP + targetPort: 9090 + selector: + app: {{ template "consul.name" . }} + release: "{{ .Release.Name }}" + component: api-gateway-controller +{{- end }} diff --git a/charts/consul/templates/api-gateway-controller-serviceaccount.yaml b/charts/consul/templates/api-gateway-controller-serviceaccount.yaml new file mode 100644 index 0000000000..98292a8dbe --- /dev/null +++ b/charts/consul/templates/api-gateway-controller-serviceaccount.yaml @@ -0,0 +1,23 @@ +{{- if .Values.apiGateway.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "consul.fullname" . }}-api-gateway-controller + namespace: {{ .Release.Namespace }} + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: api-gateway-controller + {{- if .Values.apiGateway.serviceAccount.annotations }} + annotations: + {{ tpl .Values.apiGateway.serviceAccount.annotations . | nindent 4 | trim }} + {{- end }} +{{- with .Values.global.imagePullSecrets }} +imagePullSecrets: +{{- range . }} + - name: {{ .name }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/consul/templates/api-gateway-gatewayclass.yaml b/charts/consul/templates/api-gateway-gatewayclass.yaml new file mode 100644 index 0000000000..d9ba85e633 --- /dev/null +++ b/charts/consul/templates/api-gateway-gatewayclass.yaml @@ -0,0 +1,18 @@ +{{- if (and .Values.apiGateway.enabled .Values.apiGateway.managedGatewayClass.enabled) }} +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: GatewayClass +metadata: + name: consul-api-gateway + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: api-gateway-controller +spec: + controllerName: hashicorp.com/consul-api-gateway-controller + parametersRef: + group: api-gateway.consul.hashicorp.com + kind: GatewayClassConfig + name: consul-api-gateway +{{- end }} diff --git a/charts/consul/templates/api-gateway-gatewayclassconfig.yaml b/charts/consul/templates/api-gateway-gatewayclassconfig.yaml new file mode 100644 index 0000000000..d0e6e453ae --- /dev/null +++ b/charts/consul/templates/api-gateway-gatewayclassconfig.yaml @@ -0,0 +1,46 @@ +{{- if (and .Values.apiGateway.enabled .Values.apiGateway.managedGatewayClass.enabled) }} +apiVersion: api-gateway.consul.hashicorp.com/v1alpha1 +kind: GatewayClassConfig +metadata: + name: consul-api-gateway + labels: + app: {{ template "consul.name" . }} + chart: {{ template "consul.chart" . }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} + component: api-gateway +spec: + consul: + authentication: + {{- if .Values.global.acls.manageSystemACLs }} + managed: true + method: {{ template "consul.fullname" . }}-k8s-auth-method + {{- end }} + {{- if .Values.global.tls.enabled }} + scheme: https + {{- else }} + scheme: http + {{- end }} + ports: + grpc: 8502 + {{- if .Values.global.tls.enabled }} + http: 8501 + {{- else }} + http: 8500 + {{- end }} + image: + consulAPIGateway: {{ .Values.apiGateway.image }} + envoy: {{ .Values.global.imageEnvoy }} + {{- if .Values.apiGateway.nodeSelector }} + nodeSelector: + {{ tpl .Values.apiGateway.managedGatewayClass.nodeSelector . | indent 4 | trim }} + {{- end }} + {{- if .Values.apiGateway.managedGatewayClass.copyAnnotations.service }} + copyAnnotations: + service: + {{ tpl .Values.apiGateway.managedGatewayClass.copyAnnotations.service.annotations . | nindent 6 | trim }} + {{- end }} + serviceType: {{ .Values.apiGateway.managedGatewayClass.serviceType }} + useHostPorts: {{ .Values.apiGateway.managedGatewayClass.useHostPorts }} + logLevel: {{ default .Values.global.logLevel .Values.apiGateway.managedGatewayClass.logLevel }} +{{- end }} diff --git a/charts/consul/templates/server-acl-init-job.yaml b/charts/consul/templates/server-acl-init-job.yaml index 95758c5c77..191338d41d 100644 --- a/charts/consul/templates/server-acl-init-job.yaml +++ b/charts/consul/templates/server-acl-init-job.yaml @@ -237,6 +237,10 @@ spec: -create-controller-token=true \ {{- end }} + {{- if .Values.apiGateway.enabled }} + -create-api-gateway-token=true \ + {{- end }} + {{- if .Values.global.enableConsulNamespaces }} -enable-namespaces=true \ diff --git a/charts/consul/test/unit/api-gateway-controller-clusterrole.bats b/charts/consul/test/unit/api-gateway-controller-clusterrole.bats new file mode 100644 index 0000000000..a3edec027d --- /dev/null +++ b/charts/consul/test/unit/api-gateway-controller-clusterrole.bats @@ -0,0 +1,21 @@ +#!/usr/bin/env bats + +load _helpers + +@test "apiGateway/ClusterRole: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-clusterrole.yaml \ + . +} + +@test "apiGateway/ClusterRole: enabled with apiGateway.enabled=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-clusterrole.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} diff --git a/charts/consul/test/unit/api-gateway-controller-clusterrolebinding.bats b/charts/consul/test/unit/api-gateway-controller-clusterrolebinding.bats new file mode 100644 index 0000000000..3dfd94c36f --- /dev/null +++ b/charts/consul/test/unit/api-gateway-controller-clusterrolebinding.bats @@ -0,0 +1,22 @@ +#!/usr/bin/env bats + +load _helpers + +@test "apiGateway/ClusterRoleBinding: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-clusterrolebinding.yaml \ + . +} + +@test "apiGateway/ClusterRoleBinding: enabled with global.enabled false" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-clusterrolebinding.yaml \ + --set 'global.enabled=false' \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq -s 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} diff --git a/charts/consul/test/unit/api-gateway-controller-deployment.bats b/charts/consul/test/unit/api-gateway-controller-deployment.bats new file mode 100755 index 0000000000..434c7dbd7b --- /dev/null +++ b/charts/consul/test/unit/api-gateway-controller-deployment.bats @@ -0,0 +1,462 @@ +#!/usr/bin/env bats + +load _helpers + +@test "apiGateway/Deployment: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + . +} + +@test "apiGateway/Deployment: fails if no image is set" { + cd `chart_dir` + run helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "apiGateway.image must be set to enable api gateway" ]] +} + +@test "apiGateway/Deployment: disable with apiGateway.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=false' \ + . +} + +@test "apiGateway/Deployment: disable with global.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'global.enabled=false' \ + . +} + + +@test "apiGateway/Deployment: container image overrides" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=bar' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].image' | tee /dev/stderr) + [ "${actual}" = "\"bar\"" ] +} + +#-------------------------------------------------------------------- +# nodeSelector + +@test "apiGateway/Deployment: nodeSelector is not set by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq '.spec.template.spec.nodeSelector' | tee /dev/stderr) + [ "${actual}" = "null" ] +} + +@test "apiGateway/Deployment: specified nodeSelector" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'apiGateway.controller.nodeSelector=testing' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.nodeSelector' | tee /dev/stderr) + [ "${actual}" = "testing" ] +} + +#-------------------------------------------------------------------- +# global.tls.enabled + +@test "apiGateway/Deployment: Adds tls-ca-cert volume when global.tls.enabled is true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.volumes[] | select(.name == "consul-ca-cert")' | tee /dev/stderr) + [ "${actual}" != "" ] +} + +@test "apiGateway/Deployment: Adds tls-ca-cert volumeMounts when global.tls.enabled is true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].volumeMounts[] | select(.name == "consul-ca-cert")' | tee /dev/stderr) + [ "${actual}" != "" ] +} + +@test "apiGateway/Deployment: can overwrite CA secret with the provided one" { + cd `chart_dir` + local ca_cert_volume=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.caCert.secretName=foo-ca-cert' \ + --set 'global.tls.caCert.secretKey=key' \ + --set 'global.tls.caKey.secretName=foo-ca-key' \ + --set 'global.tls.caKey.secretKey=key' \ + . | tee /dev/stderr | + yq '.spec.template.spec.volumes[] | select(.name=="consul-ca-cert")' | tee /dev/stderr) + + # check that the provided ca cert secret is attached as a volume + local actual + actual=$(echo $ca_cert_volume | jq -r '.secret.secretName' | tee /dev/stderr) + [ "${actual}" = "foo-ca-cert" ] + + # check that the volume uses the provided secret key + actual=$(echo $ca_cert_volume | jq -r '.secret.items[0].key' | tee /dev/stderr) + [ "${actual}" = "key" ] +} + +#-------------------------------------------------------------------- +# global.tls.enableAutoEncrypt + +@test "apiGateway/Deployment: consul-auto-encrypt-ca-cert volume is added when TLS with auto-encrypt is enabled" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.volumes[] | select(.name == "consul-auto-encrypt-ca-cert") | length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Deployment: consul-auto-encrypt-ca-cert volumeMount is added when TLS with auto-encrypt is enabled" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].volumeMounts[] | select(.name == "consul-auto-encrypt-ca-cert") | length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Deployment: get-auto-encrypt-client-ca init container is created when TLS with auto-encrypt is enabled" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.initContainers[] | select(.name == "get-auto-encrypt-client-ca") | length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Deployment: adds both init containers when TLS with auto-encrypt and ACLs + namespaces are enabled" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.enableConsulNamespaces=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.initContainers | length == 2' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Deployment: consul-ca-cert volume is not added if externalServers.enabled=true and externalServers.useSystemRoots=true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'externalServers.enabled=true' \ + --set 'externalServers.hosts[0]=foo.com' \ + --set 'externalServers.useSystemRoots=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.volumes[] | select(.name == "consul-ca-cert")' | tee /dev/stderr) + [ "${actual}" = "" ] +} + +#-------------------------------------------------------------------- +# global.acls.manageSystemACLs + +@test "apiGateway/Deployment: CONSUL_HTTP_TOKEN env variable created when global.acls.manageSystemACLs=true" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.acls.manageSystemACLs=true' \ + . | tee /dev/stderr | + yq '[.spec.template.spec.containers[0].env[].name] ' | tee /dev/stderr) + + local actual=$(echo $object | + yq 'any(contains("CONSUL_HTTP_TOKEN"))' | tee /dev/stderr) + [ "${actual}" = "true" ] + + local actual=$(echo $object | + yq 'map(select(test("CONSUL_HTTP_TOKEN"))) | length' | tee /dev/stderr) + [ "${actual}" = "1" ] +} + +@test "apiGateway/Deployment: init container is created when global.acls.manageSystemACLs=true" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.acls.manageSystemACLs=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.initContainers[0]' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.name' | tee /dev/stderr) + [ "${actual}" = "api-gateway-controller-acl-init" ] + + local actual=$(echo $object | + yq -r '.command | any(contains("consul-k8s-control-plane acl-init"))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +#-------------------------------------------------------------------- +# priorityClassName + +@test "apiGateway/Deployment: no priorityClassName by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.priorityClassName' | tee /dev/stderr) + + [ "${actual}" = "null" ] +} + +@test "apiGateway/Deployment: can set a priorityClassName" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'apiGateway.controller.priorityClassName=name' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.priorityClassName' | tee /dev/stderr) + + [ "${actual}" = "name" ] +} + +#-------------------------------------------------------------------- +# logLevel + +@test "apiGateway/Deployment: logLevel info by default from global" { + cd `chart_dir` + local cmd=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command' | tee /dev/stderr) + + local actual=$(echo "$cmd" | + yq 'any(contains("-log-level info"))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Deployment: logLevel can be overridden" { + cd `chart_dir` + local cmd=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'apiGateway.logLevel=debug' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command' | tee /dev/stderr) + + local actual=$(echo "$cmd" | + yq 'any(contains("-log-level debug"))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +#-------------------------------------------------------------------- +# replicas + +@test "apiGateway/Deployment: replicas defaults to 1" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq '.spec.replicas' | tee /dev/stderr) + + [ "${actual}" = "1" ] +} + +@test "apiGateway/Deployment: replicas can be set" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'apiGateway.controller.replicas=3' \ + . | tee /dev/stderr | + yq '.spec.replicas' | tee /dev/stderr) + + [ "${actual}" = "3" ] +} + +#-------------------------------------------------------------------- +# Vault + +@test "apiGateway/Deployment: vault CA is not configured by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "apiGateway/Deployment: vault CA is not configured when secretName is set but secretKey is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "apiGateway/Deployment: vault CA is not configured when secretKey is set but secretName is not" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/agent-extra-secret")') + [ "${actual}" = "false" ] + local actual=$(echo $object | yq -r '.metadata.annotations | has("vault.hashicorp.com/ca-cert")') + [ "${actual}" = "false" ] +} + +@test "apiGateway/Deployment: vault CA is configured when both secretName and secretKey are set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.tls.caCert.secretName=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=test' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.secretsBackend.vault.ca.secretName=ca' \ + --set 'global.secretsBackend.vault.ca.secretKey=tls.crt' \ + . | tee /dev/stderr | + yq -r '.spec.template' | tee /dev/stderr) + + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-extra-secret"') + [ "${actual}" = "ca" ] + local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/ca-cert"') + [ "${actual}" = "/vault/custom/tls.crt" ] +} + +@test "apiGateway/Deployment: vault tls annotations are set when tls is enabled" { + cd `chart_dir` + local cmd=$(helm template \ + -s templates/api-gateway-controller-deployment.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.secretsBackend.vault.enabled=true' \ + --set 'global.secretsBackend.vault.consulClientRole=foo' \ + --set 'global.secretsBackend.vault.consulServerRole=bar' \ + --set 'global.secretsBackend.vault.consulCARole=test' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'server.serverCert.secretName=pki_int/issue/test' \ + --set 'global.tls.caCert.secretName=pki_int/cert/ca' \ + . | tee /dev/stderr | + yq -r '.spec.template.metadata' | tee /dev/stderr) + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr)" + local expected=$'{{- with secret \"pki_int/cert/ca\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' + [ "${actual}" = "${expected}" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr)" + [ "${actual}" = "pki_int/cert/ca" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-init-first"]' | tee /dev/stderr)" + [ "${actual}" = "true" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr)" + [ "${actual}" = "true" ] + + local actual="$(echo $cmd | + yq -r '.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr)" + [ "${actual}" = "test" ] +} diff --git a/charts/consul/test/unit/api-gateway-controller-service.bats b/charts/consul/test/unit/api-gateway-controller-service.bats new file mode 100755 index 0000000000..47cb7ff9aa --- /dev/null +++ b/charts/consul/test/unit/api-gateway-controller-service.bats @@ -0,0 +1,30 @@ +#!/usr/bin/env bats + +load _helpers + +@test "apiGateway/Service: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-service.yaml \ + . +} + +@test "apiGateway/Service: enable with apiGateway.enabled set to true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-service.yaml \ + --set 'global.enabled=false' \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/Service: disable with apiGateway.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-service.yaml \ + --set 'apiGateway.enabled=false' \ + . +} diff --git a/charts/consul/test/unit/api-gateway-controller-serviceaccount.bats b/charts/consul/test/unit/api-gateway-controller-serviceaccount.bats new file mode 100644 index 0000000000..22486799b2 --- /dev/null +++ b/charts/consul/test/unit/api-gateway-controller-serviceaccount.bats @@ -0,0 +1,76 @@ +#!/usr/bin/env bats + +load _helpers + +@test "apiGateway/ServiceAccount: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-serviceaccount.yaml \ + . +} + +@test "apiGateway/ServiceAccount: enabled with apiGateway.enabled true" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-serviceaccount.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq -s 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/ServiceAccount: disabled with apiGateway.enabled false" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-controller-serviceaccount.yaml \ + --set 'apiGateway.enabled=false' \ + . +} +#-------------------------------------------------------------------- +# global.imagePullSecrets + +@test "apiGateway/ServiceAccount: can set image pull secrets" { + cd `chart_dir` + local object=$(helm template \ + -s templates/api-gateway-controller-serviceaccount.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'global.imagePullSecrets[0].name=my-secret' \ + --set 'global.imagePullSecrets[1].name=my-secret2' \ + . | tee /dev/stderr) + + local actual=$(echo "$object" | + yq -r '.imagePullSecrets[0].name' | tee /dev/stderr) + [ "${actual}" = "my-secret" ] + + local actual=$(echo "$object" | + yq -r '.imagePullSecrets[1].name' | tee /dev/stderr) + [ "${actual}" = "my-secret2" ] +} + +#-------------------------------------------------------------------- +# apiGateway.serviceAccount.annotations + +@test "apiGateway/ServiceAccount: no annotations by default" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-serviceaccount.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq '.metadata.annotations | length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "apiGateway/ServiceAccount: annotations when enabled" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-controller-serviceaccount.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set "apiGateway.serviceAccount.annotations=foo: bar" \ + . | tee /dev/stderr | + yq -r '.metadata.annotations.foo' | tee /dev/stderr) + [ "${actual}" = "bar" ] +} diff --git a/charts/consul/test/unit/api-gateway-gatewayclass.bats b/charts/consul/test/unit/api-gateway-gatewayclass.bats new file mode 100755 index 0000000000..c79753c2f3 --- /dev/null +++ b/charts/consul/test/unit/api-gateway-gatewayclass.bats @@ -0,0 +1,48 @@ +#!/usr/bin/env bats + +load _helpers + +@test "apiGateway/GatewayClass: disabled by default" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-gatewayclass.yaml \ + . +} + +@test "apiGateway/GatewayClass: enable with global.enabled false" { + cd `chart_dir` + local actual=$(helm template \ + -s templates/api-gateway-gatewayclass.yaml \ + --set 'global.enabled=false' \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "apiGateway/GatewayClass: disable with apiGateway.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-gatewayclass.yaml \ + --set 'apiGateway.enabled=false' \ + . +} + +@test "apiGateway/GatewayClass: disable with global.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-gatewayclass.yaml \ + --set 'global.enabled=false' \ + . +} + +@test "apiGateway/GatewayClass: disable with apiGateway.managedGatewayClass.enabled" { + cd `chart_dir` + assert_empty helm template \ + -s templates/api-gateway-gatewayclass.yaml \ + --set 'apiGateway.enabled=true' \ + --set 'apiGateway.image=foo' \ + --set 'apiGateway.managedGatewayClass.enabled=false' \ + . +} diff --git a/charts/consul/test/unit/helpers.bats b/charts/consul/test/unit/helpers.bats index 63148e3424..ee524a6842 100644 --- a/charts/consul/test/unit/helpers.bats +++ b/charts/consul/test/unit/helpers.bats @@ -115,7 +115,7 @@ load _helpers @test "helper/namespace: used everywhere" { cd `chart_dir` # Grep for files that don't have 'namespace: ' in them - local actual=$(grep -L 'namespace: ' templates/*.yaml | grep -v 'crd' | grep -v 'clusterrole' | tee /dev/stderr ) + local actual=$(grep -L 'namespace: ' templates/*.yaml | grep -v 'crd' | grep -v 'clusterrole' | grep -v 'api-gateway-gateway' | tee /dev/stderr ) [ "${actual}" = '' ] } diff --git a/charts/consul/values.yaml b/charts/consul/values.yaml index 30e23c92eb..7617e0a679 100644 --- a/charts/consul/values.yaml +++ b/charts/consul/values.yaml @@ -2472,6 +2472,116 @@ terminatingGateways: gateways: - name: terminating-gateway +# Configuration settings for the Consul API Gateway integration +apiGateway: + # When true the helm chart will install the Consul API Gateway controller + enabled: false + + # Image to use for the api-gateway-controller pods and gateway instances + # @type: string + image: null + + # Override global log verbosity level for api-gateway-controller pods. One of "debug", "info", "warn", or "error". + # @type: string + logLevel: info + + # Configuration settings for the optional GatewayClass installed by consul-k8s (enabled by default) + managedGatewayClass: + # When true a GatewayClass is configured to automatically work with Consul as installed by helm. + enabled: true + + # This value defines `nodeSelector` (https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) + # labels for gateway pod assignment, formatted as a multi-line string. + # + # Example: + # + # ```yaml + # nodeSelector: | + # beta.kubernetes.io/arch: amd64 + # ``` + # + # @type: string + nodeSelector: null + + # This value defines the type of service created for gateways (e.g. LoadBalancer, ClusterIP) + serviceType: LoadBalancer + + # This value toggles if the gateway ports should be mapped to host ports + useHostPorts: false + + # Configuration settings for annotations to be copied from the Gateway to other child resources. + copyAnnotations: + # This value defines a list of annotations to be copied from the Gateway to the Service created, formatted as a multi-line string. + # + # Example: + # + # ```yaml + # service: | + # - external-dns.alpha.kubernetes.io/hostname + # ``` + # + # @type: string + service: null + + # Configuration for the ServiceAccount created for the api-gateway component + serviceAccount: + # This value defines additional annotations for the client service account. This should be formatted as a multi-line + # string. + # + # ```yaml + # annotations: | + # "sample/annotation1": "foo" + # "sample/annotation2": "bar" + # ``` + # + # @type: string + annotations: null + + # Configuration for the api-gateway controller component + controller: + # This value sets the number of controller replicas to deploy. + replicas: 1 + + # Annotations to apply to the api-gateway-controller pods. + # + # ```yaml + # annotations: | + # "annotation-key": "annotation-value" + # ``` + # + # @type: string + annotations: null + + # This value references an existing + # Kubernetes `priorityClassName` (https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#pod-priority) + # that can be assigned to api-gateway-controller pods. + priorityClassName: "" + + # This value defines `nodeSelector` (https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) + # labels for api-gateway-controller pod assignment, formatted as a multi-line string. + # + # Example: + # + # ```yaml + # nodeSelector: | + # beta.kubernetes.io/arch: amd64 + # ``` + # + # @type: string + nodeSelector: null + + # Configuration for the Service created for the api-gateway-controller + service: + # Annotations to apply to the api-gateway-controller service. + # + # ```yaml + # annotations: | + # "annotation-key": "annotation-value" + # ``` + # + # @type: string + annotations: null + # Configuration settings for the webhook-cert-manager # `webhook-cert-manager` ensures that cert bundles are up to date for the mutating webhook. webhookCertManager: diff --git a/control-plane/subcommand/server-acl-init/command.go b/control-plane/subcommand/server-acl-init/command.go index 1d710de7e7..e665d1e3f0 100644 --- a/control-plane/subcommand/server-acl-init/command.go +++ b/control-plane/subcommand/server-acl-init/command.go @@ -57,6 +57,8 @@ type Command struct { flagIngressGatewayNames []string flagTerminatingGatewayNames []string + flagCreateAPIGatewayToken bool + // Flags to configure Consul connection. flagServerAddresses []string flagServerPort uint @@ -162,6 +164,8 @@ func (c *Command) init() { "Name of a terminating gateway that needs an acl token. May be specified multiple times. "+ "[Enterprise Only] If using Consul namespaces and registering the gateway outside of the "+ "default namespace, specify the value in the form ..") + c.flags.BoolVar(&c.flagCreateAPIGatewayToken, "create-api-gateway-token", false, + "Toggle for creating a token for the API Gateway controller integration.") c.flags.Var((*flags.AppendSliceValue)(&c.flagServerAddresses), "server-address", "The IP, DNS name or the cloud auto-join string of the Consul server(s). If providing IPs or DNS names, may be specified multiple times. "+ @@ -541,6 +545,19 @@ func (c *Command) Run(args []string) int { } } + if c.flagCreateAPIGatewayToken { + apigwRules, err := c.apiGatewayControllerRules() + if err != nil { + c.log.Error("Error templating api gateway rules", "err", err) + return 1 + } + err = c.createLocalACL("api-gateway-controller", apigwRules, consulDC, isPrimary, consulClient) + if err != nil { + c.log.Error(err.Error()) + return 1 + } + } + if c.flagCreateMeshGatewayToken { meshGatewayRules, err := c.meshGatewayRules() if err != nil { diff --git a/control-plane/subcommand/server-acl-init/command_test.go b/control-plane/subcommand/server-acl-init/command_test.go index 8ca8974182..b79651c8f6 100644 --- a/control-plane/subcommand/server-acl-init/command_test.go +++ b/control-plane/subcommand/server-acl-init/command_test.go @@ -15,12 +15,6 @@ import ( "testing" "time" - "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/sdk/freeport" - "github.com/hashicorp/consul/sdk/testutil" - "github.com/hashicorp/consul/sdk/testutil/retry" - "github.com/hashicorp/go-discover" - "github.com/hashicorp/go-hclog" "github.com/mitchellh/cli" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -28,6 +22,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sdk/freeport" + "github.com/hashicorp/consul/sdk/testutil" + "github.com/hashicorp/consul/sdk/testutil/retry" + "github.com/hashicorp/go-discover" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/consul-k8s/control-plane/helper/cert" "github.com/hashicorp/consul-k8s/control-plane/helper/go-discover/mocks" "github.com/hashicorp/consul-k8s/control-plane/helper/test" @@ -189,6 +190,14 @@ func TestRun_TokensPrimaryDC(t *testing.T) { SecretNames: []string{resourcePrefix + "-client-snapshot-agent-acl-token"}, LocalToken: true, }, + { + TestName: "API gateway token", + TokenFlags: []string{"-create-api-gateway-token"}, + PolicyNames: []string{"api-gateway-controller-token"}, + PolicyDCs: []string{"dc1"}, + SecretNames: []string{resourcePrefix + "-api-gateway-controller-acl-token"}, + LocalToken: true, + }, { TestName: "Mesh gateway token", TokenFlags: []string{"-create-mesh-gateway-token"}, @@ -360,6 +369,14 @@ func TestRun_TokensReplicatedDC(t *testing.T) { SecretNames: []string{resourcePrefix + "-client-snapshot-agent-acl-token"}, LocalToken: true, }, + { + TestName: "API Gateway token", + TokenFlags: []string{"-create-api-gateway-token"}, + PolicyNames: []string{"api-gateway-controller-token-dc2"}, + PolicyDCs: []string{"dc2"}, + SecretNames: []string{resourcePrefix + "-api-gateway-controller-acl-token"}, + LocalToken: true, + }, { TestName: "Mesh gateway token", TokenFlags: []string{"-create-mesh-gateway-token"}, @@ -506,6 +523,12 @@ func TestRun_TokensWithProvidedBootstrapToken(t *testing.T) { PolicyNames: []string{"client-snapshot-agent-token"}, SecretNames: []string{resourcePrefix + "-client-snapshot-agent-acl-token"}, }, + { + TestName: "API Gateway token", + TokenFlags: []string{"-create-api-gateway-token"}, + PolicyNames: []string{"api-gateway-controller-token"}, + SecretNames: []string{resourcePrefix + "-api-gateway-controller-acl-token"}, + }, { TestName: "Mesh gateway token", TokenFlags: []string{"-create-mesh-gateway-token"}, diff --git a/control-plane/subcommand/server-acl-init/rules.go b/control-plane/subcommand/server-acl-init/rules.go index 1ab5076db3..07e25b96f5 100644 --- a/control-plane/subcommand/server-acl-init/rules.go +++ b/control-plane/subcommand/server-acl-init/rules.go @@ -138,6 +138,33 @@ partition_prefix "" { return c.renderRules(anonTokenRulesTpl) } +func (c *Command) apiGatewayControllerRules() (string, error) { + apiGatewayRulesTpl := `{{- if .EnablePartitions }} +partition "{{ .PartitionName }}" { + mesh = "write" + acl = "write" +{{- else }} +operator = "write" +acl = "write" +{{- end }} +{{- if .EnableNamespaces }} +namespace_prefix "" { +{{- end }} + service_prefix "" { + policy = "write" + intentions = "write" + } +{{- if .EnableNamespaces }} +} +{{- end }} +{{- if .EnablePartitions }} +} +{{- end }} + ` + + return c.renderRules(apiGatewayRulesTpl) +} + // This assumes users are using the default name for the service, i.e. // "mesh-gateway". func (c *Command) meshGatewayRules() (string, error) { diff --git a/control-plane/subcommand/server-acl-init/rules_test.go b/control-plane/subcommand/server-acl-init/rules_test.go index ad3d385121..95c6e06f69 100644 --- a/control-plane/subcommand/server-acl-init/rules_test.go +++ b/control-plane/subcommand/server-acl-init/rules_test.go @@ -2,6 +2,7 @@ package serveraclinit import ( "fmt" + "strings" "testing" "github.com/stretchr/testify/require" @@ -139,6 +140,51 @@ partition_prefix "" { } } +func TestAPIGatewayControllerRules(t *testing.T) { + cases := []struct { + Name string + EnableNamespaces bool + Expected string + }{ + { + Name: "Namespaces are disabled", + Expected: ` +operator = "write" +acl = "write" + service_prefix "" { + policy = "write" + intentions = "write" + }`, + }, + { + Name: "Namespaces are enabled", + EnableNamespaces: true, + Expected: ` +operator = "write" +acl = "write" +namespace_prefix "" { + service_prefix "" { + policy = "write" + intentions = "write" + } +}`, + }, + } + + for _, tt := range cases { + t.Run(tt.Name, func(t *testing.T) { + cmd := Command{ + flagEnableNamespaces: tt.EnableNamespaces, + } + + meshGatewayRules, err := cmd.apiGatewayControllerRules() + + require.NoError(t, err) + require.Equal(t, tt.Expected, strings.Trim(meshGatewayRules, " ")) + }) + } +} + func TestMeshGatewayRules(t *testing.T) { cases := []struct { Name string