From 82083061a09c4b6a2c87baae3321bff8b1bc6008 Mon Sep 17 00:00:00 2001 From: Jason O'Donnell <2160810+jasonodonnell@users.noreply.github.com> Date: Thu, 19 Dec 2019 10:57:51 -0500 Subject: [PATCH] Add vault agent injector (#150) * Add vault agent injector * Fix bug with agent image env * Fix terraform GKE code * Cleanup label * Improve test reliablity * Lower sleep times in tests * Standardize image values * Update values * Update vault tag --- templates/_helpers.tpl | 11 +- templates/injector-clusterrole.yaml | 18 ++ templates/injector-clusterrolebinding.yaml | 19 +++ templates/injector-deployment.yaml | 90 ++++++++++ templates/injector-mutating-webhook.yaml | 27 +++ templates/injector-service.yaml | 19 +++ templates/injector-serviceaccount.yaml | 11 ++ test/acceptance/_helpers.bash | 33 +++- test/acceptance/injector-test/bootstrap.sh | 46 ++++++ test/acceptance/injector-test/job.yaml | 39 +++++ .../injector-test/pg-deployment.yaml | 69 ++++++++ .../injector-test/pgdump-policy.hcl | 3 + test/acceptance/injector.bats | 55 ++++++ test/acceptance/server-dev.bats | 5 + test/acceptance/server-ha.bats | 8 +- test/acceptance/server.bats | 10 +- test/terraform/.gitignore | 1 + test/terraform/main.tf | 2 +- test/unit/injector-clusterrole.bats | 22 +++ test/unit/injector-clusterrolebinding.bats | 22 +++ test/unit/injector-deployment.bats | 156 ++++++++++++++++++ test/unit/injector-mutating-webhook.bats | 77 +++++++++ test/unit/injector-service.bats | 37 +++++ test/unit/injector-serviceaccount.bats | 22 +++ test/unit/server-clusterrolebinding.bats | 22 +-- values.yaml | 58 ++++++- 26 files changed, 861 insertions(+), 21 deletions(-) create mode 100644 templates/injector-clusterrole.yaml create mode 100644 templates/injector-clusterrolebinding.yaml create mode 100644 templates/injector-deployment.yaml create mode 100644 templates/injector-mutating-webhook.yaml create mode 100644 templates/injector-service.yaml create mode 100644 templates/injector-serviceaccount.yaml create mode 100755 test/acceptance/injector-test/bootstrap.sh create mode 100644 test/acceptance/injector-test/job.yaml create mode 100644 test/acceptance/injector-test/pg-deployment.yaml create mode 100644 test/acceptance/injector-test/pgdump-policy.hcl create mode 100644 test/acceptance/injector.bats create mode 100644 test/terraform/.gitignore create mode 100755 test/unit/injector-clusterrole.bats create mode 100755 test/unit/injector-clusterrolebinding.bats create mode 100755 test/unit/injector-deployment.bats create mode 100755 test/unit/injector-mutating-webhook.bats create mode 100755 test/unit/injector-service.bats create mode 100755 test/unit/injector-serviceaccount.bats diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index 8a4888a..9e0a49f 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -226,7 +226,6 @@ Set's the node selector for pod placement when running in standalone and HA mode {{- end }} {{- end -}} - {{/* Sets extra pod annotations */}} @@ -267,6 +266,16 @@ Set's the container resources if the user has set any. {{ end }} {{- end -}} +{{/* +Sets the container resources if the user has set any. +*/}} +{{- define "injector.resources" -}} + {{- if .Values.injector.resources -}} + resources: +{{ toYaml .Values.injector.resources | indent 12}} + {{ end }} +{{- end -}} + {{/* Inject extra environment vars in the format key:value, if populated */}} diff --git a/templates/injector-clusterrole.yaml b/templates/injector-clusterrole.yaml new file mode 100644 index 0000000..4ff25ab --- /dev/null +++ b/templates/injector-clusterrole.yaml @@ -0,0 +1,18 @@ +{{- if and (eq (.Values.injector.enabled | toString) "true" ) (eq (.Values.global.enabled | toString) "true") }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ template "vault.fullname" . }}-agent-injector-clusterrole + labels: + app.kubernetes.io/name: {{ include "vault.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +rules: +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["mutatingwebhookconfigurations"] + verbs: + - "get" + - "list" + - "watch" + - "patch" +{{ end }} diff --git a/templates/injector-clusterrolebinding.yaml b/templates/injector-clusterrolebinding.yaml new file mode 100644 index 0000000..9826693 --- /dev/null +++ b/templates/injector-clusterrolebinding.yaml @@ -0,0 +1,19 @@ +{{- if and (eq (.Values.injector.enabled | toString) "true" ) (eq (.Values.global.enabled | toString) "true") }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ template "vault.fullname" . }}-agent-injector-binding + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ include "vault.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ template "vault.fullname" . }}-agent-injector-clusterrole +subjects: +- kind: ServiceAccount + name: {{ template "vault.fullname" . }}-agent-injector + namespace: {{ .Release.Namespace }} +{{ end }} diff --git a/templates/injector-deployment.yaml b/templates/injector-deployment.yaml new file mode 100644 index 0000000..0e03344 --- /dev/null +++ b/templates/injector-deployment.yaml @@ -0,0 +1,90 @@ +# Deployment for the injector +{{- if and (eq (.Values.injector.enabled | toString) "true" ) (eq (.Values.global.enabled | toString) "true") }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "vault.fullname" . }}-agent-injector + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ include "vault.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + component: webhook +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: {{ template "vault.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + component: webhook + template: + metadata: + labels: + app.kubernetes.io/name: {{ template "vault.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + component: webhook + spec: + serviceAccountName: "{{ template "vault.fullname" . }}-agent-injector" + securityContext: + runAsNonRoot: true + runAsGroup: {{ .Values.injector.gid | default 1000 }} + runAsUser: {{ .Values.injector.uid | default 100 }} + containers: + - name: sidecar-injector + {{ template "injector.resources" . }} + image: "{{ .Values.injector.image.repository }}:{{ .Values.injector.image.tag }}" + imagePullPolicy: "{{ .Values.injector.image.pullPolicy }}" + env: + - name: AGENT_INJECT_LISTEN + value: ":8080" + - name: AGENT_INJECT_LOG_LEVEL + value: {{ .Values.injector.logLevel | default "info" }} + - name: AGENT_INJECT_VAULT_ADDR + value: {{ include "vault.scheme" . }}://{{ template "vault.fullname" . }}.{{ .Release.Namespace }}.svc:{{ .Values.server.service.port }} + - name: AGENT_INJECT_VAULT_IMAGE + value: "{{ .Values.injector.agentImage.repository }}:{{ .Values.injector.agentImage.tag }}" + {{- if .Values.injector.certs.secretName }} + - name: AGENT_INJECT_CERT_FILE + value: "/etc/webhook/certs/{{ .Values.injector.certs.certName }}" + - name: AGENT_INJECT_KEY_FILE + value: "/etc/webhook/certs/{{ .Values.injector.certs.keyName }}" + {{- else }} + - name: AGENT_INJECT_TLS_AUTO + value: {{ template "vault.fullname" . }}-agent-injector-cfg + - name: AGENT_INJECT_TLS_AUTO_HOSTS + value: {{ template "vault.fullname" . }}-agent-injector-svc,{{ template "vault.fullname" . }}-agent-injector-svc.{{ .Release.Namespace }},{{ template "vault.fullname" . }}-agent-injector-svc.{{ .Release.Namespace }}.svc + {{- end }} + args: + - agent-inject + - 2>&1 + livenessProbe: + httpGet: + path: /health/ready + port: 8080 + scheme: HTTPS + failureThreshold: 2 + initialDelaySeconds: 1 + periodSeconds: 2 + successThreshold: 1 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /health/ready + port: 8080 + scheme: HTTPS + failureThreshold: 2 + initialDelaySeconds: 2 + periodSeconds: 2 + successThreshold: 1 + timeoutSeconds: 5 +{{- if .Values.injector.certs.secretName }} + volumeMounts: + - name: webhook-certs + mountPath: /etc/webhook/certs + readOnly: true + volumes: + - name: webhook-certs + secret: + secretName: "{{ .Values.injector.certs.secretName }}" +{{- end }} +{{ end }} diff --git a/templates/injector-mutating-webhook.yaml b/templates/injector-mutating-webhook.yaml new file mode 100644 index 0000000..3f0d27e --- /dev/null +++ b/templates/injector-mutating-webhook.yaml @@ -0,0 +1,27 @@ +{{- if and (eq (.Values.injector.enabled | toString) "true" ) (eq (.Values.global.enabled | toString) "true") }} +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + name: {{ template "vault.fullname" . }}-agent-injector-cfg + labels: + app.kubernetes.io/name: {{ include "vault.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +webhooks: + - name: vault.hashicorp.com + clientConfig: + service: + name: {{ template "vault.fullname" . }}-agent-injector-svc + namespace: {{ .Release.Namespace }} + path: "/mutate" + caBundle: {{ .Values.injector.certs.caBundle }} + rules: + - operations: ["CREATE", "UPDATE"] + apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods"] +{{- if .Values.injector.namespaceSelector }} + namespaceSelector: +{{ toYaml .Values.injector.namespaceSelector | indent 6}} +{{ end }} +{{ end }} diff --git a/templates/injector-service.yaml b/templates/injector-service.yaml new file mode 100644 index 0000000..79d818f --- /dev/null +++ b/templates/injector-service.yaml @@ -0,0 +1,19 @@ +{{- if and (eq (.Values.injector.enabled | toString) "true" ) (eq (.Values.global.enabled | toString) "true") }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "vault.fullname" . }}-agent-injector-svc + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ include "vault.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + ports: + - port: 443 + targetPort: 8080 + selector: + app.kubernetes.io/name: {{ include "vault.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + component: webhook +{{- end }} diff --git a/templates/injector-serviceaccount.yaml b/templates/injector-serviceaccount.yaml new file mode 100644 index 0000000..a28d38f --- /dev/null +++ b/templates/injector-serviceaccount.yaml @@ -0,0 +1,11 @@ +{{- if and (eq (.Values.injector.enabled | toString) "true" ) (eq (.Values.global.enabled | toString) "true") }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "vault.fullname" . }}-agent-injector + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ include "vault.name" . }}-agent-injector + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +{{ end }} diff --git a/test/acceptance/_helpers.bash b/test/acceptance/_helpers.bash index cee59a9..031daf5 100644 --- a/test/acceptance/_helpers.bash +++ b/test/acceptance/_helpers.bash @@ -87,7 +87,7 @@ wait_for_running() { for i in $(seq 60); do if [ -n "$(check ${POD_NAME})" ]; then echo "${POD_NAME} is ready." - sleep 10 + sleep 5 return fi @@ -117,7 +117,7 @@ wait_for_ready() { for i in $(seq 60); do if [ -n "$(check ${POD_NAME})" ]; then echo "${POD_NAME} is ready." - sleep 10 + sleep 5 return fi @@ -128,3 +128,32 @@ wait_for_ready() { echo "${POD_NAME} never became ready." exit 1 } + +wait_for_complete_job() { + POD_NAME=$1 + + check() { + # This requests the pod and checks whether the status is running + # and the ready state is true. If so, it outputs the name. Otherwise + # it outputs empty. Therefore, to check for success, check for nonzero + # string length. + kubectl get job $1 -o json | \ + jq -r 'select( + .status.succeeded == 1 + ) | .metadata.namespace + "/" + .metadata.name' + } + + for i in $(seq 60); do + if [ -n "$(check ${POD_NAME})" ]; then + echo "${POD_NAME} is complete." + sleep 5 + return + fi + + echo "Waiting for ${POD_NAME} to be complete..." + sleep 2 + done + + echo "${POD_NAME} never completed." + exit 1 +} diff --git a/test/acceptance/injector-test/bootstrap.sh b/test/acceptance/injector-test/bootstrap.sh new file mode 100755 index 0000000..d738fd2 --- /dev/null +++ b/test/acceptance/injector-test/bootstrap.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +OUTPUT=/tmp/output.txt + +vault operator init -n 1 -t 1 >> ${OUTPUT?} + +unseal=$(cat ${OUTPUT?} | grep "Unseal Key 1:" | sed -e "s/Unseal Key 1: //g") +root=$(cat ${OUTPUT?} | grep "Initial Root Token:" | sed -e "s/Initial Root Token: //g") + +vault operator unseal ${unseal?} + +vault login -no-print ${root?} + +vault policy write db-backup /vault/userconfig/test/pgdump-policy.hcl + +vault auth enable kubernetes + +vault write auth/kubernetes/config \ + token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ + kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \ + kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt + +vault write auth/kubernetes/role/db-backup \ + bound_service_account_names=pgdump \ + bound_service_account_namespaces=acceptance \ + policies=db-backup \ + ttl=1h + +vault secrets enable database + +vault write database/config/postgresql \ + plugin_name=postgresql-database-plugin \ + allowed_roles="db-backup" \ + connection_url="postgresql://{{username}}:{{password}}@postgres:5432/mydb?sslmode=disable" \ + username="vault" \ + password="vault" + +vault write database/roles/db-backup \ + db_name=postgresql \ + creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \ + GRANT CONNECT ON DATABASE mydb TO \"{{name}}\"; \ + GRANT USAGE ON SCHEMA app TO \"{{name}}\"; \ + GRANT SELECT ON ALL TABLES IN SCHEMA app TO \"{{name}}\";" \ + revocation_statements="ALTER ROLE \"{{name}}\" NOLOGIN;"\ + default_ttl="1h" \ + max_ttl="24h" diff --git a/test/acceptance/injector-test/job.yaml b/test/acceptance/injector-test/job.yaml new file mode 100644 index 0000000..d665383 --- /dev/null +++ b/test/acceptance/injector-test/job.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: pgdump + labels: + app: pgdump +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: pgdump +spec: + backoffLimit: 0 + template: + metadata: + name: pgdump + labels: + app: pgdump + annotations: + vault.hashicorp.com/agent-inject: "true" + vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/db-backup" + vault.hashicorp.com/agent-inject-template-db-creds: | + {{- with secret "database/creds/db-backup" -}} + postgresql://{{ .Data.username }}:{{ .Data.password }}@postgres.acceptance.svc.cluster.local:5432/mydb + {{- end }} + vault.hashicorp.com/role: "db-backup" + vault.hashicorp.com/agent-pre-populate-only: "true" + spec: + serviceAccountName: pgdump + containers: + - name: pgdump + image: postgres:11.5 + command: + - "/bin/sh" + - "-ec" + args: + - "/usr/bin/pg_dump $(cat /vault/secrets/db-creds) --no-owner > /dev/stdout" + restartPolicy: Never diff --git a/test/acceptance/injector-test/pg-deployment.yaml b/test/acceptance/injector-test/pg-deployment.yaml new file mode 100644 index 0000000..13389ff --- /dev/null +++ b/test/acceptance/injector-test/pg-deployment.yaml @@ -0,0 +1,69 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + labels: + app: postgres +spec: + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 + selector: + app: postgres +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + service: postgres + app: postgres + spec: + containers: + - name: postgres + image: postgres:11.5 + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: mydb + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + value: password + volumeMounts: + - mountPath: "/var/lib/postgresql/data" + name: "pgdata" + - mountPath: "/docker-entrypoint-initdb.d" + name: "pgconf" + volumes: + - name: pgdata + emptyDir: {} + - name: pgconf + configMap: + name: "pg-init" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: pg-init + labels: + app: postgres +data: + setup.sql: | + CREATE ROLE vault; + ALTER ROLE vault WITH SUPERUSER LOGIN PASSWORD 'vault'; + + \c mydb + CREATE SCHEMA app; + CREATE TABLE app.inventory(id int); + INSERT INTO app.inventory(id) VALUES (0); diff --git a/test/acceptance/injector-test/pgdump-policy.hcl b/test/acceptance/injector-test/pgdump-policy.hcl new file mode 100644 index 0000000..88a6cd6 --- /dev/null +++ b/test/acceptance/injector-test/pgdump-policy.hcl @@ -0,0 +1,3 @@ +path "database/creds/db-backup" { + capabilities = ["read"] +} diff --git a/test/acceptance/injector.bats b/test/acceptance/injector.bats new file mode 100644 index 0000000..35f4b9c --- /dev/null +++ b/test/acceptance/injector.bats @@ -0,0 +1,55 @@ +#!/usr/bin/env bats + +load _helpers + +@test "injector: testing deployment" { + cd `chart_dir` + + kubectl delete namespace acceptance --ignore-not-found=true + kubectl create namespace acceptance + kubectl config set-context --current --namespace=acceptance + + kubectl create -f ./test/acceptance/injector-test/pg-deployment.yaml + sleep 5 + wait_for_ready $(kubectl get pod -l app=postgres -o jsonpath="{.items[0].metadata.name}") + + kubectl create secret generic test \ + --from-file ./test/acceptance/injector-test/pgdump-policy.hcl \ + --from-file ./test/acceptance/injector-test/bootstrap.sh + + kubectl label secret test app=vault-agent-demo + + helm install --name="$(name_prefix)" \ + --set="server.extraVolumes[0].type=secret" \ + --set="server.extraVolumes[0].name=test" . + wait_for_running $(name_prefix)-0 + + wait_for_ready $(kubectl get pod -l component=webhook -o jsonpath="{.items[0].metadata.name}") + + kubectl exec -ti "$(name_prefix)-0" -- /bin/sh -c "cp /vault/userconfig/test/bootstrap.sh /tmp/bootstrap.sh && chmod +x /tmp/bootstrap.sh && /tmp/bootstrap.sh" + sleep 5 + + # Sealed, not initialized + local sealed_status=$(kubectl exec "$(name_prefix)-0" -- vault status -format=json | + jq -r '.sealed' ) + [ "${sealed_status}" == "false" ] + + local init_status=$(kubectl exec "$(name_prefix)-0" -- vault status -format=json | + jq -r '.initialized') + [ "${init_status}" == "true" ] + + + kubectl create -f ./test/acceptance/injector-test/job.yaml + wait_for_complete_job "pgdump" +} + +# Clean up +teardown() { + echo "helm/pvc teardown" + helm delete --purge vault + kubectl delete --all pvc + kubectl delete secret test + kubectl delete job pgdump + kubectl delete deployment postgres + kubectl delete namespace acceptance +} diff --git a/test/acceptance/server-dev.bats b/test/acceptance/server-dev.bats index e6aecbe..eeec698 100644 --- a/test/acceptance/server-dev.bats +++ b/test/acceptance/server-dev.bats @@ -4,6 +4,10 @@ load _helpers @test "server/dev: testing deployment" { cd `chart_dir` + kubectl delete namespace acceptance --ignore-not-found=true + kubectl create namespace acceptance + kubectl config set-context --current --namespace=acceptance + helm install --name="$(name_prefix)" --set='server.dev.enabled=true' . wait_for_running $(name_prefix)-0 @@ -53,4 +57,5 @@ teardown() { echo "helm/pvc teardown" helm delete --purge vault kubectl delete --all pvc + kubectl delete namespace acceptance --ignore-not-found=true } diff --git a/test/acceptance/server-ha.bats b/test/acceptance/server-ha.bats index 9e4d27e..78d5505 100644 --- a/test/acceptance/server-ha.bats +++ b/test/acceptance/server-ha.bats @@ -5,6 +5,7 @@ load _helpers @test "server/ha: testing deployment" { cd `chart_dir` + helm install --name="$(name_prefix)" \ --set='server.ha.enabled=true' . wait_for_running $(name_prefix)-0 @@ -88,10 +89,12 @@ load _helpers [ "${init_status}" == "true" ] } -# TODO: Auto unseal test - # setup a consul env setup() { + kubectl delete namespace acceptance --ignore-not-found=true + kubectl create namespace acceptance + kubectl config set-context --current --namespace=acceptance + helm install https://github.com/hashicorp/consul-helm/archive/v0.8.1.tar.gz \ --name consul \ --set 'ui.enabled=false' \ @@ -104,4 +107,5 @@ teardown() { helm delete --purge vault helm delete --purge consul kubectl delete --all pvc + kubectl delete namespace acceptance --ignore-not-found=true } diff --git a/test/acceptance/server.bats b/test/acceptance/server.bats index 1ceef85..3c4a075 100644 --- a/test/acceptance/server.bats +++ b/test/acceptance/server.bats @@ -4,6 +4,11 @@ load _helpers @test "server/standalone: testing deployment" { cd `chart_dir` + + kubectl delete namespace acceptance --ignore-not-found=true + kubectl create namespace acceptance + kubectl config set-context --current --namespace=acceptance + helm install --name="$(name_prefix)" . wait_for_running $(name_prefix)-0 @@ -86,7 +91,7 @@ load _helpers [ "${token}" != "" ] # Vault Unseal - local pods=($(kubectl get pods -o json | jq -r '.items[].metadata.name')) + local pods=($(kubectl get pods --selector='app.kubernetes.io/name=vault' -o json | jq -r '.items[].metadata.name')) for pod in "${pods[@]}" do kubectl exec -ti ${pod} -- vault operator unseal ${token} @@ -94,7 +99,7 @@ load _helpers wait_for_ready "$(name_prefix)-0" - # Sealed, not initialized + # Unsealed, initialized local sealed_status=$(kubectl exec "$(name_prefix)-0" -- vault status -format=json | jq -r '.sealed' ) [ "${sealed_status}" == "false" ] @@ -109,4 +114,5 @@ teardown() { echo "helm/pvc teardown" helm delete --purge vault kubectl delete --all pvc + kubectl delete namespace acceptance --ignore-not-found=true } diff --git a/test/terraform/.gitignore b/test/terraform/.gitignore new file mode 100644 index 0000000..d680062 --- /dev/null +++ b/test/terraform/.gitignore @@ -0,0 +1 @@ +vault-helm-dev-creds.json diff --git a/test/terraform/main.tf b/test/terraform/main.tf index 4542a49..c4f3516 100644 --- a/test/terraform/main.tf +++ b/test/terraform/main.tf @@ -41,7 +41,7 @@ resource "google_container_cluster" "cluster" { project = "${var.project}" enable_legacy_abac = true initial_node_count = 3 - zone = "${var.zone}" + location = "${var.zone}" min_master_version = "${data.google_container_engine_versions.main.latest_master_version}" node_version = "${data.google_container_engine_versions.main.latest_node_version}" diff --git a/test/unit/injector-clusterrole.bats b/test/unit/injector-clusterrole.bats new file mode 100755 index 0000000..4c5c1d9 --- /dev/null +++ b/test/unit/injector-clusterrole.bats @@ -0,0 +1,22 @@ +#!/usr/bin/env bats + +load _helpers + +@test "injector/ClusterRole: enabled by default" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-clusterrole.yaml \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "injector/ClusterRole: disable with global.enabled" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-clusterrole.yaml \ + --set 'global.enabled=false' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} diff --git a/test/unit/injector-clusterrolebinding.bats b/test/unit/injector-clusterrolebinding.bats new file mode 100755 index 0000000..efeab4c --- /dev/null +++ b/test/unit/injector-clusterrolebinding.bats @@ -0,0 +1,22 @@ +#!/usr/bin/env bats + +load _helpers + +@test "injector/ClusterRoleBinding: enabled by default" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-clusterrolebinding.yaml \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "injector/ClusterRoleBinding: disable with global.enabled" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-clusterrolebinding.yaml \ + --set 'global.enabled=false' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} diff --git a/test/unit/injector-deployment.bats b/test/unit/injector-deployment.bats new file mode 100755 index 0000000..3cc85c7 --- /dev/null +++ b/test/unit/injector-deployment.bats @@ -0,0 +1,156 @@ +#!/usr/bin/env bats + +load _helpers + +@test "injector/deployment: default injector.enabled" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-deployment.yaml \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "injector/deployment: enable with injector.enabled true" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-deployment.yaml \ + --set 'injector.enabled=true' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "injector/deployment: disable with global.enabled" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-deployment.yaml \ + --set 'global.enabled=false' \ + --set 'injector.enabled=true' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "injector/deployment: image defaults to injector.image" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-deployment.yaml \ + --set 'injector.image.repository=foo' \ + --set 'injector.image.tag=1.2.3' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].image' | tee /dev/stderr) + [ "${actual}" = "foo:1.2.3" ] + + local actual=$(helm template \ + -x templates/injector-deployment.yaml \ + --set 'injector.image.repository=foo' \ + --set 'injector.image.tag=1.2.3' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].image' | tee /dev/stderr) + [ "${actual}" = "foo:1.2.3" ] +} + +@test "injector/deployment: default imagePullPolicy" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-deployment.yaml \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].imagePullPolicy' | tee /dev/stderr) + [ "${actual}" = "IfNotPresent" ] +} + +@test "injector/deployment: default resources" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-deployment.yaml \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].resources' | tee /dev/stderr) + [ "${actual}" = "null" ] +} + +@test "injector/deployment: custom resources" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-deployment.yaml \ + --set 'injector.enabled=true' \ + --set 'injector.resources.requests.memory=256Mi' \ + --set 'injector.resources.requests.cpu=250m' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].resources.requests.memory' | tee /dev/stderr) + [ "${actual}" = "256Mi" ] + + local actual=$(helm template \ + -x templates/injector-deployment.yaml \ + --set 'injector.enabled=true' \ + --set 'injector.resources.limits.memory=256Mi' \ + --set 'injector.resources.limits.cpu=250m' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].resources.limits.memory' | tee /dev/stderr) + [ "${actual}" = "256Mi" ] + + local actual=$(helm template \ + -x templates/injector-deployment.yaml \ + --set 'injector.enabled=true' \ + --set 'injector.resources.requests.cpu=250m' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].resources.requests.cpu' | tee /dev/stderr) + [ "${actual}" = "250m" ] + + local actual=$(helm template \ + -x templates/injector-deployment.yaml \ + --set 'injector.enabled=true' \ + --set 'injector.resources.limits.cpu=250m' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].resources.limits.cpu' | tee /dev/stderr) + [ "${actual}" = "250m" ] +} + +@test "injector/deployment: manual TLS environment vars" { + cd `chart_dir` + local object=$(helm template \ + -x templates/injector-deployment.yaml \ + --set 'injector.certs.secretName=foobar' \ + --set 'injector.certs.certName=test.crt' \ + --set 'injector.certs.keyName=test.key' \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].env' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.[4].name' | tee /dev/stderr) + [ "${actual}" = "AGENT_INJECT_CERT_FILE" ] + + local actual=$(echo $object | + yq -r '.[4].value' | tee /dev/stderr) + [ "${actual}" = "/etc/webhook/certs/test.crt" ] + + local actual=$(echo $object | + yq -r '.[5].name' | tee /dev/stderr) + [ "${actual}" = "AGENT_INJECT_KEY_FILE" ] + + local actual=$(echo $object | + yq -r '.[5].value' | tee /dev/stderr) + [ "${actual}" = "/etc/webhook/certs/test.key" ] +} + +@test "injector/deployment: auto TLS by default" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-deployment.yaml \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].volumeMounts | length' | tee /dev/stderr) + [ "${actual}" = "0" ] + + local object=$(helm template \ + -x templates/injector-deployment.yaml \ + . | tee /dev/stderr | + yq -r '.spec.template.spec.containers[0].env' | tee /dev/stderr) + + local actual=$(echo $object | + yq -r '.[4].name' | tee /dev/stderr) + [ "${actual}" = "AGENT_INJECT_TLS_AUTO" ] + + local actual=$(echo $object | + yq -r '.[5].name' | tee /dev/stderr) + [ "${actual}" = "AGENT_INJECT_TLS_AUTO_HOSTS" ] +} diff --git a/test/unit/injector-mutating-webhook.bats b/test/unit/injector-mutating-webhook.bats new file mode 100755 index 0000000..dd0d643 --- /dev/null +++ b/test/unit/injector-mutating-webhook.bats @@ -0,0 +1,77 @@ +#!/usr/bin/env bats + +load _helpers + +@test "injector/MutatingWebhookConfiguration: enabled by default" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-mutating-webhook.yaml \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "injector/MutatingWebhookConfiguration: disable with global.enabled false" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-mutating-webhook.yaml \ + --set 'global.enabled=false' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "injector/MutatingWebhookConfiguration: disable with injector.enabled false" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-mutating-webhook.yaml \ + --set 'injector.enabled=false' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "injector/MutatingWebhookConfiguration: namespace is set" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-mutating-webhook.yaml \ + --set 'injector.enabled=true' \ + --namespace foo \ + . | tee /dev/stderr | + yq '.webhooks[0].clientConfig.service.namespace' | tee /dev/stderr) + [ "${actual}" = "\"foo\"" ] +} + +@test "injector/MutatingWebhookConfiguration: caBundle is empty" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-mutating-webhook.yaml \ + --set 'injector.enabled=true' \ + --namespace foo \ + . | tee /dev/stderr | + yq '.webhooks[0].clientConfig.caBundle' | tee /dev/stderr) + [ "${actual}" = "null" ] +} + +@test "injector/MutatingWebhookConfiguration: namespaceSelector empty by default" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-mutating-webhook.yaml \ + --set 'injector.enabled=true' \ + --namespace foo \ + . | tee /dev/stderr | + yq '.webhooks[0].namespaceSelector' | tee /dev/stderr) + [ "${actual}" = "null" ] +} + +@test "injector/MutatingWebhookConfiguration: can set namespaceSelector" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-mutating-webhook.yaml \ + --set 'injector.enabled=true' \ + --set 'injector.namespaceSelector.matchLabels.injector=true' \ + . | tee /dev/stderr | + yq '.webhooks[0].namespaceSelector.matchLabels.injector' | tee /dev/stderr) + + [ "${actual}" = "true" ] +} diff --git a/test/unit/injector-service.bats b/test/unit/injector-service.bats new file mode 100755 index 0000000..03f908f --- /dev/null +++ b/test/unit/injector-service.bats @@ -0,0 +1,37 @@ +#!/usr/bin/env bats + +load _helpers + +@test "injector/Service: service enabled by default" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-service.yaml \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] + + local actual=$(helm template \ + -x templates/injector-service.yaml \ + --set 'injector.enabled=true' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "injector/Service: disable with global.enabled false" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-service.yaml \ + --set 'global.enabled=false' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] + + local actual=$(helm template \ + -x templates/injector-service.yaml \ + --set 'global.enabled=false' \ + --set 'injector.enabled=true' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} diff --git a/test/unit/injector-serviceaccount.bats b/test/unit/injector-serviceaccount.bats new file mode 100755 index 0000000..7009a76 --- /dev/null +++ b/test/unit/injector-serviceaccount.bats @@ -0,0 +1,22 @@ +#!/usr/bin/env bats + +load _helpers + +@test "injector/ServiceAccount: enabled by default" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-serviceaccount.yaml \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + +@test "injector/ServiceAccount: disable with global.enabled" { + cd `chart_dir` + local actual=$(helm template \ + -x templates/injector-serviceaccount.yaml \ + --set 'global.enabled=false' \ + . | tee /dev/stderr | + yq 'length > 0' | tee /dev/stderr) + [ "${actual}" = "false" ] +} diff --git a/test/unit/server-clusterrolebinding.bats b/test/unit/server-clusterrolebinding.bats index 88a8d01..7d140b8 100755 --- a/test/unit/server-clusterrolebinding.bats +++ b/test/unit/server-clusterrolebinding.bats @@ -2,27 +2,27 @@ load _helpers -@test "server/ClusterRoleBinding: disabled by default" { +@test "server/ClusterRoleBinding: enabled by default" { cd `chart_dir` local actual=$(helm template \ -x templates/server-clusterrolebinding.yaml \ --set 'server.dev.enabled=true' \ . | tee /dev/stderr | yq 'length > 0' | tee /dev/stderr) - [ "${actual}" = "false" ] + [ "${actual}" = "true" ] local actual=$(helm template \ -x templates/server-clusterrolebinding.yaml \ --set 'server.ha.enabled=true' \ . | tee /dev/stderr | yq 'length > 0' | tee /dev/stderr) - [ "${actual}" = "false" ] + [ "${actual}" = "true" ] local actual=$(helm template \ -x templates/server-clusterrolebinding.yaml \ . | tee /dev/stderr | yq 'length > 0' | tee /dev/stderr) - [ "${actual}" = "false" ] + [ "${actual}" = "true" ] } @test "server/ClusterRoleBinding: disable with global.enabled" { @@ -35,28 +35,28 @@ load _helpers [ "${actual}" = "false" ] } -@test "server/ClusterRoleBinding: can enable with server.authDelegator" { +@test "server/ClusterRoleBinding: can disable with server.authDelegator" { cd `chart_dir` local actual=$(helm template \ -x templates/server-clusterrolebinding.yaml \ - --set 'server.authDelegator.enabled=true' \ + --set 'server.authDelegator.enabled=false' \ . | tee /dev/stderr | yq 'length > 0' | tee /dev/stderr) - [ "${actual}" = "true" ] + [ "${actual}" = "false" ] local actual=$(helm template \ -x templates/server-clusterrolebinding.yaml \ - --set 'server.authDelegator.enabled=true' \ + --set 'server.authDelegator.enabled=false' \ --set 'server.ha.enabled=true' \ . | tee /dev/stderr | yq 'length > 0' | tee /dev/stderr) - [ "${actual}" = "true" ] + [ "${actual}" = "false" ] local actual=$(helm template \ -x templates/server-clusterrolebinding.yaml \ - --set 'server.authDelegator.enabled=true' \ + --set 'server.authDelegator.enabled=false' \ --set 'server.dev.enabled=true' \ . | tee /dev/stderr | yq 'length > 0' | tee /dev/stderr) - [ "${actual}" = "true" ] + [ "${actual}" = "false" ] } diff --git a/values.yaml b/values.yaml index 638e69e..25ff73c 100644 --- a/values.yaml +++ b/values.yaml @@ -11,6 +11,60 @@ global: # TLS for end-to-end encrypted transport tlsDisable: true +injector: + # True if you want to enable vault agent injection. + enabled: true + + # image sets the repo and tag of the vault-k8s image to use for the injector. + image: + repository: "hashicorp/vault-k8s" + tag: "0.1.0" + pullPolicy: IfNotPresent + + # agentImage sets the repo and tag of the Vault image to use for the Vault Agent + # containers. This should be set to the official Vault image. Vault 1.3.1+ is + # required. + agentImage: + repository: "vault" + tag: "1.3.1" + + # namespaceSelector is the selector for restricting the webhook to only + # specific namespaces. This should be set to a multiline string. + # See https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector + # for more details. + # Example: + # namespaceSelector: | + # matchLabels: + # sidecar-injector: enabled + namespaceSelector: {} + + certs: + # secretName is the name of the secret that has the TLS certificate and + # private key to serve the injector webhook. If this is null, then the + # injector will default to its automatic management mode that will assign + # a service account to the injector to generate its own certificates. + secretName: null + + # caBundle is a base64-encoded PEM-encoded certificate bundle for the + # CA that signed the TLS certificate that the webhook serves. This must + # be set if secretName is non-null. + caBundle: "" + + # certName and keyName are the names of the files within the secret for + # the TLS cert and private key, respectively. These have reasonable + # defaults but can be customized if necessary. + certName: tls.crt + keyName: tls.key + + resources: {} + # resources: + # requests: + # memory: 256Mi + # cpu: 250m + # limits: + # memory: 256Mi + # cpu: 250m + server: # Resource requests, limits, etc. for the server cluster placement. This # should map directly to the value of the resources field for a PodSpec. @@ -18,7 +72,7 @@ server: image: repository: "vault" - tag: 1.3.0 + tag: 1.3.1 # Overrides the default Image Pull Policy pullPolicy: IfNotPresent @@ -54,7 +108,7 @@ server: # account. This cluster role binding can be used to setup Kubernetes auth # method. https://www.vaultproject.io/docs/auth/kubernetes.html authDelegator: - enabled: false + enabled: true # extraContainers is a list of sidecar containers. Specified as a raw YAML string. extraContainers: null