Delivers a `make test-kubernetes` ginkgo e2e test (#1256)

* delivers a `make test-kubernetes` ginkgo e2e test

with circle config

* chore(make) make test needs to skip e2e dirs

* using ginkgo coverage script

* chmod +x scripts/ginkgo.coverage.sh

* added ginkgo-k8s-e2e to circleci jobs options

* include test.mk

* chmod +x test/e2e/runner

* split out into two circleci workflows

* master only test filter and tab correction

* missing colon after test expansion

* undo

* removing azure region from code
This commit is contained in:
Jack Francis 2017-08-16 13:19:41 -07:00 коммит произвёл GitHub
Родитель dd31285e2d
Коммит 97ab47bd94
34 изменённых файлов: 3138 добавлений и 4 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -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

2
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

Просмотреть файл

@ -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

52
scripts/ginkgo.coverage.sh Executable file
Просмотреть файл

@ -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

26
test.mk Normal file
Просмотреть файл

@ -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

165
test/e2e/azure/cli.go Normal file
Просмотреть файл

@ -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
}

57
test/e2e/config/config.go Normal file
Просмотреть файл

@ -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
}

22
test/e2e/engine/cli.go Normal file
Просмотреть файл

@ -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
}

126
test/e2e/engine/template.go Normal file
Просмотреть файл

@ -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
}

Просмотреть файл

@ -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]
}

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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")
}

Просмотреть файл

@ -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)"))
})
})

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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
}
}
}

Просмотреть файл

@ -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
}
}
}

39
test/e2e/runner Executable file
Просмотреть файл

@ -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}

7
vendor/github.com/kelseyhightower/envconfig/.travis.yml сгенерированный поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
language: go
go:
- 1.4
- 1.5
- 1.6
- tip

19
vendor/github.com/kelseyhightower/envconfig/LICENSE сгенерированный поставляемый Normal file
Просмотреть файл

@ -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.

2
vendor/github.com/kelseyhightower/envconfig/MAINTAINERS сгенерированный поставляемый Normal file
Просмотреть файл

@ -0,0 +1,2 @@
Kelsey Hightower kelsey.hightower@gmail.com github.com/kelseyhightower
Travis Parker travis.parker@gmail.com github.com/teepark

188
vendor/github.com/kelseyhightower/envconfig/README.md сгенерированный поставляемый Normal file
Просмотреть файл

@ -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.

8
vendor/github.com/kelseyhightower/envconfig/doc.go сгенерированный поставляемый Normal file
Просмотреть файл

@ -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

7
vendor/github.com/kelseyhightower/envconfig/env_os.go сгенерированный поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
// +build appengine
package envconfig
import "os"
var lookupEnv = os.LookupEnv

7
vendor/github.com/kelseyhightower/envconfig/env_syscall.go сгенерированный поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
// +build !appengine
package envconfig
import "syscall"
var lookupEnv = syscall.Getenv

319
vendor/github.com/kelseyhightower/envconfig/envconfig.go сгенерированный поставляемый Normal file
Просмотреть файл

@ -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
}

688
vendor/github.com/kelseyhightower/envconfig/envconfig_test.go сгенерированный поставляемый Normal file
Просмотреть файл

@ -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 <nil>, 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 = &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 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
}

30
vendor/github.com/kelseyhightower/envconfig/testdata/custom.txt сгенерированный поставляемый Normal file
Просмотреть файл

@ -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=

153
vendor/github.com/kelseyhightower/envconfig/testdata/default_list.txt сгенерированный поставляемый Normal file
Просмотреть файл

@ -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]....

34
vendor/github.com/kelseyhightower/envconfig/testdata/default_table.txt сгенерированный поставляемый Normal file
Просмотреть файл

@ -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..........................................................................

30
vendor/github.com/kelseyhightower/envconfig/testdata/fault.txt сгенерированный поставляемый Normal file
Просмотреть файл

@ -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}

158
vendor/github.com/kelseyhightower/envconfig/usage.go сгенерированный поставляемый Normal file
Просмотреть файл

@ -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)
}

155
vendor/github.com/kelseyhightower/envconfig/usage_test.go сгенерированный поставляемый Normal file
Просмотреть файл

@ -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)
}