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:
Родитель
dd31285e2d
Коммит
97ab47bd94
|
@ -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
|
||||
|
@ -220,4 +252,3 @@ workflows:
|
|||
filters:
|
||||
branches:
|
||||
only: master
|
||||
|
||||
|
|
18
Makefile
18
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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}
|
|
@ -0,0 +1,7 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- tip
|
|
@ -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.
|
|
@ -0,0 +1,2 @@
|
|||
Kelsey Hightower kelsey.hightower@gmail.com github.com/kelseyhightower
|
||||
Travis Parker travis.parker@gmail.com github.com/teepark
|
|
@ -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.
|
|
@ -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
|
|
@ -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
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
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
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 = "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
|
||||
}
|
30
vendor/github.com/kelseyhightower/envconfig/testdata/custom.txt
сгенерированный
поставляемый
Normal file
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
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
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
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}
|
|
@ -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
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)
|
||||
}
|
Загрузка…
Ссылка в новой задаче