diff --git a/CHANGELOG.md b/CHANGELOG.md index dcc0bc1aef..b4be94490e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ ## UNRELEASED +FEATURES: +* CLI: + * Add the ability to install installing HCP self-managed clusters. [[GH-1540](https://github.com/hashicorp/consul-k8s/pull/1540)] + * Add the ability to install the HashiCups demo application via the -demo flag. [[GH-1540](https://github.com/hashicorp/consul-k8s/pull/1540)] ## 0.49.0 (September 29, 2022) diff --git a/charts/consul/templates/_helpers.tpl b/charts/consul/templates/_helpers.tpl index 7b3a949622..8f04ab3c6a 100644 --- a/charts/consul/templates/_helpers.tpl +++ b/charts/consul/templates/_helpers.tpl @@ -236,6 +236,9 @@ This template is for an init container. consul-k8s-control-plane get-consul-client-ca \ -output-file=/consul/tls/client/ca/tls.crt \ -consul-api-timeout={{ .Values.global.consulAPITimeout }} \ + {{- if .Values.global.cloud.enabled }} + -tls-server-name=server.{{.Values.global.datacenter}}.{{.Values.global.domain}} \ + {{- end}} {{- if .Values.externalServers.enabled }} {{- if and .Values.externalServers.enabled (not .Values.externalServers.hosts) }}{{ fail "externalServers.hosts must be set if externalServers.enabled is true" }}{{ end -}} -server-addr={{ quote (first .Values.externalServers.hosts) }} \ @@ -370,3 +373,15 @@ Consul server environment variables for consul-k8s commands. {{- end }} {{- end }} {{- end -}} + +{{/* +Fails global.cloud.enabled is true and global.cloud.secretName is nil or tempty. + +Usage: {{ template "consul.validateCloudConfiguration" . }} + +*/}} +{{- define "consul.validateCloudConfiguration" -}} +{{- if and .Values.global.cloud.enabled (not .Values.global.cloud.secretName) }} +{{fail "When global.cloud.enabled is true, global.cloud.secretName must also be set."}} +{{ end }} +{{- end -}} diff --git a/charts/consul/templates/api-gateway-controller-deployment.yaml b/charts/consul/templates/api-gateway-controller-deployment.yaml index 22d14c1e48..bdb3d90d68 100644 --- a/charts/consul/templates/api-gateway-controller-deployment.yaml +++ b/charts/consul/templates/api-gateway-controller-deployment.yaml @@ -2,6 +2,7 @@ {{- 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 }} +{{ template "consul.validateCloudConfiguration" . }} apiVersion: apps/v1 kind: Deployment metadata: diff --git a/charts/consul/templates/client-daemonset.yaml b/charts/consul/templates/client-daemonset.yaml index 36edc70ddb..e79e1fd8e1 100644 --- a/charts/consul/templates/client-daemonset.yaml +++ b/charts/consul/templates/client-daemonset.yaml @@ -10,6 +10,7 @@ {{- if (and .Values.global.enterpriseLicense.secretName (not .Values.global.enterpriseLicense.secretKey)) }}{{fail "enterpriseLicense.secretKey and secretName must both be specified." }}{{ end -}} {{- if (and (not .Values.global.enterpriseLicense.secretName) .Values.global.enterpriseLicense.secretKey) }}{{fail "enterpriseLicense.secretKey and secretName must both be specified." }}{{ end -}} {{- if and .Values.externalServers.enabled (not .Values.externalServers.hosts) }}{{ fail "externalServers.hosts must be set if externalServers.enabled is true" }}{{ end -}} +{{ template "consul.validateCloudConfiguration" . }} # DaemonSet to run the Consul clients on every node. apiVersion: apps/v1 kind: DaemonSet @@ -525,6 +526,8 @@ spec: {{- if .Values.externalServers.tlsServerName }} -tls-server-name={{ .Values.externalServers.tlsServerName }} \ {{- end }} + {{- else if .Values.global.cloud.enabled }} + -tls-server-name=server.{{ .Values.global.datacenter}}.{{ .Values.global.domain}} \ {{- end }} -consul-api-timeout={{ .Values.global.consulAPITimeout }} \ -init-type="client" diff --git a/charts/consul/templates/client-snapshot-agent-deployment.yaml b/charts/consul/templates/client-snapshot-agent-deployment.yaml index 19ffff23c0..d9d01e4521 100644 --- a/charts/consul/templates/client-snapshot-agent-deployment.yaml +++ b/charts/consul/templates/client-snapshot-agent-deployment.yaml @@ -2,6 +2,7 @@ {{- if or (and .Values.client.snapshotAgent.configSecret.secretName (not .Values.client.snapshotAgent.configSecret.secretKey)) (and (not .Values.client.snapshotAgent.configSecret.secretName) .Values.client.snapshotAgent.configSecret.secretKey) }}{{fail "client.snapshotAgent.configSecret.secretKey and client.snapshotAgent.configSecret.secretName must both be specified." }}{{ end -}} {{- if .Values.client.snapshotAgent.enabled }} {{- if or (and .Values.client.snapshotAgent.configSecret.secretName (not .Values.client.snapshotAgent.configSecret.secretKey)) (and (not .Values.client.snapshotAgent.configSecret.secretName) .Values.client.snapshotAgent.configSecret.secretKey) }}{{fail "client.snapshotAgent.configSecret.secretKey and client.snapshotAgent.configSecret.secretName must both be specified." }}{{ end -}} +{{ template "consul.validateCloudConfiguration" . }} apiVersion: apps/v1 kind: Deployment metadata: diff --git a/charts/consul/templates/connect-inject-deployment.yaml b/charts/consul/templates/connect-inject-deployment.yaml index 5771843738..acdcc3bc65 100644 --- a/charts/consul/templates/connect-inject-deployment.yaml +++ b/charts/consul/templates/connect-inject-deployment.yaml @@ -8,6 +8,7 @@ {{- $serverExposeServiceEnabled := (or (and (ne (.Values.server.exposeService.enabled | toString) "-") .Values.server.exposeService.enabled) (and (eq (.Values.server.exposeService.enabled | toString) "-") (or .Values.global.peering.enabled .Values.global.adminPartitions.enabled))) -}} {{- if not (or (eq .Values.global.peering.tokenGeneration.serverAddresses.source "") (or (eq .Values.global.peering.tokenGeneration.serverAddresses.source "static") (eq .Values.global.peering.tokenGeneration.serverAddresses.source "consul"))) }}{{ fail "global.peering.tokenGeneration.serverAddresses.source must be one of empty string, 'consul' or 'static'" }}{{ end }} {{- if and .Values.externalServers.enabled (not .Values.externalServers.hosts) }}{{ fail "externalServers.hosts must be set if externalServers.enabled is true" }}{{ end -}} +{{ template "consul.validateCloudConfiguration" . }} # The deployment for running the Connect sidecar injector apiVersion: apps/v1 kind: Deployment diff --git a/charts/consul/templates/controller-deployment.yaml b/charts/consul/templates/controller-deployment.yaml index 9a6fddd885..dfc003432c 100644 --- a/charts/consul/templates/controller-deployment.yaml +++ b/charts/consul/templates/controller-deployment.yaml @@ -2,6 +2,7 @@ {{- if and .Values.global.adminPartitions.enabled (not .Values.global.enableConsulNamespaces) }}{{ fail "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" }}{{ end }} {{- if and .Values.externalServers.enabled (not .Values.externalServers.hosts) }}{{ fail "externalServers.hosts must be set if externalServers.enabled is true" }}{{ end -}} {{ template "consul.validateVaultWebhookCertConfiguration" . }} +{{ template "consul.validateCloudConfiguration" . }} apiVersion: apps/v1 kind: Deployment metadata: diff --git a/charts/consul/templates/create-federation-secret-job.yaml b/charts/consul/templates/create-federation-secret-job.yaml index 3099c1fbf0..48c4c1514a 100644 --- a/charts/consul/templates/create-federation-secret-job.yaml +++ b/charts/consul/templates/create-federation-secret-job.yaml @@ -2,6 +2,7 @@ {{- if not .Values.global.federation.enabled }}{{ fail "global.federation.enabled must be true when global.federation.createFederationSecret is true" }}{{ end }} {{- if and (not .Values.global.acls.createReplicationToken) .Values.global.acls.manageSystemACLs }}{{ fail "global.acls.createReplicationToken must be true when global.acls.manageSystemACLs is true because the federation secret must include the replication token" }}{{ end }} {{- if eq (int .Values.server.updatePartition) 0 }} +{{ template "consul.validateCloudConfiguration" . }} apiVersion: batch/v1 kind: Job metadata: diff --git a/charts/consul/templates/ingress-gateways-deployment.yaml b/charts/consul/templates/ingress-gateways-deployment.yaml index 2c29d122f0..58081362ed 100644 --- a/charts/consul/templates/ingress-gateways-deployment.yaml +++ b/charts/consul/templates/ingress-gateways-deployment.yaml @@ -2,6 +2,7 @@ {{- if not .Values.connectInject.enabled }}{{ fail "connectInject.enabled must be true" }}{{ end -}} {{- if and .Values.global.adminPartitions.enabled (not .Values.global.enableConsulNamespaces) }}{{ fail "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" }}{{ end }} {{- if .Values.global.lifecycleSidecarContainer }}{{ fail "global.lifecycleSidecarContainer has been renamed to global.consulSidecarContainer. Please set values using global.consulSidecarContainer." }}{{ end }} +{{ template "consul.validateCloudConfiguration" . }} {{- $root := . }} {{- $defaults := .Values.ingressGateways.defaults }} diff --git a/charts/consul/templates/mesh-gateway-deployment.yaml b/charts/consul/templates/mesh-gateway-deployment.yaml index c05f28ce13..cd8d5807d5 100644 --- a/charts/consul/templates/mesh-gateway-deployment.yaml +++ b/charts/consul/templates/mesh-gateway-deployment.yaml @@ -5,6 +5,7 @@ {{- if and .Values.global.adminPartitions.enabled (not .Values.global.enableConsulNamespaces) }}{{ fail "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" }}{{ end }} {{- if and (eq .Values.meshGateway.wanAddress.source "Static") (eq .Values.meshGateway.wanAddress.static "") }}{{ fail "if meshGateway.wanAddress.source=Static then meshGateway.wanAddress.static cannot be empty" }}{{ end }} {{- if and (eq .Values.meshGateway.wanAddress.source "Service") (eq .Values.meshGateway.service.type "NodePort") (not .Values.meshGateway.service.nodePort) }}{{ fail "if meshGateway.wanAddress.source=Service and meshGateway.service.type=NodePort, meshGateway.service.nodePort must be set" }}{{ end }} +{{ template "consul.validateCloudConfiguration" . }} apiVersion: apps/v1 kind: Deployment metadata: diff --git a/charts/consul/templates/server-acl-init-job.yaml b/charts/consul/templates/server-acl-init-job.yaml index 45567fe0ea..23d6332d2f 100644 --- a/charts/consul/templates/server-acl-init-job.yaml +++ b/charts/consul/templates/server-acl-init-job.yaml @@ -7,6 +7,7 @@ {{- if or (and .Values.global.acls.bootstrapToken.secretName (not .Values.global.acls.bootstrapToken.secretKey)) (and .Values.global.acls.bootstrapToken.secretKey (not .Values.global.acls.bootstrapToken.secretName))}}{{ fail "both global.acls.bootstrapToken.secretKey and global.acls.bootstrapToken.secretName must be set if one of them is provided" }}{{ end -}} {{- if or (and .Values.global.acls.replicationToken.secretName (not .Values.global.acls.replicationToken.secretKey)) (and .Values.global.acls.replicationToken.secretKey (not .Values.global.acls.replicationToken.secretName))}}{{ fail "both global.acls.replicationToken.secretKey and global.acls.replicationToken.secretName must be set if one of them is provided" }}{{ end -}} {{- if (and .Values.global.secretsBackend.vault.enabled (and (not .Values.global.acls.bootstrapToken.secretName) (not .Values.global.acls.replicationToken.secretName ))) }}{{fail "global.acls.bootstrapToken or global.acls.replicationToken must be provided when global.secretsBackend.vault.enabled and global.acls.manageSystemACLs are true" }}{{ end -}} +{{ template "consul.validateCloudConfiguration" . }} {{- if (and .Values.global.secretsBackend.vault.enabled (not .Values.global.secretsBackend.vault.manageSystemACLsRole)) }}{{fail "global.secretsBackend.vault.manageSystemACLsRole is required when global.secretsBackend.vault.enabled and global.acls.manageSystemACLs are true" }}{{ end -}} {{- /* We don't render this job when server.updatePartition > 0 because that means a server rollout is in progress and this job won't complete unless @@ -148,6 +149,9 @@ spec: -resource-prefix=${CONSUL_FULLNAME} \ -k8s-namespace={{ .Release.Namespace }} \ -set-server-tokens={{ $serverEnabled }} \ + {{- if .Values.global.cloud.enabled }} + -consul-tls-server-name=server.{{ .Values.global.datacenter}}.{{ .Values.global.domain}} \ + {{- end}} -consul-api-timeout={{ .Values.global.consulAPITimeout }} \ {{- if .Values.externalServers.enabled }} diff --git a/charts/consul/templates/server-statefulset.yaml b/charts/consul/templates/server-statefulset.yaml index 1a8f4ca84f..2986b85b72 100644 --- a/charts/consul/templates/server-statefulset.yaml +++ b/charts/consul/templates/server-statefulset.yaml @@ -15,6 +15,7 @@ {{- if (and (not .Values.global.enterpriseLicense.secretName) .Values.global.enterpriseLicense.secretKey) }}{{fail "enterpriseLicense.secretKey and secretName must both be specified." }}{{ end -}} {{- if (and .Values.global.acls.bootstrapToken.secretName (not .Values.global.acls.bootstrapToken.secretKey)) }}{{fail "both global.acls.bootstrapToken.secretKey and global.acls.bootstrapToken.secretName must be set if one of them is provided." }}{{ end -}} {{- if (and (not .Values.global.acls.bootstrapToken.secretName) .Values.global.acls.bootstrapToken.secretKey) }}{{fail "both global.acls.bootstrapToken.secretKey and global.acls.bootstrapToken.secretName must be set if one of them is provided." }}{{ end -}} +{{ template "consul.validateCloudConfiguration" . }} # StatefulSet to run the actual Consul server cluster. apiVersion: apps/v1 kind: StatefulSet @@ -253,6 +254,43 @@ spec: name: {{ .Values.global.acls.replicationToken.secretName | quote }} key: {{ .Values.global.acls.replicationToken.secretKey | quote }} {{- end }} + {{- if and .Values.global.cloud.enabled .Values.global.cloud.secretName }} + # These are mounted as secrets so that the consul server agent can use them. + # - the hcp-go-sdk in consul agent will already look for HCP_CLIENT_ID, HCP_CLIENT_SECRET, HCP_AUTH_URL, + # HCP_SCADA_ADDRESS, and HCP_API_HOST. so nothing more needs to be done. + # - HCP_RESOURCE_ID is created for use in the + # `-hcl="cloud { resource_id = \"${HCP_RESOURCE_ID}\" }"` logic in the command below. + - name: HCP_CLIENT_ID + valueFrom: + secretKeyRef: + name: {{ .Values.global.cloud.secretName }} + key: client-id + - name: HCP_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.global.cloud.secretName }} + key: client-secret + - name: HCP_RESOURCE_ID + valueFrom: + secretKeyRef: + name: {{ .Values.global.cloud.secretName }} + key: resource-id + - name: HCP_AUTH_URL + valueFrom: + secretKeyRef: + name: {{ .Values.global.cloud.secretName }} + key: auth-url + - name: HCP_API_HOST + valueFrom: + secretKeyRef: + name: {{ .Values.global.cloud.secretName }} + key: api-hostname + - name: HCP_SCADA_ADDRESS + valueFrom: + secretKeyRef: + name: {{ .Values.global.cloud.secretName }} + key: scada-address + {{- end }} {{- include "consul.extraEnvironmentVars" .Values.server | nindent 12 }} command: - "/bin/sh" @@ -298,6 +336,9 @@ spec: {{- end }} {{- end }} -config-file=/consul/extra-config/extra-from-values.json + {{- if and .Values.global.cloud.enabled .Values.global.cloud.secretName }} + -hcl="cloud { resource_id = \"${HCP_RESOURCE_ID}\" }" + {{- end }} volumeMounts: - name: data-{{ .Release.Namespace | trunc 58 | trimSuffix "-" }} mountPath: /consul/data diff --git a/charts/consul/templates/sync-catalog-deployment.yaml b/charts/consul/templates/sync-catalog-deployment.yaml index 4c8b4359da..6821cd90b4 100644 --- a/charts/consul/templates/sync-catalog-deployment.yaml +++ b/charts/consul/templates/sync-catalog-deployment.yaml @@ -1,6 +1,7 @@ {{- $clientEnabled := (or (and (ne (.Values.client.enabled | toString) "-") .Values.client.enabled) (and (eq (.Values.client.enabled | toString) "-") .Values.global.enabled)) }} {{- if (or (and (ne (.Values.syncCatalog.enabled | toString) "-") .Values.syncCatalog.enabled) (and (eq (.Values.syncCatalog.enabled | toString) "-") .Values.global.enabled)) }} {{- template "consul.reservedNamesFailer" (list .Values.syncCatalog.consulNamespaces.consulDestinationNamespace "syncCatalog.consulNamespaces.consulDestinationNamespace") }} +{{ template "consul.validateCloudConfiguration" . }} # The deployment for running the sync-catalog pod apiVersion: apps/v1 kind: Deployment diff --git a/charts/consul/templates/terminating-gateways-deployment.yaml b/charts/consul/templates/terminating-gateways-deployment.yaml index 80ba89de83..5cbf1b661d 100644 --- a/charts/consul/templates/terminating-gateways-deployment.yaml +++ b/charts/consul/templates/terminating-gateways-deployment.yaml @@ -1,6 +1,7 @@ {{- if .Values.terminatingGateways.enabled }} {{- if not .Values.connectInject.enabled }}{{ fail "connectInject.enabled must be true" }}{{ end -}} {{- if and .Values.global.adminPartitions.enabled (not .Values.global.enableConsulNamespaces) }}{{ fail "global.enableConsulNamespaces must be true if global.adminPartitions.enabled=true" }}{{ end }} +{{ template "consul.validateCloudConfiguration" . }} {{- $root := . }} {{- $defaults := .Values.terminatingGateways.defaults }} diff --git a/charts/consul/test/unit/api-gateway-controller-deployment.bats b/charts/consul/test/unit/api-gateway-controller-deployment.bats index 60adc84076..9858533366 100755 --- a/charts/consul/test/unit/api-gateway-controller-deployment.bats +++ b/charts/consul/test/unit/api-gateway-controller-deployment.bats @@ -904,3 +904,23 @@ load _helpers yq -r '.spec.template.metadata.annotations.foo' | tee /dev/stderr) [ "${actual}" = "bar" ] } + +#-------------------------------------------------------------------- +# global.cloud + +@test "apiGateway/Deployment: fails when global.cloud.enabled is set and global.cloud.secretName is not set" { + cd `chart_dir` + run 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.datacenter=dc-foo' \ + --set 'global.domain=bar' \ + --set 'global.cloud.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "When global.cloud.enabled is true, global.cloud.secretName must also be set." ]] +} diff --git a/charts/consul/test/unit/client-daemonset.bats b/charts/consul/test/unit/client-daemonset.bats index 039f513ad5..cb2743c779 100755 --- a/charts/consul/test/unit/client-daemonset.bats +++ b/charts/consul/test/unit/client-daemonset.bats @@ -2622,3 +2622,22 @@ rollingUpdate: [ "$status" -eq 1 ] [[ "$output" =~ "global.imageK8s is not a valid key, use global.imageK8S (note the capital 'S')" ]] } + +#-------------------------------------------------------------------- +# global.cloud + +@test "client/DaemonSet: fails when global.cloud.enabled is set and global.cloud.secretName is not set" { + cd `chart_dir` + run helm template \ + -s templates/client-daemonset.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.datacenter=dc-foo' \ + --set 'global.domain=bar' \ + --set 'global.cloud.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "When global.cloud.enabled is true, global.cloud.secretName must also be set." ]] +} diff --git a/charts/consul/test/unit/client-snapshot-agent-deployment.bats b/charts/consul/test/unit/client-snapshot-agent-deployment.bats index d12f70d3bd..457495b9c8 100644 --- a/charts/consul/test/unit/client-snapshot-agent-deployment.bats +++ b/charts/consul/test/unit/client-snapshot-agent-deployment.bats @@ -1153,3 +1153,22 @@ MIICFjCCAZsCCQCdwLtdjbzlYzAKBggqhkjOPQQDAjB0MQswCQYDVQQGEwJDQTEL' \ yq -r '.spec.template.spec.containers[0].command[2] | contains("-interval=10h34m5s")' | tee /dev/stderr) [ "${actual}" = "true" ] } + +#-------------------------------------------------------------------- +# global.cloud + +@test "client/SnapshotAgentDeployment: fails when global.cloud.enabled is set and global.cloud.secretName is not set" { + cd `chart_dir` + run helm template \ + -s templates/client-snapshot-agent-deployment.yaml \ + --set 'client.snapshotAgent.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.datacenter=dc-foo' \ + --set 'global.domain=bar' \ + --set 'global.cloud.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "When global.cloud.enabled is true, global.cloud.secretName must also be set." ]] +} diff --git a/charts/consul/test/unit/connect-inject-deployment.bats b/charts/consul/test/unit/connect-inject-deployment.bats index 8b34383e34..cdc6f4aebf 100755 --- a/charts/consul/test/unit/connect-inject-deployment.bats +++ b/charts/consul/test/unit/connect-inject-deployment.bats @@ -830,7 +830,6 @@ load _helpers local actual=$(echo "$env" | jq -r '. | select( .name == "CONSUL_LOGIN_DATACENTER").value' | tee /dev/stderr) [ "${actual}" = "dc1" ] - local actual=$(echo "$env" | jq -r '. | select( .name == "CONSUL_LOGIN_META").value' | tee /dev/stderr) [ "${actual}" = 'component=connect-injector,pod=$(NAMESPACE)/$(POD_NAME)' ] @@ -2334,3 +2333,22 @@ reservedNameTest() { local actual=$(echo "$spec" | yq '.volumes[] | select(.name == "consul-ca-cert")' | tee /dev/stderr) [ "${actual}" = "" ] } + +#-------------------------------------------------------------------- +# global.cloud + +@test "connectInject/Deployment: fails when global.cloud.enabled is set and global.cloud.secretName is not set" { + cd `chart_dir` + run helm template \ + -s templates/connect-inject-deployment.yaml \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.datacenter=dc-foo' \ + --set 'global.domain=bar' \ + --set 'global.cloud.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "When global.cloud.enabled is true, global.cloud.secretName must also be set." ]] +} diff --git a/charts/consul/test/unit/controller-deployment.bats b/charts/consul/test/unit/controller-deployment.bats index 2110958d87..0e20ea3724 100644 --- a/charts/consul/test/unit/controller-deployment.bats +++ b/charts/consul/test/unit/controller-deployment.bats @@ -841,4 +841,21 @@ load _helpers [ "${actual}" = "" ] } +#-------------------------------------------------------------------- +# global.cloud +@test "controller/Deployment: fails when global.cloud.enabled is set and global.cloud.secretName is not set" { + cd `chart_dir` + run helm template \ + -s templates/controller-deployment.yaml \ + --set 'controller.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.datacenter=dc-foo' \ + --set 'global.domain=bar' \ + --set 'global.cloud.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "When global.cloud.enabled is true, global.cloud.secretName must also be set." ]] +} diff --git a/charts/consul/test/unit/ingress-gateways-deployment.bats b/charts/consul/test/unit/ingress-gateways-deployment.bats index 057e2d9a1e..33dd4823e6 100644 --- a/charts/consul/test/unit/ingress-gateways-deployment.bats +++ b/charts/consul/test/unit/ingress-gateways-deployment.bats @@ -1145,3 +1145,23 @@ key2: value2' \ yq -s -r '.[0].spec.template.spec.terminationGracePeriodSeconds' | tee /dev/stderr) [ "${actual}" = "30" ] } + +#-------------------------------------------------------------------- +# global.cloud + +@test "ingressGateways/Deployment: fails when global.cloud.enabled is set and global.cloud.secretName is not set" { + cd `chart_dir` + run helm template \ + -s templates/ingress-gateways-deployment.yaml \ + --set 'ingressGateways.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.datacenter=dc-foo' \ + --set 'global.domain=bar' \ + --set 'global.cloud.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "When global.cloud.enabled is true, global.cloud.secretName must also be set." ]] +} diff --git a/charts/consul/test/unit/mesh-gateway-deployment.bats b/charts/consul/test/unit/mesh-gateway-deployment.bats index 7f531d6f69..0c5a93f9c6 100755 --- a/charts/consul/test/unit/mesh-gateway-deployment.bats +++ b/charts/consul/test/unit/mesh-gateway-deployment.bats @@ -1344,3 +1344,23 @@ key2: value2' \ yq -r '.spec.template.metadata.annotations.foo' | tee /dev/stderr) [ "${actual}" = "bar" ] } + +#-------------------------------------------------------------------- +# global.cloud + +@test "meshGateway/Deployment: fails when global.cloud.enabled is set and global.cloud.secretName is not set" { + cd `chart_dir` + run helm template \ + -s templates/mesh-gateway-deployment.yaml \ + --set 'meshGateway.enabled=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.datacenter=dc-foo' \ + --set 'global.domain=bar' \ + --set 'global.cloud.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "When global.cloud.enabled is true, global.cloud.secretName must also be set." ]] +} diff --git a/charts/consul/test/unit/server-acl-init-job.bats b/charts/consul/test/unit/server-acl-init-job.bats index ac123f346f..a5e13b5da0 100644 --- a/charts/consul/test/unit/server-acl-init-job.bats +++ b/charts/consul/test/unit/server-acl-init-job.bats @@ -1891,3 +1891,23 @@ load _helpers yq '.spec.template.spec.containers[0].command | any(contains("-federation"))' | tee /dev/stderr) [ "${actual}" = "true" ] } + +#-------------------------------------------------------------------- +# global.cloud + +@test "serverACLInit/Job: fails when global.cloud.enabled is set and global.cloud.secretName is not set" { + cd `chart_dir` + run helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'connectInject.enabled=true' \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.datacenter=dc-foo' \ + --set 'global.domain=bar' \ + --set 'global.cloud.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "When global.cloud.enabled is true, global.cloud.secretName must also be set." ]] +} diff --git a/charts/consul/test/unit/server-statefulset.bats b/charts/consul/test/unit/server-statefulset.bats index b0aa5b65bc..354a5123cd 100755 --- a/charts/consul/test/unit/server-statefulset.bats +++ b/charts/consul/test/unit/server-statefulset.bats @@ -1902,3 +1902,188 @@ load _helpers local actual="$(echo $object | yq -r '.spec.containers[] | select(.name=="consul").command | any(contains("-config-file=/vault/secrets/replication-token-config.hcl"))' | tee /dev/stderr)" [ "${actual}" = "true" ] } + +#-------------------------------------------------------------------- +# global.cloud + +@test "server/StatefulSet: cloud config is not set in command when global.cloud.enabled is not set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + . | tee /dev/stderr) + + # Test the flag is set. + local actual=$(echo "$object" | + yq '.spec.template.spec.containers[] | select(.name == "consul") | .command | any(contains("-hcl=\"cloud { resource_id = \\\"${HCP_RESOURCE_ID}\\\" }\""))' | tee /dev/stderr) + [ "${actual}" = "false" ] + + # Test the HCP_RESOURCE_ID environment variable is set. + local envvar=$(echo "$object" | + yq -r -c '.spec.template.spec.containers[] | select(.name == "consul") | .env | select(.name == "HCP_RESOURCE_ID")' | tee /dev/stderr) + [ "${envvar}" = "" ] +} + +@test "server/StatefulSet: does not create HCP_RESOURCE_ID, HCP_CLIENT_ID, HCP_CLIENT_SECRET, HCP_AUTH_URL, HCP_SCADA_ADDRESS, and HCP_API_HOSTNAME envvars in consul container when global.cloud.enabled is not set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + . | tee /dev/stderr ) + + local container=$(echo "$object" | + yq -r '.spec.template.spec.containers[] | select(.name == "consul")' | tee /dev/stderr) + + + local envvar=$(echo "$container" | + yq -r '.env[] | select(.name == "HCP_CLIENT_ID")' | tee /dev/stderr) + [ "${envvar}" = "" ] + + envvar=$(echo "$container" | + yq -r '.env[] | select(.name == "HCP_CLIENT_SECRET")' | tee /dev/stderr) + [ "${envvar}" = "" ] + + envvar=$(echo "$container" | + yq -r '.env[] | select(.name == "HCP_RESOURCE_ID")' | tee /dev/stderr) + [ "${envvar}" = "" ] + + envvar=$(echo "$container" | + yq -r '.env[] | select(.name == "HCP_AUTH_URL")' | tee /dev/stderr) + [ "${envvar}" = "" ] + + envvar=$(echo "$container" | + yq -r '.env[] | select(.name == "HCP_API_HOSTNAME")' | tee /dev/stderr) + [ "${envvar}" = "" ] + + envvar=$(echo "$container" | + yq -r '.env[] | select(.name == "HCP_SCADA_ADDRESS")' | tee /dev/stderr) + [ "${envvar}" = "" ] + +} + +@test "server/StatefulSet: cloud config is set in command when global.cloud.enabled is set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.cloud.enabled=true' \ + --set 'global.cloud.secretName=foo' \ + . | tee /dev/stderr) + + local actual=$(echo "$object" | + yq '.spec.template.spec.containers[] | select(.name == "consul") | .command | any(contains("-hcl=\"cloud { resource_id = \\\"${HCP_RESOURCE_ID}\\\" }\""))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + + +@test "server/StatefulSet: creates HCP_RESOURCE_ID, HCP_CLIENT_ID, HCP_CLIENT_SECRET, HCP_AUTH_URL, HCP_SCADA_ADDRESS, and HCP_API_HOSTNAME envvars in consul container when global.cloud.enabled is set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.cloud.enabled=true' \ + --set 'global.cloud.secretName=foo' \ + . | tee /dev/stderr ) + + local container=$(echo "$object" | + yq -r '.spec.template.spec.containers[] | select(.name == "consul")' | tee /dev/stderr) + + # HCP_CLIENT_ID + local envvar=$(echo "$container" | + yq -r '.env[] | select(.name == "HCP_CLIENT_ID")' | tee /dev/stderr) + + local actual=$(echo "$envvar" | + yq -r '.valueFrom.secretKeyRef.name' | tee /dev/stderr) + [ "${actual}" = "foo" ] + + actual=$(echo "$envvar" | + yq -r '.valueFrom.secretKeyRef.key' | tee /dev/stderr) + [ "${actual}" = "client-id" ] + + # HCP_CLIENT_SECRET + envvar=$(echo "$container" | + yq -r '.env[] | select(.name == "HCP_CLIENT_SECRET")' | tee /dev/stderr) + + local actual=$(echo "$envvar" | + yq -r '.valueFrom.secretKeyRef.name' | tee /dev/stderr) + [ "${actual}" = "foo" ] + + actual=$(echo "$envvar" | + yq -r '.valueFrom.secretKeyRef.key' | tee /dev/stderr) + [ "${actual}" = "client-secret" ] + + # HCP_RESOURCE_ID + envvar=$(echo "$container" | + yq -r '.env[] | select(.name == "HCP_RESOURCE_ID")' | tee /dev/stderr) + + local actual=$(echo "$envvar" | + yq -r '.valueFrom.secretKeyRef.name' | tee /dev/stderr) + [ "${actual}" = "foo" ] + + actual=$(echo "$envvar" | + yq -r '.valueFrom.secretKeyRef.key' | tee /dev/stderr) + [ "${actual}" = "resource-id" ] + + # HCP_AUTH_URL + envvar=$(echo "$container" | + yq -r '.env[] | select(.name == "HCP_AUTH_URL")' | tee /dev/stderr) + + local actual=$(echo "$envvar" | + yq -r '.valueFrom.secretKeyRef.name' | tee /dev/stderr) + [ "${actual}" = "foo" ] + + actual=$(echo "$envvar" | + yq -r '.valueFrom.secretKeyRef.key' | tee /dev/stderr) + [ "${actual}" = "auth-url" ] + + # HCP_API_HOST + envvar=$(echo "$container" | + yq -r '.env[] | select(.name == "HCP_API_HOST")' | tee /dev/stderr) + + local actual=$(echo "$envvar" | + yq -r '.valueFrom.secretKeyRef.name' | tee /dev/stderr) + [ "${actual}" = "foo" ] + + actual=$(echo "$envvar" | + yq -r '.valueFrom.secretKeyRef.key' | tee /dev/stderr) + [ "${actual}" = "api-hostname" ] + + # HCP_SCADA_ADDRESS + envvar=$(echo "$container" | + yq -r '.env[] | select(.name == "HCP_SCADA_ADDRESS")' | tee /dev/stderr) + + local actual=$(echo "$envvar" | + yq -r '.valueFrom.secretKeyRef.name' | tee /dev/stderr) + [ "${actual}" = "foo" ] + + actual=$(echo "$envvar" | + yq -r '.valueFrom.secretKeyRef.key' | tee /dev/stderr) + [ "${actual}" = "scada-address" ] +} + +@test "server/StatefulSet: cloud config is set in command global.cloud.enabled is not set" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.acls.enabled=true' \ + --set 'global.acls.bootstrapToken.secretName=name' \ + --set 'global.acls.bootstrapToken.secretKey=key' \ + . | tee /dev/stderr) + + # Test the flag is set. + local actual=$(echo "$object" | + yq '.spec.template.spec.containers[0].command | any(contains("-hcl=\"acl { tokens { initial_management = \\\"${ACL_BOOTSTRAP_TOKEN}\\\" } }\""))' | tee /dev/stderr) + [ "${actual}" = "true" ] + + # Test the ACL_BOOTSTRAP_TOKEN environment variable is set. + local actual=$(echo "$object" | + yq -r -c '.spec.template.spec.containers[0].env | map(select(.name == "ACL_BOOTSTRAP_TOKEN"))' | tee /dev/stderr) + [ "${actual}" = '[{"name":"ACL_BOOTSTRAP_TOKEN","valueFrom":{"secretKeyRef":{"name":"name","key":"key"}}}]' ] +} + +@test "server/StatefulSet: fails when global.cloud.enabled is set and global.cloud.secretName is not set" { + cd `chart_dir` + run helm template \ + -s templates/server-statefulset.yaml \ + --set 'global.cloud.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "When global.cloud.enabled is true, global.cloud.secretName must also be set." ]] +} diff --git a/charts/consul/test/unit/sync-catalog-deployment.bats b/charts/consul/test/unit/sync-catalog-deployment.bats index 28c4791914..083838d7b9 100755 --- a/charts/consul/test/unit/sync-catalog-deployment.bats +++ b/charts/consul/test/unit/sync-catalog-deployment.bats @@ -1499,3 +1499,22 @@ reservedNameTest() { [ "$status" -eq 1 ] [[ "$output" =~ "The name $name set for key syncCatalog.consulNamespaces.consulDestinationNamespace is reserved by Consul for future use" ]] } + +#-------------------------------------------------------------------- +# global.cloud + +@test "syncCatalog/Deployment: fails when global.cloud.enabled is set and global.cloud.secretName is not set" { + cd `chart_dir` + run helm template \ + -s templates/sync-catalog-deployment.yaml \ + --set 'syncCatalog.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.datacenter=dc-foo' \ + --set 'global.domain=bar' \ + --set 'global.cloud.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "When global.cloud.enabled is true, global.cloud.secretName must also be set." ]] +} diff --git a/charts/consul/test/unit/terminating-gateways-deployment.bats b/charts/consul/test/unit/terminating-gateways-deployment.bats index a700b21500..f942fb753e 100644 --- a/charts/consul/test/unit/terminating-gateways-deployment.bats +++ b/charts/consul/test/unit/terminating-gateways-deployment.bats @@ -1196,3 +1196,24 @@ key2: value2' \ yq -r '.spec.template.metadata.annotations.foo' | tee /dev/stderr) [ "${actual}" = "bar" ] } + +#-------------------------------------------------------------------- +# global.cloud + +@test "terminatingGateways/Deployment: fails when global.cloud.enabled is set and global.cloud.secretName is not set" { + cd `chart_dir` + run helm template \ + -s templates/terminating-gateways-deployment.yaml \ + --set 'terminatingGateways.enabled=true' \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'connectInject.enabled=true' \ + --set 'global.tls.enabled=true' \ + --set 'global.tls.enableAutoEncrypt=true' \ + --set 'global.datacenter=dc-foo' \ + --set 'global.domain=bar' \ + --set 'global.cloud.enabled=true' \ + . + + [ "$status" -eq 1 ] + [[ "$output" =~ "When global.cloud.enabled is true, global.cloud.secretName must also be set." ]] +} diff --git a/charts/consul/values.yaml b/charts/consul/values.yaml index 4470c4c5d3..5477e3b290 100644 --- a/charts/consul/values.yaml +++ b/charts/consul/values.yaml @@ -647,6 +647,19 @@ global: # the API before cancelling the request. consulAPITimeout: 5s + # Enables installing an HCP Consul self-managed cluster. + # Requires Consul v1.14+. + cloud: + # If true, the Helm chart will enable the installation of an HCP Consul + # self-managed cluster. + enabled: false + + # The name of the Kubernetes secret that holds the HCP cloud configuration. + # It contains the HCP service principal client_id and client_secret as well + # as the HCP resource_id. + # @type: string + secretName: null + # Server, when enabled, configures a server cluster to run. This should # be disabled if you plan on connecting to a Consul cluster external to # the Kube cluster. diff --git a/charts/demo/.helmignore b/charts/demo/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/charts/demo/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/demo/Chart.yaml b/charts/demo/Chart.yaml new file mode 100644 index 0000000000..82fc51d2df --- /dev/null +++ b/charts/demo/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: consul-demo +description: A Helm chart for Consul demo app + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" diff --git a/charts/demo/templates/frontend.yaml b/charts/demo/templates/frontend.yaml new file mode 100644 index 0000000000..c72fad0d08 --- /dev/null +++ b/charts/demo/templates/frontend.yaml @@ -0,0 +1,116 @@ +# WARNING: The HashiCups files have been copied directly from +# https://github.com/hashicorp/learn-consul-kubernetes/tree/main/layer7-observability/hashicups +# Any modifications begin with the comment # BEGIN CONSUL-K8S MODIFICATION +# and end with the comment # BEGIN CONSUL-K8S MODIFICATION. +# If keeping these files manually up to date with their upstream source, +# the files will need to be copied from the above repo and transferred here. +# Once transferred, all modifications will need to be reapplied. +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend + labels: + app: frontend +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + selector: + app: frontend +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: frontend +automountServiceAccountToken: true +--- +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceDefaults +metadata: + name: frontend +spec: + protocol: "http" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-configmap +data: + config: | + # /etc/nginx/conf.d/default.conf + server { + listen 80; + server_name localhost; + #charset koi8-r; + #access_log /var/log/nginx/host.access.log main; + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + # Proxy pass the api location to save CORS + # Use location exposed by Consul connect + location /api { + # BEGIN CONSUL-K8S MODIFICATION + proxy_pass http://public-api.{{ .Release.Namespace }}.svc.cluster.local:8080; + # END CONSUL-K8S MODIFICATION + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend +spec: + replicas: 1 + selector: + matchLabels: + service: frontend + app: frontend + template: + metadata: + labels: + service: frontend + app: frontend + # BEGIN CONSUL-K8S MODIFICATION + annotations: + 'consul.hashicorp.com/connect-inject': 'true' + # END CONSUL-K8S MODIFICATION + spec: + serviceAccountName: frontend + volumes: + - name: config + configMap: + name: nginx-configmap + items: + - key: config + path: default.conf + containers: + - name: frontend + image: hashicorpdemoapp/frontend:v0.0.3 + ports: + - containerPort: 80 + volumeMounts: + - name: config + mountPath: /etc/nginx/conf.d + readOnly: true +--- +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceIntentions +metadata: + name: frontend-to-public-api +spec: + destination: + name: public-api + sources: + - name: frontend + action: allow diff --git a/charts/demo/templates/postgres.yaml b/charts/demo/templates/postgres.yaml new file mode 100644 index 0000000000..89794fa3e3 --- /dev/null +++ b/charts/demo/templates/postgres.yaml @@ -0,0 +1,76 @@ +# WARNING: The HashiCups files have been copied directly from +# https://github.com/hashicorp/learn-consul-kubernetes/tree/main/layer7-observability/hashicups +# Any modifications begin with the comment # BEGIN CONSUL-K8S MODIFICATION +# and end with the comment # BEGIN CONSUL-K8S MODIFICATION. +# If keeping these files manually up to date with their upstream source, +# the files will need to be copied from the above repo and transferred here. +# Once transferred, all modifications will need to be reapplied. +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + labels: + app: postgres +spec: + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 + selector: + app: postgres +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: postgres +automountServiceAccountToken: true +--- +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceDefaults +metadata: + name: postgres +spec: + protocol: tcp +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres +spec: + replicas: 1 + selector: + matchLabels: + service: postgres + app: postgres + template: + metadata: + labels: + service: postgres + app: postgres + # BEGIN CONSUL-K8S MODIFICATION + annotations: + 'consul.hashicorp.com/connect-inject': 'true' + # END CONSUL-K8S MODIFICATION + spec: + serviceAccountName: postgres + containers: + - name: postgres + image: hashicorpdemoapp/product-api-db:v0.0.11 + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: products + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + value: password + # only listen on loopback so only access is via connect proxy + args: ["-c", "listen_addresses=127.0.0.1"] + volumeMounts: + - mountPath: "/var/lib/postgresql/data" + name: "pgdata" + volumes: + - name: pgdata + emptyDir: {} diff --git a/charts/demo/templates/product-api.yaml b/charts/demo/templates/product-api.yaml new file mode 100644 index 0000000000..b89c25dccd --- /dev/null +++ b/charts/demo/templates/product-api.yaml @@ -0,0 +1,108 @@ +# WARNING: The HashiCups files have been copied directly from +# https://github.com/hashicorp/learn-consul-kubernetes/tree/main/layer7-observability/hashicups +# Any modifications begin with the comment # BEGIN CONSUL-K8S MODIFICATION +# and end with the comment # BEGIN CONSUL-K8S MODIFICATION. +# If keeping these files manually up to date with their upstream source, +# the files will need to be copied from the above repo and transferred here. +# Once transferred, all modifications will need to be reapplied. +--- +apiVersion: v1 +kind: Service +metadata: + name: product-api +spec: + selector: + app: product-api + ports: + - name: http + protocol: TCP + port: 9090 + targetPort: 9090 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: product-api +automountServiceAccountToken: true +--- +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceDefaults +metadata: + name: product-api +spec: + protocol: "http" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: db-configmap +data: + # BEGIN CONSUL-K8S MODIFICATION + config: | + { + "db_connection": "host=postgres.{{ .Release.Namespace }}.svc.cluster.local port=5432 user=postgres password=password dbname=products sslmode=disable", + "bind_address": ":9090", + "metrics_address": ":9103" + } + # END CONSUL-K8S MODIFICATION +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: product-api + labels: + app: product-api +spec: + replicas: 1 + selector: + matchLabels: + app: product-api + template: + metadata: + labels: + app: product-api + # BEGIN CONSUL-K8S MODIFICATION + annotations: + 'consul.hashicorp.com/connect-inject': 'true' + # END CONSUL-K8S MODIFICATION + spec: + serviceAccountName: product-api + volumes: + - name: config + configMap: + name: db-configmap + items: + - key: config + path: conf.json + containers: + - name: product-api + image: hashicorpdemoapp/product-api:v0.0.12 + ports: + - containerPort: 9090 + - containerPort: 9103 + env: + - name: "CONFIG_FILE" + value: "/config/conf.json" + livenessProbe: + httpGet: + path: /health + port: 9090 + initialDelaySeconds: 15 + timeoutSeconds: 1 + periodSeconds: 10 + failureThreshold: 30 + volumeMounts: + - name: config + mountPath: /config + readOnly: true +--- +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceIntentions +metadata: + name: product-api-to-postgres +spec: + destination: + name: postgres + sources: + - name: product-api + action: allow diff --git a/charts/demo/templates/public-api.yaml b/charts/demo/templates/public-api.yaml new file mode 100644 index 0000000000..3c812c26f6 --- /dev/null +++ b/charts/demo/templates/public-api.yaml @@ -0,0 +1,79 @@ +# WARNING: The HashiCups files have been copied directly from +# https://github.com/hashicorp/learn-consul-kubernetes/tree/main/layer7-observability/hashicups +# Any modifications begin with the comment # BEGIN CONSUL-K8S MODIFICATION +# and end with the comment # BEGIN CONSUL-K8S MODIFICATION. +# If keeping these files manually up to date with their upstream source, +# the files will need to be copied from the above repo and transferred here. +# Once transferred, all modifications will need to be reapplied. +--- +apiVersion: v1 +kind: Service +metadata: + name: public-api + labels: + app: public-api +spec: + type: ClusterIP + ports: + - port: 8080 + targetPort: 8080 + selector: + app: public-api +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: public-api +automountServiceAccountToken: true +--- +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceDefaults +metadata: + name: public-api +spec: + protocol: "http" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: public-api +spec: + replicas: 1 + selector: + matchLabels: + service: public-api + app: public-api + template: + metadata: + labels: + service: public-api + app: public-api + # BEGIN CONSUL-K8S MODIFICATION + annotations: + 'consul.hashicorp.com/connect-inject': 'true' + # END CONSUL-K8S MODIFICATION + spec: + serviceAccountName: public-api + containers: + - name: public-api + image: hashicorpdemoapp/public-api:v0.0.3 + ports: + - containerPort: 8080 + env: + - name: BIND_ADDRESS + value: ":8080" + - name: PRODUCT_API_URI + # BEGIN CONSUL-K8S MODIFICATION + value: "http://product-api.{{ .Release.Namespace }}.svc.cluster.local:9090" + # END CONSUL-K8S MODIFICATION +--- +apiVersion: consul.hashicorp.com/v1alpha1 +kind: ServiceIntentions +metadata: + name: public-api-to-product-api +spec: + destination: + name: product-api + sources: + - name: public-api + action: allow diff --git a/charts/demo/values.yaml b/charts/demo/values.yaml new file mode 100644 index 0000000000..2dd99602c7 --- /dev/null +++ b/charts/demo/values.yaml @@ -0,0 +1 @@ +# Default values for demo. diff --git a/charts/embed_chart.go b/charts/embed_chart.go index 6393508ebb..29e7e9635e 100644 --- a/charts/embed_chart.go +++ b/charts/embed_chart.go @@ -14,3 +14,6 @@ import "embed" // explicitly embedded. //go:embed consul/Chart.yaml consul/values.yaml consul/templates consul/templates/_helpers.tpl var ConsulHelmChart embed.FS + +//go:embed demo/Chart.yaml demo/values.yaml demo/templates +var DemoHelmChart embed.FS diff --git a/cli/cmd/install/install.go b/cli/cmd/install/install.go index 61742cebbe..785e66206a 100644 --- a/cli/cmd/install/install.go +++ b/cli/cmd/install/install.go @@ -3,6 +3,7 @@ package install import ( "errors" "fmt" + "net/http" "os" "strings" "sync" @@ -14,9 +15,12 @@ import ( "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/consul-k8s/cli/config" "github.com/hashicorp/consul-k8s/cli/helm" + "github.com/hashicorp/consul-k8s/cli/preset" "github.com/hashicorp/consul-k8s/cli/release" "github.com/hashicorp/consul-k8s/cli/validation" "github.com/posener/complete" + "golang.org/x/text/cases" + "golang.org/x/text/language" "helm.sh/helm/v3/pkg/action" helmCLI "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/cli/values" @@ -25,6 +29,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/utils/strings/slices" "sigs.k8s.io/yaml" ) @@ -56,6 +61,17 @@ const ( flagNameContext = "context" flagNameKubeconfig = "kubeconfig" + + flagNameHCPResourceID = "hcp-resource-id" + + envHCPClientID = "HCP_CLIENT_ID" + envHCPClientSecret = "HCP_CLIENT_SECRET" + envHCPAuthURL = "HCP_AUTH_URL" + envHCPAPIHost = "HCP_API_HOST" + envHCPScadaAddress = "HCP_SCADA_ADDRESS" + + flagNameDemo = "demo" + defaultDemo = false ) type Command struct { @@ -63,20 +79,26 @@ type Command struct { kubernetes kubernetes.Interface + helmActionsRunner helm.HelmActionsRunner + + httpClient *http.Client + set *flag.Sets - flagPreset string - flagNamespace string - flagDryRun bool - flagAutoApprove bool - flagValueFiles []string - flagSetStringValues []string - flagSetValues []string - flagFileValues []string - flagTimeout string - timeoutDuration time.Duration - flagVerbose bool - flagWait bool + flagPreset string + flagNamespace string + flagDryRun bool + flagAutoApprove bool + flagValueFiles []string + flagSetStringValues []string + flagSetValues []string + flagFileValues []string + flagTimeout string + timeoutDuration time.Duration + flagVerbose bool + flagWait bool + flagDemo bool + flagNameHCPResourceID string flagKubeConfig string flagKubeContext string @@ -86,12 +108,6 @@ type Command struct { } func (c *Command) init() { - // Store all the possible preset values in 'presetList'. Printed in the help message. - var presetList []string - for name := range config.Presets { - presetList = append(presetList, name) - } - c.set = flag.NewSets() f := c.set.NewSet("Command Options") f.BoolVar(&flag.BoolVar{ @@ -122,7 +138,7 @@ func (c *Command) init() { Name: flagNamePreset, Target: &c.flagPreset, Default: defaultPreset, - Usage: fmt.Sprintf("Use an installation preset, one of %s. Defaults to none", strings.Join(presetList, ", ")), + Usage: fmt.Sprintf("Use an installation preset, one of %s. Defaults to none", strings.Join(preset.Presets, ", ")), }) f.StringSliceVar(&flag.StringSliceVar{ Name: flagNameSetValues, @@ -159,6 +175,19 @@ func (c *Command) init() { Default: defaultWait, Usage: "Wait for Kubernetes resources in installation to be ready before exiting command.", }) + f.BoolVar(&flag.BoolVar{ + Name: flagNameDemo, + Target: &c.flagDemo, + Default: defaultDemo, + Usage: fmt.Sprintf("Install %s immediately after installing %s.", + common.ReleaseTypeConsulDemo, common.ReleaseTypeConsul), + }) + f.StringVar(&flag.StringVar{ + Name: flagNameHCPResourceID, + Target: &c.flagNameHCPResourceID, + Default: "", + Usage: "Set the HCP resource_id when using the 'cloud' preset.", + }) f = c.set.NewSet("Global Options") f.StringVar(&flag.StringVar{ @@ -181,6 +210,9 @@ func (c *Command) init() { // Run installs Consul into a Kubernetes cluster. func (c *Command) Run(args []string) int { c.once.Do(c.init) + if c.helmActionsRunner == nil { + c.helmActionsRunner = &helm.ActionRunner{} + } // The logger is initialized in main with the name cli. Here, we reset the name to install so log lines would be prefixed with install. c.Log.ResetNamed("install") @@ -243,7 +275,11 @@ func (c *Command) Run(args []string) int { c.UI.Output("Checking if Consul can be installed", terminal.WithHeaderStyle()) // Ensure there is not an existing Consul installation which would cause a conflict. - if name, ns, err := common.CheckForInstallations(settings, uiLogger); err == nil { + if found, name, ns, _ := c.helmActionsRunner.CheckForInstallations(&helm.CheckForInstallationsOptions{ + Settings: settings, + ReleaseName: common.DefaultReleaseName, + DebugLog: uiLogger, + }); found { c.UI.Output("Cannot install Consul. A Consul cluster is already installed in namespace %s with name %s.", ns, name, terminal.WithErrorStyle()) c.UI.Output("Use the command `consul-k8s uninstall` to uninstall Consul from the cluster.", terminal.WithInfoStyle()) return 1 @@ -257,6 +293,38 @@ func (c *Command) Run(args []string) int { } c.UI.Output("No existing Consul persistent volume claims found", terminal.WithSuccessStyle()) + release := release.Release{ + Name: common.DefaultReleaseName, + Namespace: c.flagNamespace, + } + + msg, err := c.checkForPreviousSecrets(release) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + c.UI.Output(msg, terminal.WithSuccessStyle()) + + if c.flagDemo { + c.UI.Output("Checking if %s can be installed", + cases.Title(language.English).String(common.ReleaseTypeConsulDemo), + terminal.WithHeaderStyle()) + + // Ensure there is not an existing Consul demo installation which would cause a conflict. + if found, name, ns, _ := c.helmActionsRunner.CheckForInstallations(&helm.CheckForInstallationsOptions{ + Settings: settings, + ReleaseName: common.ConsulDemoAppReleaseName, + DebugLog: uiLogger, + }); found { + c.UI.Output("Cannot install %s. A %s cluster is already installed in namespace %s with name %s.", + common.ReleaseTypeConsulDemo, common.ReleaseTypeConsulDemo, ns, name, terminal.WithErrorStyle()) + c.UI.Output("Use the command `consul-k8s uninstall` to uninstall the %s from the cluster.", + common.ReleaseTypeConsulDemo, terminal.WithInfoStyle()) + return 1 + } + c.UI.Output("No existing %s installations found.", common.ReleaseTypeConsulDemo, terminal.WithSuccessStyle()) + } + // Handle preset, value files, and set values logic. vals, err := c.mergeValuesFlagsWithPrecedence(settings) if err != nil { @@ -276,104 +344,104 @@ func (c *Command) Run(args []string) int { return 1 } - rel := release.Release{ - Name: "consul", - Namespace: c.flagNamespace, - Configuration: helmVals, - } - - msg, err := c.checkForPreviousSecrets(rel) - if err != nil { - c.UI.Output(err.Error(), terminal.WithErrorStyle()) - return 1 - } - c.UI.Output(msg, terminal.WithSuccessStyle()) + release.Configuration = helmVals // If an enterprise license secret was provided, check that the secret exists and that the enterprise Consul image is set. if helmVals.Global.EnterpriseLicense.SecretName != "" { - if err := c.checkValidEnterprise(rel.Configuration.Global.EnterpriseLicense.SecretName); err != nil { + if err := c.checkValidEnterprise(release.Configuration.Global.EnterpriseLicense.SecretName); err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } c.UI.Output("Valid enterprise Consul secret found.", terminal.WithSuccessStyle()) } - // Print out the installation summary. - if !c.flagAutoApprove { - c.UI.Output("Consul Installation Summary", terminal.WithHeaderStyle()) - c.UI.Output("Name: %s", common.DefaultReleaseName, terminal.WithInfoStyle()) - c.UI.Output("Namespace: %s", c.flagNamespace, terminal.WithInfoStyle()) - - if len(vals) == 0 { - c.UI.Output("\nNo overrides provided, using the default Helm values.", terminal.WithInfoStyle()) - } else { - c.UI.Output("\nHelm value overrides\n-------------------\n"+string(valuesYaml), terminal.WithInfoStyle()) - } - } - - // Without informing the user, default global.name to consul if it hasn't been set already. We don't allow setting - // the release name, and since that is hardcoded to "consul", setting global.name to "consul" makes it so resources - // aren't double prefixed with "consul-consul-...". - vals = common.MergeMaps(config.Convert(config.GlobalNameConsul), vals) - - if c.flagDryRun { - c.UI.Output("Dry run complete. No changes were made to the Kubernetes cluster.\n"+ - "Installation can proceed with this configuration.", terminal.WithInfoStyle()) - return 0 + err = c.installConsul(valuesYaml, vals, settings, uiLogger) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 } - if !c.flagAutoApprove { - confirmation, err := c.UI.Input(&terminal.Input{ - Prompt: "Proceed with installation? (y/N)", - Style: terminal.InfoStyle, - Secret: false, - }) - + if c.flagDemo { + timeout, err := time.ParseDuration(c.flagTimeout) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } - if common.Abort(confirmation) { - c.UI.Output("Install aborted. Use the command `consul-k8s install -help` to learn how to customize your installation.", - terminal.WithInfoStyle()) + options := &helm.InstallOptions{ + ReleaseName: common.ConsulDemoAppReleaseName, + ReleaseType: common.ReleaseTypeConsulDemo, + Namespace: c.flagNamespace, + Values: make(map[string]interface{}), + Settings: settings, + EmbeddedChart: consulChart.DemoHelmChart, + ChartDirName: "demo", + UILogger: uiLogger, + DryRun: c.flagDryRun, + AutoApprove: c.flagAutoApprove, + Wait: c.flagWait, + Timeout: timeout, + UI: c.UI, + HelmActionsRunner: c.helmActionsRunner, + } + err = helm.InstallDemoApp(options) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } } - c.UI.Output("Installing Consul", terminal.WithHeaderStyle()) + if c.flagDryRun { + c.UI.Output("Dry run complete. No changes were made to the Kubernetes cluster.\n"+ + "Installation can proceed with this configuration.", terminal.WithInfoStyle()) + } + + return 0 +} - // Setup action configuration for Helm Go SDK function calls. - actionConfig := new(action.Configuration) - actionConfig, err = helm.InitActionConfig(actionConfig, c.flagNamespace, settings, uiLogger) - if err != nil { - c.UI.Output(err.Error(), terminal.WithErrorStyle()) - return 1 +func (c *Command) installConsul(valuesYaml []byte, vals map[string]interface{}, settings *helmCLI.EnvSettings, uiLogger action.DebugLog) error { + // Print out the installation summary. + c.UI.Output("Consul Installation Summary", terminal.WithHeaderStyle()) + c.UI.Output("Name: %s", common.DefaultReleaseName, terminal.WithInfoStyle()) + c.UI.Output("Namespace: %s", c.flagNamespace, terminal.WithInfoStyle()) + + if len(vals) == 0 { + c.UI.Output("\nNo overrides provided, using the default Helm values.", terminal.WithInfoStyle()) + } else { + c.UI.Output("\nHelm value overrides\n-------------------\n"+string(valuesYaml), terminal.WithInfoStyle()) } - // Setup the installation action. - install := action.NewInstall(actionConfig) - install.ReleaseName = common.DefaultReleaseName - install.Namespace = c.flagNamespace - install.CreateNamespace = true - install.Wait = c.flagWait - install.Timeout = c.timeoutDuration + // Without informing the user, default global.name to consul if it hasn't been set already. We don't allow setting + // the release name, and since that is hardcoded to "consul", setting global.name to "consul" makes it so resources + // aren't double prefixed with "consul-consul-...". + vals = common.MergeMaps(config.ConvertToMap(config.GlobalNameConsul), vals) - // Load the Helm chart. - chart, err := helm.LoadChart(consulChart.ConsulHelmChart, common.TopLevelChartDirName) + timeout, err := time.ParseDuration(c.flagTimeout) if err != nil { - c.UI.Output(err.Error(), terminal.WithErrorStyle()) - return 1 + return err } - c.UI.Output("Downloaded charts", terminal.WithSuccessStyle()) - - // Run the install. - if _, err = install.Run(chart, vals); err != nil { - c.UI.Output(err.Error(), terminal.WithErrorStyle()) - return 1 + installOptions := &helm.InstallOptions{ + ReleaseName: common.DefaultReleaseName, + ReleaseType: common.ReleaseTypeConsul, + Namespace: c.flagNamespace, + Values: vals, + Settings: settings, + EmbeddedChart: consulChart.ConsulHelmChart, + ChartDirName: common.TopLevelChartDirName, + UILogger: uiLogger, + DryRun: c.flagDryRun, + AutoApprove: c.flagAutoApprove, + Wait: c.flagWait, + Timeout: timeout, + UI: c.UI, + HelmActionsRunner: c.helmActionsRunner, + } + + err = helm.InstallHelmRelease(installOptions) + if err != nil { + return err } - c.UI.Output("Consul installed in namespace %q.", c.flagNamespace, terminal.WithSuccessStyle()) - return 0 + return nil } // Help returns a description of the command and how it is used. @@ -405,6 +473,8 @@ func (c *Command) AutocompleteFlags() complete.Flags { fmt.Sprintf("-%s", flagNameWait): complete.PredictNothing, fmt.Sprintf("-%s", flagNameContext): complete.PredictNothing, fmt.Sprintf("-%s", flagNameKubeconfig): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameDemo): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameHCPResourceID): complete.PredictNothing, } } @@ -500,7 +570,14 @@ func (c *Command) mergeValuesFlagsWithPrecedence(settings *helmCLI.EnvSettings) } if c.flagPreset != defaultPreset { // Note the ordering of the function call, presets have lower precedence than set vals. - presetMap := config.Presets[c.flagPreset].(map[string]interface{}) + p, err := c.getPreset(c.flagPreset) + if err != nil { + return nil, fmt.Errorf("error getting preset provider: %s", err) + } + presetMap, err := p.GetValueMap() + if err != nil { + return nil, fmt.Errorf("error getting preset values: %s", err) + } vals = common.MergeMaps(presetMap, vals) } return vals, err @@ -517,13 +594,28 @@ func (c *Command) validateFlags(args []string) error { if len(c.flagValueFiles) != 0 && c.flagPreset != defaultPreset { return fmt.Errorf("cannot set both -%s and -%s", flagNameConfigFile, flagNamePreset) } - if _, ok := config.Presets[c.flagPreset]; c.flagPreset != defaultPreset && !ok { + if ok := slices.Contains(preset.Presets, c.flagPreset); c.flagPreset != defaultPreset && !ok { return fmt.Errorf("'%s' is not a valid preset", c.flagPreset) } if !common.IsValidLabel(c.flagNamespace) { return fmt.Errorf("'%s' is an invalid namespace. Namespaces follow the RFC 1123 label convention and must "+ "consist of a lower case alphanumeric character or '-' and must start/end with an alphanumeric character", c.flagNamespace) } + + if c.flagPreset == preset.PresetCloud { + clientID := os.Getenv(envHCPClientID) + clientSecret := os.Getenv(envHCPClientSecret) + if clientID == "" { + return fmt.Errorf("When '%s' is specified as the preset, the '%s' environment variable must also be set", preset.PresetCloud, envHCPClientID) + } else if clientSecret == "" { + return fmt.Errorf("When '%s' is specified as the preset, the '%s' environment variable must also be set", preset.PresetCloud, envHCPClientSecret) + } else if c.flagNameHCPResourceID == "" { + return fmt.Errorf("When '%s' is specified as the preset, the '%s' flag must also be provided", preset.PresetCloud, flagNameHCPResourceID) + } + } else if c.flagNameHCPResourceID != "" { + return fmt.Errorf("The '%s' flag can only be used with the '%s' preset", flagNameHCPResourceID, preset.PresetCloud) + } + duration, err := time.ParseDuration(c.flagTimeout) if err != nil { return fmt.Errorf("unable to parse -%s: %s", flagNameTimeout, err) @@ -552,3 +644,29 @@ func (c *Command) checkValidEnterprise(secretName string) error { } return nil } + +// getPreset is a factory function that, given a string, produces a struct that +// implements the Preset interface. If the string is not recognized an error is +// returned. +func (c *Command) getPreset(name string) (preset.Preset, error) { + hcpConfig := &preset.HCPConfig{ + ResourceID: c.flagNameHCPResourceID, + ClientID: os.Getenv(envHCPClientID), + ClientSecret: os.Getenv(envHCPClientSecret), + AuthURL: os.Getenv(envHCPAuthURL), + APIHostname: os.Getenv(envHCPAPIHost), + ScadaAddress: os.Getenv(envHCPScadaAddress), + } + getPresetConfig := &preset.GetPresetConfig{ + Name: name, + CloudPreset: &preset.CloudPreset{ + KubernetesClient: c.kubernetes, + KubernetesNamespace: c.flagNamespace, + HCPConfig: hcpConfig, + UI: c.UI, + HTTPClient: c.httpClient, + Context: c.Ctx, + }, + } + return preset.GetPreset(getPresetConfig) +} diff --git a/cli/cmd/install/install_test.go b/cli/cmd/install/install_test.go index a66febc336..15bb3a6f69 100644 --- a/cli/cmd/install/install_test.go +++ b/cli/cmd/install/install_test.go @@ -1,40 +1,41 @@ package install import ( + "bytes" "context" + "errors" "flag" "fmt" + "io" "os" "testing" "github.com/hashicorp/consul-k8s/cli/common" cmnFlag "github.com/hashicorp/consul-k8s/cli/common/flag" + "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/consul-k8s/cli/helm" + "github.com/hashicorp/consul-k8s/cli/preset" "github.com/hashicorp/consul-k8s/cli/release" "github.com/hashicorp/go-hclog" "github.com/posener/complete" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + helmRelease "helm.sh/helm/v3/pkg/release" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" ) func TestCheckForPreviousPVCs(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() - pvc := &v1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: "consul-server-test1", - }, - } - pvc2 := &v1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: "consul-server-test2", - }, - } - c.kubernetes.CoreV1().PersistentVolumeClaims("default").Create(context.Background(), pvc, metav1.CreateOptions{}) - c.kubernetes.CoreV1().PersistentVolumeClaims("default").Create(context.Background(), pvc2, metav1.CreateOptions{}) + + createPVC(t, "consul-server-test1", "default", c.kubernetes) + createPVC(t, "consul-server-test2", "default", c.kubernetes) + err := c.checkForPreviousPVCs() require.Error(t, err) require.Equal(t, err.Error(), "found persistent volume claims from previous installations, delete before reinstalling: default/consul-server-test1,default/consul-server-test2") @@ -45,12 +46,7 @@ func TestCheckForPreviousPVCs(t *testing.T) { require.NoError(t, err) // Add a new irrelevant PVC and make sure the check continues to pass. - pvc = &v1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: "irrelevant-pvc", - }, - } - c.kubernetes.CoreV1().PersistentVolumeClaims("default").Create(context.Background(), pvc, metav1.CreateOptions{}) + createPVC(t, "irrelevant-pvc", "default", c.kubernetes) err = c.checkForPreviousPVCs() require.NoError(t, err) } @@ -146,7 +142,7 @@ func TestCheckForPreviousSecrets(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() c.kubernetes.CoreV1().Secrets("consul").Create(context.Background(), tc.secret, metav1.CreateOptions{}) @@ -194,7 +190,7 @@ func TestValidateFlags(t *testing.T) { } for _, testCase := range testCases { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) t.Run(testCase.description, func(t *testing.T) { if err := c.validateFlags(testCase.input); err == nil { t.Errorf("Test case should have failed.") @@ -204,16 +200,22 @@ func TestValidateFlags(t *testing.T) { } // getInitializedCommand sets up a command struct for tests. -func getInitializedCommand(t *testing.T) *Command { +func getInitializedCommand(t *testing.T, buf io.Writer) *Command { t.Helper() log := hclog.New(&hclog.LoggerOptions{ Name: "cli", Level: hclog.Info, Output: os.Stdout, }) - + var ui terminal.UI + if buf != nil { + ui = terminal.NewUI(context.Background(), buf) + } else { + ui = terminal.NewBasicUI(context.Background()) + } baseCommand := &common.BaseCommand{ Log: log, + UI: ui, } c := &Command{ @@ -224,7 +226,7 @@ func getInitializedCommand(t *testing.T) *Command { } func TestCheckValidEnterprise(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() secret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -238,7 +240,7 @@ func TestCheckValidEnterprise(t *testing.T) { } // Enterprise secret is valid. - c.kubernetes.CoreV1().Secrets("consul").Create(context.Background(), secret, metav1.CreateOptions{}) + createSecret(t, secret, "consul", c.kubernetes) err := c.checkValidEnterprise(secret.Name) require.NoError(t, err) @@ -256,7 +258,7 @@ func TestCheckValidEnterprise(t *testing.T) { func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { t.Parallel() - cmd := getInitializedCommand(t) + cmd := getInitializedCommand(t, nil) predictor := cmd.AutocompleteFlags() @@ -279,7 +281,437 @@ func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { } func TestTaskCreateCommand_AutocompleteArgs(t *testing.T) { - cmd := getInitializedCommand(t) + cmd := getInitializedCommand(t, nil) c := cmd.AutocompleteArgs() assert.Equal(t, complete.PredictNothing, c) } + +// TestValidateCloudPresets tests the validate flags function when passed the cloud preset. +func TestValidateCloudPresets(t *testing.T) { + testCases := []struct { + description string + input []string + preProcessingFunc func() + postProcessingFunc func() + expectError bool + }{ + { + "Should not error on cloud preset when HCP_CLIENT_ID and HCP_CLIENT_SECRET envvars are present and hcp-resource-id parameter is provided.", + []string{"-preset=cloud", "-hcp-resource-id=foobar"}, + func() { + os.Setenv("HCP_CLIENT_ID", "foo") + os.Setenv("HCP_CLIENT_SECRET", "bar") + }, + func() { + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") + }, + false, + }, + { + "Should error on cloud preset when HCP_CLIENT_ID is not provided.", + []string{"-preset=cloud", "-hcp-resource-id=foobar"}, + func() { + os.Unsetenv("HCP_CLIENT_ID") + os.Setenv("HCP_CLIENT_SECRET", "bar") + }, + func() { + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") + }, + true, + }, + { + "Should error on cloud preset when HCP_CLIENT_SECRET is not provided.", + []string{"-preset=cloud", "-hcp-resource-id=foobar"}, + func() { + os.Setenv("HCP_CLIENT_ID", "foo") + os.Unsetenv("HCP_CLIENT_SECRET") + }, + func() { + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") + }, + true, + }, + { + "Should error on cloud preset when -hcp-resource-id flag is not provided.", + []string{"-preset=cloud"}, + func() { + os.Setenv("HCP_CLIENT_ID", "foo") + os.Setenv("HCP_CLIENT_SECRET", "bar") + }, + func() { + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") + }, + true, + }, + { + "Should error when -hcp-resource-id flag is provided but cloud preset is not specified.", + []string{"-hcp-resource-id=foobar"}, + func() { + os.Setenv("HCP_CLIENT_ID", "foo") + os.Setenv("HCP_CLIENT_SECRET", "bar") + }, + func() { + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") + }, + true, + }, + } + + for _, testCase := range testCases { + testCase.preProcessingFunc() + c := getInitializedCommand(t, nil) + t.Run(testCase.description, func(t *testing.T) { + err := c.validateFlags(testCase.input) + if testCase.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + defer testCase.postProcessingFunc() + } +} + +func TestGetPreset(t *testing.T) { + testCases := []struct { + description string + presetName string + }{ + { + "'cloud' should return a CloudPreset'.", + preset.PresetCloud, + }, + { + "'quickstart' should return a QuickstartPreset'.", + preset.PresetQuickstart, + }, + { + "'secure' should return a SecurePreset'.", + preset.PresetSecure, + }, + } + + for _, tc := range testCases { + c := getInitializedCommand(t, nil) + t.Run(tc.description, func(t *testing.T) { + p, err := c.getPreset(tc.presetName) + require.NoError(t, err) + switch p.(type) { + case *preset.CloudPreset: + require.Equal(t, preset.PresetCloud, tc.presetName) + case *preset.QuickstartPreset: + require.Equal(t, preset.PresetQuickstart, tc.presetName) + case *preset.SecurePreset: + require.Equal(t, preset.PresetSecure, tc.presetName) + } + }) + } +} + +func TestInstall(t *testing.T) { + var k8s kubernetes.Interface + licenseSecretName := "consul-license" + cases := map[string]struct { + input []string + messages []string + helmActionsRunner *helm.MockActionRunner + preProcessingFunc func() + expectedReturnCode int + expectCheckedForConsulInstallations bool + expectCheckedForConsulDemoInstallations bool + expectConsulInstalled bool + expectConsulDemoInstalled bool + }{ + "install with no arguments returns success": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n No overrides provided, using the default Helm values.\n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: true, + expectConsulDemoInstalled: false, + }, + "install when consul installation errors returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n No overrides provided, using the default Helm values.\n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ! Helm returned an error.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + InstallFunc: func(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*helmRelease.Release, error) { + return nil, errors.New("Helm returned an error.") + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, + }, + "install with no arguments when consul installation already exists returns error": { + input: []string{ + "--auto-approve", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ! Cannot install Consul. A Consul cluster is already installed in namespace consul with name consul.\n Use the command `consul-k8s uninstall` to uninstall Consul from the cluster.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + return true, "consul", "consul", nil + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, + }, + "install with no arguments when PVCs exist returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ! found persistent volume claims from previous installations, delete before reinstalling: consul/consul-server-test1\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + preProcessingFunc: func() { + createPVC(t, "consul-server-test1", "consul", k8s) + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, + }, + "install with no arguments when secrets exist returns error": { + input: []string{ + "--auto-approve", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ! Found Consul secrets, possibly from a previous installation.\nDelete existing Consul secrets from Kubernetes:\n\nkubectl delete secret consul-secret --namespace consul\n\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + preProcessingFunc: func() { + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-secret", + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + }, + } + createSecret(t, secret, "consul", k8s) + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, + }, + "enterprise install when license secret exists returns success": { + input: []string{ + "--set", fmt.Sprintf("global.enterpriseLicense.secretName=%s", licenseSecretName), + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n ✓ Valid enterprise Consul secret found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n Helm value overrides\n -------------------\n global:\n enterpriseLicense:\n secretName: consul-license\n \n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + preProcessingFunc: func() { + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: licenseSecretName, + }, + } + createSecret(t, secret, "consul", k8s) + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: true, + expectConsulDemoInstalled: false, + }, + "enterprise install when license secret does not exist returns error": { + input: []string{ + "--set", fmt.Sprintf("global.enterpriseLicense.secretName=%s", licenseSecretName), + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n ! enterprise license secret \"consul-license\" is not found in the \"consul\" namespace; please make sure that the secret exists in the \"consul\" namespace\n"}, + helmActionsRunner: &helm.MockActionRunner{}, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, + }, + "install for quickstart preset returns success": { + input: []string{ + "-preset", "quickstart", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n Helm value overrides\n -------------------\n connectInject:\n enabled: true\n metrics:\n defaultEnableMerging: true\n defaultEnabled: true\n enableGatewayMetrics: true\n controller:\n enabled: true\n global:\n metrics:\n enableAgentMetrics: true\n enabled: true\n name: consul\n prometheus:\n enabled: true\n server:\n replicas: 1\n ui:\n enabled: true\n service:\n enabled: true\n \n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: true, + expectConsulDemoInstalled: false, + }, + "install for secure preset returns success": { + input: []string{ + "-preset", "secure", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n Helm value overrides\n -------------------\n connectInject:\n enabled: true\n controller:\n enabled: true\n global:\n acls:\n manageSystemACLs: true\n gossipEncryption:\n autoGenerate: true\n name: consul\n tls:\n enableAutoEncrypt: true\n enabled: true\n server:\n replicas: 1\n \n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: true, + expectConsulDemoInstalled: false, + }, + "install with demo flag returns success": { + input: []string{ + "-demo", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Checking if Consul Demo Application can be installed\n ✓ No existing Consul demo application installations found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n No overrides provided, using the default Helm values.\n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul\".\n", + "\n==> Consul Demo Application Installation Summary\n Name: consul-demo\n Namespace: consul\n \n \n", + "\n==> Installing Consul demo application\n ✓ Downloaded charts.\n ✓ Consul demo application installed in namespace \"consul\".\n", + "\n==> Accessing Consul Demo Application UI\n kubectl port-forward deploy/frontend 8080:80 --namespace consul\n Browse to http://localhost:8080.\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulInstalled: true, + expectConsulDemoInstalled: true, + }, + "install with demo flag when consul demo installation errors returns error": { + input: []string{ + "-demo", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Checking if Consul Demo Application can be installed\n ✓ No existing Consul demo application installations found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n No overrides provided, using the default Helm values.\n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul\".\n", + "\n==> Consul Demo Application Installation Summary\n Name: consul-demo\n Namespace: consul\n \n \n", + "\n==> Installing Consul demo application\n ✓ Downloaded charts.\n ! Helm returned an error.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + InstallFunc: func(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*helmRelease.Release, error) { + if install.ReleaseName == "consul" { + return &helmRelease.Release{Name: install.ReleaseName}, nil + } + return nil, errors.New("Helm returned an error.") + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulInstalled: true, + expectConsulDemoInstalled: false, + }, + "install with demo flag when demo is already installed returns error and does not install consul or the demo": { + input: []string{ + "-demo", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Checking if Consul Demo Application can be installed\n ! Cannot install Consul demo application. A Consul demo application cluster is already installed in namespace consul-demo with name consul-demo.\n Use the command `consul-k8s uninstall` to uninstall the Consul demo application from the cluster.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return false, "", "", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, + }, + "install with --dry-run flag returns success": { + input: []string{ + "--dry-run", + }, + messages: []string{ + "\n==> Performing dry run install. No changes will be made to the cluster.\n", + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n No overrides provided, using the default Helm values.\n Dry run complete. No changes were made to the Kubernetes cluster.\n Installation can proceed with this configuration.\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buf := new(bytes.Buffer) + c := getInitializedCommand(t, buf) + k8s = fake.NewSimpleClientset() + c.kubernetes = k8s + mock := tc.helmActionsRunner + c.helmActionsRunner = mock + if tc.preProcessingFunc != nil { + tc.preProcessingFunc() + } + input := append([]string{ + "--auto-approve", + }, tc.input...) + returnCode := c.Run(input) + require.Equal(t, tc.expectedReturnCode, returnCode) + require.Equal(t, tc.expectCheckedForConsulInstallations, mock.CheckedForConsulInstallations) + require.Equal(t, tc.expectCheckedForConsulDemoInstallations, mock.CheckedForConsulDemoInstallations) + require.Equal(t, tc.expectConsulInstalled, mock.ConsulInstalled) + require.Equal(t, tc.expectConsulDemoInstalled, mock.ConsulDemoInstalled) + output := buf.String() + for _, msg := range tc.messages { + require.Contains(t, output, msg) + } + }) + } +} + +func createPVC(t *testing.T, name string, namespace string, k8s kubernetes.Interface) { + t.Helper() + + pvc := &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + _, err := k8s.CoreV1().PersistentVolumeClaims(namespace).Create(context.Background(), pvc, metav1.CreateOptions{}) + require.NoError(t, err) +} + +func createSecret(t *testing.T, secret *v1.Secret, namespace string, k8s kubernetes.Interface) { + t.Helper() + _, err := k8s.CoreV1().Secrets(namespace).Create(context.Background(), secret, metav1.CreateOptions{}) + require.NoError(t, err) +} diff --git a/cli/cmd/status/status.go b/cli/cmd/status/status.go index 19f5a52398..ee6947e8be 100644 --- a/cli/cmd/status/status.go +++ b/cli/cmd/status/status.go @@ -28,6 +28,8 @@ const ( type Command struct { *common.BaseCommand + helmActionsRunner helm.HelmActionsRunner + kubernetes kubernetes.Interface set *flag.Sets @@ -63,6 +65,9 @@ func (c *Command) init() { // Run checks the status of a Consul installation on Kubernetes. func (c *Command) Run(args []string) int { c.once.Do(c.init) + if c.helmActionsRunner == nil { + c.helmActionsRunner = &helm.ActionRunner{} + } // The logger is initialized in main with the name cli. Here, we reset the name to status so log lines would be prefixed with status. c.Log.ResetNamed("status") @@ -101,7 +106,11 @@ func (c *Command) Run(args []string) int { c.UI.Output("Consul Status Summary", terminal.WithHeaderStyle()) - releaseName, namespace, err := common.CheckForInstallations(settings, uiLogger) + _, releaseName, namespace, err := c.helmActionsRunner.CheckForInstallations(&helm.CheckForInstallationsOptions{ + Settings: settings, + ReleaseName: common.DefaultReleaseName, + DebugLog: uiLogger, + }) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 @@ -165,7 +174,7 @@ func (c *Command) checkHelmInstallation(settings *helmCLI.EnvSettings, uiLogger } statuser := action.NewStatus(statusConfig) - rel, err := statuser.Run(releaseName) + rel, err := c.helmActionsRunner.GetStatus(statuser, releaseName) if err != nil { return fmt.Errorf("couldn't check for installations: %s", err) } diff --git a/cli/cmd/status/status_test.go b/cli/cmd/status/status_test.go index b45ffef556..ee8ad99f79 100644 --- a/cli/cmd/status/status_test.go +++ b/cli/cmd/status/status_test.go @@ -1,26 +1,36 @@ package status import ( + "bytes" "context" + "errors" "flag" "fmt" + "io" "os" "testing" "github.com/hashicorp/consul-k8s/cli/common" cmnFlag "github.com/hashicorp/consul-k8s/cli/common/flag" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/consul-k8s/cli/helm" "github.com/hashicorp/go-hclog" "github.com/posener/complete" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + helmRelease "helm.sh/helm/v3/pkg/release" + helmTime "helm.sh/helm/v3/pkg/time" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" ) // TestCheckConsulServers creates a fake stateful set and tests the checkConsulServers function. func TestCheckConsulServers(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() // First check that no stateful sets causes an error. @@ -31,22 +41,7 @@ func TestCheckConsulServers(t *testing.T) { // Next create a stateful set with 3 desired replicas and 3 ready replicas. var replicas int32 = 3 - ss := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "consul-server-test1", - Namespace: "default", - Labels: map[string]string{"app": "consul", "chart": "consul-helm", "component": "server"}, - }, - Spec: appsv1.StatefulSetSpec{ - Replicas: &replicas, - }, - Status: appsv1.StatefulSetStatus{ - Replicas: replicas, - ReadyReplicas: replicas, - }, - } - - c.kubernetes.AppsV1().StatefulSets("default").Create(context.Background(), ss, metav1.CreateOptions{}) + createStatefulSet("consul-server-test1", "default", replicas, replicas, c.kubernetes) // Now we run the checkConsulServers() function and it should succeed. s, err := c.checkConsulServers("default") @@ -54,44 +49,14 @@ func TestCheckConsulServers(t *testing.T) { require.Equal(t, "Consul servers healthy (3/3)", s) // If you then create another stateful set it should error. - ss2 := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "consul-server-test2", - Namespace: "default", - Labels: map[string]string{"app": "consul", "chart": "consul-helm", "component": "server"}, - }, - Spec: appsv1.StatefulSetSpec{ - Replicas: &replicas, - }, - Status: appsv1.StatefulSetStatus{ - Replicas: replicas, - ReadyReplicas: replicas, - }, - } - c.kubernetes.AppsV1().StatefulSets("default").Create(context.Background(), ss2, metav1.CreateOptions{}) - + createStatefulSet("consul-server-test2", "default", replicas, replicas, c.kubernetes) _, err = c.checkConsulServers("default") require.Error(t, err) require.Contains(t, err.Error(), "found multiple server stateful sets") // Clear out the client and now run a test where the stateful set isn't ready. c.kubernetes = fake.NewSimpleClientset() - - ss3 := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "consul-server-test3", - Namespace: "default", - Labels: map[string]string{"app": "consul", "chart": "consul-helm", "component": "server"}, - }, - Spec: appsv1.StatefulSetSpec{ - Replicas: &replicas, - }, - Status: appsv1.StatefulSetStatus{ - Replicas: replicas, - ReadyReplicas: replicas - 1, // Let's just set one of the servers to unhealthy - }, - } - c.kubernetes.AppsV1().StatefulSets("default").Create(context.Background(), ss3, metav1.CreateOptions{}) + createStatefulSet("consul-server-test2", "default", replicas, replicas-1, c.kubernetes) _, err = c.checkConsulServers("default") require.Error(t, err) @@ -100,7 +65,7 @@ func TestCheckConsulServers(t *testing.T) { // TestCheckConsulClients is very similar to TestCheckConsulServers() in structure. func TestCheckConsulClients(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() // No client daemon set should cause an error. @@ -169,17 +134,219 @@ func TestCheckConsulClients(t *testing.T) { require.Contains(t, err.Error(), fmt.Sprintf("%d/%d Consul clients unhealthy", 1, desired)) } +// TestStatus creates a fake stateful set and tests the checkConsulServers function. +func TestStatus(t *testing.T) { + nowTime := helmTime.Now() + timezone, _ := nowTime.Zone() + notImeStr := nowTime.Format("2006/01/02 15:04:05") + " " + timezone + cases := map[string]struct { + input []string + messages []string + preProcessingFunc func(k8s kubernetes.Interface) + helmActionsRunner *helm.MockActionRunner + expectedReturnCode int + }{ + "status with clients and servers returns success": { + input: []string{}, + messages: []string{ + fmt.Sprintf("\n==> Consul Status Summary\nName\tNamespace\tStatus\tChart Version\tAppVersion\tRevision\tLast Updated \n \t \tREADY \t1.0.0 \t \t0 \t%s\t\n", notImeStr), + "\n==> Config:\n {}\n \n ✓ Consul servers healthy (3/3)\n ✓ Consul clients healthy (3/3)\n", + }, + preProcessingFunc: func(k8s kubernetes.Interface) { + createDaemonset("consul-client-test1", "consul", 3, 3, k8s) + createStatefulSet("consul-server-test1", "consul", 3, 3, k8s) + }, + + helmActionsRunner: &helm.MockActionRunner{ + GetStatusFunc: func(status *action.Status, name string) (*helmRelease.Release, error) { + return &helmRelease.Release{ + Name: "consul", Namespace: "consul", + Info: &helmRelease.Info{LastDeployed: nowTime, Status: "READY"}, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Version: "1.0.0", + }, + }, + Config: make(map[string]interface{})}, nil + }, + }, + expectedReturnCode: 0, + }, + "status with no servers returns error": { + input: []string{}, + messages: []string{ + fmt.Sprintf("\n==> Consul Status Summary\nName\tNamespace\tStatus\tChart Version\tAppVersion\tRevision\tLast Updated \n \t \tREADY \t1.0.0 \t \t0 \t%s\t\n", notImeStr), + "\n==> Config:\n {}\n \n ! no server stateful set found\n", + }, + preProcessingFunc: func(k8s kubernetes.Interface) { + createDaemonset("consul-client-test1", "consul", 3, 3, k8s) + }, + helmActionsRunner: &helm.MockActionRunner{ + GetStatusFunc: func(status *action.Status, name string) (*helmRelease.Release, error) { + return &helmRelease.Release{ + Name: "consul", Namespace: "consul", + Info: &helmRelease.Info{LastDeployed: nowTime, Status: "READY"}, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Version: "1.0.0", + }, + }, + Config: make(map[string]interface{})}, nil + }, + }, + expectedReturnCode: 1, + }, + "status with no clients returns error": { + input: []string{}, + messages: []string{ + fmt.Sprintf("\n==> Consul Status Summary\nName\tNamespace\tStatus\tChart Version\tAppVersion\tRevision\tLast Updated \n \t \tREADY \t1.0.0 \t \t0 \t%s\t\n", notImeStr), + "\n==> Config:\n {}\n \n ✓ Consul servers healthy (3/3)\n ! no client daemon set found\n", + }, + preProcessingFunc: func(k8s kubernetes.Interface) { + createStatefulSet("consul-server-test1", "consul", 3, 3, k8s) + }, + helmActionsRunner: &helm.MockActionRunner{ + GetStatusFunc: func(status *action.Status, name string) (*helmRelease.Release, error) { + return &helmRelease.Release{ + Name: "consul", Namespace: "consul", + Info: &helmRelease.Info{LastDeployed: nowTime, Status: "READY"}, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Version: "1.0.0", + }, + }, + Config: make(map[string]interface{})}, nil + }, + }, + expectedReturnCode: 1, + }, + "status with pre-install and pre-upgrade hooks returns success and outputs hook status": { + input: []string{}, + messages: []string{ + fmt.Sprintf("\n==> Consul Status Summary\nName\tNamespace\tStatus\tChart Version\tAppVersion\tRevision\tLast Updated \n \t \tREADY \t1.0.0 \t \t0 \t%s\t\n", notImeStr), + "\n==> Config:\n {}\n \n", + "\n==> Status Of Helm Hooks:\npre-install-hook pre-install: Succeeded\npre-upgrade-hook pre-upgrade: Succeeded\n ✓ Consul servers healthy (3/3)\n ✓ Consul clients healthy (3/3)\n", + }, + preProcessingFunc: func(k8s kubernetes.Interface) { + createDaemonset("consul-client-test1", "consul", 3, 3, k8s) + createStatefulSet("consul-server-test1", "consul", 3, 3, k8s) + }, + + helmActionsRunner: &helm.MockActionRunner{ + GetStatusFunc: func(status *action.Status, name string) (*helmRelease.Release, error) { + return &helmRelease.Release{ + Name: "consul", Namespace: "consul", + Info: &helmRelease.Info{LastDeployed: nowTime, Status: "READY"}, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Version: "1.0.0", + }, + }, + Config: make(map[string]interface{}), + Hooks: []*helmRelease.Hook{ + { + Name: "pre-install-hook", + Kind: "pre-install", LastRun: helmRelease.HookExecution{ + Phase: helmRelease.HookPhaseSucceeded, + }, + Events: []helmRelease.HookEvent{ + "pre-install", + }, + }, + { + Name: "pre-upgrade-hook", + Kind: "pre-upgrade", LastRun: helmRelease.HookExecution{ + Phase: helmRelease.HookPhaseSucceeded, + }, + Events: []helmRelease.HookEvent{ + "pre-install", + }, + }, + { + Name: "post-delete-hook", + Kind: "post-delete", LastRun: helmRelease.HookExecution{ + Phase: helmRelease.HookPhaseSucceeded, + }, + Events: []helmRelease.HookEvent{ + "post-delete", + }, + }, + }}, nil + }, + }, + expectedReturnCode: 0, + }, + "status with CheckForInstallations error returns ": { + input: []string{}, + messages: []string{ + "\n==> Consul Status Summary\n ! kaboom!\n", + }, + preProcessingFunc: func(k8s kubernetes.Interface) { + createDaemonset("consul-client-test1", "consul", 3, 3, k8s) + createStatefulSet("consul-server-test1", "consul", 3, 3, k8s) + }, + + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + return false, "", "", errors.New("kaboom!") + }, + }, + expectedReturnCode: 1, + }, + "status with GetStatus error returns ": { + input: []string{}, + messages: []string{ + "\n==> Consul Status Summary\n ! couldn't check for installations: kaboom!\n", + }, + preProcessingFunc: func(k8s kubernetes.Interface) { + createDaemonset("consul-client-test1", "consul", 3, 3, k8s) + createStatefulSet("consul-server-test1", "consul", 3, 3, k8s) + }, + + helmActionsRunner: &helm.MockActionRunner{ + GetStatusFunc: func(status *action.Status, name string) (*helmRelease.Release, error) { + return nil, errors.New("kaboom!") + }, + }, + expectedReturnCode: 1, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buf := new(bytes.Buffer) + c := getInitializedCommand(t, buf) + c.kubernetes = fake.NewSimpleClientset() + c.helmActionsRunner = tc.helmActionsRunner + if tc.preProcessingFunc != nil { + tc.preProcessingFunc(c.kubernetes) + } + returnCode := c.Run([]string{}) + require.Equal(t, tc.expectedReturnCode, returnCode) + output := buf.String() + for _, msg := range tc.messages { + require.Contains(t, output, msg) + } + }) + } +} + // getInitializedCommand sets up a command struct for tests. -func getInitializedCommand(t *testing.T) *Command { +func getInitializedCommand(t *testing.T, buf io.Writer) *Command { t.Helper() log := hclog.New(&hclog.LoggerOptions{ Name: "cli", Level: hclog.Info, Output: os.Stdout, }) - + var ui terminal.UI + if buf != nil { + ui = terminal.NewUI(context.Background(), buf) + } else { + ui = terminal.NewBasicUI(context.Background()) + } baseCommand := &common.BaseCommand{ Log: log, + UI: ui, } c := &Command{ @@ -191,7 +358,7 @@ func getInitializedCommand(t *testing.T) *Command { func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { t.Parallel() - cmd := getInitializedCommand(t) + cmd := getInitializedCommand(t, nil) predictor := cmd.AutocompleteFlags() @@ -214,7 +381,42 @@ func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { } func TestTaskCreateCommand_AutocompleteArgs(t *testing.T) { - cmd := getInitializedCommand(t) + cmd := getInitializedCommand(t, nil) c := cmd.AutocompleteArgs() assert.Equal(t, complete.PredictNothing, c) } + +func createStatefulSet(name, namespace string, replicas, readyReplicas int32, k8s kubernetes.Interface) { + ss := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{"app": "consul", "chart": "consul-helm", "component": "server"}, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: &replicas, + }, + Status: appsv1.StatefulSetStatus{ + Replicas: replicas, + ReadyReplicas: readyReplicas, + }, + } + + k8s.AppsV1().StatefulSets(namespace).Create(context.Background(), ss, metav1.CreateOptions{}) +} + +func createDaemonset(name, namespace string, replicas, readyReplicas int32, k8s kubernetes.Interface) { + ds := &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{"app": "consul", "chart": "consul-helm"}, + }, + Status: appsv1.DaemonSetStatus{ + DesiredNumberScheduled: replicas, + NumberReady: readyReplicas, + }, + } + + k8s.AppsV1().DaemonSets(namespace).Create(context.Background(), ds, metav1.CreateOptions{}) +} diff --git a/cli/cmd/uninstall/uninstall.go b/cli/cmd/uninstall/uninstall.go index 07b945bf79..28a7e969b6 100644 --- a/cli/cmd/uninstall/uninstall.go +++ b/cli/cmd/uninstall/uninstall.go @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/consul-k8s/cli/helm" "github.com/posener/complete" + "golang.org/x/text/cases" + "golang.org/x/text/language" "helm.sh/helm/v3/pkg/action" helmCLI "helm.sh/helm/v3/pkg/cli" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -41,6 +43,8 @@ const ( type Command struct { *common.BaseCommand + helmActionsRunner helm.HelmActionsRunner + kubernetes kubernetes.Interface set *flag.Sets @@ -124,6 +128,10 @@ func (c *Command) Run(args []string) int { } }() + if c.helmActionsRunner == nil { + c.helmActionsRunner = &helm.ActionRunner{} + } + if err := c.set.Parse(args); err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 @@ -174,9 +182,6 @@ func (c *Command) Run(args []string) int { c.UI.Output(logMsg, terminal.WithLibraryStyle()) } - c.UI.Output("Existing Installation", terminal.WithHeaderStyle()) - - // Search for Consul installation by calling `helm list`. Depends on what's already specified. actionConfig := new(action.Configuration) actionConfig, err = helm.InitActionConfig(actionConfig, c.flagNamespace, settings, uiLogger) if err != nil { @@ -184,51 +189,48 @@ func (c *Command) Run(args []string) int { return 1 } - found, foundReleaseName, foundReleaseNamespace, err := c.findExistingInstallation(settings, uiLogger) + c.UI.Output(fmt.Sprintf("Checking if %s can be uninstalled", common.ReleaseTypeConsulDemo), terminal.WithHeaderStyle()) + foundConsulDemo, foundDemoReleaseName, foundDemoReleaseNamespace, err := c.findExistingInstallation(&helm.CheckForInstallationsOptions{ + Settings: settings, + ReleaseName: common.ConsulDemoAppReleaseName, + DebugLog: uiLogger, + SkipErrorWhenNotFound: true, + }) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 - } else { - c.UI.Output("Existing Consul installation found.", terminal.WithSuccessStyle()) - c.UI.Output("Consul Uninstall Summary", terminal.WithHeaderStyle()) - c.UI.Output("Name: %s", foundReleaseName, terminal.WithInfoStyle()) - c.UI.Output("Namespace: %s", foundReleaseNamespace, terminal.WithInfoStyle()) - - // Prompt for approval to uninstall Helm release. - if !c.flagAutoApprove { - confirmation, err := c.UI.Input(&terminal.Input{ - Prompt: "Proceed with uninstall? (y/N)", - Style: terminal.InfoStyle, - Secret: false, - }) - if err != nil { - c.UI.Output(err.Error(), terminal.WithErrorStyle()) - return 1 - } - if common.Abort(confirmation) { - c.UI.Output("Uninstall aborted. To learn how to customize the uninstall, run:\nconsul-k8s uninstall --help", terminal.WithInfoStyle()) - return 1 - } - } + } else if !foundConsulDemo { + c.UI.Output(fmt.Sprintf("No existing %s installation found.", common.ReleaseTypeConsulDemo), terminal.WithInfoStyle()) + } - // Actually call out to `helm delete`. - actionConfig, err = helm.InitActionConfig(actionConfig, foundReleaseNamespace, settings, uiLogger) + found, foundReleaseName, foundReleaseNamespace, err := + c.findExistingInstallation(&helm.CheckForInstallationsOptions{ + Settings: settings, + ReleaseName: common.DefaultReleaseName, + DebugLog: uiLogger, + }) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + + if foundConsulDemo { + err = c.uninstallHelmRelease(foundDemoReleaseName, foundDemoReleaseNamespace, common.ReleaseTypeConsulDemo, settings, uiLogger, actionConfig) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } + } else { + c.UI.Output(fmt.Sprintf("No existing %s installation found.", common.ReleaseTypeConsulDemo), terminal.WithInfoStyle()) + } - uninstaller := action.NewUninstall(actionConfig) - uninstaller.Timeout = c.timeoutDuration - res, err := uninstaller.Run(foundReleaseName) + c.UI.Output("Checking if Consul can be uninstalled", terminal.WithHeaderStyle()) + if found { + err = c.uninstallHelmRelease(foundReleaseName, foundReleaseNamespace, common.ReleaseTypeConsul, settings, uiLogger, actionConfig) if err != nil { - c.UI.Output("unable to uninstall: %s", err, terminal.WithErrorStyle()) + c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } - if res != nil && res.Info != "" { - c.UI.Output("Uninstall result: %s", res.Info, terminal.WithInfoStyle()) - } - c.UI.Output("Successfully uninstalled Consul Helm release", terminal.WithSuccessStyle()) } // If -auto-approve=true and -wipe-data=false, we should only uninstall the release, and skip deleting resources. @@ -319,6 +321,50 @@ func (c *Command) Run(args []string) int { return 0 } +func (c *Command) uninstallHelmRelease(releaseName, namespace, releaseType string, settings *helmCLI.EnvSettings, + uiLogger action.DebugLog, actionConfig *action.Configuration) error { + c.UI.Output(fmt.Sprintf("Existing %s installation found.", releaseType), terminal.WithSuccessStyle()) + c.UI.Output(fmt.Sprintf("%s Uninstall Summary", cases.Title(language.English).String(releaseType)), terminal.WithHeaderStyle()) + c.UI.Output("Name: %s", releaseName, terminal.WithInfoStyle()) + c.UI.Output("Namespace: %s", namespace, terminal.WithInfoStyle()) + + // Prompt for approval to uninstall Helm release. + // Actually call out to `helm delete`. + if !c.flagAutoApprove { + confirmation, err := c.UI.Input(&terminal.Input{ + Prompt: "Proceed with uninstall? (y/N)", + Style: terminal.InfoStyle, + Secret: false, + }) + if err != nil { + return err + } + if common.Abort(confirmation) { + c.UI.Output("Uninstall aborted. To learn how to customize the uninstall, run:\nconsul-k8s uninstall --help", terminal.WithInfoStyle()) + return nil + } + } + + actionConfig, err := helm.InitActionConfig(actionConfig, namespace, settings, uiLogger) + if err != nil { + return err + } + + uninstall := action.NewUninstall(actionConfig) + uninstall.Timeout = c.timeoutDuration + + res, err := c.helmActionsRunner.Uninstall(uninstall, releaseName) + if err != nil { + return err + } + if res != nil && res.Info != "" { + c.UI.Output("Uninstall result: %s", res.Info, terminal.WithInfoStyle()) + return nil + } + c.UI.Output(fmt.Sprintf("Successfully uninstalled %s Helm release.", releaseType), terminal.WithSuccessStyle()) + return nil +} + func (c *Command) Help() string { c.once.Do(c.init) s := "Usage: consul-k8s uninstall [flags]" + "\n" + "Uninstall Consul with options to delete data and resources associated with Consul installation." + "\n\n" + c.help @@ -351,14 +397,18 @@ func (c *Command) AutocompleteArgs() complete.Predictor { return complete.PredictNothing } -func (c *Command) findExistingInstallation(settings *helmCLI.EnvSettings, uiLogger action.DebugLog) (bool, string, string, error) { - releaseName, namespace, err := common.CheckForInstallations(settings, uiLogger) +func (c *Command) findExistingInstallation(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + found, releaseName, namespace, err := c.helmActionsRunner.CheckForInstallations(options) if err != nil { return false, "", "", err - } else if c.flagNamespace == defaultAllNamespaces || c.flagNamespace == namespace { + } else if found && (c.flagNamespace == defaultAllNamespaces || c.flagNamespace == namespace) { return true, releaseName, namespace, nil } else { - return false, "", "", fmt.Errorf("could not find consul installation in namespace %s", c.flagNamespace) + var notFoundError error + if !options.SkipErrorWhenNotFound { + notFoundError = fmt.Errorf("could not find %s installation in cluster", common.ReleaseTypeConsul) + } + return false, "", "", notFoundError } } diff --git a/cli/cmd/uninstall/uninstall_test.go b/cli/cmd/uninstall/uninstall_test.go index 8fa92e92b7..3b89261915 100644 --- a/cli/cmd/uninstall/uninstall_test.go +++ b/cli/cmd/uninstall/uninstall_test.go @@ -1,28 +1,35 @@ package uninstall import ( + "bytes" "context" + "errors" "flag" "fmt" + "io" "os" "testing" "github.com/hashicorp/consul-k8s/cli/common" cmnFlag "github.com/hashicorp/consul-k8s/cli/common/flag" "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/consul-k8s/cli/helm" "github.com/hashicorp/go-hclog" "github.com/posener/complete" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/action" + helmRelease "helm.sh/helm/v3/pkg/release" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" ) func TestDeletePVCs(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() pvc := &v1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ @@ -63,7 +70,7 @@ func TestDeletePVCs(t *testing.T) { } func TestDeleteSecrets(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() secret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -106,7 +113,7 @@ func TestDeleteSecrets(t *testing.T) { } func TestDeleteServiceAccounts(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() sa := &v1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ @@ -147,7 +154,7 @@ func TestDeleteServiceAccounts(t *testing.T) { } func TestDeleteRoles(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() role := &rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ @@ -188,7 +195,7 @@ func TestDeleteRoles(t *testing.T) { } func TestDeleteRoleBindings(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() rolebinding := &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ @@ -229,7 +236,7 @@ func TestDeleteRoleBindings(t *testing.T) { } func TestDeleteJobs(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ @@ -270,7 +277,7 @@ func TestDeleteJobs(t *testing.T) { } func TestDeleteClusterRoles(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() clusterrole := &rbacv1.ClusterRole{ ObjectMeta: metav1.ObjectMeta{ @@ -311,7 +318,7 @@ func TestDeleteClusterRoles(t *testing.T) { } func TestDeleteClusterRoleBindings(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() clusterrolebinding := &rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ @@ -352,17 +359,22 @@ func TestDeleteClusterRoleBindings(t *testing.T) { } // getInitializedCommand sets up a command struct for tests. -func getInitializedCommand(t *testing.T) *Command { +func getInitializedCommand(t *testing.T, buf io.Writer) *Command { t.Helper() log := hclog.New(&hclog.LoggerOptions{ Name: "cli", Level: hclog.Info, Output: os.Stdout, }) - + var ui terminal.UI + if buf != nil { + ui = terminal.NewUI(context.Background(), buf) + } else { + ui = terminal.NewBasicUI(context.Background()) + } baseCommand := &common.BaseCommand{ Log: log, - UI: terminal.NewBasicUI(context.TODO()), + UI: ui, } c := &Command{ @@ -374,7 +386,7 @@ func getInitializedCommand(t *testing.T) *Command { func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { t.Parallel() - cmd := getInitializedCommand(t) + cmd := getInitializedCommand(t, nil) predictor := cmd.AutocompleteFlags() @@ -397,7 +409,192 @@ func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { } func TestTaskCreateCommand_AutocompleteArgs(t *testing.T) { - cmd := getInitializedCommand(t) + cmd := getInitializedCommand(t, nil) c := cmd.AutocompleteArgs() assert.Equal(t, complete.PredictNothing, c) } + +func TestUninstall(t *testing.T) { + var k8s kubernetes.Interface + cases := map[string]struct { + input []string + messages []string + helmActionsRunner *helm.MockActionRunner + preProcessingFunc func() + expectedReturnCode int + expectCheckedForConsulInstallations bool + expectCheckedForConsulDemoInstallations bool + expectConsulUninstalled bool + expectConsulDemoUninstalled bool + }{ + "uninstall when consul installation exists returns success": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul demo application can be uninstalled\n No existing Consul demo application installation found.\n", + "\n==> Checking if Consul can be uninstalled\n ✓ Existing Consul installation found.\n", + "\n==> Consul Uninstall Summary\n Name: consul\n Namespace: consul\n ✓ Successfully uninstalled Consul Helm release.\n ✓ Skipping deleting PVCs, secrets, and service accounts.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUninstalled: true, + expectConsulDemoUninstalled: false, + }, + "uninstall when consul installation does not exist returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul demo application can be uninstalled\n No existing Consul demo application installation found.\n ! could not find Consul installation in cluster\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return false, "", "", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUninstalled: false, + expectConsulDemoUninstalled: false, + }, + "uninstall with -wipe-data flag processes other rescource and returns success": { + input: []string{ + "-wipe-data", + }, + messages: []string{ + "\n==> Checking if Consul demo application can be uninstalled\n No existing Consul demo application installation found.\n No existing Consul demo application installation found.\n", + "\n==> Checking if Consul can be uninstalled\n ✓ Existing Consul installation found.\n", + "\n==> Consul Uninstall Summary\n Name: consul\n Namespace: consul\n ✓ Successfully uninstalled Consul Helm release.\n", + "\n==> Other Consul Resources\n Deleting data for installation: \n Name: consul\n Namespace consul\n ✓ No PVCs found.\n ✓ No Consul secrets found.\n ✓ No Consul service accounts found.\n ✓ No Consul roles found.\n ✓ No Consul rolebindings found.\n ✓ No Consul jobs found.\n ✓ No Consul cluster roles found.\n ✓ No Consul cluster role bindings found.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUninstalled: true, + expectConsulDemoUninstalled: false, + }, + "uninstall when both consul and consul demo installations exist returns success": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul demo application can be uninstalled\n ✓ Existing Consul demo application installation found.\n", + "\n==> Consul Demo Application Uninstall Summary\n Name: consul-demo\n Namespace: consul-demo\n ✓ Successfully uninstalled Consul demo application Helm release.\n", + "\n==> Checking if Consul can be uninstalled\n ✓ Existing Consul installation found.\n", + "\n==> Consul Uninstall Summary\n Name: consul\n Namespace: consul\n ✓ Successfully uninstalled Consul Helm release.\n ✓ Skipping deleting PVCs, secrets, and service accounts.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUninstalled: true, + expectConsulDemoUninstalled: true, + }, + "uninstall when consul uninstall errors returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul demo application can be uninstalled\n No existing Consul demo application installation found.\n", + "\n==> Checking if Consul can be uninstalled\n ✓ Existing Consul installation found.\n", + "\n==> Consul Uninstall Summary\n Name: consul\n Namespace: consul\n ! Helm returned an error.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + UninstallFunc: func(uninstall *action.Uninstall, name string) (*helmRelease.UninstallReleaseResponse, error) { + return nil, errors.New("Helm returned an error.") + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUninstalled: false, + expectConsulDemoUninstalled: false, + }, + "uninstall when consul demo is installed consul demo uninstall errors returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul demo application can be uninstalled\n ✓ Existing Consul demo application installation found.\n", + "\n==> Consul Demo Application Uninstall Summary\n Name: consul-demo\n Namespace: consul-demo\n ! Helm returned an error.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + UninstallFunc: func(uninstall *action.Uninstall, name string) (*helmRelease.UninstallReleaseResponse, error) { + if name == "consul" { + return &helmRelease.UninstallReleaseResponse{}, nil + } else { + return nil, errors.New("Helm returned an error.") + } + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUninstalled: false, + expectConsulDemoUninstalled: false, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buf := new(bytes.Buffer) + c := getInitializedCommand(t, buf) + k8s = fake.NewSimpleClientset() + c.kubernetes = k8s + mock := tc.helmActionsRunner + c.helmActionsRunner = mock + if tc.preProcessingFunc != nil { + tc.preProcessingFunc() + } + input := append([]string{ + "--auto-approve", + }, tc.input...) + returnCode := c.Run(input) + require.Equal(t, tc.expectedReturnCode, returnCode) + require.Equal(t, tc.expectCheckedForConsulInstallations, mock.CheckedForConsulInstallations) + require.Equal(t, tc.expectCheckedForConsulDemoInstallations, mock.CheckedForConsulDemoInstallations) + require.Equal(t, tc.expectConsulUninstalled, mock.ConsulUninstalled) + require.Equal(t, tc.expectConsulDemoUninstalled, mock.ConsulDemoUninstalled) + output := buf.String() + for _, msg := range tc.messages { + require.Contains(t, output, msg) + } + }) + } +} diff --git a/cli/cmd/upgrade/upgrade.go b/cli/cmd/upgrade/upgrade.go index e1bb744ce1..73b30a568e 100644 --- a/cli/cmd/upgrade/upgrade.go +++ b/cli/cmd/upgrade/upgrade.go @@ -3,6 +3,7 @@ package upgrade import ( "errors" "fmt" + "net/http" "os" "strings" "sync" @@ -14,12 +15,14 @@ import ( "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/consul-k8s/cli/config" "github.com/hashicorp/consul-k8s/cli/helm" + "github.com/hashicorp/consul-k8s/cli/preset" "github.com/posener/complete" - "helm.sh/helm/v3/pkg/action" + helmCLI "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v3/pkg/getter" "k8s.io/client-go/kubernetes" + "k8s.io/utils/strings/slices" ) const ( @@ -48,26 +51,42 @@ const ( flagNameContext = "context" flagNameKubeconfig = "kubeconfig" + + flagNameDemo = "demo" + defaultDemo = false + + flagNameHCPResourceID = "hcp-resource-id" + + envHCPClientID = "HCP_CLIENT_ID" + envHCPClientSecret = "HCP_CLIENT_SECRET" + + consulDemoChartPath = "demo" ) type Command struct { *common.BaseCommand + helmActionsRunner helm.HelmActionsRunner + kubernetes kubernetes.Interface + httpClient *http.Client + set *flag.Sets - flagPreset string - flagDryRun bool - flagAutoApprove bool - flagValueFiles []string - flagSetStringValues []string - flagSetValues []string - flagFileValues []string - flagTimeout string - timeoutDuration time.Duration - flagVerbose bool - flagWait bool + flagPreset string + flagDryRun bool + flagAutoApprove bool + flagValueFiles []string + flagSetStringValues []string + flagSetValues []string + flagFileValues []string + flagTimeout string + timeoutDuration time.Duration + flagVerbose bool + flagWait bool + flagNameHCPResourceID string + flagDemo bool flagKubeConfig string flagKubeContext string @@ -77,12 +96,6 @@ type Command struct { } func (c *Command) init() { - // Store all the possible preset values in 'presetList'. Printed in the help message. - var presetList []string - for name := range config.Presets { - presetList = append(presetList, name) - } - c.set = flag.NewSets() f := c.set.NewSet("Command Options") f.BoolVar(&flag.BoolVar{ @@ -107,7 +120,7 @@ func (c *Command) init() { Name: flagNamePreset, Target: &c.flagPreset, Default: defaultPreset, - Usage: fmt.Sprintf("Use an upgrade preset, one of %s. Defaults to none", strings.Join(presetList, ", ")), + Usage: fmt.Sprintf("Use an upgrade preset, one of %s. Defaults to none", strings.Join(preset.Presets, ", ")), }) f.StringSliceVar(&flag.StringSliceVar{ Name: flagNameSetValues, @@ -159,6 +172,19 @@ func (c *Command) init() { Default: "", Usage: "Set the Kubernetes context to use.", }) + f.StringVar(&flag.StringVar{ + Name: flagNameHCPResourceID, + Target: &c.flagNameHCPResourceID, + Default: "", + Usage: "Set the HCP resource_id when using the 'cloud' preset.", + }) + f.BoolVar(&flag.BoolVar{ + Name: flagNameDemo, + Target: &c.flagDemo, + Default: defaultDemo, + Usage: fmt.Sprintf("Install %s immediately after installing %s.", + common.ReleaseTypeConsulDemo, common.ReleaseTypeConsul), + }) c.help = c.set.Help() } @@ -169,6 +195,10 @@ func (c *Command) Run(args []string) int { defer common.CloseWithError(c.BaseCommand) + if c.helmActionsRunner == nil { + c.helmActionsRunner = &helm.ActionRunner{} + } + err := c.validateFlags(args) if err != nil { c.UI.Output(err.Error()) @@ -216,29 +246,45 @@ func (c *Command) Run(args []string) int { c.UI.Output("Checking if Consul can be upgraded", terminal.WithHeaderStyle()) uiLogger := c.createUILogger() - name, namespace, err := common.CheckForInstallations(settings, uiLogger) - if err != nil { - c.UI.Output("Cannot upgrade Consul. Existing Consul installation not found. Use the command `consul-k8s install` to install Consul.", terminal.WithErrorStyle()) - return 1 - } - c.UI.Output("Existing Consul installation found to be upgraded.", terminal.WithSuccessStyle()) - c.UI.Output("Name: %s\nNamespace: %s", name, namespace, terminal.WithInfoStyle()) + found, consulName, consulNamespace, err := c.helmActionsRunner.CheckForInstallations(&helm.CheckForInstallationsOptions{ + Settings: settings, + ReleaseName: common.DefaultReleaseName, + DebugLog: uiLogger, + }) - chart, err := helm.LoadChart(consulChart.ConsulHelmChart, common.TopLevelChartDirName) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } - c.UI.Output("Loaded charts", terminal.WithSuccessStyle()) - - currentChartValues, err := helm.FetchChartValues(namespace, name, settings, uiLogger) - if err != nil { - c.UI.Output(err.Error(), terminal.WithErrorStyle()) + if !found { + c.UI.Output("Cannot upgrade Consul. Existing Consul installation not found. Use the command `consul-k8s install` to install Consul.", terminal.WithErrorStyle()) return 1 + } else { + c.UI.Output("Existing %s installation found to be upgraded.", common.ReleaseTypeConsul, terminal.WithSuccessStyle()) + c.UI.Output("Name: %s\nNamespace: %s", consulName, consulNamespace, terminal.WithInfoStyle()) + } + + c.UI.Output(fmt.Sprintf("Checking if %s can be upgraded", common.ReleaseTypeConsulDemo), terminal.WithHeaderStyle()) + // Ensure there is not an existing Consul demo installation which would cause a conflict. + foundDemo, demoName, demoNamespace, _ := c.helmActionsRunner.CheckForInstallations(&helm.CheckForInstallationsOptions{ + Settings: settings, + ReleaseName: common.ConsulDemoAppReleaseName, + DebugLog: uiLogger, + }) + if foundDemo { + c.UI.Output("Existing %s installation found to be upgraded.", common.ReleaseTypeConsulDemo, terminal.WithSuccessStyle()) + c.UI.Output("Name: %s\nNamespace: %s", demoName, demoNamespace, terminal.WithInfoStyle()) + } else { + if c.flagDemo { + c.UI.Output("No existing %s installation found, but -demo flag provided. %s will be installed in namespace %s.", + common.ConsulDemoAppReleaseName, common.ConsulDemoAppReleaseName, consulNamespace, terminal.WithInfoStyle()) + } else { + c.UI.Output("No existing %s installation found.", common.ReleaseTypeConsulDemo, terminal.WithInfoStyle()) + } } // Handle preset, value files, and set values logic. - chartValues, err := c.mergeValuesFlagsWithPrecedence(settings) + chartValues, err := c.mergeValuesFlagsWithPrecedence(settings, consulNamespace) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 @@ -247,68 +293,97 @@ func (c *Command) Run(args []string) int { // Without informing the user, default global.name to consul if it hasn't been set already. We don't allow setting // the release name, and since that is hardcoded to "consul", setting global.name to "consul" makes it so resources // aren't double prefixed with "consul-consul-...". - chartValues = common.MergeMaps(config.Convert(config.GlobalNameConsul), chartValues) + chartValues = common.MergeMaps(config.ConvertToMap(config.GlobalNameConsul), chartValues) - // Print out the upgrade summary. - if err = c.printDiff(currentChartValues, chartValues); err != nil { - c.UI.Output("Could not print the different between current and upgraded charts: %v", err, terminal.WithErrorStyle()) + timeout, err := time.ParseDuration(c.flagTimeout) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } - - // Check if the user is OK with the upgrade unless the auto approve or dry run flags are true. - if !c.flagAutoApprove && !c.flagDryRun { - confirmation, err := c.UI.Input(&terminal.Input{ - Prompt: "Proceed with upgrade? (y/N)", - Style: terminal.InfoStyle, - Secret: false, - }) - - if err != nil { - c.UI.Output(err.Error(), terminal.WithErrorStyle()) - return 1 - } - if common.Abort(confirmation) { - c.UI.Output("Upgrade aborted. Use the command `consul-k8s upgrade -help` to learn how to customize your upgrade.", - terminal.WithInfoStyle()) - return 1 - } - } - - if !c.flagDryRun { - c.UI.Output("Upgrading Consul", terminal.WithHeaderStyle()) - } else { - c.UI.Output("Performing Dry Run Upgrade", terminal.WithHeaderStyle()) + options := &helm.UpgradeOptions{ + ReleaseName: consulName, + ReleaseType: common.ReleaseTypeConsul, + ReleaseTypeName: common.ReleaseTypeConsul, + Namespace: consulNamespace, + Values: chartValues, + Settings: settings, + EmbeddedChart: consulChart.ConsulHelmChart, + ChartDirName: common.TopLevelChartDirName, + UILogger: uiLogger, + DryRun: c.flagDryRun, + AutoApprove: c.flagAutoApprove, + Wait: c.flagWait, + Timeout: timeout, + UI: c.UI, + HelmActionsRunner: c.helmActionsRunner, } - // Setup action configuration for Helm Go SDK function calls. - actionConfig := new(action.Configuration) - actionConfig, err = helm.InitActionConfig(actionConfig, namespace, settings, uiLogger) + err = helm.UpgradeHelmRelease(options) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } - // Setup the upgrade action. - upgrade := action.NewUpgrade(actionConfig) - upgrade.Namespace = namespace - upgrade.DryRun = c.flagDryRun - upgrade.Wait = c.flagWait - upgrade.Timeout = c.timeoutDuration - - // Run the upgrade. Note that the dry run config is passed into the upgrade action, so upgrade.Run is called even during a dry run. - _, err = upgrade.Run(common.DefaultReleaseName, chart, chartValues) + timeout, err = time.ParseDuration(c.flagTimeout) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } + if foundDemo { + options := &helm.UpgradeOptions{ + ReleaseName: demoName, + ReleaseType: common.ReleaseTypeConsulDemo, + ReleaseTypeName: common.ConsulDemoAppReleaseName, + Namespace: demoNamespace, + Values: make(map[string]interface{}), + Settings: settings, + EmbeddedChart: consulChart.DemoHelmChart, + ChartDirName: consulDemoChartPath, + UILogger: uiLogger, + DryRun: c.flagDryRun, + AutoApprove: c.flagAutoApprove, + Wait: c.flagWait, + Timeout: timeout, + UI: c.UI, + HelmActionsRunner: c.helmActionsRunner, + } + + err = helm.UpgradeHelmRelease(options) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + } else if c.flagDemo { + + options := &helm.InstallOptions{ + ReleaseName: common.ConsulDemoAppReleaseName, + ReleaseType: common.ReleaseTypeConsulDemo, + Namespace: settings.Namespace(), + Values: make(map[string]interface{}), + Settings: settings, + EmbeddedChart: consulChart.DemoHelmChart, + ChartDirName: consulDemoChartPath, + UILogger: uiLogger, + DryRun: c.flagDryRun, + AutoApprove: c.flagAutoApprove, + Wait: c.flagWait, + Timeout: timeout, + UI: c.UI, + HelmActionsRunner: c.helmActionsRunner, + } + err = helm.InstallDemoApp(options) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + } + if c.flagDryRun { c.UI.Output("Dry run complete. No changes were made to the Kubernetes cluster.\n"+ "Upgrade can proceed with this configuration.", terminal.WithInfoStyle()) return 0 } - - c.UI.Output("Consul upgraded in namespace %q.", namespace, terminal.WithSuccessStyle()) return 0 } @@ -329,6 +404,8 @@ func (c *Command) AutocompleteFlags() complete.Flags { fmt.Sprintf("-%s", flagNameWait): complete.PredictNothing, fmt.Sprintf("-%s", flagNameContext): complete.PredictNothing, fmt.Sprintf("-%s", flagNameKubeconfig): complete.PredictFiles("*"), + fmt.Sprintf("-%s", flagNameDemo): complete.PredictNothing, + fmt.Sprintf("-%s", flagNameHCPResourceID): complete.PredictNothing, } } @@ -350,7 +427,7 @@ func (c *Command) validateFlags(args []string) error { if len(c.flagValueFiles) != 0 && c.flagPreset != defaultPreset { return fmt.Errorf("cannot set both -%s and -%s", flagNameConfigFile, flagNamePreset) } - if _, ok := config.Presets[c.flagPreset]; c.flagPreset != defaultPreset && !ok { + if ok := slices.Contains(preset.Presets, c.flagPreset); c.flagPreset != defaultPreset && !ok { return fmt.Errorf("'%s' is not a valid preset", c.flagPreset) } if _, err := time.ParseDuration(c.flagTimeout); err != nil { @@ -364,6 +441,20 @@ func (c *Command) validateFlags(args []string) error { } } + if c.flagPreset == preset.PresetCloud { + clientID := os.Getenv(envHCPClientID) + clientSecret := os.Getenv(envHCPClientSecret) + if clientID == "" { + return fmt.Errorf("When '%s' is specified as the preset, the '%s' environment variable must also be set", preset.PresetCloud, envHCPClientID) + } else if clientSecret == "" { + return fmt.Errorf("When '%s' is specified as the preset, the '%s' environment variable must also be set", preset.PresetCloud, envHCPClientSecret) + } else if c.flagNameHCPResourceID == "" { + return fmt.Errorf("When '%s' is specified as the preset, the '%s' flag must also be provided", preset.PresetCloud, flagNameHCPResourceID) + } + } else if c.flagNameHCPResourceID != "" { + return fmt.Errorf("The '%s' flag can only be used with the '%s' preset", flagNameHCPResourceID, preset.PresetCloud) + } + return nil } @@ -376,7 +467,7 @@ func (c *Command) validateFlags(args []string) error { // 5. -set-file // For example, -set-file will override a value provided via -set. // Within each of these groups the rightmost flag value has the highest precedence. -func (c *Command) mergeValuesFlagsWithPrecedence(settings *helmCLI.EnvSettings) (map[string]interface{}, error) { +func (c *Command) mergeValuesFlagsWithPrecedence(settings *helmCLI.EnvSettings, namespace string) (map[string]interface{}, error) { p := getter.All(settings) v := &values.Options{ ValueFiles: c.flagValueFiles, @@ -390,7 +481,14 @@ func (c *Command) mergeValuesFlagsWithPrecedence(settings *helmCLI.EnvSettings) } if c.flagPreset != defaultPreset { // Note the ordering of the function call, presets have lower precedence than set vals. - presetMap := config.Presets[c.flagPreset].(map[string]interface{}) + p, err := c.getPreset(c.flagPreset, namespace) + if err != nil { + return nil, fmt.Errorf("error getting preset provider: %s", err) + } + presetMap, err := p.GetValueMap() + if err != nil { + return nil, fmt.Errorf("error getting preset values: %s", err) + } vals = common.MergeMaps(presetMap, vals) } return vals, err @@ -424,24 +522,26 @@ func (c *Command) createUILogger() func(string, ...interface{}) { } } -// printDiff marshals both maps to YAML and prints the diff between the two. -func (c *Command) printDiff(old, new map[string]interface{}) error { - diff, err := common.Diff(old, new) - if err != nil { - return err +// getPreset is a factory function that, given a string, produces a struct that +// implements the Preset interface. If the string is not recognized an error is +// returned. +func (c *Command) getPreset(name string, namespace string) (preset.Preset, error) { + hcpConfig := &preset.HCPConfig{ + ResourceID: c.flagNameHCPResourceID, + ClientID: os.Getenv(envHCPClientID), + ClientSecret: os.Getenv(envHCPClientSecret), } - - c.UI.Output("\nDifference between user overrides for current and upgraded charts"+ - "\n--------------------------------------------------------------", terminal.WithInfoStyle()) - for _, line := range strings.Split(diff, "\n") { - if strings.HasPrefix(line, "+") { - c.UI.Output(line, terminal.WithDiffAddedStyle()) - } else if strings.HasPrefix(line, "-") { - c.UI.Output(line, terminal.WithDiffRemovedStyle()) - } else { - c.UI.Output(line, terminal.WithDiffUnchangedStyle()) - } + getPresetConfig := &preset.GetPresetConfig{ + Name: name, + CloudPreset: &preset.CloudPreset{ + KubernetesClient: c.kubernetes, + KubernetesNamespace: namespace, + SkipSavingSecrets: true, + UI: c.UI, + HTTPClient: c.httpClient, + HCPConfig: hcpConfig, + Context: c.Ctx, + }, } - - return nil + return preset.GetPreset(getPresetConfig) } diff --git a/cli/cmd/upgrade/upgrade_test.go b/cli/cmd/upgrade/upgrade_test.go index 9b4636eb57..2f2168496d 100644 --- a/cli/cmd/upgrade/upgrade_test.go +++ b/cli/cmd/upgrade/upgrade_test.go @@ -1,16 +1,29 @@ package upgrade import ( + "bytes" + "context" + "errors" "flag" "fmt" + "io" "os" "testing" "github.com/hashicorp/consul-k8s/cli/common" cmnFlag "github.com/hashicorp/consul-k8s/cli/common/flag" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/consul-k8s/cli/helm" + "github.com/hashicorp/consul-k8s/cli/preset" "github.com/hashicorp/go-hclog" "github.com/posener/complete" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + helmRelease "helm.sh/helm/v3/pkg/release" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" ) // TestValidateFlags tests the validate flags function. @@ -43,7 +56,7 @@ func TestValidateFlags(t *testing.T) { } for _, testCase := range testCases { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) t.Run(testCase.description, func(t *testing.T) { if err := c.validateFlags(testCase.input); err == nil { t.Errorf("Test case should have failed.") @@ -53,16 +66,22 @@ func TestValidateFlags(t *testing.T) { } // getInitializedCommand sets up a command struct for tests. -func getInitializedCommand(t *testing.T) *Command { +func getInitializedCommand(t *testing.T, buf io.Writer) *Command { t.Helper() log := hclog.New(&hclog.LoggerOptions{ Name: "cli", Level: hclog.Info, Output: os.Stdout, }) - + var ui terminal.UI + if buf != nil { + ui = terminal.NewUI(context.Background(), buf) + } else { + ui = terminal.NewBasicUI(context.Background()) + } baseCommand := &common.BaseCommand{ Log: log, + UI: ui, } c := &Command{ @@ -74,7 +93,7 @@ func getInitializedCommand(t *testing.T) *Command { func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { t.Parallel() - cmd := getInitializedCommand(t) + cmd := getInitializedCommand(t, nil) predictor := cmd.AutocompleteFlags() @@ -97,7 +116,437 @@ func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { } func TestTaskCreateCommand_AutocompleteArgs(t *testing.T) { - cmd := getInitializedCommand(t) + cmd := getInitializedCommand(t, nil) c := cmd.AutocompleteArgs() assert.Equal(t, complete.PredictNothing, c) } + +func TestGetPreset(t *testing.T) { + testCases := []struct { + description string + presetName string + }{ + { + "'cloud' should return a CloudPreset'.", + preset.PresetCloud, + }, + { + "'quickstart' should return a QuickstartPreset'.", + preset.PresetQuickstart, + }, + { + "'secure' should return a SecurePreset'.", + preset.PresetSecure, + }, + } + + for _, tc := range testCases { + c := getInitializedCommand(t, nil) + t.Run(tc.description, func(t *testing.T) { + p, err := c.getPreset(tc.presetName, "consul") + require.NoError(t, err) + switch p.(type) { + case *preset.CloudPreset: + require.Equal(t, preset.PresetCloud, tc.presetName) + case *preset.QuickstartPreset: + require.Equal(t, preset.PresetQuickstart, tc.presetName) + case *preset.SecurePreset: + require.Equal(t, preset.PresetSecure, tc.presetName) + } + }) + } +} + +// TestValidateCloudPresets tests the validate flags function when passed the cloud preset. +func TestValidateCloudPresets(t *testing.T) { + testCases := []struct { + description string + input []string + preProcessingFunc func() + postProcessingFunc func() + expectError bool + }{ + { + "Should not error on cloud preset when HCP_CLIENT_ID and HCP_CLIENT_SECRET envvars are present and hcp-resource-id parameter is provided.", + []string{"-preset=cloud", "-hcp-resource-id=foobar"}, + func() { + os.Setenv("HCP_CLIENT_ID", "foo") + os.Setenv("HCP_CLIENT_SECRET", "bar") + }, + func() { + os.Setenv("HCP_CLIENT_ID", "") + os.Setenv("HCP_CLIENT_SECRET", "") + }, + false, + }, + { + "Should error on cloud preset when HCP_CLIENT_ID is not provided.", + []string{"-preset=cloud", "-hcp-resource-id=foobar"}, + func() { + os.Unsetenv("HCP_CLIENT_ID") + os.Setenv("HCP_CLIENT_SECRET", "bar") + }, + func() { + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") + }, + true, + }, + { + "Should error on cloud preset when HCP_CLIENT_SECRET is not provided.", + []string{"-preset=cloud", "-hcp-resource-id=foobar"}, + func() { + os.Setenv("HCP_CLIENT_ID", "foo") + os.Unsetenv("HCP_CLIENT_SECRET") + }, + func() { + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") + }, + true, + }, + { + "Should error on cloud preset when -hcp-resource-id flag is not provided.", + []string{"-preset=cloud"}, + func() { + os.Setenv("HCP_CLIENT_ID", "foo") + os.Setenv("HCP_CLIENT_SECRET", "bar") + }, + func() { + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") + }, + true, + }, + { + "Should error when -hcp-resource-id flag is provided but cloud preset is not specified.", + []string{"-hcp-resource-id=foobar"}, + func() { + os.Setenv("HCP_CLIENT_ID", "foo") + os.Setenv("HCP_CLIENT_SECRET", "bar") + }, + func() { + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") + }, + true, + }, + } + + for _, testCase := range testCases { + testCase.preProcessingFunc() + c := getInitializedCommand(t, nil) + t.Run(testCase.description, func(t *testing.T) { + err := c.validateFlags(testCase.input) + if testCase.expectError && err == nil { + t.Errorf("Test case should have failed.") + } else if !testCase.expectError && err != nil { + t.Errorf("Test case should not have failed.") + } + }) + testCase.postProcessingFunc() + } +} + +func TestUpgrade(t *testing.T) { + var k8s kubernetes.Interface + cases := map[string]struct { + input []string + messages []string + helmActionsRunner *helm.MockActionRunner + preProcessingFunc func() + expectedReturnCode int + expectCheckedForConsulInstallations bool + expectCheckedForConsulDemoInstallations bool + expectConsulUpgraded bool + expectConsulDemoUpgraded bool + expectConsulDemoInstalled bool + }{ + "upgrade when consul installation exists returns success": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n No existing Consul demo application installation found.\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: false, + }, + "upgrade when consul installation does not exists returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ! Cannot upgrade Consul. Existing Consul installation not found. Use the command `consul-k8s install` to install Consul.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return false, "", "", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulUpgraded: false, + expectConsulDemoUpgraded: false, + }, + "upgrade when consul upgrade errors returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n No existing Consul demo application installation found.\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n\n==> Upgrading Consul\n ! Helm returned an error.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + UpgradeFunc: func(upgrade *action.Upgrade, name string, chart *chart.Chart, vals map[string]interface{}) (*helmRelease.Release, error) { + return nil, errors.New("Helm returned an error.") + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: false, + expectConsulDemoUpgraded: false, + }, + "upgrade when demo flag provided but no demo installation exists installs demo and returns success": { + input: []string{ + "-demo", + }, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n No existing consul-demo installation found, but -demo flag provided. consul-demo will be installed in namespace consul.\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + "\n==> Consul Demo Application Installation Summary\n Name: consul-demo\n Namespace: consul\n \n \n", + "\n==> Installing Consul demo application\n ✓ Downloaded charts.\n ✓ Consul demo application installed in namespace \"consul\".\n", + "\n==> Accessing Consul Demo Application UI\n kubectl port-forward deploy/frontend 8080:80 --namespace consul\n Browse to http://localhost:8080.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: false, + expectConsulDemoInstalled: true, + }, + "upgrade when demo flag provided and demo installation exists upgrades demo and returns success": { + input: []string{ + "-demo", + }, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n ✓ Existing Consul demo application installation found to be upgraded.\n Name: consul-demo\n Namespace: consul-demo\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + "\n==> Consul-Demo Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n \n", + "\n==> Upgrading consul-demo\n ✓ Consul-Demo upgraded in namespace \"consul-demo\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: true, + expectConsulDemoInstalled: false, + }, + "upgrade when demo flag not provided but demo installation exists upgrades demo and returns success": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n ✓ Existing Consul demo application installation found to be upgraded.\n Name: consul-demo\n Namespace: consul-demo\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + "\n==> Consul-Demo Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n \n", + "\n==> Upgrading consul-demo\n ✓ Consul-Demo upgraded in namespace \"consul-demo\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: true, + expectConsulDemoInstalled: false, + }, + "upgrade when demo upgrade errors returns error with consul being upgraded but demo not being upgraded": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n ✓ Existing Consul demo application installation found to be upgraded.\n Name: consul-demo\n Namespace: consul-demo\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + "\n==> Consul-Demo Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n \n", + "\n==> Upgrading consul-demo\n ! Helm returned an error.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + UpgradeFunc: func(upgrade *action.Upgrade, name string, chart *chart.Chart, vals map[string]interface{}) (*helmRelease.Release, error) { + if name == "consul" { + return &helmRelease.Release{}, nil + } else { + return nil, errors.New("Helm returned an error.") + } + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: false, + }, + "upgrade with quickstart preset when consul installation exists returns success": { + input: []string{ + "-preset", "quickstart", + }, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n No existing Consul demo application installation found.\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + connectInject:\n + enabled: true\n + metrics:\n + defaultEnableMerging: true\n + defaultEnabled: true\n + enableGatewayMetrics: true\n + controller:\n + enabled: true\n + global:\n + metrics:\n + enableAgentMetrics: true\n + enabled: true\n + name: consul\n + prometheus:\n + enabled: true\n + server:\n + replicas: 1\n + ui:\n + enabled: true\n + service:\n + enabled: true\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: false, + }, + "upgrade with secure preset when consul installation exists returns success": { + input: []string{ + "-preset", "secure", + }, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n No existing Consul demo application installation found.\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + connectInject:\n + enabled: true\n + controller:\n + enabled: true\n + global:\n + acls:\n + manageSystemACLs: true\n + gossipEncryption:\n + autoGenerate: true\n + name: consul\n + tls:\n + enableAutoEncrypt: true\n + enabled: true\n + server:\n + replicas: 1\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: false, + }, + "upgrade with --dry-run flag when consul installation exists returns success": { + input: []string{ + "--dry-run", + }, + messages: []string{ + " Performing dry run upgrade. No changes will be made to the cluster.\n", + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n No existing Consul demo application installation found.\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n", + "\n==> Performing Dry Run Upgrade\n Dry run complete. No changes were made to the Kubernetes cluster.\n Upgrade can proceed with this configuration.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: false, + expectConsulDemoUpgraded: false, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buf := new(bytes.Buffer) + c := getInitializedCommand(t, buf) + k8s = fake.NewSimpleClientset() + c.kubernetes = k8s + mock := tc.helmActionsRunner + c.helmActionsRunner = mock + if tc.preProcessingFunc != nil { + tc.preProcessingFunc() + } + input := append([]string{ + "--auto-approve", + }, tc.input...) + returnCode := c.Run(input) + require.Equal(t, tc.expectedReturnCode, returnCode) + require.Equal(t, tc.expectCheckedForConsulInstallations, mock.CheckedForConsulInstallations) + require.Equal(t, tc.expectCheckedForConsulDemoInstallations, mock.CheckedForConsulDemoInstallations) + require.Equal(t, tc.expectConsulUpgraded, mock.ConsulUpgraded) + require.Equal(t, tc.expectConsulDemoUpgraded, mock.ConsulDemoUpgraded) + require.Equal(t, tc.expectConsulDemoInstalled, mock.ConsulDemoInstalled) + output := buf.String() + for _, msg := range tc.messages { + require.Contains(t, output, msg) + } + }) + } +} diff --git a/cli/common/utils.go b/cli/common/utils.go index e03238bfb0..b2e9714a9d 100644 --- a/cli/common/utils.go +++ b/cli/common/utils.go @@ -1,19 +1,17 @@ package common import ( - "errors" - "fmt" "os" "strings" - - "helm.sh/helm/v3/pkg/action" - helmCLI "helm.sh/helm/v3/pkg/cli" ) const ( - DefaultReleaseName = "consul" - DefaultReleaseNamespace = "consul" - TopLevelChartDirName = "consul" + DefaultReleaseName = "consul" + DefaultReleaseNamespace = "consul" + ConsulDemoAppReleaseName = "consul-demo" + TopLevelChartDirName = "consul" + ReleaseTypeConsul = "Consul" + ReleaseTypeConsulDemo = "Consul demo application" // CLILabelKey and CLILabelValue are added to each secret on creation so the CLI knows // which key to delete on an uninstall. @@ -27,32 +25,6 @@ func Abort(raw string) bool { return !(strings.ToLower(confirmation) == "y" || strings.ToLower(confirmation) == "yes") } -// CheckForInstallations uses the helm Go SDK to find helm releases in all namespaces where the chart name is -// "consul", and returns the release name and namespace if found, or an error if not found. -func CheckForInstallations(settings *helmCLI.EnvSettings, uiLogger action.DebugLog) (string, string, error) { - // Need a specific action config to call helm list, where namespace is NOT specified. - listConfig := new(action.Configuration) - if err := listConfig.Init(settings.RESTClientGetter(), "", - os.Getenv("HELM_DRIVER"), uiLogger); err != nil { - return "", "", fmt.Errorf("couldn't initialize helm config: %s", err) - } - - lister := action.NewList(listConfig) - lister.AllNamespaces = true - lister.StateMask = action.ListAll - res, err := lister.Run() - if err != nil { - return "", "", fmt.Errorf("couldn't check for installations: %s", err) - } - - for _, rel := range res { - if rel.Chart.Metadata.Name == "consul" { - return rel.Name, rel.Namespace, nil - } - } - return "", "", errors.New("couldn't find consul installation") -} - // MergeMaps merges two maps giving b precedent. // @source: https://github.com/helm/helm/blob/main/pkg/cli/values/options.go func MergeMaps(a, b map[string]interface{}) map[string]interface{} { diff --git a/cli/config/config.go b/cli/config/config.go new file mode 100644 index 0000000000..d964bc3b5c --- /dev/null +++ b/cli/config/config.go @@ -0,0 +1,16 @@ +package config + +import "sigs.k8s.io/yaml" + +// GlobalNameConsul is used to set the global name of an install to consul. +const GlobalNameConsul = ` +global: + name: consul +` + +// ConvertToMap is a helper function that converts a YAML string to a map. +func ConvertToMap(s string) map[string]interface{} { + var m map[string]interface{} + _ = yaml.Unmarshal([]byte(s), &m) + return m +} diff --git a/cli/config/presets.go b/cli/config/presets.go deleted file mode 100644 index 06b91ce8ce..0000000000 --- a/cli/config/presets.go +++ /dev/null @@ -1,71 +0,0 @@ -package config - -import "sigs.k8s.io/yaml" - -const ( - PresetDemo = "demo" - PresetSecure = "secure" -) - -// Presets is a map of pre-configured helm values. -var Presets = map[string]interface{}{ - PresetDemo: Convert(demo), - PresetSecure: Convert(secure), -} - -// demo is a preset of common values for setting up Consul. -const demo = ` -global: - name: consul - metrics: - enabled: true - enableAgentMetrics: true -connectInject: - enabled: true - metrics: - defaultEnabled: true - defaultEnableMerging: true - enableGatewayMetrics: true -server: - replicas: 1 -controller: - enabled: true -ui: - enabled: true - service: - enabled: true -prometheus: - enabled: true -` - -// secure is a preset of common values for setting up Consul in a secure manner. -const secure = ` -global: - name: consul - gossipEncryption: - autoGenerate: true - tls: - enabled: true - enableAutoEncrypt: true - acls: - manageSystemACLs: true -server: - replicas: 1 -connectInject: - enabled: true -controller: - enabled: true -` - -// GlobalNameConsul is used to set the global name of an install to consul. -const GlobalNameConsul = ` -global: - name: consul -` - -// convert is a helper function that converts a YAML string to a map. -func Convert(s string) map[string]interface{} { - var m map[string]interface{} - _ = yaml.Unmarshal([]byte(s), &m) - return m -} diff --git a/cli/go.mod b/cli/go.mod index f130971e0a..d075c5e35c 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -9,18 +9,20 @@ require ( github.com/google/go-cmp v0.5.6 github.com/hashicorp/consul-k8s/charts v0.0.0-00010101000000-000000000000 github.com/hashicorp/go-hclog v0.16.2 + github.com/hashicorp/hcp-sdk-go v0.23.1-0.20220921131124-49168300a7dc github.com/kr/text v0.2.0 github.com/mattn/go-isatty v0.0.14 github.com/mitchellh/cli v1.1.2 github.com/olekukonko/tablewriter v0.0.5 github.com/posener/complete v1.1.1 github.com/stretchr/testify v1.7.2 + golang.org/x/text v0.3.7 helm.sh/helm/v3 v3.9.4 - k8s.io/api v0.24.2 - k8s.io/apimachinery v0.24.2 - k8s.io/cli-runtime v0.24.2 - k8s.io/client-go v0.24.2 - k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 + k8s.io/api v0.24.3 + k8s.io/apimachinery v0.24.3 + k8s.io/cli-runtime v0.24.3 + k8s.io/client-go v0.24.3 + k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e sigs.k8s.io/yaml v1.3.0 ) @@ -44,7 +46,7 @@ require ( github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 // indirect - github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect + github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect @@ -64,9 +66,18 @@ require ( github.com/go-errors/errors v1.0.1 // indirect github.com/go-gorp/gorp/v3 v3.0.2 // indirect github.com/go-logr/logr v1.2.2 // indirect + github.com/go-openapi/analysis v0.20.0 // indirect + github.com/go-openapi/errors v0.20.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/loads v0.20.2 // indirect + github.com/go-openapi/runtime v0.19.24 // indirect + github.com/go-openapi/spec v0.20.3 // indirect + github.com/go-openapi/strfmt v0.20.0 // indirect github.com/go-openapi/swag v0.19.14 // indirect + github.com/go-openapi/validate v0.20.2 // indirect + github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect + github.com/go-stack/stack v1.8.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.2.0 // indirect @@ -80,6 +91,7 @@ require ( github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.12 // indirect @@ -96,8 +108,10 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/spdystream v0.2.0 // indirect @@ -120,6 +134,7 @@ require ( github.com/russross/blackfriday v1.5.2 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect + github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/cobra v1.4.0 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -127,6 +142,7 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect + go.mongodb.org/mongo-driver v1.4.6 // indirect go.starlark.net v0.0.0-20200707032745-474f21a9602d // indirect golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect @@ -134,7 +150,6 @@ require ( golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect diff --git a/cli/go.sum b/cli/go.sum index 13d8d9c526..fedf1a3dd9 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -89,16 +89,19 @@ github.com/Microsoft/hcsshim v0.9.3 h1:k371PzBuRrz2b+ebGuI2nVgVhgsVX60jMfSw80NEC github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -107,9 +110,13 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -190,6 +197,7 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= @@ -230,6 +238,8 @@ github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui72 github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -249,26 +259,134 @@ github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ= +github.com/go-openapi/analysis v0.19.16/go.mod h1:GLInF007N83Ad3m8a/CbQ5TPzdnGT7workfHwuVjNVk= +github.com/go-openapi/analysis v0.20.0 h1:UN09o0kNhleunxW7LR+KnltD0YrJ8FF03pSqvAN3Vro= +github.com/go-openapi/analysis v0.20.0/go.mod h1:BMchjvaHDykmRMsK40iPtvyOfFdMMxlOmQr9FBZk+Og= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.7/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= +github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY= +github.com/go-openapi/loads v0.19.6/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= +github.com/go-openapi/loads v0.19.7/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= +github.com/go-openapi/loads v0.20.0/go.mod h1:2LhKquiE513rN5xC6Aan6lYOSddlL8Mp20AW9kpviM4= +github.com/go-openapi/loads v0.20.2 h1:z5p5Xf5wujMxS1y8aP+vxwW5qYT2zdJBbXKmQUG3lcc= +github.com/go-openapi/loads v0.20.2/go.mod h1:hTVUotJ+UonAMMZsvakEgmWKgtulweO9vYP2bQYKA/o= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= +github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= +github.com/go-openapi/runtime v0.19.24 h1:TqagMVlRAOTwllE/7hNKx6rQ10O6T8ZzeJdMjSTKaD4= +github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.15/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/spec v0.20.1/go.mod h1:93x7oh+d+FQsmsieroS4cmR3u0p/ywH649a3qwC9OsQ= +github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ= +github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.19.11/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc= +github.com/go-openapi/strfmt v0.20.0 h1:l2omNtmNbMc39IGptl9BuXBEKcZfS8zjrTsPKTiJiDM= +github.com/go-openapi/strfmt v0.20.0/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= +github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= +github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8= +github.com/go-openapi/validate v0.19.12/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4= +github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9GA7monOmWBbeCI= +github.com/go-openapi/validate v0.20.1/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0= +github.com/go-openapi/validate v0.20.2 h1:AhqDegYV3J3iQkMPJSXkvzymHKMTw0BST3RK3hTT4ts= +github.com/go-openapi/validate v0.20.2/go.mod h1:e7OJoKNgd0twXZwIn0A43tHbvIcr/rZIVCbJBpTUoY0= +github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= +github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -317,6 +435,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= @@ -366,6 +485,7 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLe github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= @@ -394,6 +514,7 @@ github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyN github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= @@ -411,6 +532,8 @@ github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcp-sdk-go v0.23.1-0.20220921131124-49168300a7dc h1:on26TCKYnX7JzZCtwkR/LWHSqMu40PoZ6h/0e6Pq8ug= +github.com/hashicorp/hcp-sdk-go v0.23.1-0.20220921131124-49168300a7dc/go.mod h1:/9UoDY2FYYA8lFaKBb2HmM/jKYZGANmf65q9QRc/cVw= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= @@ -427,8 +550,11 @@ github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -445,15 +571,19 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -463,6 +593,7 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -479,13 +610,17 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9 github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= @@ -515,6 +650,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.2 h1:PvH+lL2B7IQ101xQL63Of8yFS2y+aDlsFcsqNc+u/Kw= github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -528,6 +665,10 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -548,6 +689,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= @@ -579,7 +721,10 @@ github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3 github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= @@ -629,6 +774,8 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rubenv/sql-migrate v1.1.1 h1:haR5Hn8hbW9/SpAICrXoZqXnywS7Q5WijwkQENPeNWY= @@ -639,17 +786,22 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -662,6 +814,7 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= @@ -688,8 +841,13 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -723,6 +881,14 @@ go.etcd.io/etcd/client/v3 v3.5.1/go.mod h1:OnjH4M8OnAotwaB2l9bVgZzRFKru7/ZMoS46O go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.4.3/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.4.4/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.4.6 h1:rh7GdYmDrb8AQSkF8yteAus8qYOgOASWDOv1BWqBXkU= +go.mongodb.org/mongo-driver v1.4.6/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -757,9 +923,14 @@ go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -810,6 +981,7 @@ golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -817,6 +989,7 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -838,6 +1011,7 @@ golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -846,6 +1020,7 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= @@ -881,6 +1056,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -900,11 +1076,16 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -993,15 +1174,22 @@ golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -1229,6 +1417,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -1245,18 +1434,22 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.24.2 h1:g518dPU/L7VRLxWfcadQn2OnsiGWVOadTLpdnqgY2OI= k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg= +k8s.io/api v0.24.3 h1:tt55QEmKd6L2k5DP6G/ZzdMQKvG5ro4H4teClqm0sTY= +k8s.io/api v0.24.3/go.mod h1:elGR/XSZrS7z7cSZPzVWaycpJuGIw57j9b95/1PdJNI= k8s.io/apiextensions-apiserver v0.24.2 h1:/4NEQHKlEz1MlaK/wHT5KMKC9UKYz6NZz6JE6ov4G6k= k8s.io/apiextensions-apiserver v0.24.2/go.mod h1:e5t2GMFVngUEHUd0wuCJzw8YDwZoqZfJiGOW6mm2hLQ= -k8s.io/apimachinery v0.24.2 h1:5QlH9SL2C8KMcrNJPor+LbXVTaZRReml7svPEh4OKDM= k8s.io/apimachinery v0.24.2/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= +k8s.io/apimachinery v0.24.3 h1:hrFiNSA2cBZqllakVYyH/VyEh4B581bQRmqATJSeQTg= +k8s.io/apimachinery v0.24.3/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= k8s.io/apiserver v0.24.2 h1:orxipm5elPJSkkFNlwH9ClqaKEDJJA3yR2cAAlCnyj4= k8s.io/apiserver v0.24.2/go.mod h1:pSuKzr3zV+L+MWqsEo0kHHYwCo77AT5qXbFXP2jbvFI= -k8s.io/cli-runtime v0.24.2 h1:KxY6tSgPGsahA6c1/dmR3uF5jOxXPx2QQY6C5ZrLmtE= k8s.io/cli-runtime v0.24.2/go.mod h1:1LIhKL2RblkhfG4v5lZEt7FtgFG5mVb8wqv5lE9m5qY= -k8s.io/client-go v0.24.2 h1:CoXFSf8if+bLEbinDqN9ePIDGzcLtqhfd6jpfnwGOFA= +k8s.io/cli-runtime v0.24.3 h1:O9YvUHrDSCQUPlsqVmaqDrueqjpJ7IO6Yas9B6xGSoo= +k8s.io/cli-runtime v0.24.3/go.mod h1:In84wauoMOqa7JDvDSXGbf8lTNlr70fOGpYlYfJtSqA= k8s.io/client-go v0.24.2/go.mod h1:zg4Xaoo+umDsfCWr4fCnmLEtQXyCNXCvJuSsglNcV30= +k8s.io/client-go v0.24.3 h1:Nl1840+6p4JqkFWEW2LnMKU667BUxw03REfLAVhuKQY= +k8s.io/client-go v0.24.3/go.mod h1:AAovolf5Z9bY1wIg2FZ8LPQlEdKHjLI7ZD4rw920BJw= k8s.io/code-generator v0.24.2/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= k8s.io/component-base v0.24.2 h1:kwpQdoSfbcH+8MPN4tALtajLDfSfYxBDYlXobNWI6OU= k8s.io/component-base v0.24.2/go.mod h1:ucHwW76dajvQ9B7+zecZAP3BVqvrHoOxm8olHEg0nmM= @@ -1276,8 +1469,9 @@ k8s.io/kubectl v0.24.2 h1:+RfQVhth8akUmIc2Ge8krMl/pt66V7210ka3RE/p0J4= k8s.io/kubectl v0.24.2/go.mod h1:+HIFJc0bA6Tzu5O/YcuUt45APAxnNL8LeMuXwoiGsPg= k8s.io/metrics v0.24.2/go.mod h1:5NWURxZ6Lz5gj8TFU83+vdWIVASx7W8lwPpHYCqopMo= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 h1:HNSDgDCrr/6Ly3WEGKZftiE7IY19Vz2GdbOCyI4qqhc= k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e h1:W1yba+Bpkwb5BatGKZALQ1yylhwnuD6CkYmrTibyLDM= +k8s.io/utils v0.0.0-20220713171938-56c0de1e6f5e/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= oras.land/oras-go v1.2.0 h1:yoKosVIbsPoFMqAIFHTnrmOuafHal+J/r+I5bdbVWu4= oras.land/oras-go v1.2.0/go.mod h1:pFNs7oHp2dYsYMSS82HaX5l4mpnGO7hbpPN6EWH2ltc= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/cli/helm/action.go b/cli/helm/action.go index df8ba5fb07..d71014c762 100644 --- a/cli/helm/action.go +++ b/cli/helm/action.go @@ -1,11 +1,14 @@ package helm import ( + "embed" "fmt" "os" "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" helmCLI "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/release" "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -23,3 +26,83 @@ func InitActionConfig(actionConfig *action.Configuration, namespace string, sett } return actionConfig, nil } + +// HelmActionsRunner is a thin interface over existing Helm actions that normally +// require a Kubernetes cluster. This interface allows us to mock it in tests +// and get better coverage of CLI commands. +type HelmActionsRunner interface { + // A thin wrapper around the Helm list function. + CheckForInstallations(options *CheckForInstallationsOptions) (bool, string, string, error) + // A thin wrapper around the Helm status function. + GetStatus(status *action.Status, name string) (*release.Release, error) + // A thin wrapper around the Helm install function. + Install(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) + // A thin wrapper around the LoadChart function in consul-k8s CLI that reads the charts withing the embedded fle system. + LoadChart(chart embed.FS, chartDirName string) (*chart.Chart, error) + // A thin wrapper around the Helm uninstall function. + Uninstall(uninstall *action.Uninstall, name string) (*release.UninstallReleaseResponse, error) + // A thin wrapper around the Helm upgrade function. + Upgrade(upgrade *action.Upgrade, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) +} + +// ActionRunner is the implementation of HelmActionsRunner interface that +// truly calls Helm sdk functions and requires a real Kubernetes cluster. It +// is the non-mock implementation of HelmActionsRunner that is used in the CLI. +type ActionRunner struct{} + +func (h *ActionRunner) Uninstall(uninstall *action.Uninstall, name string) (*release.UninstallReleaseResponse, error) { + return uninstall.Run(name) +} + +func (h *ActionRunner) Install(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + return install.Run(chrt, vals) +} + +type CheckForInstallationsOptions struct { + Settings *helmCLI.EnvSettings + ReleaseName string + DebugLog action.DebugLog + SkipErrorWhenNotFound bool +} + +// CheckForInstallations uses the helm Go SDK to find helm releases in all namespaces where the chart name is +// "consul", and returns the release name and namespace if found, or an error if not found. +func (h *ActionRunner) CheckForInstallations(options *CheckForInstallationsOptions) (bool, string, string, error) { + // Need a specific action config to call helm list, where namespace is NOT specified. + listConfig := new(action.Configuration) + if err := listConfig.Init(options.Settings.RESTClientGetter(), "", + os.Getenv("HELM_DRIVER"), options.DebugLog); err != nil { + return false, "", "", fmt.Errorf("couldn't initialize helm config: %s", err) + } + + lister := action.NewList(listConfig) + lister.AllNamespaces = true + lister.StateMask = action.ListAll + res, err := lister.Run() + if err != nil { + return false, "", "", fmt.Errorf("couldn't check for installations: %s", err) + } + + for _, rel := range res { + if rel.Chart.Metadata.Name == options.ReleaseName { + return true, rel.Name, rel.Namespace, nil + } + } + var notFoundError error + if !options.SkipErrorWhenNotFound { + notFoundError = fmt.Errorf("couldn't find installation named '%s'", options.ReleaseName) + } + return false, "", "", notFoundError +} + +func (h *ActionRunner) GetStatus(status *action.Status, name string) (*release.Release, error) { + return status.Run(name) +} + +func (h *ActionRunner) Upgrade(upgrade *action.Upgrade, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + return upgrade.Run(name, chart, vals) +} + +func (h *ActionRunner) LoadChart(chart embed.FS, chartDirName string) (*chart.Chart, error) { + return LoadChart(chart, chartDirName) +} diff --git a/cli/helm/chart.go b/cli/helm/chart.go index 1a91ee19d5..f679ca591d 100644 --- a/cli/helm/chart.go +++ b/cli/helm/chart.go @@ -29,7 +29,7 @@ func LoadChart(chart embed.FS, chartDirName string) (*chart.Chart, error) { // FetchChartValues will attempt to fetch the values from the currently // installed Helm chart. -func FetchChartValues(namespace, name string, settings *helmCLI.EnvSettings, uiLogger action.DebugLog) (map[string]interface{}, error) { +func FetchChartValues(actionRunner HelmActionsRunner, namespace, name string, settings *helmCLI.EnvSettings, uiLogger action.DebugLog) (map[string]interface{}, error) { cfg := new(action.Configuration) cfg, err := InitActionConfig(cfg, namespace, settings, uiLogger) if err != nil { @@ -37,7 +37,7 @@ func FetchChartValues(namespace, name string, settings *helmCLI.EnvSettings, uiL } status := action.NewStatus(cfg) - release, err := status.Run(name) + release, err := actionRunner.GetStatus(status, name) if err != nil { return nil, err } diff --git a/cli/helm/install.go b/cli/helm/install.go new file mode 100644 index 0000000000..1bb5f3c886 --- /dev/null +++ b/cli/helm/install.go @@ -0,0 +1,140 @@ +package helm + +import ( + "embed" + "fmt" + "time" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "helm.sh/helm/v3/pkg/action" + helmCLI "helm.sh/helm/v3/pkg/cli" +) + +// InstallOptions is used when calling InstallHelmRelease. +type InstallOptions struct { + // ReleaseName is the name of the Helm release to be installed. + ReleaseName string + // ReleaseType is the helm upgrade type - consul vs consul-demo. + ReleaseType string + // Namespace is the Kubernetes namespace where the release is to be + // installed. + Namespace string + // Values the Helm chart values in a map form. + Values map[string]interface{} + // Settings is the Helm CLI environment settings. + Settings *helmCLI.EnvSettings + // Embedded chart specifies the Consul or Consul Demo Helm chart that has + // been embedded into the consul-k8s CLI. + EmbeddedChart embed.FS + // ChartDirName is the top level directory name fo the EmbeddedChart. + ChartDirName string + // UILogger is a DebugLog used to return messages from Helm to the UI. + UILogger action.DebugLog + // DryRun specifies whether the install/upgrade should actually modify the + // Kubernetes cluster. + DryRun bool + // AutoApprove will bypass any terminal prompts with an automatic yes. + AutoApprove bool + // Wait specifies whether the Helm install should wait until all pods + // are ready. + Wait bool + // Timeout is the duration that Helm will wait for the command to complete + // before it throws an error. + Timeout time.Duration + // UI is the terminal output representation that is used to prompt the user + // and output messages. + UI terminal.UI + // HelmActionsRunner is a thin interface around Helm actions for install, + // upgrade, and uninstall. + HelmActionsRunner HelmActionsRunner +} + +// InstallDemoApp will perform the following actions +// - Print out the installation summary. +// - Setup action configuration for Helm Go SDK function calls. +// - Setup the installation action. +// - Load the Helm chart. +// - Run the install. +func InstallDemoApp(options *InstallOptions) error { + options.UI.Output(fmt.Sprintf("%s Installation Summary", + cases.Title(language.English).String(common.ReleaseTypeConsulDemo)), + terminal.WithHeaderStyle()) + options.UI.Output("Name: %s", common.ConsulDemoAppReleaseName, terminal.WithInfoStyle()) + options.UI.Output("Namespace: %s", options.Settings.Namespace(), terminal.WithInfoStyle()) + options.UI.Output("\n", terminal.WithInfoStyle()) + + err := InstallHelmRelease(options) + if err != nil { + return err + } + + options.UI.Output("Accessing %s UI", cases.Title(language.English).String(common.ReleaseTypeConsulDemo), terminal.WithHeaderStyle()) + port := "8080" + portForwardCmd := fmt.Sprintf("kubectl port-forward deploy/frontend %s:80", port) + if options.Settings.Namespace() != "default" { + portForwardCmd += fmt.Sprintf(" --namespace %s", options.Settings.Namespace()) + } + options.UI.Output(portForwardCmd, terminal.WithInfoStyle()) + options.UI.Output("Browse to http://localhost:%s.", port, terminal.WithInfoStyle()) + return nil +} + +// InstallHelmRelease handles downloading the embedded helm chart, loading the +// values and runnning the Helm install command. +func InstallHelmRelease(options *InstallOptions) error { + if options.DryRun { + return nil + } + + if !options.AutoApprove { + confirmation, err := options.UI.Input(&terminal.Input{ + Prompt: "Proceed with installation? (y/N)", + Style: terminal.InfoStyle, + Secret: false, + }) + + if err != nil { + return err + } + if common.Abort(confirmation) { + options.UI.Output("Install aborted. Use the command `consul-k8s install -help` to learn how to customize your installation.", + terminal.WithInfoStyle()) + return err + } + } + + options.UI.Output("Installing %s", options.ReleaseType, terminal.WithHeaderStyle()) + + // Setup action configuration for Helm Go SDK function calls. + actionConfig := new(action.Configuration) + actionConfig, err := InitActionConfig(actionConfig, options.Namespace, options.Settings, options.UILogger) + if err != nil { + return err + } + + // Setup the installation action. + install := action.NewInstall(actionConfig) + install.ReleaseName = options.ReleaseName + install.Namespace = options.Namespace + install.CreateNamespace = true + install.Wait = options.Wait + install.Timeout = options.Timeout + + // Load the Helm chart. + chart, err := options.HelmActionsRunner.LoadChart(options.EmbeddedChart, options.ChartDirName) + if err != nil { + return err + } + options.UI.Output("Downloaded charts.", terminal.WithSuccessStyle()) + + // Run the install. + if _, err = options.HelmActionsRunner.Install(install, chart, options.Values); err != nil { + return err + } + + options.UI.Output("%s installed in namespace %q.", options.ReleaseType, options.Namespace, terminal.WithSuccessStyle()) + return nil +} diff --git a/cli/helm/install_test.go b/cli/helm/install_test.go new file mode 100644 index 0000000000..2cd98ca5a8 --- /dev/null +++ b/cli/helm/install_test.go @@ -0,0 +1,82 @@ +package helm + +import ( + "bytes" + "context" + "embed" + "errors" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + helmCLI "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/release" +) + +func TestInstallDemoApp(t *testing.T) { + cases := map[string]struct { + messages []string + helmActionsRunner *MockActionRunner + expectError bool + }{ + "basic success": { + messages: []string{ + "\n==> Consul Demo Application Installation Summary\n Name: consul-demo\n Namespace: default\n \n \n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul-namespace\".\n", + "\n==> Accessing Consul Demo Application UI\n kubectl port-forward deploy/frontend 8080:80 --namespace consul-namespace\n Browse to http://localhost:8080.\n", + }, + helmActionsRunner: &MockActionRunner{}, + }, + "failure because LoadChart returns failure": { + messages: []string{ + "\n==> Consul Demo Application Installation Summary\n Name: consul-demo\n Namespace: default\n \n \n\n==> Installing Consul\n", + }, + helmActionsRunner: &MockActionRunner{ + LoadChartFunc: func(chrt embed.FS, chartDirName string) (*chart.Chart, error) { + return nil, errors.New("sad trombone!") + }, + }, + expectError: true, + }, + "failure because Install returns failure": { + messages: []string{ + "\n==> Consul Demo Application Installation Summary\n Name: consul-demo\n Namespace: default\n \n \n\n==> Installing Consul\n", + }, + helmActionsRunner: &MockActionRunner{ + InstallFunc: func(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + return nil, errors.New("sad trombone!") + }, + }, + expectError: true, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buf := new(bytes.Buffer) + mock := tc.helmActionsRunner + options := &InstallOptions{ + HelmActionsRunner: mock, + UI: terminal.NewUI(context.Background(), buf), + UILogger: func(format string, v ...interface{}) {}, + ReleaseName: "consul-release", + ReleaseType: common.ReleaseTypeConsul, + Namespace: "consul-namespace", + Settings: helmCLI.New(), + AutoApprove: true, + } + err := InstallDemoApp(options) + if tc.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + output := buf.String() + for _, msg := range tc.messages { + require.Contains(t, output, msg) + } + }) + } +} diff --git a/cli/helm/mock.go b/cli/helm/mock.go new file mode 100644 index 0000000000..05d3b6edb4 --- /dev/null +++ b/cli/helm/mock.go @@ -0,0 +1,136 @@ +package helm + +import ( + "embed" + + "github.com/hashicorp/consul-k8s/cli/common" + + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/release" +) + +type MockActionRunner struct { + CheckForInstallationsFunc func(options *CheckForInstallationsOptions) (bool, string, string, error) + GetStatusFunc func(status *action.Status, name string) (*release.Release, error) + InstallFunc func(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) + LoadChartFunc func(chrt embed.FS, chartDirName string) (*chart.Chart, error) + UninstallFunc func(uninstall *action.Uninstall, name string) (*release.UninstallReleaseResponse, error) + UpgradeFunc func(upgrade *action.Upgrade, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) + CheckedForConsulInstallations bool + CheckedForConsulDemoInstallations bool + GotStatusConsulRelease bool + GotStatusConsulDemoRelease bool + ConsulInstalled bool + ConsulUninstalled bool + ConsulUpgraded bool + ConsulDemoInstalled bool + ConsulDemoUninstalled bool + ConsulDemoUpgraded bool +} + +func (m *MockActionRunner) Install(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + var installFunc func(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) + if m.InstallFunc == nil { + installFunc = func(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + return &release.Release{}, nil + } + } else { + installFunc = m.InstallFunc + } + + release, err := installFunc(install, chrt, vals) + if err == nil { + if install.ReleaseName == common.DefaultReleaseName { + m.ConsulInstalled = true + } else if install.ReleaseName == common.ConsulDemoAppReleaseName { + m.ConsulDemoInstalled = true + } + } + return release, err +} + +func (m *MockActionRunner) Uninstall(uninstall *action.Uninstall, name string) (*release.UninstallReleaseResponse, error) { + var uninstallFunc func(uninstall *action.Uninstall, name string) (*release.UninstallReleaseResponse, error) + + if m.UninstallFunc == nil { + uninstallFunc = func(uninstall *action.Uninstall, name string) (*release.UninstallReleaseResponse, error) { + return &release.UninstallReleaseResponse{}, nil + } + } else { + uninstallFunc = m.UninstallFunc + } + + release, err := uninstallFunc(uninstall, name) + if err == nil { + if name == common.DefaultReleaseName { + m.ConsulUninstalled = true + } else if name == common.ConsulDemoAppReleaseName { + m.ConsulDemoUninstalled = true + } + } + return release, err +} + +func (m *MockActionRunner) CheckForInstallations(options *CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == common.DefaultReleaseName { + m.CheckedForConsulInstallations = true + } else if options.ReleaseName == common.ConsulDemoAppReleaseName { + m.CheckedForConsulDemoInstallations = true + } + + if m.CheckForInstallationsFunc == nil { + return false, "", "", nil + } + return m.CheckForInstallationsFunc(options) +} + +func (m *MockActionRunner) GetStatus(status *action.Status, name string) (*release.Release, error) { + if name == common.DefaultReleaseName { + m.GotStatusConsulRelease = true + } else if name == common.ConsulDemoAppReleaseName { + m.GotStatusConsulDemoRelease = true + } + + if m.GetStatusFunc == nil { + return &release.Release{}, nil + } + return m.GetStatusFunc(status, name) +} + +func (m *MockActionRunner) Upgrade(upgrade *action.Upgrade, name string, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + var upgradeFunc func(upgrade *action.Upgrade, name string, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) + + if m.UpgradeFunc == nil { + upgradeFunc = func(upgrade *action.Upgrade, name string, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + return &release.Release{}, nil + } + } else { + upgradeFunc = m.UpgradeFunc + } + + release, err := upgradeFunc(upgrade, name, chrt, vals) + if err == nil { + if name == common.DefaultReleaseName { + m.ConsulUpgraded = true + } else if name == common.ConsulDemoAppReleaseName { + m.ConsulDemoUpgraded = true + } + } + return release, err +} + +func (m *MockActionRunner) LoadChart(chrt embed.FS, chartDirName string) (*chart.Chart, error) { + var loadChartFunc func(chrt embed.FS, chartDirName string) (*chart.Chart, error) + + if m.LoadChartFunc == nil { + loadChartFunc = func(chrt embed.FS, chartDirName string) (*chart.Chart, error) { + return &chart.Chart{}, nil + } + } else { + loadChartFunc = m.LoadChartFunc + } + + release, err := loadChartFunc(chrt, chartDirName) + return release, err +} diff --git a/cli/helm/upgrade.go b/cli/helm/upgrade.go new file mode 100644 index 0000000000..d2b8523c5f --- /dev/null +++ b/cli/helm/upgrade.go @@ -0,0 +1,149 @@ +package helm + +import ( + "embed" + "strings" + "time" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "helm.sh/helm/v3/pkg/action" + helmCLI "helm.sh/helm/v3/pkg/cli" +) + +// UpgradeOptions is used when calling UpgradeHelmRelease. +type UpgradeOptions struct { + // ReleaseName is the name of the installed Helm release to upgrade. + ReleaseName string + // ReleaseType is the helm upgrade type - consul vs consul-demo. + ReleaseType string + // ReleaseTypeName is a user friendly version of ReleaseType. The values + // are consul and consul demo application. + ReleaseTypeName string + // Namespace is the Kubernetes namespace where the release is installed. + Namespace string + // Values the Helm chart values in a map form. + Values map[string]interface{} + // Settings is the Helm CLI environment settings. + Settings *helmCLI.EnvSettings + // Embedded chart specifies the Consul or Consul Demo Helm chart that has + // been embedded into the consul-k8s CLI. + EmbeddedChart embed.FS + // ChartDirName is the top level directory name fo the EmbeddedChart. + ChartDirName string + // UILogger is a DebugLog used to return messages from Helm to the UI. + UILogger action.DebugLog + // DryRun specifies whether the upgrade should actually modify the + // Kubernetes cluster. + DryRun bool + // AutoApprove will bypass any terminal prompts with an automatic yes. + AutoApprove bool + // Wait specifies whether the Helm install should wait until all pods + // are ready. + Wait bool + // Timeout is the duration that Helm will wait for the command to complete + // before it throws an error. + Timeout time.Duration + // UI is the terminal output representation that is used to prompt the user + // and output messages. + UI terminal.UI + // HelmActionsRunner is a thin interface around Helm actions for install, + // upgrade, and uninstall. + HelmActionsRunner HelmActionsRunner +} + +// UpgradeHelmRelease handles downloading the embedded helm chart, loading the +// values, showing the diff between new and installed values, and runnning the +// Helm install command. +func UpgradeHelmRelease(options *UpgradeOptions) error { + options.UI.Output("%s Upgrade Summary", cases.Title(language.English).String(options.ReleaseTypeName), terminal.WithHeaderStyle()) + + chart, err := options.HelmActionsRunner.LoadChart(options.EmbeddedChart, options.ChartDirName) + if err != nil { + return err + } + options.UI.Output("Downloaded charts.", terminal.WithSuccessStyle()) + + currentChartValues, err := FetchChartValues(options.HelmActionsRunner, + options.Namespace, options.ReleaseName, options.Settings, options.UILogger) + if err != nil { + return err + } + + // Print out the upgrade summary. + if err = printDiff(currentChartValues, options.Values, options.UI); err != nil { + options.UI.Output("Could not print the different between current and upgraded charts: %v", err, terminal.WithErrorStyle()) + return err + } + + // Check if the user is OK with the upgrade unless the auto approve or dry run flags are true. + if !options.AutoApprove && !options.DryRun { + confirmation, err := options.UI.Input(&terminal.Input{ + Prompt: "Proceed with upgrade? (y/N)", + Style: terminal.InfoStyle, + Secret: false, + }) + + if err != nil { + return err + } + if common.Abort(confirmation) { + options.UI.Output("Upgrade aborted. Use the command `consul-k8s upgrade -help` to learn how to customize your upgrade.", + terminal.WithInfoStyle()) + return err + } + } + + if !options.DryRun { + options.UI.Output("Upgrading %s", options.ReleaseTypeName, terminal.WithHeaderStyle()) + } else { + options.UI.Output("Performing Dry Run Upgrade", terminal.WithHeaderStyle()) + return nil + } + + // Setup action configuration for Helm Go SDK function calls. + actionConfig := new(action.Configuration) + actionConfig, err = InitActionConfig(actionConfig, options.Namespace, options.Settings, options.UILogger) + if err != nil { + return err + } + + // Setup the upgrade action. + upgrade := action.NewUpgrade(actionConfig) + upgrade.Namespace = options.Namespace + upgrade.DryRun = options.DryRun + upgrade.Wait = options.Wait + upgrade.Timeout = options.Timeout + + // Run the upgrade. Note that the dry run config is passed into the upgrade action, so upgrade.Run is called even during a dry run. + _, err = options.HelmActionsRunner.Upgrade(upgrade, options.ReleaseName, chart, options.Values) + if err != nil { + return err + } + options.UI.Output("%s upgraded in namespace %q.", cases.Title(language.English).String(options.ReleaseTypeName), options.Namespace, terminal.WithSuccessStyle()) + return nil +} + +// printDiff marshals both maps to YAML and prints the diff between the two. +func printDiff(old, new map[string]interface{}, ui terminal.UI) error { + diff, err := common.Diff(old, new) + if err != nil { + return err + } + + ui.Output("\nDifference between user overrides for current and upgraded charts"+ + "\n--------------------------------------------------------------", terminal.WithInfoStyle()) + for _, line := range strings.Split(diff, "\n") { + if strings.HasPrefix(line, "+") { + ui.Output(line, terminal.WithDiffAddedStyle()) + } else if strings.HasPrefix(line, "-") { + ui.Output(line, terminal.WithDiffRemovedStyle()) + } else { + ui.Output(line, terminal.WithDiffUnchangedStyle()) + } + } + + return nil +} diff --git a/cli/helm/upgrade_test.go b/cli/helm/upgrade_test.go new file mode 100644 index 0000000000..9ffb7dc201 --- /dev/null +++ b/cli/helm/upgrade_test.go @@ -0,0 +1,117 @@ +package helm + +import ( + "bytes" + "context" + "embed" + "errors" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + helmCLI "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/release" +) + +func TestUpgrade(t *testing.T) { + buf := new(bytes.Buffer) + mock := &MockActionRunner{ + CheckForInstallationsFunc: func(options *CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return false, "", "", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + } + + options := &UpgradeOptions{ + HelmActionsRunner: mock, + UI: terminal.NewUI(context.Background(), buf), + UILogger: func(format string, v ...interface{}) {}, + ReleaseName: "consul-release", + ReleaseType: common.ReleaseTypeConsul, + Namespace: "consul-namespace", + Settings: helmCLI.New(), + AutoApprove: true, + } + + expectedMessages := []string{ + "\n==> Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n \n", + "\n==> Upgrading \n ✓ upgraded in namespace \"consul-namespace\".\n", + } + err := UpgradeHelmRelease(options) + require.NoError(t, err) + output := buf.String() + for _, msg := range expectedMessages { + require.Contains(t, output, msg) + } +} + +func TestUpgradeHelmRelease(t *testing.T) { + cases := map[string]struct { + messages []string + helmActionsRunner *MockActionRunner + expectError bool + }{ + "basic success": { + messages: []string{ + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul-namespace\".\n", + }, + helmActionsRunner: &MockActionRunner{}, + }, + "failure because LoadChart returns failure": { + messages: []string{ + "\n==> Consul Upgrade Summary\n", + }, + helmActionsRunner: &MockActionRunner{ + LoadChartFunc: func(chrt embed.FS, chartDirName string) (*chart.Chart, error) { + return nil, errors.New("sad trombone!") + }, + }, + expectError: true, + }, + "failure because Upgrade returns failure": { + messages: []string{ + "\n==> Consul Upgrade Summary\n", + }, + helmActionsRunner: &MockActionRunner{ + UpgradeFunc: func(upgrade *action.Upgrade, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + return nil, errors.New("sad trombone!") + }, + }, + expectError: true, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buf := new(bytes.Buffer) + mock := tc.helmActionsRunner + options := &UpgradeOptions{ + HelmActionsRunner: mock, + UI: terminal.NewUI(context.Background(), buf), + UILogger: func(format string, v ...interface{}) {}, + ReleaseName: "consul-release", + ReleaseType: common.ReleaseTypeConsul, + ReleaseTypeName: common.ReleaseTypeConsul, + Namespace: "consul-namespace", + Settings: helmCLI.New(), + AutoApprove: true, + } + err := UpgradeHelmRelease(options) + if tc.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + output := buf.String() + for _, msg := range tc.messages { + require.Contains(t, output, msg) + } + }) + } +} diff --git a/cli/preset/cloud_preset.go b/cli/preset/cloud_preset.go new file mode 100644 index 0000000000..a97cdf46b6 --- /dev/null +++ b/cli/preset/cloud_preset.go @@ -0,0 +1,362 @@ +package preset + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/consul-k8s/cli/config" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-global-network-manager-service/preview/2022-02-15/models" + "github.com/hashicorp/hcp-sdk-go/httpclient" + "github.com/hashicorp/hcp-sdk-go/resource" + + hcpgnm "github.com/hashicorp/hcp-sdk-go/clients/cloud-global-network-manager-service/preview/2022-02-15/client/global_network_manager_service" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +const ( + secretNameHCPConfig = "consul-hcp-config" + secretNameGossipKey = "consul-gossip-key" + secretNameBootstrapToken = "consul-bootstrap-token" + secretNameServerCA = "consul-server-ca" + secretNameServerCert = "consul-server-cert" + secretKeyHCPClientID = "client-id" + secretKeyHCPClientSecret = "client-secret" + secretKeyHCPResourceID = "resource-id" + secretKeyHCPAuthURL = "auth-url" + secretKeyHCPAPIHostname = "api-hostname" + secretKeyHCPScadaAddress = "scada-address" + secretKeyGossipKey = "key" + secretKeyBootstrapToken = "token" +) + +// CloudBootstrapConfig represents the response fetched from the agent +// bootstrap config endpoint in HCP. +type CloudBootstrapConfig struct { + BootstrapResponse *models.HashicorpCloudGlobalNetworkManager20220215AgentBootstrapResponse + ConsulConfig ConsulConfig + HCPConfig HCPConfig +} + +// HCPConfig represents the resource-id, client-id, and client-secret +// provided by the user in order to make a call to fetch the agent bootstrap +// config data from the endpoint in HCP. +type HCPConfig struct { + ResourceID string + ClientID string + ClientSecret string + AuthURL string + APIHostname string + ScadaAddress string +} + +// ConsulConfig represents 'cluster.consul_config' in the response +// fetched from the agent bootstrap config endpoint in HCP. +type ConsulConfig struct { + ACL ACL `json:"acl"` +} + +// ACL represents 'cluster.consul_config.acl' in the response +// fetched from the agent bootstrap config endpoint in HCP. +type ACL struct { + Tokens Tokens `json:"tokens"` +} + +// Tokens represents 'cluster.consul_config.acl.tokens' in the +// response fetched from the agent bootstrap config endpoint in HCP. +type Tokens struct { + Agent string `json:"agent"` + InitialManagement string `json:"initial_management"` +} + +// CloudPreset struct is an implementation of the Preset interface that is used +// to fetch agent bootrap config from HCP, save it to secrets, and provide a +// Helm values map that is used during installation. +type CloudPreset struct { + HCPConfig *HCPConfig + KubernetesClient kubernetes.Interface + KubernetesNamespace string + UI terminal.UI + SkipSavingSecrets bool + Context context.Context + HTTPClient *http.Client +} + +// GetValueMap must fetch configuration from HCP, save various secrets from +// the response, and map the secret names into the returned value map. +func (i *CloudPreset) GetValueMap() (map[string]interface{}, error) { + bootstrapConfig, err := i.fetchAgentBootstrapConfig() + if err != nil { + return nil, err + } + + if !i.SkipSavingSecrets { + err = i.saveSecretsFromBootstrapConfig(bootstrapConfig) + if err != nil { + return nil, err + } + } + + return i.getHelmConfigWithMapSecretNames(bootstrapConfig), nil +} + +// fetchAgentBootstrapConfig use the resource-id, client-id, and client-secret +// to call to the agent bootstrap config endpoint and parse the response into a +// CloudBootstrapConfig struct. +func (i *CloudPreset) fetchAgentBootstrapConfig() (*CloudBootstrapConfig, error) { + i.UI.Output("Fetching Consul cluster configuration from HCP", terminal.WithHeaderStyle()) + httpClientCfg := httpclient.Config{} + clientRuntime, err := httpclient.New(httpClientCfg) + if err != nil { + return nil, err + } + + hcpgnmClient := hcpgnm.New(clientRuntime, nil) + clusterResource, err := resource.FromString(i.HCPConfig.ResourceID) + if err != nil { + return nil, err + } + + params := hcpgnm.NewAgentBootstrapConfigParamsWithContext(i.Context). + WithID(clusterResource.ID). + WithLocationOrganizationID(clusterResource.Organization). + WithLocationProjectID(clusterResource.Project). + WithHTTPClient(i.HTTPClient) + + resp, err := hcpgnmClient.AgentBootstrapConfig(params, nil) + if err != nil { + return nil, err + } + + bootstrapConfig := resp.GetPayload() + i.UI.Output("HCP configuration successfully fetched.", terminal.WithSuccessStyle()) + + return i.parseBootstrapConfigResponse(bootstrapConfig) +} + +// parseBootstrapConfigResponse unmarshals the boostrap parseBootstrapConfigResponse +// and also sets the HCPConfig values to return CloudBootstrapConfig struct. +func (i *CloudPreset) parseBootstrapConfigResponse(bootstrapRepsonse *models.HashicorpCloudGlobalNetworkManager20220215AgentBootstrapResponse) (*CloudBootstrapConfig, error) { + var cbc CloudBootstrapConfig + var consulConfig ConsulConfig + err := json.Unmarshal([]byte(bootstrapRepsonse.Bootstrap.ConsulConfig), &consulConfig) + if err != nil { + return nil, err + } + cbc.ConsulConfig = consulConfig + cbc.HCPConfig = *i.HCPConfig + cbc.BootstrapResponse = bootstrapRepsonse + + return &cbc, nil +} + +// getHelmConfigWithMapSecretNames maps the secret names were agent bootstrap +// config values have been saved, maps them into the Helm values template for +// the cloud preset, and returns the value map. +func (i *CloudPreset) getHelmConfigWithMapSecretNames(cfg *CloudBootstrapConfig) map[string]interface{} { + values := fmt.Sprintf(` +global: + datacenter: %s + tls: + enabled: true + enableAutoEncrypt: true + caCert: + secretName: %s + secretKey: %s + gossipEncryption: + secretName: %s + secretKey: %s + acls: + manageSystemACLs: true + bootstrapToken: + secretName: %s + secretKey: %s + cloud: + enabled: true + secretName: %s +server: + replicas: %d + serverCert: + secretName: %s +connectInject: + enabled: true +controller: + enabled: true +`, cfg.BootstrapResponse.Cluster.ID, secretNameServerCA, corev1.TLSCertKey, secretNameGossipKey, + secretKeyGossipKey, secretNameBootstrapToken, secretKeyBootstrapToken, + secretNameHCPConfig, cfg.BootstrapResponse.Cluster.BootstrapExpect, secretNameServerCert) + valuesMap := config.ConvertToMap(values) + return valuesMap +} + +// saveSecretsFromBootstrapConfig takes the following items from the +// agent bootstrap config from HCP and saves them into known secret names and +// keys: +// - HCP config (resource-id, client-id, client-secret). +// - ACL bootstrap token. +// - gossip encryption key. +// - server tls cert and key. +// - server CA cert. +func (i *CloudPreset) saveSecretsFromBootstrapConfig(config *CloudBootstrapConfig) error { + if err := i.createNamespaceIfNotExists(); err != nil { + return err + } + + i.UI.Output(fmt.Sprintf("Saving HCP configuration as secrets in %s namespace", i.KubernetesNamespace), terminal.WithHeaderStyle()) + if err := i.saveServerHCPConfigSecret(config); err != nil { + return err + } + i.UI.Output(fmt.Sprintf("HCP config saved in '%s' secret in namespace '%s'.", + secretNameHCPConfig, i.KubernetesNamespace), terminal.WithSuccessStyle()) + + if err := i.saveBootstrapTokenSecret(config); err != nil { + return err + } + i.UI.Output(fmt.Sprintf("ACL bootstrap token saved as '%s' key in '%s' secret in namespace '%s'.", + secretKeyBootstrapToken, secretNameBootstrapToken, i.KubernetesNamespace), terminal.WithSuccessStyle()) + + if err := i.saveGossipKeySecret(config); err != nil { + return err + } + i.UI.Output(fmt.Sprintf("Gossip encryption key saved as '%s' key in '%s' secret in namespace '%s'.", + secretKeyGossipKey, secretNameGossipKey, i.KubernetesNamespace), terminal.WithSuccessStyle()) + + if err := i.saveServerCertSecret(config); err != nil { + return err + } + i.UI.Output(fmt.Sprintf("Server TLS cert and key saved as '%s' and '%s' key in '%s secret in namespace '%s'.", + corev1.TLSCertKey, corev1.TLSPrivateKeyKey, secretNameServerCert, i.KubernetesNamespace), terminal.WithSuccessStyle()) + + if err := i.saveServerCASecret(config); err != nil { + return err + } + i.UI.Output(fmt.Sprintf("Server TLS CA saved as '%s' key in '%s' secret in namespace '%s'.", + corev1.TLSCertKey, secretNameServerCA, i.KubernetesNamespace), terminal.WithSuccessStyle()) + + return nil +} + +// createNamespaceIfNotExists checks to see if a given namespace exists and if +// it does not will create it. This function is needed to ensure a namespace +// exists before HCP config secrets are saved. +func (i *CloudPreset) createNamespaceIfNotExists() error { + i.UI.Output(fmt.Sprintf("Checking if %s namespace needs to be created", i.KubernetesNamespace), terminal.WithHeaderStyle()) + // Create k8s namespace if it doesn't exist. + _, err := i.KubernetesClient.CoreV1().Namespaces().Get(context.Background(), i.KubernetesNamespace, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + _, err = i.KubernetesClient.CoreV1().Namespaces().Create(context.Background(), &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: i.KubernetesNamespace, + }, + }, metav1.CreateOptions{}) + if err != nil { + return err + } + i.UI.Output(fmt.Sprintf("Namespace '%s' has been created.", i.KubernetesNamespace), terminal.WithSuccessStyle()) + + } else if err != nil { + return err + } else { + i.UI.Output(fmt.Sprintf("Namespace '%s' already exists.", i.KubernetesNamespace), terminal.WithSuccessStyle()) + } + return nil +} + +// saveSecret saves given key value pairs into a given secret in a given +// namespace. It is the generic function that helps saves all of the specific +// cloud preset secrets. +func (i *CloudPreset) saveSecret(secretName string, kvps map[string][]byte, secretType corev1.SecretType) error { + _, err := i.KubernetesClient.CoreV1().Secrets(i.KubernetesNamespace).Get(context.Background(), secretName, metav1.GetOptions{}) + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: i.KubernetesNamespace, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + }, + Data: kvps, + Type: secretType, + } + if k8serrors.IsNotFound(err) { + _, err = i.KubernetesClient.CoreV1().Secrets(i.KubernetesNamespace).Create(context.Background(), secret, metav1.CreateOptions{}) + if err != nil { + return err + } + } else if err != nil { + return err + } else { + return fmt.Errorf("'%s' secret in '%s' namespace already exists.", secretName, i.KubernetesNamespace) + } + return nil +} + +// saveServerHCPConfigSecret saves the resource-id, client-id, and client-secret +// to a given secret in a given namespace. +func (i *CloudPreset) saveServerHCPConfigSecret(config *CloudBootstrapConfig) error { + data := map[string][]byte{ + secretKeyHCPClientID: []byte(config.HCPConfig.ClientID), + secretKeyHCPClientSecret: []byte(config.HCPConfig.ClientSecret), + secretKeyHCPResourceID: []byte(config.HCPConfig.ResourceID), + secretKeyHCPAuthURL: []byte(config.HCPConfig.AuthURL), + secretKeyHCPAPIHostname: []byte(config.HCPConfig.APIHostname), + secretKeyHCPScadaAddress: []byte(config.HCPConfig.ScadaAddress), + } + if err := i.saveSecret(secretNameHCPConfig, data, corev1.SecretTypeOpaque); err != nil { + return err + } + return nil +} + +// saveBootstrapTokenSecret saves the ACL bootstrap token to a given secret in +// a given namespace. +func (i *CloudPreset) saveBootstrapTokenSecret(config *CloudBootstrapConfig) error { + data := map[string][]byte{ + secretKeyBootstrapToken: []byte(config.ConsulConfig.ACL.Tokens.InitialManagement), + } + if err := i.saveSecret(secretNameBootstrapToken, data, corev1.SecretTypeOpaque); err != nil { + return err + } + return nil +} + +// saveGossipKeySecret saves the gossip encryption key to a given secret +// in a given namespace. +func (i *CloudPreset) saveGossipKeySecret(config *CloudBootstrapConfig) error { + data := map[string][]byte{ + secretKeyGossipKey: []byte(config.BootstrapResponse.Bootstrap.GossipKey), + } + if err := i.saveSecret(secretNameGossipKey, data, corev1.SecretTypeOpaque); err != nil { + return err + } + return nil +} + +// saveServerCertSecret saves the server TLS cert and key to a given secret +// in a given namespace. +func (i *CloudPreset) saveServerCertSecret(config *CloudBootstrapConfig) error { + data := map[string][]byte{ + corev1.TLSCertKey: []byte(config.BootstrapResponse.Bootstrap.ServerTLS.Cert), + corev1.TLSPrivateKeyKey: []byte(config.BootstrapResponse.Bootstrap.ServerTLS.PrivateKey), + } + if err := i.saveSecret(secretNameServerCert, data, corev1.SecretTypeTLS); err != nil { + return err + } + return nil +} + +// saveServerCASecret saves the server CA cert to a given secret in a +// given namespace. +func (i *CloudPreset) saveServerCASecret(config *CloudBootstrapConfig) error { + data := map[string][]byte{ + corev1.TLSCertKey: []byte(config.BootstrapResponse.Bootstrap.ServerTLS.CertificateAuthorities[0]), + } + if err := i.saveSecret(secretNameServerCA, data, corev1.SecretTypeOpaque); err != nil { + return err + } + return nil +} diff --git a/cli/preset/cloud_preset_test.go b/cli/preset/cloud_preset_test.go new file mode 100644 index 0000000000..81d81f8b73 --- /dev/null +++ b/cli/preset/cloud_preset_test.go @@ -0,0 +1,463 @@ +package preset + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-global-network-manager-service/preview/2022-02-15/models" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + "sigs.k8s.io/yaml" +) + +const ( + hcpClientID = "RAxJflDbxDXw8kLY6jWmwqMz3kVe7NnL" + hcpClientSecret = "1fNzurLatQPLPwf7jnD4fRtU9f5nH31RKBHayy08uQ6P-6nwI1rFZjMXb4m3cCKH" + hcpResourceID = "organization/ccbdd191-5dc3-4a73-9e05-6ac30ca67992/project/36019e0d-ed59-4df6-9990-05bb7fc793b6/hashicorp.consul.global-network-manager.cluster/prod-on-prem" + expectedSecretNameHCPConfig = "consul-hcp-config" + expectedSecretNameGossipKey = "consul-gossip-key" + expectedSecretNameBootstrap = "consul-bootstrap-token" + expectedSecretNameServerCA = "consul-server-ca" + expectedSecretNameServerCert = "consul-server-cert" + namespace = "consul" + validResponse = ` +{ + "cluster": + { + "id": "dc1", + "bootstrap_expect" : 3 + }, + "bootstrap": + { + "gossip_key": "Wa6/XFAnYy0f9iqVH2iiG+yore3CqHSemUy4AIVTa/w=", + "server_tls": { + "certificate_authorities": [ + "-----BEGIN CERTIFICATE-----\nMIIC6TCCAo+gAwIBAgIQA3pUmJcy9uw8MNIDZPiaZjAKBggqhkjOPQQDAjCBtzEL\nMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2Nv\nMRowGAYDVQQJExExMDEgU2Vjb25kIFN0cmVldDEOMAwGA1UEERMFOTQxMDUxFzAV\nBgNVBAoTDkhhc2hpQ29ycCBJbmMuMT4wPAYDVQQDEzVDb25zdWwgQWdlbnQgQ0Eg\nNDYyMjg2MDAxNTk3NzI1NDMzMTgxNDQ4OTAzODMyNjg5NzI1NDAeFw0yMjAzMjkx\nMTEyNDNaFw0yNzAzMjgxMTEyNDNaMIG3MQswCQYDVQQGEwJVUzELMAkGA1UECBMC\nQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGjAYBgNVBAkTETEwMSBTZWNvbmQg\nU3RyZWV0MQ4wDAYDVQQREwU5NDEwNTEXMBUGA1UEChMOSGFzaGlDb3JwIEluYy4x\nPjA8BgNVBAMTNUNvbnN1bCBBZ2VudCBDQSA0NjIyODYwMDE1OTc3MjU0MzMxODE0\nNDg5MDM4MzI2ODk3MjU0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERs73JA+K\n9xMorTz6fA5x8Dmin6l8pNgka3/Ye3SFWJD/0lKFTXEX7Li8+hXG31WMLdXgoWHS\nkL1HoLboV8hEAKN7MHkwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8w\nKQYDVR0OBCIEICst9kpfDK0LtEbUghWf4ahjpzd7Mlh07OLT/e38PKDmMCsGA1Ud\nIwQkMCKAICst9kpfDK0LtEbUghWf4ahjpzd7Mlh07OLT/e38PKDmMAoGCCqGSM49\nBAMCA0gAMEUCIQCuk/n49np4m76jTFLk2zeiSi7UfubMeS2BD4bkMt6v/wIgbO0R\npTqCOYQr3cji1EpEQca95VCZ26lBEjqLQF3osGc=\n-----END CERTIFICATE-----\n" + ], + "private_key": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIA+DFWCFz+SujFCuWM3GpoTLPX8igerwMw+8efNbx7a+oAoGCCqGSM49\nAwEHoUQDQgAE7LdWJpna88mohlnuTyGJ+WZ3P6BCxGqBRWNJn3+JEoHhmaifx7Sq\nWLMCEB1UNbH5Z1esaS4h33Gb0pyyiCy19A==\n-----END EC PRIVATE KEY-----\n", + "cert": "-----BEGIN CERTIFICATE-----\nMIICmzCCAkGgAwIBAgIRAKZ77a2h+plK2yXFsW0kfgAwCgYIKoZIzj0EAwIwgbcx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNj\nbzEaMBgGA1UECRMRMTAxIFNlY29uZCBTdHJlZXQxDjAMBgNVBBETBTk0MTA1MRcw\nFQYDVQQKEw5IYXNoaUNvcnAgSW5jLjE+MDwGA1UEAxM1Q29uc3VsIEFnZW50IENB\nIDQ2MjI4NjAwMTU5NzcyNTQzMzE4MTQ0ODkwMzgzMjY4OTcyNTQwHhcNMjIwMzI5\nMTExMjUwWhcNMjMwMzI5MTExMjUwWjAcMRowGAYDVQQDExFzZXJ2ZXIuZGMxLmNv\nbnN1bDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOy3ViaZ2vPJqIZZ7k8hiflm\ndz+gQsRqgUVjSZ9/iRKB4Zmon8e0qlizAhAdVDWx+WdXrGkuId9xm9KcsogstfSj\ngccwgcQwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF\nBQcDAjAMBgNVHRMBAf8EAjAAMCkGA1UdDgQiBCDaH9x1CRRqM5BYCMKBnAFyZjQq\nSY9IcJnhZUZIIJHU4jArBgNVHSMEJDAigCArLfZKXwytC7RG1IIVn+GoY6c3ezJY\ndOzi0/3t/Dyg5jAtBgNVHREEJjAkghFzZXJ2ZXIuZGMxLmNvbnN1bIIJbG9jYWxo\nb3N0hwR/AAABMAoGCCqGSM49BAMCA0gAMEUCIQCOxQHGF2483Cdd9nXcqAoOcxYP\nIqNP/WM03qyERyYNNQIgbtFBLIAgrhdXdjEvHMjU5ceHSwle/K0p0OTSIwSk8xI=\n-----END CERTIFICATE-----\n" + }, + "consul_config": "{\"acl\":{\"default_policy\":\"deny\",\"enable_token_persistence\":true,\"enabled\":true,\"tokens\":{\"agent\":\"74044c72-03c8-42b0-b57f-728bb22ca7fb\",\"initial_management\":\"74044c72-03c8-42b0-b57f-728bb22ca7fb\"}},\"auto_encrypt\":{\"allow_tls\":true},\"bootstrap_expect\":1,\"encrypt\":\"yUPhgtteok1/bHoVIoRnJMfOrKrb1TDDyWJRh9rlUjg=\",\"encrypt_verify_incoming\":true,\"encrypt_verify_outgoing\":true,\"ports\":{\"http\":-1,\"https\":8501},\"retry_join\":[],\"verify_incoming\":true,\"verify_outgoing\":true,\"verify_server_hostname\":true}" + } +}` +) + +var validBootstrapReponse *models.HashicorpCloudGlobalNetworkManager20220215AgentBootstrapResponse = &models.HashicorpCloudGlobalNetworkManager20220215AgentBootstrapResponse{ + Bootstrap: &models.HashicorpCloudGlobalNetworkManager20220215ClusterBootstrap{ + ID: "dc1", + GossipKey: "Wa6/XFAnYy0f9iqVH2iiG+yore3CqHSemUy4AIVTa/w=", + BootstrapExpect: 3, + ServerTLS: &models.HashicorpCloudGlobalNetworkManager20220215ServerTLS{ + CertificateAuthorities: []string{"-----BEGIN CERTIFICATE-----\nMIIC6TCCAo+gAwIBAgIQA3pUmJcy9uw8MNIDZPiaZjAKBggqhkjOPQQDAjCBtzEL\nMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2Nv\nMRowGAYDVQQJExExMDEgU2Vjb25kIFN0cmVldDEOMAwGA1UEERMFOTQxMDUxFzAV\nBgNVBAoTDkhhc2hpQ29ycCBJbmMuMT4wPAYDVQQDEzVDb25zdWwgQWdlbnQgQ0Eg\nNDYyMjg2MDAxNTk3NzI1NDMzMTgxNDQ4OTAzODMyNjg5NzI1NDAeFw0yMjAzMjkx\nMTEyNDNaFw0yNzAzMjgxMTEyNDNaMIG3MQswCQYDVQQGEwJVUzELMAkGA1UECBMC\nQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGjAYBgNVBAkTETEwMSBTZWNvbmQg\nU3RyZWV0MQ4wDAYDVQQREwU5NDEwNTEXMBUGA1UEChMOSGFzaGlDb3JwIEluYy4x\nPjA8BgNVBAMTNUNvbnN1bCBBZ2VudCBDQSA0NjIyODYwMDE1OTc3MjU0MzMxODE0\nNDg5MDM4MzI2ODk3MjU0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERs73JA+K\n9xMorTz6fA5x8Dmin6l8pNgka3/Ye3SFWJD/0lKFTXEX7Li8+hXG31WMLdXgoWHS\nkL1HoLboV8hEAKN7MHkwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8w\nKQYDVR0OBCIEICst9kpfDK0LtEbUghWf4ahjpzd7Mlh07OLT/e38PKDmMCsGA1Ud\nIwQkMCKAICst9kpfDK0LtEbUghWf4ahjpzd7Mlh07OLT/e38PKDmMAoGCCqGSM49\nBAMCA0gAMEUCIQCuk/n49np4m76jTFLk2zeiSi7UfubMeS2BD4bkMt6v/wIgbO0R\npTqCOYQr3cji1EpEQca95VCZ26lBEjqLQF3osGc=\n-----END CERTIFICATE-----\n"}, + PrivateKey: "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIA+DFWCFz+SujFCuWM3GpoTLPX8igerwMw+8efNbx7a+oAoGCCqGSM49\nAwEHoUQDQgAE7LdWJpna88mohlnuTyGJ+WZ3P6BCxGqBRWNJn3+JEoHhmaifx7Sq\nWLMCEB1UNbH5Z1esaS4h33Gb0pyyiCy19A==\n-----END EC PRIVATE KEY-----\n", + Cert: "-----BEGIN CERTIFICATE-----\nMIICmzCCAkGgAwIBAgIRAKZ77a2h+plK2yXFsW0kfgAwCgYIKoZIzj0EAwIwgbcx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNj\nbzEaMBgGA1UECRMRMTAxIFNlY29uZCBTdHJlZXQxDjAMBgNVBBETBTk0MTA1MRcw\nFQYDVQQKEw5IYXNoaUNvcnAgSW5jLjE+MDwGA1UEAxM1Q29uc3VsIEFnZW50IENB\nIDQ2MjI4NjAwMTU5NzcyNTQzMzE4MTQ0ODkwMzgzMjY4OTcyNTQwHhcNMjIwMzI5\nMTExMjUwWhcNMjMwMzI5MTExMjUwWjAcMRowGAYDVQQDExFzZXJ2ZXIuZGMxLmNv\nbnN1bDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOy3ViaZ2vPJqIZZ7k8hiflm\ndz+gQsRqgUVjSZ9/iRKB4Zmon8e0qlizAhAdVDWx+WdXrGkuId9xm9KcsogstfSj\ngccwgcQwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF\nBQcDAjAMBgNVHRMBAf8EAjAAMCkGA1UdDgQiBCDaH9x1CRRqM5BYCMKBnAFyZjQq\nSY9IcJnhZUZIIJHU4jArBgNVHSMEJDAigCArLfZKXwytC7RG1IIVn+GoY6c3ezJY\ndOzi0/3t/Dyg5jAtBgNVHREEJjAkghFzZXJ2ZXIuZGMxLmNvbnN1bIIJbG9jYWxo\nb3N0hwR/AAABMAoGCCqGSM49BAMCA0gAMEUCIQCOxQHGF2483Cdd9nXcqAoOcxYP\nIqNP/WM03qyERyYNNQIgbtFBLIAgrhdXdjEvHMjU5ceHSwle/K0p0OTSIwSk8xI=\n-----END CERTIFICATE-----\n"}, + ConsulConfig: "{\"acl\":{\"default_policy\":\"deny\",\"enable_token_persistence\":true,\"enabled\":true,\"tokens\":{\"agent\":\"74044c72-03c8-42b0-b57f-728bb22ca7fb\",\"initial_management\":\"74044c72-03c8-42b0-b57f-728bb22ca7fb\"}},\"auto_encrypt\":{\"allow_tls\":true},\"bootstrap_expect\":1,\"encrypt\":\"yUPhgtteok1/bHoVIoRnJMfOrKrb1TDDyWJRh9rlUjg=\",\"encrypt_verify_incoming\":true,\"encrypt_verify_outgoing\":true,\"ports\":{\"http\":-1,\"https\":8501},\"retry_join\":[],\"verify_incoming\":true,\"verify_outgoing\":true,\"verify_server_hostname\":true}", + }, + Cluster: &models.HashicorpCloudGlobalNetworkManager20220215Cluster{ + ID: "dc1", + BootstrapExpect: 3, + }, +} + +var hcpConfig *HCPConfig = &HCPConfig{ + ResourceID: hcpResourceID, + ClientID: hcpClientID, + ClientSecret: hcpClientSecret, + AuthURL: "https://foobar", + APIHostname: "https://foo.bar", + ScadaAddress: "10.10.10.10", +} + +var validBootstrapConfig *CloudBootstrapConfig = &CloudBootstrapConfig{ + HCPConfig: *hcpConfig, + ConsulConfig: ConsulConfig{ + ACL: ACL{ + Tokens: Tokens{ + Agent: "74044c72-03c8-42b0-b57f-728bb22ca7fb", + InitialManagement: "74044c72-03c8-42b0-b57f-728bb22ca7fb", + }, + }, + }, + BootstrapResponse: validBootstrapReponse, +} + +func TestGetValueMap(t *testing.T) { + // Create fake k8s. + k8s := fake.NewSimpleClientset() + namespace := "consul" + + // Start the mock HCP server. + hcpMockServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + if r != nil && r.URL.Path == "/global-network-manager/2022-02-15/organizations/ccbdd191-5dc3-4a73-9e05-6ac30ca67992/projects/36019e0d-ed59-4df6-9990-05bb7fc793b6/clusters/prod-on-prem/agent/bootstrap_config" && + r.Method == "GET" { + w.Write([]byte(validResponse)) + } else { + w.Write([]byte(` + { + "access_token": "dummy-token" + } + `)) + } + })) + hcpMockServer.StartTLS() + t.Cleanup(hcpMockServer.Close) + mockServerURL, err := url.Parse(hcpMockServer.URL) + require.NoError(t, err) + os.Setenv("HCP_AUTH_URL", hcpMockServer.URL) + os.Setenv("HCP_API_HOST", mockServerURL.Host) + os.Setenv("HCP_CLIENT_ID", "fGY34fkOxcQmpkcygQmGHQZkEcLDhBde") + os.Setenv("HCP_CLIENT_SECRET", "8EWngREObMe90HNDN6oQv3YKQlRtVkg-28AgZylz1en0DHwyiE2pYCbwi61oF8dr") + bsConfig := getDeepCopyOfValidBootstrapConfig() + bsConfig.HCPConfig.APIHostname = mockServerURL.Host + bsConfig.HCPConfig.AuthURL = hcpMockServer.URL + + testCases := []struct { + description string + installer *CloudPreset + expectedConfig *CloudBootstrapConfig + postProcessingFunc func() + }{ + { + "Should save secrets when SkipSavingSecrets is false.", + &CloudPreset{ + HCPConfig: &bsConfig.HCPConfig, + KubernetesClient: k8s, + KubernetesNamespace: namespace, + UI: terminal.NewBasicUI(context.Background()), + HTTPClient: hcpMockServer.Client(), + Context: context.Background(), + }, + bsConfig, + func() { + deleteSecrets(k8s) + }, + }, + { + "Should not save secrets when SkipSavingSecrets is true.", + &CloudPreset{ + HCPConfig: &bsConfig.HCPConfig, + KubernetesClient: k8s, + KubernetesNamespace: namespace, + UI: terminal.NewBasicUI(context.Background()), + SkipSavingSecrets: true, + HTTPClient: hcpMockServer.Client(), + Context: context.Background(), + }, + bsConfig, + func() { + deleteSecrets(k8s) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + config, err := tc.installer.GetValueMap() + require.NoError(t, err) + require.NotNil(t, config) + if tc.installer.SkipSavingSecrets { + checkSecretsWereNotSaved(k8s) + } else { + checkSecretsWereSaved(t, k8s, bsConfig) + } + tc.postProcessingFunc() + }) + } + os.Unsetenv("HCP_AUTH_URL") + os.Unsetenv("HCP_API_HOST") + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") +} + +// TestParseBootstrapConfigResponse tests that response string from agent bootstrap +// config endpoint can be converted into CloudBootstrapConfig bootstrap object. +func TestParseBootstrapConfigResponse(t *testing.T) { + testCases := []struct { + description string + input string + expectedConfig *CloudBootstrapConfig + }{ + { + "Should properly parse a valid response.", + validResponse, + validBootstrapConfig, + }, + } + + cloudPreset := &CloudPreset{ + HCPConfig: hcpConfig, + KubernetesNamespace: namespace, + UI: terminal.NewBasicUI(context.Background()), + } + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + config, err := cloudPreset.parseBootstrapConfigResponse(validBootstrapReponse) + require.NoError(t, err) + require.Equal(t, tc.expectedConfig, config) + }) + } +} + +func TestSaveSecretsFromBootstrapConfig(t *testing.T) { + t.Parallel() + + // Create fake k8s. + k8s := fake.NewSimpleClientset() + + testCases := []struct { + description string + expectsError bool + expectedErrorMessage string + preProcessingFunc func() + postProcessingFunc func() + }{ + { + "Properly saves secrets with a full bootstrapConfig.", + false, + "", + func() {}, + func() { + deleteSecrets(k8s) + }, + }, + { + "Errors when hcp config secret already exists.", + true, + fmt.Sprintf("'%s' secret in '%s' namespace already exists.", expectedSecretNameHCPConfig, namespace), + func() { + savePlaceholderSecret(expectedSecretNameHCPConfig, k8s) + }, + func() { + deleteSecrets(k8s) + }, + }, + { + "Errors when bootstrap token secret already exists.", + true, + fmt.Sprintf("'%s' secret in '%s' namespace already exists.", expectedSecretNameBootstrap, namespace), + func() { + savePlaceholderSecret(expectedSecretNameBootstrap, k8s) + }, + func() { + deleteSecrets(k8s) + }, + }, + { + "Errors when gossip key secret already exists.", + true, + fmt.Sprintf("'%s' secret in '%s' namespace already exists.", expectedSecretNameGossipKey, namespace), + func() { + savePlaceholderSecret(expectedSecretNameGossipKey, k8s) + }, + func() { + deleteSecrets(k8s) + }, + }, + { + "Errors when server cert secret already exists.", + true, + fmt.Sprintf("'%s' secret in '%s' namespace already exists.", expectedSecretNameServerCert, namespace), + func() { + savePlaceholderSecret(expectedSecretNameServerCert, k8s) + }, + func() { + deleteSecrets(k8s) + }, + }, + { + "Errors when server CA secret already exists.", + true, + fmt.Sprintf("'%s' secret in '%s' namespace already exists.", expectedSecretNameServerCA, namespace), + func() { + savePlaceholderSecret(expectedSecretNameServerCA, k8s) + }, + func() { + deleteSecrets(k8s) + }, + }, + } + cloudPreset := &CloudPreset{ + HCPConfig: hcpConfig, + KubernetesClient: k8s, + KubernetesNamespace: namespace, + UI: terminal.NewBasicUI(context.Background()), + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + tc.preProcessingFunc() + err := cloudPreset.saveSecretsFromBootstrapConfig(validBootstrapConfig) + if tc.expectsError && err != nil { + require.Equal(t, tc.expectedErrorMessage, err.Error()) + + } else { + require.NoError(t, err) + require.Equal(t, expectedSecretNameBootstrap, secretNameBootstrapToken) + require.Equal(t, expectedSecretNameGossipKey, secretNameGossipKey) + require.Equal(t, expectedSecretNameHCPConfig, secretNameHCPConfig) + require.Equal(t, expectedSecretNameServerCA, secretNameServerCA) + require.Equal(t, expectedSecretNameServerCert, secretNameServerCert) + + checkSecretsWereSaved(t, k8s, validBootstrapConfig) + + } + tc.postProcessingFunc() + }) + } + +} + +func TestGetHelmConfigWithMapSecretNames(t *testing.T) { + t.Parallel() + + const expected = `connectInject: + enabled: true +controller: + enabled: true +global: + acls: + bootstrapToken: + secretKey: token + secretName: consul-bootstrap-token + manageSystemACLs: true + cloud: + enabled: true + secretName: consul-hcp-config + datacenter: dc1 + gossipEncryption: + secretKey: key + secretName: consul-gossip-key + tls: + caCert: + secretKey: tls.crt + secretName: consul-server-ca + enableAutoEncrypt: true + enabled: true +server: + replicas: 3 + serverCert: + secretName: consul-server-cert +` + + cloudPreset := &CloudPreset{} + cfg := &CloudBootstrapConfig{ + BootstrapResponse: &models.HashicorpCloudGlobalNetworkManager20220215AgentBootstrapResponse{ + Cluster: &models.HashicorpCloudGlobalNetworkManager20220215Cluster{ + BootstrapExpect: 3, + ID: "dc1", + }, + }, + } + cloudHelmValues := cloudPreset.getHelmConfigWithMapSecretNames(cfg) + require.NotNil(t, cloudHelmValues) + valuesYaml, err := yaml.Marshal(cloudHelmValues) + yml := string(valuesYaml) + require.NoError(t, err) + require.Equal(t, expected, yml) +} + +func savePlaceholderSecret(secretName string, k8sClient kubernetes.Interface) { + data := map[string][]byte{} + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + }, + Data: data, + Type: corev1.SecretTypeOpaque, + } + k8sClient.CoreV1().Secrets(namespace).Create(context.Background(), secret, metav1.CreateOptions{}) +} + +func deleteSecrets(k8sClient kubernetes.Interface) { + k8sClient.CoreV1().Secrets(namespace).Delete(context.Background(), expectedSecretNameHCPConfig, metav1.DeleteOptions{}) + k8sClient.CoreV1().Secrets(namespace).Delete(context.Background(), expectedSecretNameBootstrap, metav1.DeleteOptions{}) + k8sClient.CoreV1().Secrets(namespace).Delete(context.Background(), expectedSecretNameGossipKey, metav1.DeleteOptions{}) + k8sClient.CoreV1().Secrets(namespace).Delete(context.Background(), expectedSecretNameServerCert, metav1.DeleteOptions{}) + k8sClient.CoreV1().Secrets(namespace).Delete(context.Background(), expectedSecretNameServerCA, metav1.DeleteOptions{}) +} + +func checkSecretsWereSaved(t require.TestingT, k8s kubernetes.Interface, expectedConfig *CloudBootstrapConfig) { + + // Check that namespace is created + _, err := k8s.CoreV1().Namespaces().Get(context.Background(), namespace, metav1.GetOptions{}) + require.NoError(t, err) + + // Check the hcp config secret is as expected. + hcpConfigSecret, err := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secretNameHCPConfig, metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, expectedConfig.HCPConfig.ClientID, string(hcpConfigSecret.Data[secretKeyHCPClientID])) + require.Equal(t, expectedConfig.HCPConfig.ClientSecret, string(hcpConfigSecret.Data[secretKeyHCPClientSecret])) + require.Equal(t, expectedConfig.HCPConfig.ResourceID, string(hcpConfigSecret.Data[secretKeyHCPResourceID])) + require.Equal(t, expectedConfig.HCPConfig.AuthURL, string(hcpConfigSecret.Data[secretKeyHCPAuthURL])) + require.Equal(t, expectedConfig.HCPConfig.ScadaAddress, string(hcpConfigSecret.Data[secretKeyHCPScadaAddress])) + require.Equal(t, expectedConfig.HCPConfig.APIHostname, string(hcpConfigSecret.Data[secretKeyHCPAPIHostname])) + require.Equal(t, corev1.SecretTypeOpaque, hcpConfigSecret.Type) + require.Equal(t, common.CLILabelValue, hcpConfigSecret.Labels[common.CLILabelKey]) + + // Check the bootstrap token secret is as expected. + bootstrapSecret, err := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secretNameBootstrapToken, metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, expectedConfig.ConsulConfig.ACL.Tokens.InitialManagement, string(bootstrapSecret.Data["token"])) + require.Equal(t, corev1.SecretTypeOpaque, bootstrapSecret.Type) + require.Equal(t, common.CLILabelValue, bootstrapSecret.Labels[common.CLILabelKey]) + + // Check the gossip key secret is as expected. + gossipKeySecret, err := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secretNameGossipKey, metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, expectedConfig.BootstrapResponse.Bootstrap.GossipKey, string(gossipKeySecret.Data["key"])) + require.Equal(t, corev1.SecretTypeOpaque, gossipKeySecret.Type) + require.Equal(t, common.CLILabelValue, gossipKeySecret.Labels[common.CLILabelKey]) + + // Check the server cert secret is as expected. + serverCertSecret, err := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secretNameServerCert, metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, expectedConfig.BootstrapResponse.Bootstrap.ServerTLS.Cert, string(serverCertSecret.Data[corev1.TLSCertKey])) + require.Equal(t, expectedConfig.BootstrapResponse.Bootstrap.ServerTLS.PrivateKey, string(serverCertSecret.Data[corev1.TLSPrivateKeyKey])) + require.Equal(t, corev1.SecretTypeTLS, serverCertSecret.Type) + require.Equal(t, common.CLILabelValue, serverCertSecret.Labels[common.CLILabelKey]) + + // Check the server CA secret is as expected. + serverCASecret, err := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secretNameServerCA, metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, expectedConfig.BootstrapResponse.Bootstrap.ServerTLS.CertificateAuthorities[0], string(serverCASecret.Data[corev1.TLSCertKey])) + require.Equal(t, corev1.SecretTypeOpaque, serverCASecret.Type) + require.Equal(t, common.CLILabelValue, serverCASecret.Labels[common.CLILabelKey]) +} + +func checkSecretsWereNotSaved(k8s kubernetes.Interface) bool { + ns, _ := k8s.CoreV1().Namespaces().Get(context.Background(), namespace, metav1.GetOptions{}) + hcpConfigSecret, _ := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secretNameHCPConfig, metav1.GetOptions{}) + bootstrapSecret, _ := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secretNameBootstrapToken, metav1.GetOptions{}) + gossipKeySecret, _ := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secretNameGossipKey, metav1.GetOptions{}) + serverCertSecret, _ := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secretNameServerCert, metav1.GetOptions{}) + serverCASecret, _ := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secretNameServerCA, metav1.GetOptions{}) + return ns == nil && hcpConfigSecret == nil && bootstrapSecret == nil && + gossipKeySecret == nil && serverCASecret == nil && serverCertSecret == nil +} + +func getDeepCopyOfValidBootstrapConfig() *CloudBootstrapConfig { + data, err := json.Marshal(validBootstrapConfig) + if err != nil { + panic(err) + } + + var copy *CloudBootstrapConfig + if err := json.Unmarshal(data, ©); err != nil { + panic(err) + } + return copy +} diff --git a/cli/preset/demo.go b/cli/preset/demo.go new file mode 100644 index 0000000000..bf6c0bb122 --- /dev/null +++ b/cli/preset/demo.go @@ -0,0 +1,43 @@ +package preset + +import "github.com/hashicorp/consul-k8s/cli/config" + +// DemoPreset struct is an implementation of the Preset interface that provides +// a Helm values map that is used during installation and represents the +// the quickstart configuration for Consul on Kubernetes. +type DemoPreset struct{} + +// GetValueMap returns the Helm value map representing the quickstart +// configuration for Consul on Kubernetes. It does the following: +// - server replicas equal to 1. +// - enables the service mesh. +// - enables the ui. +// - enables metrics. +// - enables Prometheus. +func (i *DemoPreset) GetValueMap() (map[string]interface{}, error) { + values := ` +global: + name: consul + metrics: + enabled: true + enableAgentMetrics: true +connectInject: + enabled: true + metrics: + defaultEnabled: true + defaultEnableMerging: true + enableGatewayMetrics: true +server: + replicas: 1 +controller: + enabled: true +ui: + enabled: true + service: + enabled: true +prometheus: + enabled: true +` + + return config.ConvertToMap(values), nil +} diff --git a/cli/preset/preset.go b/cli/preset/preset.go new file mode 100644 index 0000000000..8825358819 --- /dev/null +++ b/cli/preset/preset.go @@ -0,0 +1,44 @@ +package preset + +import ( + "fmt" +) + +const ( + PresetSecure = "secure" + PresetQuickstart = "quickstart" + PresetCloud = "cloud" +) + +// Presets is a list of all the available presets for use with CLI's install +// and uninstall commands. +var Presets = []string{PresetCloud, PresetQuickstart, PresetSecure} + +// Preset is the interface that each instance must implement. For demo and +// secure presets, they merely return a pre-configred value map. For cloud, +// it must fetch configuration from HCP, save various secrets from the response, +// and map the secret names into the value map. +type Preset interface { + GetValueMap() (map[string]interface{}, error) +} + +type GetPresetConfig struct { + Name string + CloudPreset *CloudPreset +} + +// GetPreset is a factory function that, given a configuration, produces a +// struct that implements the Preset interface based on the name in the +// configuration. If the string is not recognized an error is returned. This +// helper function is utilized by both the cli install and upgrade commands. +func GetPreset(config *GetPresetConfig) (Preset, error) { + switch config.Name { + case PresetCloud: + return config.CloudPreset, nil + case PresetQuickstart: + return &QuickstartPreset{}, nil + case PresetSecure: + return &SecurePreset{}, nil + } + return nil, fmt.Errorf("'%s' is not a valid preset", config.Name) +} diff --git a/cli/preset/quickstart.go b/cli/preset/quickstart.go new file mode 100644 index 0000000000..52b3f000b1 --- /dev/null +++ b/cli/preset/quickstart.go @@ -0,0 +1,43 @@ +package preset + +import "github.com/hashicorp/consul-k8s/cli/config" + +// QuickstartPreset struct is an implementation of the Preset interface that provides +// a Helm values map that is used during installation and represents the +// the quickstart configuration for Consul on Kubernetes. +type QuickstartPreset struct{} + +// GetValueMap returns the Helm value map representing the quickstart +// configuration for Consul on Kubernetes. It does the following: +// - server replicas equal to 1. +// - enables the service mesh. +// - enables the ui. +// - enables metrics. +// - enables Prometheus. +func (i *QuickstartPreset) GetValueMap() (map[string]interface{}, error) { + values := ` +global: + name: consul + metrics: + enabled: true + enableAgentMetrics: true +connectInject: + enabled: true + metrics: + defaultEnabled: true + defaultEnableMerging: true + enableGatewayMetrics: true +server: + replicas: 1 +controller: + enabled: true +ui: + enabled: true + service: + enabled: true +prometheus: + enabled: true +` + + return config.ConvertToMap(values), nil +} diff --git a/cli/preset/secure.go b/cli/preset/secure.go new file mode 100644 index 0000000000..ded436804c --- /dev/null +++ b/cli/preset/secure.go @@ -0,0 +1,37 @@ +package preset + +import "github.com/hashicorp/consul-k8s/cli/config" + +// SecurePreset struct is an implementation of the Preset interface that provides +// a Helm values map that is used during installation and represents the +// the quickstart configuration for Consul on Kubernetes. +type SecurePreset struct{} + +// GetValueMap returns the Helm value map representing the quickstart +// configuration for Consul on Kubernetes. It does the following: +// - server replicas equal to 1. +// - enables the service mesh. +// - enables tls. +// - enables gossip encryption. +// - enables ACLs. +func (i *SecurePreset) GetValueMap() (map[string]interface{}, error) { + values := ` +global: + name: consul + gossipEncryption: + autoGenerate: true + tls: + enabled: true + enableAutoEncrypt: true + acls: + manageSystemACLs: true +server: + replicas: 1 +connectInject: + enabled: true +controller: + enabled: true +` + + return config.ConvertToMap(values), nil +}