diff --git a/.circleci/config.yml b/.circleci/config.yml index 401560c34..6c6eead31 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -183,6 +183,26 @@ jobs: no_output_timeout: "30m" - store_artifacts: path: /go/src/github.com/Azure/acs-engine/_logs + ginkgo-k8s-e2e: + working_directory: /go/src/github.com/Azure/acs-engine + docker: + - image: quay.io/deis/go-dev:v1.2.0 + environment: + GOPATH: /go + steps: + - checkout + - setup_remote_docker + - run: | + echo 'export CLUSTER_DEFINITION=examples/kubernetes.json' >> $BASH_ENV + echo 'export SSH_KEY_NAME=id_rsa' >> $BASH_ENV + echo 'export CLIENT_ID=${CLUSTER_SERVICE_PRINCIPAL_CLIENT_ID}' >> $BASH_ENV + echo 'export CLIENT_SECRET=${CLUSTER_SERVICE_PRINCIPAL_CLIENT_SECRET}' >> $BASH_ENV + - run: + name: ginkgo k8s e2e tests + command: make build-binary test-kubernetes + no_output_timeout: "30m" + - store_artifacts: + path: /go/src/github.com/Azure/acs-engine/_logs workflows: version: 2 build_and_test: @@ -193,9 +213,21 @@ workflows: - non-k8s-pr-e2e: requires: - pr-e2e-hold + filters: + branches: + ignore: master - pr-kubernetes-e2e: requires: - pr-e2e-hold + filters: + branches: + ignore: master + - ginkgo-k8s-e2e: + requires: + - pr-e2e-hold + filters: + branches: + ignore: master - kubernetes-e2e: requires: - test @@ -219,5 +251,4 @@ workflows: - test filters: branches: - only: master - + only: master \ No newline at end of file diff --git a/Makefile b/Makefile index 2f081b8ac..380ff6992 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,16 @@ VERSION := $(shell git rev-parse HEAD) GOFILES=`glide novendor | xargs go list` +REPO_PATH := github.com/Azure/acs-engine +DEV_ENV_IMAGE := quay.io/deis/go-dev:v1.2.0 +DEV_ENV_WORK_DIR := /go/src/${REPO_PATH} +DEV_ENV_OPTS := --rm -v ${CURDIR}:${DEV_ENV_WORK_DIR} -w ${DEV_ENV_WORK_DIR} ${DEV_ENV_VARS} +DEV_ENV_CMD := docker run ${DEV_ENV_OPTS} ${DEV_ENV_IMAGE} +DEV_ENV_CMD_IT := docker run -it ${DEV_ENV_OPTS} ${DEV_ENV_IMAGE} +DEV_CMD_RUN := docker run ${DEV_ENV_OPTS} +LDFLAGS := -s -X main.version=${VERSION} +BINARY_DEST_DIR ?= bin + all: build .PHONY: generate @@ -28,6 +38,9 @@ build: generate GOBIN=$(BINDIR) $(GO) install $(GOFLAGS) -ldflags '$(LDFLAGS)' cd test/acs-engine-test; go build +build-binary: bootstrap generate + go build -v -ldflags "${LDFLAGS}" -o ${BINARY_DEST_DIR}/acs-engine . + # usage: make clean build-cross dist VERSION=v0.4.0 .PHONY: build-cross build-cross: LDFLAGS += -extldflags "-static" @@ -60,7 +73,7 @@ ifneq ($(GIT_BASEDIR),) endif test: - ginkgo -r -ldflags='$(LDFLAGS)' . + ginkgo -skipPackage test/e2e -r . .PHONY: test-style test-style: @@ -106,9 +119,10 @@ ci: bootstrap test-style build test lint .PHONY: coverage coverage: - @scripts/coverage.sh + @scripts/ginkgo.coverage.sh devenv: ./scripts/devenv.sh include versioning.mk +include test.mk \ No newline at end of file diff --git a/glide.lock b/glide.lock index 67e59795f..eda6f8923 100644 --- a/glide.lock +++ b/glide.lock @@ -38,6 +38,8 @@ imports: version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 - name: github.com/JiangtianLi/gettext version: a8983c062be4b565d723c478922d7736e04fdba4 +- name: github.com/kelseyhightower/envconfig + version: f611eb38b3875cc3bd991ca91c51d06446afa14c - name: github.com/leonelquinteros/gotext version: a735812a72672008b3902f8b2bc1302166a9a8ea - name: github.com/Masterminds/semver diff --git a/glide.yaml b/glide.yaml index 40c9af56c..a4a1461e7 100644 --- a/glide.yaml +++ b/glide.yaml @@ -53,3 +53,6 @@ import: testImport: - package: github.com/onsi/gomega - package: github.com/onsi/ginkgo + version: v1.3.1 +- package: github.com/kelseyhightower/envconfig + version: ~1.3.0 diff --git a/scripts/ginkgo.coverage.sh b/scripts/ginkgo.coverage.sh new file mode 100755 index 000000000..2590938a1 --- /dev/null +++ b/scripts/ginkgo.coverage.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +# Copyright 2016 The Kubernetes Authors All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eo pipefail + +covermode=${COVERMODE:-atomic} +coverdir=$(mktemp -d /tmp/coverage.XXXXXXXXXX) +profile="${coverdir}/cover.out" + +hash goveralls 2>/dev/null || go get github.com/mattn/goveralls +hash godir 2>/dev/null || go get github.com/Masterminds/godir + +generate_cover_data() { + ginkgo -skipPackage test/e2e -cover -r . + find . -type f -name "*.coverprofile" | while read -r file; do mv $file ${coverdir}; done + + echo "mode: $covermode" >"$profile" + grep -h -v "^mode:" "$coverdir"/*.coverprofile >>"$profile" +} + +push_to_coveralls() { + goveralls -coverprofile="${profile}" -repotoken $COVERALLS_TOKEN +} + +generate_cover_data +go tool cover -func "${profile}" + +case "${1-}" in + --html) + go tool cover -html "${profile}" + ;; + --coveralls) + if [ -z $COVERALLS_TOKEN ]; then + echo '$COVERALLS_TOKEN not set. Skipping pushing coverage report to coveralls.io' + exit + fi + push_to_coveralls + ;; +esac \ No newline at end of file diff --git a/test.mk b/test.mk new file mode 100644 index 000000000..e66806061 --- /dev/null +++ b/test.mk @@ -0,0 +1,26 @@ +LOCATION ?= westus2 +CLUSTER_DEFINITION ?= examples/kubernetes.json +SSH_KEY_NAME ?= id_rsa + +TEST_CMD = docker run --rm \ + -v ${CURDIR}:${DEV_ENV_WORK_DIR} \ + -w ${DEV_ENV_WORK_DIR} \ + -e LOCATION=${LOCATION} \ + -e CLIENT_ID=${CLIENT_ID} \ + -e CLIENT_SECRET=${CLIENT_SECRET} \ + -e TENANT_ID=${TENANT_ID} \ + -e SUBSCRIPTION_ID=${SUBSCRIPTION_ID} \ + -e CLUSTER_DEFINITION=${CLUSTER_DEFINITION} \ + -e DNS_PREFIX=${DNS_PREFIX} \ + -e SSH_KEY_NAME=${SSH_KEY_NAME} + +test-interactive: + ${TEST_CMD} -it -e TEST=kubernetes ${DEV_ENV_IMAGE} bash + +test-functional: test-kubernetes + +test-kubernetes-with-container: + ${TEST_CMD} -e TEST=kubernetes ${DEV_ENV_IMAGE} test/e2e/runner + +test-kubernetes: + TEST=kubernetes ./test/e2e/runner \ No newline at end of file diff --git a/test/e2e/azure/cli.go b/test/e2e/azure/cli.go new file mode 100644 index 000000000..b1d839633 --- /dev/null +++ b/test/e2e/azure/cli.go @@ -0,0 +1,165 @@ +package azure + +import ( + "encoding/json" + "log" + "os/exec" + + "github.com/Azure/acs-engine/test/e2e/engine" + + "github.com/kelseyhightower/envconfig" +) + +// Account holds the values needed to talk to the Azure API +type Account struct { + User *User `json:"user"` + TenantID string `json:"tenantId" envconfig:"TENANT_ID" required:"true"` + SubscriptionID string `json:"id" envconfig:"SUBSCRIPTION_ID" required:"true"` + ResourceGroup ResourceGroup + Deployment Deployment +} + +// ResourceGroup represents a collection of azure resources +type ResourceGroup struct { + Name string + Location string +} + +// Deployment represents a deployment of an acs cluster +type Deployment struct { + Name string // Name of the deployment + TemplateDirectory string // engine.GeneratedDefinitionPath +} + +// User represents the user currently logged into an Account +type User struct { + ID string `json:"name" envconfig:"CLIENT_ID" required:"true"` + Secret string `envconfig:"CLIENT_SECRET" required:"true"` + Type string `json:"type"` +} + +// NewAccount will parse env vars and return a new struct +func NewAccount() (*Account, error) { + a := new(Account) + if err := envconfig.Process("account", a); err != nil { + return nil, err + } + u := new(User) + if err := envconfig.Process("user", u); err != nil { + return nil, err + } + a.User = u + return a, nil +} + +// Login will login to a given subscription +func (a *Account) Login() error { + cmd := exec.Command("az", "login", + "--service-principal", + "--username", a.User.ID, + "--password", a.User.Secret, + "--tenant", a.TenantID) + err := cmd.Start() + if err != nil { + log.Printf("Error while trying to start login:%s\n", err) + return err + } + err = cmd.Wait() + if err != nil { + log.Printf("Error occurred while waiting for login to complete:%s\n", err) + return err + } + return nil +} + +// SetSubscription will call az account set --subscription for the given Account +func (a *Account) SetSubscription() error { + cmd := exec.Command("az", "account", "set", "--subscription", a.SubscriptionID) + err := cmd.Start() + if err != nil { + log.Printf("Error while trying to start account set for subscription %s:%s\n", a.SubscriptionID, err) + return err + } + err = cmd.Wait() + if err != nil { + log.Printf("Error occurred while waiting for account set for subscription %s to complete:%s\n", a.SubscriptionID, err) + return err + } + return nil +} + +// CreateGroup will create a resource group in a given location +func (a *Account) CreateGroup(name, location string) error { + cmd := exec.Command("az", "group", "create", "--name", name, "--location", location) + err := cmd.Start() + if err != nil { + log.Printf("Error while trying to start command to create resource group (%s) in %s:%s", name, location, err) + return err + } + err = cmd.Wait() + if err != nil { + log.Printf("Error occurred while waiting for resource group (%s) in %s:%s", name, location, err) + return err + } + r := ResourceGroup{ + Name: name, + Location: location, + } + a.ResourceGroup = r + return nil +} + +// DeleteGroup delets a given resource group by name +func (a *Account) DeleteGroup() error { + cmd := exec.Command("az", "group", "delete", "--name", a.Deployment.Name, "--no-wait", "--yes") + err := cmd.Start() + + if err != nil { + log.Printf("Error while trying to start command to delete resource group (%s):%s", a.Deployment.Name, err) + return err + } + err = cmd.Wait() + if err != nil { + log.Printf("Error occurred while waiting for resource group (%s):%s", a.Deployment.Name, err) + return err + } + return nil +} + +// CreateDeployment will deploy a cluster to a given resource group using the template and parameters on disk +func (a *Account) CreateDeployment(name string, e *engine.Engine) error { + d := Deployment{ + Name: name, + TemplateDirectory: e.GeneratedDefinitionPath, + } + cmd := exec.Command("az", "group", "deployment", "create", + "--name", d.Name, + "--resource-group", a.ResourceGroup.Name, + "--template-file", e.GeneratedTemplatePath, + "--parameters", e.GeneratedParametersPath) + + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("Error while trying to start deployment for %s in resource group %s:%s", d.Name, a.ResourceGroup.Name, err) + log.Printf("Command Output: %s\n", output) + return err + } + + a.Deployment = d + return nil +} + +// GetCurrentAccount will run an az account show and parse that into an account strcut +func GetCurrentAccount() (*Account, error) { + out, err := exec.Command("az", "account", "show").CombinedOutput() + if err != nil { + log.Printf("Error trying to run 'account show':%s\n", err) + return nil, err + } + a := Account{} + err = json.Unmarshal(out, &a) + if err != nil { + log.Printf("Error unmarshalling account json:%s\n", err) + } + return &a, nil +} diff --git a/test/e2e/config/config.go b/test/e2e/config/config.go new file mode 100644 index 000000000..5bde5ca7b --- /dev/null +++ b/test/e2e/config/config.go @@ -0,0 +1,57 @@ +package config + +import ( + "fmt" + "log" + "math/rand" + "os" + "path/filepath" + "time" + + "github.com/kelseyhightower/envconfig" +) + +// Config holds global test configuration +type Config struct { + Name string `envconfig:"NAME"` // Name allows you to set the name of a cluster already created + Location string `envconfig:"LOCATION" required:"true"` // Location where you want to create the cluster + ClusterDefinition string `envconfig:"CLUSTER_DEFINITION" required:"true"` // ClusterDefinition is the path on disk to the json template these are normally located in examples/ + CleanUpOnExit bool `envconfig:"CLEANUP_ON_EXIT" default:"true"` // if set the tests will not clean up rgs when tests finish + SSHKeyName string `envconfig:"SSH_KEY_NAME"` // not absolute path +} + +// ParseConfig will parse needed environment variables for running the tests +func ParseConfig() (*Config, error) { + c := new(Config) + if err := envconfig.Process("config", c); err != nil { + return nil, err + } + return c, nil +} + +// GenerateName will generate a new name if one has not been set +func (c *Config) GenerateName() string { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + suffix := r.Intn(99999) + prefix := fmt.Sprintf("k8s-%s", c.Location) + return fmt.Sprintf("%s-%v", prefix, suffix) +} + +// GetKubeConfig returns the absolute path to the kubeconfig for c.Location +func (c *Config) GetKubeConfig() string { + cwd, _ := os.Getwd() + file := fmt.Sprintf("kubeconfig.%s.json", c.Location) + kubeconfig := filepath.Join(cwd, "../../../_output", c.Name, "kubeconfig", file) + return kubeconfig +} + +// GetSSHKeyPath will return the absolute path to the ssh private key +func (c *Config) GetSSHKeyPath() (string, error) { + cwd, err := os.Getwd() + if err != nil { + log.Printf("Error while trying to get the current working directory: %s\n", err) + return "", err + } + sshKeyPath := filepath.Join(cwd, "../../../_output", c.SSHKeyName) + return sshKeyPath, nil +} diff --git a/test/e2e/engine/cli.go b/test/e2e/engine/cli.go new file mode 100644 index 000000000..f067c6ad1 --- /dev/null +++ b/test/e2e/engine/cli.go @@ -0,0 +1,22 @@ +package engine + +import ( + "log" + "os/exec" +) + +// Generate will run acs-engine generate on a given cluster definition +func (e *Engine) Generate() error { + cmd := exec.Command("acs-engine", "generate", e.ClusterDefinitionTemplate, "--output-directory", e.GeneratedDefinitionPath) + err := cmd.Start() + if err != nil { + log.Printf("Error while trying to start generate:%s\n", err) + return err + } + err = cmd.Wait() + if err != nil { + log.Printf("Error while trying to generate acs-engine template with cluster definition - %s: %s", e.GeneratedDefinitionPath, err) + return err + } + return nil +} diff --git a/test/e2e/engine/template.go b/test/e2e/engine/template.go new file mode 100644 index 000000000..2ba909918 --- /dev/null +++ b/test/e2e/engine/template.go @@ -0,0 +1,126 @@ +package engine + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + + "github.com/Azure/acs-engine/pkg/api" + "github.com/kelseyhightower/envconfig" +) + +// Config represents the configuration values of a template stored as env vars +type Config struct { + ClientID string `envconfig:"CLIENT_ID"` + ClientSecret string `envconfig:"CLIENT_SECRET"` + MasterDNSPrefix string `envconfig:"DNS_PREFIX"` + PublicSSHKey string `envconfig:"PUBLIC_SSH_KEY"` + WindowsAdminPasssword string `envconfig:"WINDOWS_ADMIN_PASSWORD"` +} + +// Engine holds necessary information to interact with acs-engine cli +type Engine struct { + Config *Config + ClusterDefinitionPath string // The original template we want to use to build the cluster from. + ClusterDefinitionTemplate string // This is the template after we splice in the environment variables + GeneratedDefinitionPath string // Holds the contents of running acs-engine generate + OutputPath string // This is the root output path + DefinitionName string // Unique cluster name + GeneratedTemplatePath string // azuredeploy.json path + GeneratedParametersPath string // azuredeploy.parameters.json path +} + +// ParseConfig will return a new engine config struct taking values from env vars +func ParseConfig() (*Config, error) { + c := new(Config) + if err := envconfig.Process("config", c); err != nil { + return nil, err + } + return c, nil +} + +// Build takes a template path and will inject values based on provided environment variables +// it will then serialize the structs back into json and save it to outputPath +func Build(templatePath, outputPath, definitionName string) (*Engine, error) { + config, err := ParseConfig() + if err != nil { + log.Printf("Error while trying to build Engine Configuration:%s\n", err) + } + + cwd, err := os.Getwd() + if err != nil { + log.Printf("Error while trying to get the current working directory: %s\n", err) + return nil, err + } + + clusterDefinitionTemplate := fmt.Sprintf("%s/%s.json", outputPath, definitionName) + generatedDefinitionPath := fmt.Sprintf("%s/%s", outputPath, definitionName) + engine := Engine{ + Config: config, + DefinitionName: definitionName, + ClusterDefinitionPath: filepath.Join(cwd, "../../..", templatePath), + ClusterDefinitionTemplate: filepath.Join(cwd, "../../..", clusterDefinitionTemplate), + OutputPath: filepath.Join(cwd, "../../..", outputPath), + GeneratedDefinitionPath: filepath.Join(cwd, "../../..", generatedDefinitionPath), + GeneratedTemplatePath: filepath.Join(cwd, "../../..", generatedDefinitionPath, "azuredeploy.json"), + GeneratedParametersPath: filepath.Join(cwd, "../../..", generatedDefinitionPath, "azuredeploy.parameters.json"), + } + + cs, err := engine.parse() + if err != nil { + return nil, err + } + + if config.ClientID != "" && config.ClientSecret != "" { + cs.ContainerService.Properties.ServicePrincipalProfile.ClientID = config.ClientID + cs.ContainerService.Properties.ServicePrincipalProfile.Secret = config.ClientSecret + } + + if config.MasterDNSPrefix != "" { + cs.ContainerService.Properties.MasterProfile.DNSPrefix = config.MasterDNSPrefix + } + + if config.PublicSSHKey != "" { + cs.ContainerService.Properties.LinuxProfile.SSH.PublicKeys[0].KeyData = config.PublicSSHKey + } + + if config.WindowsAdminPasssword != "" { + cs.ContainerService.Properties.WindowsProfile.AdminPassword = config.WindowsAdminPasssword + } + err = engine.write(cs) + if err != nil { + return nil, err + } + return &engine, nil +} + +// Parse takes a template path and will parse that into a api.VlabsARMContainerService +func (e *Engine) parse() (*api.VlabsARMContainerService, error) { + contents, err := ioutil.ReadFile(e.ClusterDefinitionPath) + if err != nil { + log.Printf("Error while trying to read cluster definition at (%s):%s\n", e.ClusterDefinitionPath, err) + return nil, err + } + cs := api.VlabsARMContainerService{} + if err = json.Unmarshal(contents, &cs); err != nil { + log.Printf("Error while trying to unmarshal container service json:%s\n%s\n", err, string(contents)) + return nil, err + } + return &cs, nil +} + +func (e *Engine) write(cs *api.VlabsARMContainerService) error { + json, err := json.Marshal(cs) + if err != nil { + log.Printf("Error while trying to serialize Container Service object to json:%s\n%+v\n", err, cs) + return err + } + err = ioutil.WriteFile(e.ClusterDefinitionTemplate, json, 0777) + if err != nil { + log.Printf("Error while trying to write container service definition to file (%s):%s\n%s\n", e.ClusterDefinitionTemplate, err, string(json)) + } + return nil +} diff --git a/test/e2e/kubernetes/config.go b/test/e2e/kubernetes/config.go new file mode 100644 index 000000000..044103fe0 --- /dev/null +++ b/test/e2e/kubernetes/config.go @@ -0,0 +1,45 @@ +package kubernetes + +import ( + "encoding/json" + "log" + "os/exec" + "strings" +) + +// Config represents a kubernetes config object +type Config struct { + Clusters []Cluster `json:"clusters"` +} + +// Cluster contains the name and the cluster info +type Cluster struct { + Name string `json:"name"` + ClusterInfo ClusterInfo `json:"cluster"` +} + +// ClusterInfo holds the server and cert +type ClusterInfo struct { + Server string `json:"server"` +} + +// GetConfig returns a Config value representing the current kubeconfig +func GetConfig() (*Config, error) { + out, err := exec.Command("kubectl", "config", "view", "-o", "json").CombinedOutput() + if err != nil { + log.Printf("Error trying to run 'kubectl config view':%s\n", err) + return nil, err + } + c := Config{} + err = json.Unmarshal(out, &c) + if err != nil { + log.Printf("Error unmarshalling config json:%s\n", err) + } + return &c, nil +} + +// GetServerName returns the server for the given config in an sshable format +func (c *Config) GetServerName() string { + s := c.Clusters[0].ClusterInfo.Server + return strings.Split(s, "://")[1] +} diff --git a/test/e2e/kubernetes/deployment/deployment.go b/test/e2e/kubernetes/deployment/deployment.go new file mode 100644 index 000000000..0a7cc82bd --- /dev/null +++ b/test/e2e/kubernetes/deployment/deployment.go @@ -0,0 +1,105 @@ +package deployment + +import ( + "encoding/json" + "fmt" + "log" + "os/exec" + "strconv" + "time" +) + +// List holds a list of deployments returned from kubectl get deploy +type List struct { + Deployments []Deployment `json:"items"` +} + +// Deployment repesentes a kubernetes deployment +type Deployment struct { + Metadata Metadata `json:"metadata"` +} + +// Metadata holds information like labels, name, and namespace +type Metadata struct { + CreatedAt time.Time `json:"creationTimestamp"` + Labels map[string]string `json:"labels"` + Name string `json:"name"` + Namespace string `json:"namespace"` +} + +// Spec holds information the deployment strategy and number of replicas +type Spec struct { + Replicas int `json:"replicas"` + Template Template `json:"template"` +} + +// Template is used for fetching the deployment spec -> containers +type Template struct { + TemplateSpec TemplateSpec `json:"spec"` +} + +// TemplateSpec holds the list of containers for a deployment, the dns policy, and restart policy +type TemplateSpec struct { + Containers []Container `json:"containers"` + DNSPolicy string `json:"dnsPolicy"` + RestartPolicy string `json:"restartPolicy"` +} + +// Container holds information like image, pull policy, name, etc... +type Container struct { + Image string `json:"image"` + PullPolicy string `json:"imagePullPolicy"` + Name string `json:"name"` +} + +// Create will create a deployment for a given image with a name in a namespace +func Create(image, name, namespace string) (*Deployment, error) { + out, err := exec.Command("kubectl", "run", "-n", namespace, "--image", "library/nginx:latest", name).CombinedOutput() + if err != nil { + log.Printf("Error trying to deploy %s [%s] in namespace %s:%s\n", name, image, namespace, string(out)) + return nil, err + } + d, err := Get(name, namespace) + if err != nil { + log.Printf("Error while trying to fetch Deployment %s in namespace %s:%s\n", name, namespace, err) + return nil, err + } + return d, nil +} + +// Get returns a deployment from a name and namespace +func Get(name, namespace string) (*Deployment, error) { + out, err := exec.Command("kubectl", "get", "deploy", "-o", "json", "-n", namespace, name).CombinedOutput() + if err != nil { + log.Printf("Error while trying to fetch deployment %s in namespace %s:%s\n", name, namespace, string(out)) + return nil, err + } + d := Deployment{} + err = json.Unmarshal(out, &d) + if err != nil { + log.Printf("Error while trying to unmarshal deployment json:%s\n%s\n", err, string(out)) + return nil, err + } + return &d, nil +} + +// Delete will delete a deployment in a given namespace +func (d *Deployment) Delete() error { + out, err := exec.Command("kubectl", "delete", "deploy", "-n", d.Metadata.Namespace, d.Metadata.Name).CombinedOutput() + if err != nil { + log.Printf("Error while trying to delete deployment %s in namespace %s:%s\n", d.Metadata.Namespace, d.Metadata.Name, string(out)) + return err + } + return nil +} + +// Expose will create a load balancer and expose the deployment on a given port +func (d *Deployment) Expose(port int) error { + ref := fmt.Sprintf("deployments/%s", d.Metadata.Name) + out, err := exec.Command("kubectl", "expose", ref, "--type", "LoadBalancer", "-n", d.Metadata.Namespace, "--port", strconv.Itoa(port)).CombinedOutput() + if err != nil { + log.Printf("Error while trying to expose deployment %s in namespace %s on port %v:%s\n", d.Metadata.Name, d.Metadata.Namespace, port, string(out)) + return err + } + return nil +} diff --git a/test/e2e/kubernetes/kubernetes_suite_test.go b/test/e2e/kubernetes/kubernetes_suite_test.go new file mode 100644 index 000000000..b7670216e --- /dev/null +++ b/test/e2e/kubernetes/kubernetes_suite_test.go @@ -0,0 +1,13 @@ +package kubernetes_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestKubernetes(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Kubernetes Suite") +} diff --git a/test/e2e/kubernetes/kubernetes_test.go b/test/e2e/kubernetes/kubernetes_test.go new file mode 100644 index 000000000..53a6e868e --- /dev/null +++ b/test/e2e/kubernetes/kubernetes_test.go @@ -0,0 +1,173 @@ +package kubernetes + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "time" + + "github.com/Azure/acs-engine/test/e2e/azure" + "github.com/Azure/acs-engine/test/e2e/config" + "github.com/Azure/acs-engine/test/e2e/engine" + "github.com/Azure/acs-engine/test/e2e/kubernetes/deployment" + "github.com/Azure/acs-engine/test/e2e/kubernetes/node" + "github.com/Azure/acs-engine/test/e2e/kubernetes/pod" + "github.com/Azure/acs-engine/test/e2e/kubernetes/service" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var ( + cfg config.Config + err error + acct azure.Account +) + +var _ = BeforeSuite(func() { + c, err := config.ParseConfig() + Expect(err).NotTo(HaveOccurred()) + cfg = *c // We have to do this because golang anon functions and scoping and stuff + + a, err := azure.NewAccount() + Expect(err).NotTo(HaveOccurred()) + acct = *a // We have to do this because golang anon functions and scoping and stuff + + acct.Login() + acct.SetSubscription() + + if cfg.Name == "" { + cfg.Name = cfg.GenerateName() + log.Printf("Cluster name:%s\n", cfg.Name) + // Lets modify our template and call acs-engine generate on it + e, err := engine.Build(cfg.ClusterDefinition, "_output", cfg.Name) + Expect(err).NotTo(HaveOccurred()) + + err = e.Generate() + Expect(err).NotTo(HaveOccurred()) + + err = acct.CreateGroup(cfg.Name, cfg.Location) + Expect(err).NotTo(HaveOccurred()) + + // Lets start by just using the normal az group deployment cli for creating a cluster + log.Println("Creating deployment this make take a few minutes...") + err = acct.CreateDeployment(cfg.Name, e) + Expect(err).NotTo(HaveOccurred()) + } + + err = os.Setenv("KUBECONFIG", cfg.GetKubeConfig()) + Expect(err).NotTo(HaveOccurred()) + + log.Println("Waiting on nodes to go into ready state...") + ready := node.WaitOnReady(10*time.Second, 10*time.Minute) + Expect(ready).To(BeTrue()) +}) + +var _ = AfterSuite(func() { + if cfg.CleanUpOnExit { + log.Printf("Deleting Group:%s\n", cfg.Name) + acct.DeleteGroup() + } + + if _, err := os.Stat(cfg.GetKubeConfig()); os.IsExist(err) { + if svc, _ := service.Get(cfg.Name, "default"); svc != nil { + svc.Delete() + } + + if d, _ := deployment.Get(cfg.Name, "default"); d != nil { + d.Delete() + } + } +}) + +var _ = Describe("Azure Container Cluster using the Kubernetes Orchestrator", func() { + + It("should be logged into the correct account", func() { + current, err := azure.GetCurrentAccount() + + Expect(err).NotTo(HaveOccurred()) + Expect(current.User.ID).To(Equal(acct.User.ID)) + Expect(current.TenantID).To(Equal(acct.TenantID)) + Expect(current.SubscriptionID).To(Equal(acct.SubscriptionID)) + }) + + It("should have have the appropriate node count", func() { + nodeList, err := node.Get() + Expect(err).NotTo(HaveOccurred()) + Expect(len(nodeList.Nodes)).To(Equal(4)) + }) + + It("should be running the expected default version", func() { + version, err := node.Version() + Expect(err).NotTo(HaveOccurred()) + Expect(version).To(Equal("v1.6.6")) + }) + + It("should have kube-dns running", func() { + pod.WaitOnReady("kube-dns", "kube-system", 5*time.Second, 3*time.Minute) + running, err := pod.AreAllPodsRunning("kube-dns", "kube-system") + Expect(err).NotTo(HaveOccurred()) + Expect(running).To(Equal(true)) + }) + + It("should have kube-dashboard running", func() { + pod.WaitOnReady("kubernetes-dashboard", "kube-system", 5*time.Second, 3*time.Minute) + running, err := pod.AreAllPodsRunning("kubernetes-dashboard", "kube-system") + Expect(err).NotTo(HaveOccurred()) + Expect(running).To(Equal(true)) + }) + + It("should have kube-proxy running", func() { + pod.WaitOnReady("kube-proxy", "kube-system", 5*time.Second, 3*time.Minute) + running, err := pod.AreAllPodsRunning("kube-proxy", "kube-system") + Expect(err).NotTo(HaveOccurred()) + Expect(running).To(Equal(true)) + }) + + It("should be able to access the dashboard from each node", func() { + kubeConfig, err := GetConfig() + Expect(err).NotTo(HaveOccurred()) + + sshKeyPath, err := cfg.GetSSHKeyPath() + Expect(err).NotTo(HaveOccurred()) + + s, err := service.Get("kubernetes-dashboard", "kube-system") + Expect(err).NotTo(HaveOccurred()) + port := s.GetNodePort(80) + + master := fmt.Sprintf("azureuser@%s", kubeConfig.GetServerName()) + nodeList, err := node.Get() + Expect(err).NotTo(HaveOccurred()) + + for _, node := range nodeList.Nodes { + dashboardURL := fmt.Sprintf("http://%s:%v", node.Status.GetAddressByType("InternalIP").Address, port) + curlCMD := fmt.Sprintf("curl --max-time 60 %s", dashboardURL) + _, err := exec.Command("ssh", "-i", sshKeyPath, "-o", "ConnectTimeout=10", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", master, curlCMD).CombinedOutput() + Expect(err).NotTo(HaveOccurred()) + } + }) + + It("should be able to deploy an nginx service", func() { + d, err := deployment.Create("library/nginx:latest", cfg.Name, "default") + Expect(err).NotTo(HaveOccurred()) + err = d.Expose(80) + Expect(err).NotTo(HaveOccurred()) + + s, err := service.Get(cfg.Name, "default") + Expect(err).NotTo(HaveOccurred()) + s, err = s.WaitForExternalIP(360, 5) + Expect(err).NotTo(HaveOccurred()) + Expect(s.Status.LoadBalancer.Ingress).NotTo(BeEmpty()) + + url := fmt.Sprintf("http://%s", s.Status.LoadBalancer.Ingress[0]["ip"]) + resp, err := http.Get(url) + Expect(err).NotTo(HaveOccurred()) + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + Expect(err).NotTo(HaveOccurred()) + Expect(string(body)).To(MatchRegexp("(Welcome to nginx)")) + }) + +}) diff --git a/test/e2e/kubernetes/namespace/namespace.go b/test/e2e/kubernetes/namespace/namespace.go new file mode 100644 index 000000000..eb0fd0525 --- /dev/null +++ b/test/e2e/kubernetes/namespace/namespace.go @@ -0,0 +1,54 @@ +package namespace + +import ( + "encoding/json" + "log" + "os/exec" + "time" +) + +// Namespace holds namespace metadata +type Namespace struct { + Metadata Metadata `json:"metadata"` +} + +// Metadata holds information like name and created timestamp +type Metadata struct { + CreatedAt time.Time `json:"creationTimestamp"` + Name string `json:"name"` +} + +// Create a namespace with the given name +func Create(name string) (*Namespace, error) { + out, err := exec.Command("kubectl", "create", "namespace", name).CombinedOutput() + if err != nil { + log.Printf("Error trying to create namespace (%s):%s\n", name, string(out)) + return nil, err + } + return Get(name) +} + +// Get returns a namespace for with a given name +func Get(name string) (*Namespace, error) { + out, err := exec.Command("kubectl", "get", "namespace", name, "-o", "json").CombinedOutput() + if err != nil { + log.Printf("Error trying to get namespace (%s):%s\n", name, string(out)) + return nil, err + } + n := Namespace{} + err = json.Unmarshal(out, &n) + if err != nil { + log.Printf("Error unmarshalling namespace json:%s\n", err) + } + return &n, nil +} + +// Delete a namespace +func (n *Namespace) Delete() error { + out, err := exec.Command("kubectl", "delete", "namespace", n.Metadata.Name).CombinedOutput() + if err != nil { + log.Printf("Error while trying to delete namespace (%s):%s\n", n.Metadata.Name, out) + return err + } + return nil +} diff --git a/test/e2e/kubernetes/node/node.go b/test/e2e/kubernetes/node/node.go new file mode 100644 index 000000000..8fe9de5e8 --- /dev/null +++ b/test/e2e/kubernetes/node/node.go @@ -0,0 +1,153 @@ +package node + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os/exec" + "regexp" + "strings" + "time" +) + +const ( + //ServerVersion is used to parse out the version of the API running + ServerVersion = `(Server Version:\s)+(v\d.\d.\d)+` +) + +// Node represents the kubernetes Node Resource +type Node struct { + Status Status `json:"status"` + Metadata Metadata `json:"metadata"` +} + +// Metadata contains things like name and created at +type Metadata struct { + Name string `json:"name"` + CreatedAt time.Time `json:"creationTimestamp"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` +} + +// Status parses information from the status key +type Status struct { + Info Info `json:"Info"` + NodeAddresses []Address `json:"addresses"` + Conditions []Condition `json:"conditions"` +} + +// Address contains an address and a type +type Address struct { + Address string `json:"address"` + Type string `json:"type"` +} + +// Info contains information like what version the kubelet is running +type Info struct { + ContainerRuntimeVersion string `json:"containerRuntimeVersion"` + KubeProxyVersion string `json:"kubeProxyVersion"` + KubeletProxyVersion string `json:"kubeletVersion"` + OperatingSystem string `json:"operatingSystem"` +} + +// Condition contains various status information +type Condition struct { + LastHeartbeatTime time.Time `json:"lastHeartbeatTime"` + LastTransitionTime time.Time `json:"lastTransitionTime"` + Message string `json:"message"` + Reason string `json:"reason"` + Status string `json:"status"` + Type string `json:"type"` +} + +// List is used to parse out Nodes from a list +type List struct { + Nodes []Node `json:"items"` +} + +// AreAllReady returns a bool depending on cluster state +func AreAllReady() bool { + list, _ := Get() + if list != nil { + for _, node := range list.Nodes { + for _, condition := range node.Status.Conditions { + if condition.Type == "KubeletReady" && condition.Status == "false" { + return false + } + } + } + return true + } + return false +} + +// WaitOnReady will block until all nodes are in ready state +func WaitOnReady(sleep, duration time.Duration) bool { + readyCh := make(chan bool, 1) + errCh := make(chan error) + ctx, cancel := context.WithTimeout(context.Background(), duration) + defer cancel() + go func() { + for { + select { + case <-ctx.Done(): + errCh <- fmt.Errorf("Timeout exceeded (%s) while waiting for Nodes to become ready", duration.String()) + default: + if AreAllReady() == true { + readyCh <- true + } + time.Sleep(sleep) + } + } + }() + for { + select { + case <-errCh: + return false + case ready := <-readyCh: + return ready + } + } +} + +// Get returns the current nodes for a given kubeconfig +func Get() (*List, error) { + out, err := exec.Command("kubectl", "get", "nodes", "-o", "json").CombinedOutput() + if err != nil { + log.Printf("Error trying to run 'kubectl get nodes':%s", string(out)) + return nil, err + } + nl := List{} + err = json.Unmarshal(out, &nl) + if err != nil { + log.Printf("Error unmarshalling nodes json:%s", err) + } + return &nl, nil +} + +// Version get the version of the server +func Version() (string, error) { + out, err := exec.Command("kubectl", "version", "--short").CombinedOutput() + if err != nil { + log.Printf("Error trying to run 'kubectl version':%s", string(out)) + return "", err + } + split := strings.Split(string(out), "\n") + exp, err := regexp.Compile(ServerVersion) + if err != nil { + log.Printf("Error while compiling regexp:%s", ServerVersion) + } + s := exp.FindStringSubmatch(split[1]) + return s[2], nil +} + +// GetAddressByType will return the Address object for a given Kubernetes node +func (ns *Status) GetAddressByType(t string) *Address { + for _, a := range ns.NodeAddresses { + if a.Type == t { + return &a + } + } + return nil +} diff --git a/test/e2e/kubernetes/pod/pod.go b/test/e2e/kubernetes/pod/pod.go new file mode 100644 index 000000000..c9fa70a4a --- /dev/null +++ b/test/e2e/kubernetes/pod/pod.go @@ -0,0 +1,128 @@ +package pod + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os/exec" + "regexp" + "time" +) + +// List is a container that holds all pods returned from doing a kubectl get pods +type List struct { + Pods []Pod `json:"items"` +} + +// Pod is used to parse data from kubectl get pods +type Pod struct { + Metadata Metadata `json:"metadata"` + Status Status `json:"status"` +} + +// Metadata holds information like name, createdat, labels, and namespace +type Metadata struct { + CreatedAt time.Time `json:"creationTimestamp"` + Labels map[string]string `json:"labels"` + Name string `json:"name"` + Namespace string `json:"namespace"` +} + +// Status holds information like hostIP and phase +type Status struct { + HostIP string `json:"hostIP"` + Phase string `json:"phase"` + PodIP string `json:"podIP"` + StartTime time.Time `json:"startTime"` +} + +// GetAll will return all pods in a given namespace +func GetAll(namespace string) (*List, error) { + out, err := exec.Command("kubectl", "get", "pods", "-n", namespace, "-o", "json").CombinedOutput() + if err != nil { + log.Printf("Error trying to run 'kubectl get pods':%s\n", string(out)) + return nil, err + } + pl := List{} + err = json.Unmarshal(out, &pl) + if err != nil { + log.Printf("Error unmarshalling nodes json:%s\n", err) + return nil, err + } + return &pl, nil +} + +// Get will return a pod with a given name and namespace +func Get(podName, namespace string) (*Pod, error) { + out, err := exec.Command("kubectl", "get", "pods", podName, "-n", namespace, "-o", "json").CombinedOutput() + if err != nil { + log.Printf("Error trying to run 'kubectl get pods':%s\n", string(out)) + return nil, err + } + p := Pod{} + err = json.Unmarshal(out, &p) + if err != nil { + log.Printf("Error unmarshalling nodes json:%s\n", err) + return nil, err + } + return &p, nil +} + +// AreAllPodsRunning will return true if all pods are in a Running State +func AreAllPodsRunning(podPrefix, namespace string) (bool, error) { + status := true + pl, err := GetAll(namespace) + if err != nil { + log.Printf("Error while trying to check if all pods are in running state:%s", err) + return false, err + } + + for _, pod := range pl.Pods { + matched, err := regexp.MatchString(podPrefix+"-.*", pod.Metadata.Name) + if err != nil { + log.Printf("Error trying to match pod name:%s\n", err) + return false, err + } + if matched { + if pod.Status.Phase != "Running" { + status = false + } + } + } + + return status, nil +} + +// WaitOnReady will block until all nodes are in ready state +func WaitOnReady(podPrefix, namespace string, sleep, duration time.Duration) bool { + readyCh := make(chan bool, 1) + errCh := make(chan error) + ctx, cancel := context.WithTimeout(context.Background(), duration) + defer cancel() + go func() { + for { + select { + case <-ctx.Done(): + errCh <- fmt.Errorf("Timeout exceeded (%s) while waiting for Pods (%s) to become ready in namespace (%s)", duration.String(), podPrefix, namespace) + default: + ready, err := AreAllPodsRunning(podPrefix, namespace) + if err != nil { + log.Printf("Error while waiting on pods to become ready:%s", err) + } + if ready == true { + readyCh <- true + } + time.Sleep(sleep) + } + } + }() + for { + select { + case <-errCh: + return false + case ready := <-readyCh: + return ready + } + } +} diff --git a/test/e2e/kubernetes/service/service.go b/test/e2e/kubernetes/service/service.go new file mode 100644 index 000000000..84b56c2c9 --- /dev/null +++ b/test/e2e/kubernetes/service/service.go @@ -0,0 +1,121 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os/exec" + "time" +) + +// Service represents a kubernetes service +type Service struct { + Metadata Metadata `json:"metadata"` + Spec Spec `json:"spec"` + Status Status `json:"status"` +} + +// Metadata holds information like name, namespace, and labels +type Metadata struct { + CreatedAt time.Time `json:"creationTimestamp"` + Labels map[string]string `json:"labels"` + Name string `json:"name"` + Namespace string `json:"namespace"` +} + +// Spec holds information like clusterIP and port +type Spec struct { + ClusterIP string `json:"clusterIP"` + Ports []Port `json:"ports"` + Type string `json:"type"` +} + +// Port represents a service port definition +type Port struct { + NodePort int `json:"nodePort"` + Port int `json:"port"` + Protocol string `json:"protocol"` + TargetPort int `json:"targetPort"` +} + +// Status holds the load balancer definition +type Status struct { + LoadBalancer LoadBalancer `json:"loadBalancer"` +} + +// LoadBalancer holds the ingress definitions +type LoadBalancer struct { + Ingress []map[string]string `json:"ingress"` +} + +// Get returns the service definition specified in a given namespace +func Get(name, namespace string) (*Service, error) { + out, err := exec.Command("kubectl", "get", "svc", "-o", "json", "-n", namespace, name).CombinedOutput() + if err != nil { + log.Printf("Error trying to run 'kubectl get svc':%s\n", string(out)) + return nil, err + } + s := Service{} + err = json.Unmarshal(out, &s) + if err != nil { + log.Printf("Error unmarshalling service json:%s\n", err) + return nil, err + } + return &s, nil +} + +// Delete will delete a service in a given namespace +func (s *Service) Delete() error { + out, err := exec.Command("kubectl", "delete", "svc", "-n", s.Metadata.Namespace, s.Metadata.Name).CombinedOutput() + if err != nil { + log.Printf("Error while trying to delete service %s in namespace %s:%s\n", s.Metadata.Namespace, s.Metadata.Name, string(out)) + return err + } + return nil +} + +// GetNodePort will return the node port for a given pod +func (s *Service) GetNodePort(port int) int { + for _, p := range s.Spec.Ports { + if p.Port == port { + return p.NodePort + } + } + return 0 +} + +// WaitForExternalIP waits for an external ip to be provisioned +func (s *Service) WaitForExternalIP(wait, sleep int) (*Service, error) { + svcCh := make(chan *Service) + errCh := make(chan error) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(wait)) + defer cancel() + go func() { + var svc *Service + var err error + for { + select { + case <-ctx.Done(): + errCh <- fmt.Errorf("Timeout exceeded while waiting for External IP to be provisioned") + default: + svc, err = Get(s.Metadata.Name, s.Metadata.Namespace) + if err != nil { + errCh <- err + } + if svc.Status.LoadBalancer.Ingress != nil { + svcCh <- svc + } + time.Sleep(time.Second * time.Duration(sleep)) + } + } + }() + for { + select { + case err := <-errCh: + return nil, err + case svc := <-svcCh: + return svc, nil + } + } +} diff --git a/test/e2e/runner b/test/e2e/runner new file mode 100755 index 000000000..7dff97cb0 --- /dev/null +++ b/test/e2e/runner @@ -0,0 +1,39 @@ +#! /bin/bash +export PATH=${PATH}:${GOPATH}/src/github.com/Azure/acs-engine/bin + +if [ -z ${CLIENT_ID} ]; then + echo "Client ID is required to run tests!" + exit 1 +fi + +if [ -z ${CLIENT_SECRET} ]; then + echo "Client Secret is required to run tests!" + exit 1 +fi + +if [ -z ${TENANT_ID} ]; then + echo "Tenant ID is required to run tests!" + exit 1 +fi + +if [ -z ${SUBSCRIPTION_ID} ]; then + echo "Subscription ID is required to run tests!" + exit 1 +fi + +if [ -z "${NAME}" ]; then + echo "Removing _output dir" + rm -rf _output || true + + echo "Creating _output dir" + mkdir -p _output + + echo "Generating new SSH Keys" + ssh-keygen -f _output/${SSH_KEY_NAME} -b 2048 -t rsa -q -N '' + chmod 0600 _output/${SSH_KEY_NAME}* +fi + +export PUBLIC_SSH_KEY="$(cat _output/${SSH_KEY_NAME}.pub)" +export DNS_PREFIX=test-$(echo $RANDOM) + +ginkgo -slowSpecThreshold 60 -r test/e2e/${TEST} \ No newline at end of file diff --git a/vendor/github.com/kelseyhightower/envconfig/.travis.yml b/vendor/github.com/kelseyhightower/envconfig/.travis.yml new file mode 100644 index 000000000..e15301a59 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/.travis.yml @@ -0,0 +1,7 @@ +language: go + +go: + - 1.4 + - 1.5 + - 1.6 + - tip diff --git a/vendor/github.com/kelseyhightower/envconfig/LICENSE b/vendor/github.com/kelseyhightower/envconfig/LICENSE new file mode 100644 index 000000000..4bfa7a84d --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013 Kelsey Hightower + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/kelseyhightower/envconfig/MAINTAINERS b/vendor/github.com/kelseyhightower/envconfig/MAINTAINERS new file mode 100644 index 000000000..6527a9f2c --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/MAINTAINERS @@ -0,0 +1,2 @@ +Kelsey Hightower kelsey.hightower@gmail.com github.com/kelseyhightower +Travis Parker travis.parker@gmail.com github.com/teepark diff --git a/vendor/github.com/kelseyhightower/envconfig/README.md b/vendor/github.com/kelseyhightower/envconfig/README.md new file mode 100644 index 000000000..b6c65a8a3 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/README.md @@ -0,0 +1,188 @@ +# envconfig + +[![Build Status](https://travis-ci.org/kelseyhightower/envconfig.png)](https://travis-ci.org/kelseyhightower/envconfig) + +```Go +import "github.com/kelseyhightower/envconfig" +``` + +## Documentation + +See [godoc](http://godoc.org/github.com/kelseyhightower/envconfig) + +## Usage + +Set some environment variables: + +```Bash +export MYAPP_DEBUG=false +export MYAPP_PORT=8080 +export MYAPP_USER=Kelsey +export MYAPP_RATE="0.5" +export MYAPP_TIMEOUT="3m" +export MYAPP_USERS="rob,ken,robert" +export MYAPP_COLORCODES="red:1,green:2,blue:3" +``` + +Write some code: + +```Go +package main + +import ( + "fmt" + "log" + "time" + + "github.com/kelseyhightower/envconfig" +) + +type Specification struct { + Debug bool + Port int + User string + Users []string + Rate float32 + Timeout time.Duration + ColorCodes map[string]int +} + +func main() { + var s Specification + err := envconfig.Process("myapp", &s) + if err != nil { + log.Fatal(err.Error()) + } + format := "Debug: %v\nPort: %d\nUser: %s\nRate: %f\nTimeout: %s\n" + _, err = fmt.Printf(format, s.Debug, s.Port, s.User, s.Rate) + if err != nil { + log.Fatal(err.Error()) + } + + fmt.Println("Users:") + for _, u := range s.Users { + fmt.Printf(" %s\n", u) + } + + fmt.Println("Color codes:") + for k, v := range s.ColorCodes { + fmt.Printf(" %s: %d\n", k, v) + } +} +``` + +Results: + +```Bash +Debug: false +Port: 8080 +User: Kelsey +Rate: 0.500000 +Timeout: 3m0s +Users: + rob + ken + robert +Color codes: + red: 1 + green: 2 + blue: 3 +``` + +## Struct Tag Support + +Envconfig supports the use of struct tags to specify alternate, default, and required +environment variables. + +For example, consider the following struct: + +```Go +type Specification struct { + ManualOverride1 string `envconfig:"manual_override_1"` + DefaultVar string `default:"foobar"` + RequiredVar string `required:"true"` + IgnoredVar string `ignored:"true"` + AutoSplitVar string `split_words:"true"` +} +``` + +Envconfig has automatic support for CamelCased struct elements when the +`split_words:"true"` tag is supplied. Without this tag, `AutoSplitVar` above +would look for an environment variable called `MYAPP_AUTOSPLITVAR`. With the +setting applied it will look for `MYAPP_AUTO_SPLIT_VAR`. Note that numbers +will get globbed into the previous word. If the setting does not do the +right thing, you may use a manual override. + +Envconfig will process value for `ManualOverride1` by populating it with the +value for `MYAPP_MANUAL_OVERRIDE_1`. Without this struct tag, it would have +instead looked up `MYAPP_MANUALOVERRIDE1`. With the `split_words:"true"` tag +it would have looked up `MYAPP_MANUAL_OVERRIDE1`. + +```Bash +export MYAPP_MANUAL_OVERRIDE_1="this will be the value" + +# export MYAPP_MANUALOVERRIDE1="and this will not" +``` + +If envconfig can't find an environment variable value for `MYAPP_DEFAULTVAR`, +it will populate it with "foobar" as a default value. + +If envconfig can't find an environment variable value for `MYAPP_REQUIREDVAR`, +it will return an error when asked to process the struct. + +If envconfig can't find an environment variable in the form `PREFIX_MYVAR`, and there +is a struct tag defined, it will try to populate your variable with an environment +variable that directly matches the envconfig tag in your struct definition: + +```shell +export SERVICE_HOST=127.0.0.1 +export MYAPP_DEBUG=true +``` +```Go +type Specification struct { + ServiceHost string `envconfig:"SERVICE_HOST"` + Debug bool +} +``` + +Envconfig won't process a field with the "ignored" tag set to "true", even if a corresponding +environment variable is set. + +## Supported Struct Field Types + +envconfig supports supports these struct field types: + + * string + * int8, int16, int32, int64 + * bool + * float32, float64 + * slices of any supported type + * maps (keys and values of any supported type) + * [encoding.TextUnmarshaler](https://golang.org/pkg/encoding/#TextUnmarshaler) + +Embedded structs using these fields are also supported. + +## Custom Decoders + +Any field whose type (or pointer-to-type) implements `envconfig.Decoder` can +control its own deserialization: + +```Bash +export DNS_SERVER=8.8.8.8 +``` + +```Go +type IPDecoder net.IP + +func (ipd *IPDecoder) Decode(value string) error { + *ipd = IPDecoder(net.ParseIP(value)) + return nil +} + +type DNSConfig struct { + Address IPDecoder `envconfig:"DNS_SERVER"` +} +``` + +Also, envconfig will use a `Set(string) error` method like from the +[flag.Value](https://godoc.org/flag#Value) interface if implemented. diff --git a/vendor/github.com/kelseyhightower/envconfig/doc.go b/vendor/github.com/kelseyhightower/envconfig/doc.go new file mode 100644 index 000000000..f28561cd1 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/doc.go @@ -0,0 +1,8 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the MIT License that can be found in +// the LICENSE file. + +// Package envconfig implements decoding of environment variables based on a user +// defined specification. A typical use is using environment variables for +// configuration settings. +package envconfig diff --git a/vendor/github.com/kelseyhightower/envconfig/env_os.go b/vendor/github.com/kelseyhightower/envconfig/env_os.go new file mode 100644 index 000000000..a6a014a2b --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/env_os.go @@ -0,0 +1,7 @@ +// +build appengine + +package envconfig + +import "os" + +var lookupEnv = os.LookupEnv diff --git a/vendor/github.com/kelseyhightower/envconfig/env_syscall.go b/vendor/github.com/kelseyhightower/envconfig/env_syscall.go new file mode 100644 index 000000000..9d98085b9 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/env_syscall.go @@ -0,0 +1,7 @@ +// +build !appengine + +package envconfig + +import "syscall" + +var lookupEnv = syscall.Getenv diff --git a/vendor/github.com/kelseyhightower/envconfig/envconfig.go b/vendor/github.com/kelseyhightower/envconfig/envconfig.go new file mode 100644 index 000000000..892d74699 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/envconfig.go @@ -0,0 +1,319 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the MIT License that can be found in +// the LICENSE file. + +package envconfig + +import ( + "encoding" + "errors" + "fmt" + "reflect" + "regexp" + "strconv" + "strings" + "time" +) + +// ErrInvalidSpecification indicates that a specification is of the wrong type. +var ErrInvalidSpecification = errors.New("specification must be a struct pointer") + +// A ParseError occurs when an environment variable cannot be converted to +// the type required by a struct field during assignment. +type ParseError struct { + KeyName string + FieldName string + TypeName string + Value string + Err error +} + +// Decoder has the same semantics as Setter, but takes higher precedence. +// It is provided for historical compatibility. +type Decoder interface { + Decode(value string) error +} + +// Setter is implemented by types can self-deserialize values. +// Any type that implements flag.Value also implements Setter. +type Setter interface { + Set(value string) error +} + +func (e *ParseError) Error() string { + return fmt.Sprintf("envconfig.Process: assigning %[1]s to %[2]s: converting '%[3]s' to type %[4]s. details: %[5]s", e.KeyName, e.FieldName, e.Value, e.TypeName, e.Err) +} + +// varInfo maintains information about the configuration variable +type varInfo struct { + Name string + Alt string + Key string + Field reflect.Value + Tags reflect.StructTag +} + +// GatherInfo gathers information about the specified struct +func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) { + expr := regexp.MustCompile("([^A-Z]+|[A-Z][^A-Z]+|[A-Z]+)") + s := reflect.ValueOf(spec) + + if s.Kind() != reflect.Ptr { + return nil, ErrInvalidSpecification + } + s = s.Elem() + if s.Kind() != reflect.Struct { + return nil, ErrInvalidSpecification + } + typeOfSpec := s.Type() + + // over allocate an info array, we will extend if needed later + infos := make([]varInfo, 0, s.NumField()) + for i := 0; i < s.NumField(); i++ { + f := s.Field(i) + ftype := typeOfSpec.Field(i) + if !f.CanSet() || ftype.Tag.Get("ignored") == "true" { + continue + } + + for f.Kind() == reflect.Ptr { + if f.IsNil() { + if f.Type().Elem().Kind() != reflect.Struct { + // nil pointer to a non-struct: leave it alone + break + } + // nil pointer to struct: create a zero instance + f.Set(reflect.New(f.Type().Elem())) + } + f = f.Elem() + } + + // Capture information about the config variable + info := varInfo{ + Name: ftype.Name, + Field: f, + Tags: ftype.Tag, + Alt: strings.ToUpper(ftype.Tag.Get("envconfig")), + } + + // Default to the field name as the env var name (will be upcased) + info.Key = info.Name + + // Best effort to un-pick camel casing as separate words + if ftype.Tag.Get("split_words") == "true" { + words := expr.FindAllStringSubmatch(ftype.Name, -1) + if len(words) > 0 { + var name []string + for _, words := range words { + name = append(name, words[0]) + } + + info.Key = strings.Join(name, "_") + } + } + if info.Alt != "" { + info.Key = info.Alt + } + if prefix != "" { + info.Key = fmt.Sprintf("%s_%s", prefix, info.Key) + } + info.Key = strings.ToUpper(info.Key) + infos = append(infos, info) + + if f.Kind() == reflect.Struct { + // honor Decode if present + if decoderFrom(f) == nil && setterFrom(f) == nil && textUnmarshaler(f) == nil { + innerPrefix := prefix + if !ftype.Anonymous { + innerPrefix = info.Key + } + + embeddedPtr := f.Addr().Interface() + embeddedInfos, err := gatherInfo(innerPrefix, embeddedPtr) + if err != nil { + return nil, err + } + infos = append(infos[:len(infos)-1], embeddedInfos...) + + continue + } + } + } + return infos, nil +} + +// Process populates the specified struct based on environment variables +func Process(prefix string, spec interface{}) error { + infos, err := gatherInfo(prefix, spec) + + for _, info := range infos { + + // `os.Getenv` cannot differentiate between an explicitly set empty value + // and an unset value. `os.LookupEnv` is preferred to `syscall.Getenv`, + // but it is only available in go1.5 or newer. We're using Go build tags + // here to use os.LookupEnv for >=go1.5 + value, ok := lookupEnv(info.Key) + if !ok && info.Alt != "" { + value, ok = lookupEnv(info.Alt) + } + + def := info.Tags.Get("default") + if def != "" && !ok { + value = def + } + + req := info.Tags.Get("required") + if !ok && def == "" { + if req == "true" { + return fmt.Errorf("required key %s missing value", info.Key) + } + continue + } + + err := processField(value, info.Field) + if err != nil { + return &ParseError{ + KeyName: info.Key, + FieldName: info.Name, + TypeName: info.Field.Type().String(), + Value: value, + Err: err, + } + } + } + + return err +} + +// MustProcess is the same as Process but panics if an error occurs +func MustProcess(prefix string, spec interface{}) { + if err := Process(prefix, spec); err != nil { + panic(err) + } +} + +func processField(value string, field reflect.Value) error { + typ := field.Type() + + decoder := decoderFrom(field) + if decoder != nil { + return decoder.Decode(value) + } + // look for Set method if Decode not defined + setter := setterFrom(field) + if setter != nil { + return setter.Set(value) + } + + if t := textUnmarshaler(field); t != nil { + return t.UnmarshalText([]byte(value)) + } + + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + if field.IsNil() { + field.Set(reflect.New(typ)) + } + field = field.Elem() + } + + switch typ.Kind() { + case reflect.String: + field.SetString(value) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + var ( + val int64 + err error + ) + if field.Kind() == reflect.Int64 && typ.PkgPath() == "time" && typ.Name() == "Duration" { + var d time.Duration + d, err = time.ParseDuration(value) + val = int64(d) + } else { + val, err = strconv.ParseInt(value, 0, typ.Bits()) + } + if err != nil { + return err + } + + field.SetInt(val) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + val, err := strconv.ParseUint(value, 0, typ.Bits()) + if err != nil { + return err + } + field.SetUint(val) + case reflect.Bool: + val, err := strconv.ParseBool(value) + if err != nil { + return err + } + field.SetBool(val) + case reflect.Float32, reflect.Float64: + val, err := strconv.ParseFloat(value, typ.Bits()) + if err != nil { + return err + } + field.SetFloat(val) + case reflect.Slice: + vals := strings.Split(value, ",") + sl := reflect.MakeSlice(typ, len(vals), len(vals)) + for i, val := range vals { + err := processField(val, sl.Index(i)) + if err != nil { + return err + } + } + field.Set(sl) + case reflect.Map: + pairs := strings.Split(value, ",") + mp := reflect.MakeMap(typ) + for _, pair := range pairs { + kvpair := strings.Split(pair, ":") + if len(kvpair) != 2 { + return fmt.Errorf("invalid map item: %q", pair) + } + k := reflect.New(typ.Key()).Elem() + err := processField(kvpair[0], k) + if err != nil { + return err + } + v := reflect.New(typ.Elem()).Elem() + err = processField(kvpair[1], v) + if err != nil { + return err + } + mp.SetMapIndex(k, v) + } + field.Set(mp) + } + + return nil +} + +func interfaceFrom(field reflect.Value, fn func(interface{}, *bool)) { + // it may be impossible for a struct field to fail this check + if !field.CanInterface() { + return + } + var ok bool + fn(field.Interface(), &ok) + if !ok && field.CanAddr() { + fn(field.Addr().Interface(), &ok) + } +} + +func decoderFrom(field reflect.Value) (d Decoder) { + interfaceFrom(field, func(v interface{}, ok *bool) { d, *ok = v.(Decoder) }) + return d +} + +func setterFrom(field reflect.Value) (s Setter) { + interfaceFrom(field, func(v interface{}, ok *bool) { s, *ok = v.(Setter) }) + return s +} + +func textUnmarshaler(field reflect.Value) (t encoding.TextUnmarshaler) { + interfaceFrom(field, func(v interface{}, ok *bool) { t, *ok = v.(encoding.TextUnmarshaler) }) + return t +} diff --git a/vendor/github.com/kelseyhightower/envconfig/envconfig_test.go b/vendor/github.com/kelseyhightower/envconfig/envconfig_test.go new file mode 100644 index 000000000..e754058e9 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/envconfig_test.go @@ -0,0 +1,688 @@ +// Copyright (c) 2013 Kelsey Hightower. All rights reserved. +// Use of this source code is governed by the MIT License that can be found in +// the LICENSE file. + +package envconfig + +import ( + "flag" + "fmt" + "os" + "testing" + "time" +) + +type HonorDecodeInStruct struct { + Value string +} + +func (h *HonorDecodeInStruct) Decode(env string) error { + h.Value = "decoded" + return nil +} + +type Specification struct { + Embedded `desc:"can we document a struct"` + EmbeddedButIgnored `ignored:"true"` + Debug bool + Port int + Rate float32 + User string + TTL uint32 + Timeout time.Duration + AdminUsers []string + MagicNumbers []int + ColorCodes map[string]int + MultiWordVar string + MultiWordVarWithAutoSplit uint32 `split_words:"true"` + SomePointer *string + SomePointerWithDefault *string `default:"foo2baz" desc:"foorbar is the word"` + MultiWordVarWithAlt string `envconfig:"MULTI_WORD_VAR_WITH_ALT" desc:"what alt"` + MultiWordVarWithLowerCaseAlt string `envconfig:"multi_word_var_with_lower_case_alt"` + NoPrefixWithAlt string `envconfig:"SERVICE_HOST"` + DefaultVar string `default:"foobar"` + RequiredVar string `required:"true"` + NoPrefixDefault string `envconfig:"BROKER" default:"127.0.0.1"` + RequiredDefault string `required:"true" default:"foo2bar"` + Ignored string `ignored:"true"` + NestedSpecification struct { + Property string `envconfig:"inner"` + PropertyWithDefault string `default:"fuzzybydefault"` + } `envconfig:"outer"` + AfterNested string + DecodeStruct HonorDecodeInStruct `envconfig:"honor"` + Datetime time.Time +} + +type Embedded struct { + Enabled bool `desc:"some embedded value"` + EmbeddedPort int + MultiWordVar string + MultiWordVarWithAlt string `envconfig:"MULTI_WITH_DIFFERENT_ALT"` + EmbeddedAlt string `envconfig:"EMBEDDED_WITH_ALT"` + EmbeddedIgnored string `ignored:"true"` +} + +type EmbeddedButIgnored struct { + FirstEmbeddedButIgnored string + SecondEmbeddedButIgnored string +} + +func TestProcess(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_DEBUG", "true") + os.Setenv("ENV_CONFIG_PORT", "8080") + os.Setenv("ENV_CONFIG_RATE", "0.5") + os.Setenv("ENV_CONFIG_USER", "Kelsey") + os.Setenv("ENV_CONFIG_TIMEOUT", "2m") + os.Setenv("ENV_CONFIG_ADMINUSERS", "John,Adam,Will") + os.Setenv("ENV_CONFIG_MAGICNUMBERS", "5,10,20") + os.Setenv("ENV_CONFIG_COLORCODES", "red:1,green:2,blue:3") + os.Setenv("SERVICE_HOST", "127.0.0.1") + os.Setenv("ENV_CONFIG_TTL", "30") + os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") + os.Setenv("ENV_CONFIG_IGNORED", "was-not-ignored") + os.Setenv("ENV_CONFIG_OUTER_INNER", "iamnested") + os.Setenv("ENV_CONFIG_AFTERNESTED", "after") + os.Setenv("ENV_CONFIG_HONOR", "honor") + os.Setenv("ENV_CONFIG_DATETIME", "2016-08-16T18:57:05Z") + os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT", "24") + err := Process("env_config", &s) + if err != nil { + t.Error(err.Error()) + } + if s.NoPrefixWithAlt != "127.0.0.1" { + t.Errorf("expected %v, got %v", "127.0.0.1", s.NoPrefixWithAlt) + } + if !s.Debug { + t.Errorf("expected %v, got %v", true, s.Debug) + } + if s.Port != 8080 { + t.Errorf("expected %d, got %v", 8080, s.Port) + } + if s.Rate != 0.5 { + t.Errorf("expected %f, got %v", 0.5, s.Rate) + } + if s.TTL != 30 { + t.Errorf("expected %d, got %v", 30, s.TTL) + } + if s.User != "Kelsey" { + t.Errorf("expected %s, got %s", "Kelsey", s.User) + } + if s.Timeout != 2*time.Minute { + t.Errorf("expected %s, got %s", 2*time.Minute, s.Timeout) + } + if s.RequiredVar != "foo" { + t.Errorf("expected %s, got %s", "foo", s.RequiredVar) + } + if len(s.AdminUsers) != 3 || + s.AdminUsers[0] != "John" || + s.AdminUsers[1] != "Adam" || + s.AdminUsers[2] != "Will" { + t.Errorf("expected %#v, got %#v", []string{"John", "Adam", "Will"}, s.AdminUsers) + } + if len(s.MagicNumbers) != 3 || + s.MagicNumbers[0] != 5 || + s.MagicNumbers[1] != 10 || + s.MagicNumbers[2] != 20 { + t.Errorf("expected %#v, got %#v", []int{5, 10, 20}, s.MagicNumbers) + } + if s.Ignored != "" { + t.Errorf("expected empty string, got %#v", s.Ignored) + } + + if len(s.ColorCodes) != 3 || + s.ColorCodes["red"] != 1 || + s.ColorCodes["green"] != 2 || + s.ColorCodes["blue"] != 3 { + t.Errorf( + "expected %#v, got %#v", + map[string]int{ + "red": 1, + "green": 2, + "blue": 3, + }, + s.ColorCodes, + ) + } + + if s.NestedSpecification.Property != "iamnested" { + t.Errorf("expected '%s' string, got %#v", "iamnested", s.NestedSpecification.Property) + } + + if s.NestedSpecification.PropertyWithDefault != "fuzzybydefault" { + t.Errorf("expected default '%s' string, got %#v", "fuzzybydefault", s.NestedSpecification.PropertyWithDefault) + } + + if s.AfterNested != "after" { + t.Errorf("expected default '%s' string, got %#v", "after", s.AfterNested) + } + + if s.DecodeStruct.Value != "decoded" { + t.Errorf("expected default '%s' string, got %#v", "decoded", s.DecodeStruct.Value) + } + + if expected := time.Date(2016, 8, 16, 18, 57, 05, 0, time.UTC); !s.Datetime.Equal(expected) { + t.Errorf("expected %s, got %s", expected.Format(time.RFC3339), s.Datetime.Format(time.RFC3339)) + } + + if s.MultiWordVarWithAutoSplit != 24 { + t.Errorf("expected %q, got %q", 24, s.MultiWordVarWithAutoSplit) + } +} + +func TestParseErrorBool(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_DEBUG", "string") + os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") + err := Process("env_config", &s) + v, ok := err.(*ParseError) + if !ok { + t.Errorf("expected ParseError, got %v", v) + } + if v.FieldName != "Debug" { + t.Errorf("expected %s, got %v", "Debug", v.FieldName) + } + if s.Debug != false { + t.Errorf("expected %v, got %v", false, s.Debug) + } +} + +func TestParseErrorFloat32(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_RATE", "string") + os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") + err := Process("env_config", &s) + v, ok := err.(*ParseError) + if !ok { + t.Errorf("expected ParseError, got %v", v) + } + if v.FieldName != "Rate" { + t.Errorf("expected %s, got %v", "Rate", v.FieldName) + } + if s.Rate != 0 { + t.Errorf("expected %v, got %v", 0, s.Rate) + } +} + +func TestParseErrorInt(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_PORT", "string") + os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") + err := Process("env_config", &s) + v, ok := err.(*ParseError) + if !ok { + t.Errorf("expected ParseError, got %v", v) + } + if v.FieldName != "Port" { + t.Errorf("expected %s, got %v", "Port", v.FieldName) + } + if s.Port != 0 { + t.Errorf("expected %v, got %v", 0, s.Port) + } +} + +func TestParseErrorUint(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_TTL", "-30") + err := Process("env_config", &s) + v, ok := err.(*ParseError) + if !ok { + t.Errorf("expected ParseError, got %v", v) + } + if v.FieldName != "TTL" { + t.Errorf("expected %s, got %v", "TTL", v.FieldName) + } + if s.TTL != 0 { + t.Errorf("expected %v, got %v", 0, s.TTL) + } +} + +func TestParseErrorSplitWords(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT", "shakespeare") + err := Process("env_config", &s) + v, ok := err.(*ParseError) + if !ok { + t.Errorf("expected ParseError, got %v", v) + } + if v.FieldName != "MultiWordVarWithAutoSplit" { + t.Errorf("expected %s, got %v", "", v.FieldName) + } + if s.MultiWordVarWithAutoSplit != 0 { + t.Errorf("expected %v, got %v", 0, s.MultiWordVarWithAutoSplit) + } +} + +func TestErrInvalidSpecification(t *testing.T) { + m := make(map[string]string) + err := Process("env_config", &m) + if err != ErrInvalidSpecification { + t.Errorf("expected %v, got %v", ErrInvalidSpecification, err) + } +} + +func TestUnsetVars(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("USER", "foo") + os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + + // If the var is not defined the non-prefixed version should not be used + // unless the struct tag says so + if s.User != "" { + t.Errorf("expected %q, got %q", "", s.User) + } +} + +func TestAlternateVarNames(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_MULTI_WORD_VAR", "foo") + os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_ALT", "bar") + os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_LOWER_CASE_ALT", "baz") + os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + + // Setting the alt version of the var in the environment has no effect if + // the struct tag is not supplied + if s.MultiWordVar != "" { + t.Errorf("expected %q, got %q", "", s.MultiWordVar) + } + + // Setting the alt version of the var in the environment correctly sets + // the value if the struct tag IS supplied + if s.MultiWordVarWithAlt != "bar" { + t.Errorf("expected %q, got %q", "bar", s.MultiWordVarWithAlt) + } + + // Alt value is not case sensitive and is treated as all uppercase + if s.MultiWordVarWithLowerCaseAlt != "baz" { + t.Errorf("expected %q, got %q", "baz", s.MultiWordVarWithLowerCaseAlt) + } +} + +func TestRequiredVar(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_REQUIREDVAR", "foobar") + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + + if s.RequiredVar != "foobar" { + t.Errorf("expected %s, got %s", "foobar", s.RequiredVar) + } +} + +func TestBlankDefaultVar(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_REQUIREDVAR", "requiredvalue") + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + + if s.DefaultVar != "foobar" { + t.Errorf("expected %s, got %s", "foobar", s.DefaultVar) + } + + if *s.SomePointerWithDefault != "foo2baz" { + t.Errorf("expected %s, got %s", "foo2baz", *s.SomePointerWithDefault) + } +} + +func TestNonBlankDefaultVar(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_DEFAULTVAR", "nondefaultval") + os.Setenv("ENV_CONFIG_REQUIREDVAR", "requiredvalue") + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + + if s.DefaultVar != "nondefaultval" { + t.Errorf("expected %s, got %s", "nondefaultval", s.DefaultVar) + } +} + +func TestExplicitBlankDefaultVar(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_DEFAULTVAR", "") + os.Setenv("ENV_CONFIG_REQUIREDVAR", "") + + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + + if s.DefaultVar != "" { + t.Errorf("expected %s, got %s", "\"\"", s.DefaultVar) + } +} + +func TestAlternateNameDefaultVar(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("BROKER", "betterbroker") + os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + + if s.NoPrefixDefault != "betterbroker" { + t.Errorf("expected %q, got %q", "betterbroker", s.NoPrefixDefault) + } + + os.Clearenv() + os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + + if s.NoPrefixDefault != "127.0.0.1" { + t.Errorf("expected %q, got %q", "127.0.0.1", s.NoPrefixDefault) + } +} + +func TestRequiredDefault(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + + if s.RequiredDefault != "foo2bar" { + t.Errorf("expected %q, got %q", "foo2bar", s.RequiredDefault) + } +} + +func TestPointerFieldBlank(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + + if s.SomePointer != nil { + t.Errorf("expected , got %q", *s.SomePointer) + } +} + +func TestMustProcess(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_DEBUG", "true") + os.Setenv("ENV_CONFIG_PORT", "8080") + os.Setenv("ENV_CONFIG_RATE", "0.5") + os.Setenv("ENV_CONFIG_USER", "Kelsey") + os.Setenv("SERVICE_HOST", "127.0.0.1") + os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") + MustProcess("env_config", &s) + + defer func() { + if err := recover(); err != nil { + return + } + + t.Error("expected panic") + }() + m := make(map[string]string) + MustProcess("env_config", &m) +} + +func TestEmbeddedStruct(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_REQUIREDVAR", "required") + os.Setenv("ENV_CONFIG_ENABLED", "true") + os.Setenv("ENV_CONFIG_EMBEDDEDPORT", "1234") + os.Setenv("ENV_CONFIG_MULTIWORDVAR", "foo") + os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_ALT", "bar") + os.Setenv("ENV_CONFIG_MULTI_WITH_DIFFERENT_ALT", "baz") + os.Setenv("ENV_CONFIG_EMBEDDED_WITH_ALT", "foobar") + os.Setenv("ENV_CONFIG_SOMEPOINTER", "foobaz") + os.Setenv("ENV_CONFIG_EMBEDDED_IGNORED", "was-not-ignored") + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + if !s.Enabled { + t.Errorf("expected %v, got %v", true, s.Enabled) + } + if s.EmbeddedPort != 1234 { + t.Errorf("expected %d, got %v", 1234, s.EmbeddedPort) + } + if s.MultiWordVar != "foo" { + t.Errorf("expected %s, got %s", "foo", s.MultiWordVar) + } + if s.Embedded.MultiWordVar != "foo" { + t.Errorf("expected %s, got %s", "foo", s.Embedded.MultiWordVar) + } + if s.MultiWordVarWithAlt != "bar" { + t.Errorf("expected %s, got %s", "bar", s.MultiWordVarWithAlt) + } + if s.Embedded.MultiWordVarWithAlt != "baz" { + t.Errorf("expected %s, got %s", "baz", s.Embedded.MultiWordVarWithAlt) + } + if s.EmbeddedAlt != "foobar" { + t.Errorf("expected %s, got %s", "foobar", s.EmbeddedAlt) + } + if *s.SomePointer != "foobaz" { + t.Errorf("expected %s, got %s", "foobaz", *s.SomePointer) + } + if s.EmbeddedIgnored != "" { + t.Errorf("expected empty string, got %#v", s.Ignored) + } +} + +func TestEmbeddedButIgnoredStruct(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_REQUIREDVAR", "required") + os.Setenv("ENV_CONFIG_FIRSTEMBEDDEDBUTIGNORED", "was-not-ignored") + os.Setenv("ENV_CONFIG_SECONDEMBEDDEDBUTIGNORED", "was-not-ignored") + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + if s.FirstEmbeddedButIgnored != "" { + t.Errorf("expected empty string, got %#v", s.Ignored) + } + if s.SecondEmbeddedButIgnored != "" { + t.Errorf("expected empty string, got %#v", s.Ignored) + } +} + +func TestNonPointerFailsProperly(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_REQUIREDVAR", "snap") + + err := Process("env_config", s) + if err != ErrInvalidSpecification { + t.Errorf("non-pointer should fail with ErrInvalidSpecification, was instead %s", err) + } +} + +func TestCustomValueFields(t *testing.T) { + var s struct { + Foo string + Bar bracketed + Baz quoted + Struct setterStruct + } + + // Set would panic when the receiver is nil, + // so make sure it has an initial value to replace. + s.Baz = quoted{new(bracketed)} + + os.Clearenv() + os.Setenv("ENV_CONFIG_FOO", "foo") + os.Setenv("ENV_CONFIG_BAR", "bar") + os.Setenv("ENV_CONFIG_BAZ", "baz") + os.Setenv("ENV_CONFIG_STRUCT", "inner") + + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + + if want := "foo"; s.Foo != want { + t.Errorf("foo: got %#q, want %#q", s.Foo, want) + } + + if want := "[bar]"; s.Bar.String() != want { + t.Errorf("bar: got %#q, want %#q", s.Bar, want) + } + + if want := `["baz"]`; s.Baz.String() != want { + t.Errorf(`baz: got %#q, want %#q`, s.Baz, want) + } + + if want := `setterstruct{"inner"}`; s.Struct.Inner != want { + t.Errorf(`Struct.Inner: got %#q, want %#q`, s.Struct.Inner, want) + } +} + +func TestCustomPointerFields(t *testing.T) { + var s struct { + Foo string + Bar *bracketed + Baz *quoted + Struct *setterStruct + } + + // Set would panic when the receiver is nil, + // so make sure they have initial values to replace. + s.Bar = new(bracketed) + s.Baz = "ed{new(bracketed)} + + os.Clearenv() + os.Setenv("ENV_CONFIG_FOO", "foo") + os.Setenv("ENV_CONFIG_BAR", "bar") + os.Setenv("ENV_CONFIG_BAZ", "baz") + os.Setenv("ENV_CONFIG_STRUCT", "inner") + + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + + if want := "foo"; s.Foo != want { + t.Errorf("foo: got %#q, want %#q", s.Foo, want) + } + + if want := "[bar]"; s.Bar.String() != want { + t.Errorf("bar: got %#q, want %#q", s.Bar, want) + } + + if want := `["baz"]`; s.Baz.String() != want { + t.Errorf(`baz: got %#q, want %#q`, s.Baz, want) + } + + if want := `setterstruct{"inner"}`; s.Struct.Inner != want { + t.Errorf(`Struct.Inner: got %#q, want %#q`, s.Struct.Inner, want) + } +} + +func TestEmptyPrefixUsesFieldNames(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("REQUIREDVAR", "foo") + + err := Process("", &s) + if err != nil { + t.Errorf("Process failed: %s", err) + } + + if s.RequiredVar != "foo" { + t.Errorf( + `RequiredVar not populated correctly: expected "foo", got %q`, + s.RequiredVar, + ) + } +} + +func TestNestedStructVarName(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_REQUIREDVAR", "required") + val := "found with only short name" + os.Setenv("INNER", val) + if err := Process("env_config", &s); err != nil { + t.Error(err.Error()) + } + if s.NestedSpecification.Property != val { + t.Errorf("expected %s, got %s", val, s.NestedSpecification.Property) + } +} + +func TestTextUnmarshalerError(t *testing.T) { + var s Specification + os.Clearenv() + os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo") + os.Setenv("ENV_CONFIG_DATETIME", "I'M NOT A DATE") + + err := Process("env_config", &s) + + v, ok := err.(*ParseError) + if !ok { + t.Errorf("expected ParseError, got %v", v) + } + if v.FieldName != "Datetime" { + t.Errorf("expected %s, got %v", "Debug", v.FieldName) + } + + expectedLowLevelError := time.ParseError{ + Layout: time.RFC3339, + Value: "I'M NOT A DATE", + LayoutElem: "2006", + ValueElem: "I'M NOT A DATE", + } + + if v.Err.Error() != expectedLowLevelError.Error() { + t.Errorf("expected %s, got %s", expectedLowLevelError, v.Err) + } + if s.Debug != false { + t.Errorf("expected %v, got %v", false, s.Debug) + } +} + +type bracketed string + +func (b *bracketed) Set(value string) error { + *b = bracketed("[" + value + "]") + return nil +} + +func (b bracketed) String() string { + return string(b) +} + +// quoted is used to test the precedence of Decode over Set. +// The sole field is a flag.Value rather than a setter to validate that +// all flag.Value implementations are also Setter implementations. +type quoted struct{ flag.Value } + +func (d quoted) Decode(value string) error { + return d.Set(`"` + value + `"`) +} + +type setterStruct struct { + Inner string +} + +func (ss *setterStruct) Set(value string) error { + ss.Inner = fmt.Sprintf("setterstruct{%q}", value) + return nil +} diff --git a/vendor/github.com/kelseyhightower/envconfig/testdata/custom.txt b/vendor/github.com/kelseyhightower/envconfig/testdata/custom.txt new file mode 100644 index 000000000..243e82c4c --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/testdata/custom.txt @@ -0,0 +1,30 @@ +ENV_CONFIG_ENABLED=some.embedded.value +ENV_CONFIG_EMBEDDEDPORT= +ENV_CONFIG_MULTIWORDVAR= +ENV_CONFIG_MULTI_WITH_DIFFERENT_ALT= +ENV_CONFIG_EMBEDDED_WITH_ALT= +ENV_CONFIG_DEBUG= +ENV_CONFIG_PORT= +ENV_CONFIG_RATE= +ENV_CONFIG_USER= +ENV_CONFIG_TTL= +ENV_CONFIG_TIMEOUT= +ENV_CONFIG_ADMINUSERS= +ENV_CONFIG_MAGICNUMBERS= +ENV_CONFIG_COLORCODES= +ENV_CONFIG_MULTIWORDVAR= +ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT= +ENV_CONFIG_SOMEPOINTER= +ENV_CONFIG_SOMEPOINTERWITHDEFAULT=foorbar.is.the.word +ENV_CONFIG_MULTI_WORD_VAR_WITH_ALT=what.alt +ENV_CONFIG_MULTI_WORD_VAR_WITH_LOWER_CASE_ALT= +ENV_CONFIG_SERVICE_HOST= +ENV_CONFIG_DEFAULTVAR= +ENV_CONFIG_REQUIREDVAR= +ENV_CONFIG_BROKER= +ENV_CONFIG_REQUIREDDEFAULT= +ENV_CONFIG_OUTER_INNER= +ENV_CONFIG_OUTER_PROPERTYWITHDEFAULT= +ENV_CONFIG_AFTERNESTED= +ENV_CONFIG_HONOR= +ENV_CONFIG_DATETIME= diff --git a/vendor/github.com/kelseyhightower/envconfig/testdata/default_list.txt b/vendor/github.com/kelseyhightower/envconfig/testdata/default_list.txt new file mode 100644 index 000000000..bc29211b0 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/testdata/default_list.txt @@ -0,0 +1,153 @@ +This.application.is.configured.via.the.environment..The.following.environment +variables.can.be.used: + +ENV_CONFIG_ENABLED +..[description].some.embedded.value +..[type]........True.or.False +..[default]..... +..[required].... +ENV_CONFIG_EMBEDDEDPORT +..[description]. +..[type]........Integer +..[default]..... +..[required].... +ENV_CONFIG_MULTIWORDVAR +..[description]. +..[type]........String +..[default]..... +..[required].... +ENV_CONFIG_MULTI_WITH_DIFFERENT_ALT +..[description]. +..[type]........String +..[default]..... +..[required].... +ENV_CONFIG_EMBEDDED_WITH_ALT +..[description]. +..[type]........String +..[default]..... +..[required].... +ENV_CONFIG_DEBUG +..[description]. +..[type]........True.or.False +..[default]..... +..[required].... +ENV_CONFIG_PORT +..[description]. +..[type]........Integer +..[default]..... +..[required].... +ENV_CONFIG_RATE +..[description]. +..[type]........Float +..[default]..... +..[required].... +ENV_CONFIG_USER +..[description]. +..[type]........String +..[default]..... +..[required].... +ENV_CONFIG_TTL +..[description]. +..[type]........Unsigned.Integer +..[default]..... +..[required].... +ENV_CONFIG_TIMEOUT +..[description]. +..[type]........Duration +..[default]..... +..[required].... +ENV_CONFIG_ADMINUSERS +..[description]. +..[type]........Comma-separated.list.of.String +..[default]..... +..[required].... +ENV_CONFIG_MAGICNUMBERS +..[description]. +..[type]........Comma-separated.list.of.Integer +..[default]..... +..[required].... +ENV_CONFIG_COLORCODES +..[description]. +..[type]........Comma-separated.list.of.String:Integer.pairs +..[default]..... +..[required].... +ENV_CONFIG_MULTIWORDVAR +..[description]. +..[type]........String +..[default]..... +..[required].... +ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT +..[description]. +..[type]........Unsigned.Integer +..[default]..... +..[required].... +ENV_CONFIG_SOMEPOINTER +..[description]. +..[type]........String +..[default]..... +..[required].... +ENV_CONFIG_SOMEPOINTERWITHDEFAULT +..[description].foorbar.is.the.word +..[type]........String +..[default].....foo2baz +..[required].... +ENV_CONFIG_MULTI_WORD_VAR_WITH_ALT +..[description].what.alt +..[type]........String +..[default]..... +..[required].... +ENV_CONFIG_MULTI_WORD_VAR_WITH_LOWER_CASE_ALT +..[description]. +..[type]........String +..[default]..... +..[required].... +ENV_CONFIG_SERVICE_HOST +..[description]. +..[type]........String +..[default]..... +..[required].... +ENV_CONFIG_DEFAULTVAR +..[description]. +..[type]........String +..[default].....foobar +..[required].... +ENV_CONFIG_REQUIREDVAR +..[description]. +..[type]........String +..[default]..... +..[required]....true +ENV_CONFIG_BROKER +..[description]. +..[type]........String +..[default].....127.0.0.1 +..[required].... +ENV_CONFIG_REQUIREDDEFAULT +..[description]. +..[type]........String +..[default].....foo2bar +..[required]....true +ENV_CONFIG_OUTER_INNER +..[description]. +..[type]........String +..[default]..... +..[required].... +ENV_CONFIG_OUTER_PROPERTYWITHDEFAULT +..[description]. +..[type]........String +..[default].....fuzzybydefault +..[required].... +ENV_CONFIG_AFTERNESTED +..[description]. +..[type]........String +..[default]..... +..[required].... +ENV_CONFIG_HONOR +..[description]. +..[type]........HonorDecodeInStruct +..[default]..... +..[required].... +ENV_CONFIG_DATETIME +..[description]. +..[type]........Time +..[default]..... +..[required].... diff --git a/vendor/github.com/kelseyhightower/envconfig/testdata/default_table.txt b/vendor/github.com/kelseyhightower/envconfig/testdata/default_table.txt new file mode 100644 index 000000000..f3cf945f7 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/testdata/default_table.txt @@ -0,0 +1,34 @@ +This.application.is.configured.via.the.environment..The.following.environment +variables.can.be.used: + +KEY..............................................TYPE............................................DEFAULT...........REQUIRED....DESCRIPTION +ENV_CONFIG_ENABLED...............................True.or.False.................................................................some.embedded.value +ENV_CONFIG_EMBEDDEDPORT..........................Integer....................................................................... +ENV_CONFIG_MULTIWORDVAR..........................String........................................................................ +ENV_CONFIG_MULTI_WITH_DIFFERENT_ALT..............String........................................................................ +ENV_CONFIG_EMBEDDED_WITH_ALT.....................String........................................................................ +ENV_CONFIG_DEBUG.................................True.or.False................................................................. +ENV_CONFIG_PORT..................................Integer....................................................................... +ENV_CONFIG_RATE..................................Float......................................................................... +ENV_CONFIG_USER..................................String........................................................................ +ENV_CONFIG_TTL...................................Unsigned.Integer.............................................................. +ENV_CONFIG_TIMEOUT...............................Duration...................................................................... +ENV_CONFIG_ADMINUSERS............................Comma-separated.list.of.String................................................ +ENV_CONFIG_MAGICNUMBERS..........................Comma-separated.list.of.Integer............................................... +ENV_CONFIG_COLORCODES............................Comma-separated.list.of.String:Integer.pairs.................................. +ENV_CONFIG_MULTIWORDVAR..........................String........................................................................ +ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT........Unsigned.Integer.............................................................. +ENV_CONFIG_SOMEPOINTER...........................String........................................................................ +ENV_CONFIG_SOMEPOINTERWITHDEFAULT................String..........................................foo2baz.......................foorbar.is.the.word +ENV_CONFIG_MULTI_WORD_VAR_WITH_ALT...............String........................................................................what.alt +ENV_CONFIG_MULTI_WORD_VAR_WITH_LOWER_CASE_ALT....String........................................................................ +ENV_CONFIG_SERVICE_HOST..........................String........................................................................ +ENV_CONFIG_DEFAULTVAR............................String..........................................foobar........................ +ENV_CONFIG_REQUIREDVAR...........................String............................................................true........ +ENV_CONFIG_BROKER................................String..........................................127.0.0.1..................... +ENV_CONFIG_REQUIREDDEFAULT.......................String..........................................foo2bar...........true........ +ENV_CONFIG_OUTER_INNER...........................String........................................................................ +ENV_CONFIG_OUTER_PROPERTYWITHDEFAULT.............String..........................................fuzzybydefault................ +ENV_CONFIG_AFTERNESTED...........................String........................................................................ +ENV_CONFIG_HONOR.................................HonorDecodeInStruct........................................................... +ENV_CONFIG_DATETIME..............................Time.......................................................................... diff --git a/vendor/github.com/kelseyhightower/envconfig/testdata/fault.txt b/vendor/github.com/kelseyhightower/envconfig/testdata/fault.txt new file mode 100644 index 000000000..30e28ce08 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/testdata/fault.txt @@ -0,0 +1,30 @@ +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} +{.Key} diff --git a/vendor/github.com/kelseyhightower/envconfig/usage.go b/vendor/github.com/kelseyhightower/envconfig/usage.go new file mode 100644 index 000000000..184635380 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/usage.go @@ -0,0 +1,158 @@ +// Copyright (c) 2016 Kelsey Hightower and others. All rights reserved. +// Use of this source code is governed by the MIT License that can be found in +// the LICENSE file. + +package envconfig + +import ( + "encoding" + "fmt" + "io" + "os" + "reflect" + "strconv" + "strings" + "text/tabwriter" + "text/template" +) + +const ( + // DefaultListFormat constant to use to display usage in a list format + DefaultListFormat = `This application is configured via the environment. The following environment +variables can be used: +{{range .}} +{{usage_key .}} + [description] {{usage_description .}} + [type] {{usage_type .}} + [default] {{usage_default .}} + [required] {{usage_required .}}{{end}} +` + // DefaultTableFormat constant to use to display usage in a tabluar format + DefaultTableFormat = `This application is configured via the environment. The following environment +variables can be used: + +KEY TYPE DEFAULT REQUIRED DESCRIPTION +{{range .}}{{usage_key .}} {{usage_type .}} {{usage_default .}} {{usage_required .}} {{usage_description .}} +{{end}}` +) + +var ( + decoderType = reflect.TypeOf((*Decoder)(nil)).Elem() + setterType = reflect.TypeOf((*Setter)(nil)).Elem() + unmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() +) + +func implementsInterface(t reflect.Type) bool { + return t.Implements(decoderType) || + reflect.PtrTo(t).Implements(decoderType) || + t.Implements(setterType) || + reflect.PtrTo(t).Implements(setterType) || + t.Implements(unmarshalerType) || + reflect.PtrTo(t).Implements(unmarshalerType) +} + +// toTypeDescription converts Go types into a human readable description +func toTypeDescription(t reflect.Type) string { + switch t.Kind() { + case reflect.Array, reflect.Slice: + return fmt.Sprintf("Comma-separated list of %s", toTypeDescription(t.Elem())) + case reflect.Map: + return fmt.Sprintf( + "Comma-separated list of %s:%s pairs", + toTypeDescription(t.Key()), + toTypeDescription(t.Elem()), + ) + case reflect.Ptr: + return toTypeDescription(t.Elem()) + case reflect.Struct: + if implementsInterface(t) && t.Name() != "" { + return t.Name() + } + return "" + case reflect.String: + name := t.Name() + if name != "" && name != "string" { + return name + } + return "String" + case reflect.Bool: + name := t.Name() + if name != "" && name != "bool" { + return name + } + return "True or False" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + name := t.Name() + if name != "" && !strings.HasPrefix(name, "int") { + return name + } + return "Integer" + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + name := t.Name() + if name != "" && !strings.HasPrefix(name, "uint") { + return name + } + return "Unsigned Integer" + case reflect.Float32, reflect.Float64: + name := t.Name() + if name != "" && !strings.HasPrefix(name, "float") { + return name + } + return "Float" + } + return fmt.Sprintf("%+v", t) +} + +// Usage writes usage information to stderr using the default header and table format +func Usage(prefix string, spec interface{}) error { + // The default is to output the usage information as a table + // Create tabwriter instance to support table output + tabs := tabwriter.NewWriter(os.Stdout, 1, 0, 4, ' ', 0) + + err := Usagef(prefix, spec, tabs, DefaultTableFormat) + tabs.Flush() + return err +} + +// Usagef writes usage information to the specified io.Writer using the specifed template specification +func Usagef(prefix string, spec interface{}, out io.Writer, format string) error { + + // Specify the default usage template functions + functions := template.FuncMap{ + "usage_key": func(v varInfo) string { return v.Key }, + "usage_description": func(v varInfo) string { return v.Tags.Get("desc") }, + "usage_type": func(v varInfo) string { return toTypeDescription(v.Field.Type()) }, + "usage_default": func(v varInfo) string { return v.Tags.Get("default") }, + "usage_required": func(v varInfo) (string, error) { + req := v.Tags.Get("required") + if req != "" { + reqB, err := strconv.ParseBool(req) + if err != nil { + return "", err + } + if reqB { + req = "true" + } + } + return req, nil + }, + } + + tmpl, err := template.New("envconfig").Funcs(functions).Parse(format) + if err != nil { + return err + } + + return Usaget(prefix, spec, out, tmpl) +} + +// Usaget writes usage information to the specified io.Writer using the specified template +func Usaget(prefix string, spec interface{}, out io.Writer, tmpl *template.Template) error { + // gather first + infos, err := gatherInfo(prefix, spec) + if err != nil { + return err + } + + return tmpl.Execute(out, infos) +} diff --git a/vendor/github.com/kelseyhightower/envconfig/usage_test.go b/vendor/github.com/kelseyhightower/envconfig/usage_test.go new file mode 100644 index 000000000..b433d1982 --- /dev/null +++ b/vendor/github.com/kelseyhightower/envconfig/usage_test.go @@ -0,0 +1,155 @@ +// Copyright (c) 2016 Kelsey Hightower and others. All rights reserved. +// Use of this source code is governed by the MIT License that can be found in +// the LICENSE file. + +package envconfig + +import ( + "bytes" + "io" + "io/ioutil" + "log" + "os" + "strings" + "testing" + "text/tabwriter" +) + +var testUsageTableResult, testUsageListResult, testUsageCustomResult, testUsageBadFormatResult string + +func TestMain(m *testing.M) { + + // Load the expected test results from a text file + data, err := ioutil.ReadFile("testdata/default_table.txt") + if err != nil { + log.Fatal(err) + } + testUsageTableResult = string(data) + + data, err = ioutil.ReadFile("testdata/default_list.txt") + if err != nil { + log.Fatal(err) + } + testUsageListResult = string(data) + + data, err = ioutil.ReadFile("testdata/custom.txt") + if err != nil { + log.Fatal(err) + } + testUsageCustomResult = string(data) + + data, err = ioutil.ReadFile("testdata/fault.txt") + if err != nil { + log.Fatal(err) + } + testUsageBadFormatResult = string(data) + + retCode := m.Run() + os.Exit(retCode) +} + +func compareUsage(want, got string, t *testing.T) { + got = strings.Replace(got, " ", ".", -1) + if want != got { + shortest := len(want) + if len(got) < shortest { + shortest = len(got) + } + if len(want) != len(got) { + t.Errorf("expected result length of %d, found %d", len(want), len(got)) + } + for i := 0; i < shortest; i++ { + if want[i] != got[i] { + t.Errorf("difference at index %d, expected '%c' (%v), found '%c' (%v)\n", + i, want[i], want[i], got[i], got[i]) + break + } + } + t.Errorf("Complete Expected:\n'%s'\nComplete Found:\n'%s'\n", want, got) + } +} + +func TestUsageDefault(t *testing.T) { + var s Specification + os.Clearenv() + save := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + err := Usage("env_config", &s) + outC := make(chan string) + // copy the output in a separate goroutine so printing can't block indefinitely + go func() { + var buf bytes.Buffer + io.Copy(&buf, r) + outC <- buf.String() + }() + w.Close() + os.Stdout = save // restoring the real stdout + out := <-outC + + if err != nil { + t.Error(err.Error()) + } + compareUsage(testUsageTableResult, out, t) +} + +func TestUsageTable(t *testing.T) { + var s Specification + os.Clearenv() + buf := new(bytes.Buffer) + tabs := tabwriter.NewWriter(buf, 1, 0, 4, ' ', 0) + err := Usagef("env_config", &s, tabs, DefaultTableFormat) + tabs.Flush() + if err != nil { + t.Error(err.Error()) + } + compareUsage(testUsageTableResult, buf.String(), t) +} + +func TestUsageList(t *testing.T) { + var s Specification + os.Clearenv() + buf := new(bytes.Buffer) + err := Usagef("env_config", &s, buf, DefaultListFormat) + if err != nil { + t.Error(err.Error()) + } + compareUsage(testUsageListResult, buf.String(), t) +} + +func TestUsageCustomFormat(t *testing.T) { + var s Specification + os.Clearenv() + buf := new(bytes.Buffer) + err := Usagef("env_config", &s, buf, "{{range .}}{{usage_key .}}={{usage_description .}}\n{{end}}") + if err != nil { + t.Error(err.Error()) + } + compareUsage(testUsageCustomResult, buf.String(), t) +} + +func TestUsageUnknownKeyFormat(t *testing.T) { + var s Specification + unknownError := "template: envconfig:1:2: executing \"envconfig\" at <.UnknownKey>" + os.Clearenv() + buf := new(bytes.Buffer) + err := Usagef("env_config", &s, buf, "{{.UnknownKey}}") + if err == nil { + t.Errorf("expected 'unknown key' error, but got no error") + } + if strings.Index(err.Error(), unknownError) == -1 { + t.Errorf("expected '%s', but got '%s'", unknownError, err.Error()) + } +} + +func TestUsageBadFormat(t *testing.T) { + var s Specification + os.Clearenv() + // If you don't use two {{}} then you get a lieteral + buf := new(bytes.Buffer) + err := Usagef("env_config", &s, buf, "{{range .}}{.Key}\n{{end}}") + if err != nil { + t.Error(err.Error()) + } + compareUsage(testUsageBadFormatResult, buf.String(), t) +}