diff --git a/go.mod b/go.mod index 59d03738777..81071562a33 100644 --- a/go.mod +++ b/go.mod @@ -15,17 +15,22 @@ require ( github.com/json-iterator/go v1.1.9 // indirect github.com/kelseyhightower/envconfig v1.4.0 github.com/mailru/easyjson v0.7.1-0.20191009090205-6c0755d89d1e // indirect + github.com/mitchellh/go-homedir v1.1.0 github.com/openzipkin/zipkin-go v0.2.2 + github.com/pelletier/go-toml v1.8.0 + github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 github.com/robfig/cron/v3 v3.0.1 github.com/rogpeppe/fastuuid v1.2.0 github.com/rogpeppe/go-internal v1.5.0 // indirect github.com/stretchr/testify v1.5.1 github.com/tsenart/vegeta v12.7.1-0.20190725001342-b5f4fca92137+incompatible + github.com/wavesoftware/go-ensure v1.0.0 go.opencensus.io v0.22.3 go.opentelemetry.io/otel v0.2.3 go.uber.org/atomic v1.6.0 go.uber.org/zap v1.14.1 + golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a gomodules.xyz/jsonpatch/v2 v2.1.0 // indirect google.golang.org/grpc v1.28.0 k8s.io/api v0.17.3 diff --git a/go.sum b/go.sum index dab9d8f7809..c93c2890a8b 100644 --- a/go.sum +++ b/go.sum @@ -763,8 +763,12 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.3.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= +github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= @@ -910,6 +914,8 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/vdemeester/k8s-pkg-credentialprovider v0.0.0-20200107171650-7c61ffa44238/go.mod h1:JwQJCMWpUDqjZrB5jpw0f5VbN7U95zxFy1ZDpoEarGo= github.com/vdemeester/k8s-pkg-credentialprovider v1.13.12-1/go.mod h1:Fko0rTxEtDW2kju5Ky7yFJNS3IcNvW8IPsp4/e9oev0= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= +github.com/wavesoftware/go-ensure v1.0.0 h1:6X3gQL5psBWwtu/H9a+69xQ+JGTUELaLhgOB/iB3AQk= +github.com/wavesoftware/go-ensure v1.0.0/go.mod h1:K2UAFSwMTvpiRGay/M3aEYYuurcR8S4A6HkQlJPV8k4= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= @@ -1353,6 +1359,8 @@ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= helm.sh/helm/v3 v3.1.1/go.mod h1:WYsFJuMASa/4XUqLyv54s0U/f3mlAaRErGmyy4z921g= diff --git a/hack/update-deps.sh b/hack/update-deps.sh index 980d88f780f..85b5f808894 100755 --- a/hack/update-deps.sh +++ b/hack/update-deps.sh @@ -14,8 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -readonly ROOT_DIR=$(dirname $0)/.. -source ${ROOT_DIR}/vendor/knative.dev/test-infra/scripts/library.sh +readonly ROOT_DIR=$(dirname "$0")/.. +source "${ROOT_DIR}/vendor/knative.dev/test-infra/scripts/library.sh" set -o errexit set -o nounset @@ -26,7 +26,7 @@ export GO111MODULE=on # This controls the release branch we track. VERSION="master" -cd ${ROOT_DIR} +cd "${ROOT_DIR}" # The list of dependencies that we track at HEAD and periodically # float forward in this repository. @@ -55,8 +55,8 @@ fi go mod tidy go mod vendor -rm -rf $(find vendor/ -name 'OWNERS') -rm -rf $(find vendor/ -name '*_test.go') +find vendor/ -name 'OWNERS' -delete +find vendor/ -name '*_test.go'-delete export GOFLAGS=-mod=vendor diff --git a/test/e2e-common.sh b/test/e2e-common.sh index 27e3c6cfb1c..3a143bd38c7 100755 --- a/test/e2e-common.sh +++ b/test/e2e-common.sh @@ -21,7 +21,7 @@ export GO111MODULE=on source $(dirname $0)/../vendor/knative.dev/test-infra/scripts/e2e-tests.sh # If gcloud is not available make it a no-op, not an error. -which gcloud &> /dev/null || gcloud() { echo "[ignore-gcloud $*]" 1>&2; } +which gcloud &>/dev/null || gcloud() { echo "[ignore-gcloud $*]" 1>&2; } # Use GNU tools on macOS. Requires the 'grep' and 'gnu-sed' Homebrew formulae. if [ "$(uname)" == "Darwin" ]; then @@ -43,19 +43,67 @@ readonly MT_CHANNEL_BASED_BROKER_DEFAULT_CONFIG="test/config/mt-channel-broker.y # Channel Based Broker Controller. readonly CHANNEL_BASED_BROKER_CONTROLLER="config/brokers/channel-broker" -# Setup the Knative environment for running tests. This installs -# Everything from the config dir but then removes the Channel Based Broker. -# TODO: This should only install the core. +# Should deploy a Knative Monitoring as well +readonly DEPLOY_KNATIVE_MONITORING="${DEPLOY_KNATIVE_MONITORING:-1}" + +# Latest release. If user does not supply this as a flag, the latest +# tagged release on the current branch will be used. +readonly LATEST_RELEASE_VERSION=$(git describe --match "v[0-9]*" --abbrev=0) + +UNINSTALL_LIST=() + +# Setup the Knative environment for running tests. function knative_setup() { - # Install the latest Knative/eventing in the current cluster. - echo ">> Starting Knative Eventing" - echo "Installing Knative Eventing" - ko apply --strict -f ${EVENTING_CONFIG} || return 1 + install_knative_eventing +} + +# This installs everything from the config dir but then removes the Channel Based Broker. +# TODO: This should only install the core. +# Args: +# - $1 - if passed, it will be used as eventing config directory +function install_knative_eventing() { + local kne_config + kne_config="${1:-${EVENTING_CONFIG}}" + # Install Knative Eventing in the current cluster. + echo "Installing Knative Eventing from: ${kne_config}" + if [ -f "${kne_config}" ] || [ -d "${kne_config}" ]; then + ko apply --strict -f "${kne_config}" || return $? + else + kubectl apply -f "${kne_config}" || return $? + UNINSTALL_LIST+=( "${kne_config}" ) + fi wait_until_pods_running knative-eventing || fail_test "Knative Eventing did not come up" - echo "Installing Knative Monitoring" - start_knative_monitoring "${KNATIVE_MONITORING_RELEASE}" || fail_test "Knative Monitoring did not come up" + if ! (( DEPLOY_KNATIVE_MONITORING )); then return 0; fi + + # Ensure knative monitoring is installed only once + knative_monitoring_pods=$(kubectl get pods -n knative-monitoring \ + --field-selector status.phase=Running 2> /dev/null | tail -n +2 | wc -l) + if ! [[ ${knative_monitoring_pods} -gt 0 ]]; then + echo ">> Installing Knative Monitoring" + start_knative_monitoring "${KNATIVE_MONITORING_RELEASE}" || fail_test "Knative Monitoring did not come up" + UNINSTALL_LIST+=( "${KNATIVE_MONITORING_RELEASE}" ) + else + echo ">> Knative Monitoring seems to be running, pods running: ${knative_monitoring_pods}." + fi +} + +function install_head { + # Install Knative Eventing from HEAD in the current cluster. + echo ">> Installing Knative Eventing from HEAD" + install_knative_eventing || \ + fail_test "Knative HEAD installation failed" +} + +function install_latest_release() { + header ">> Installing Knative Eventing latest public release" + local url="https://github.com/knative/eventing/releases/download/${LATEST_RELEASE_VERSION}" + local yaml="eventing.yaml" + + install_knative_eventing \ + "${url}/${yaml}" || \ + fail_test "Knative latest release installation failed" } function install_broker() { @@ -81,6 +129,14 @@ function knative_teardown() { echo "Uninstalling Knative Eventing" ko delete --ignore-not-found=true --now --timeout 60s -f ${EVENTING_CONFIG} wait_until_object_does_not_exist namespaces knative-eventing + + echo ">> Uninstalling dependencies" + for i in ${!UNINSTALL_LIST[@]}; do + # We uninstall elements in the reverse of the order they were installed. + local YAML="${UNINSTALL_LIST[$(( ${#array[@]} - $i ))]}" + echo ">> Bringing down YAML: ${YAML}" + kubectl delete --ignore-not-found=true -f "${YAML}" || return 1 + done } # Add function call to trap @@ -102,20 +158,20 @@ function test_setup() { echo ">> Setting up logging..." # Install kail if needed. - if ! which kail > /dev/null; then - bash <( curl -sfL https://raw.githubusercontent.com/boz/kail/master/godownloader.sh) -b "$GOPATH/bin" + if ! which kail >/dev/null; then + bash <(curl -sfL https://raw.githubusercontent.com/boz/kail/master/godownloader.sh) -b "$GOPATH/bin" fi # Capture all logs. - kail > ${ARTIFACTS}/k8s.log.txt & + kail >${ARTIFACTS}/k8s.log.txt & local kail_pid=$! # Clean up kail so it doesn't interfere with job shutting down add_trap "kill $kail_pid || true" EXIT install_test_resources || return 1 - # Publish test images. - $(dirname $0)/upload-test-images.sh e2e || fail_test "Error uploading test images" + echo ">> Publish test images" + "$(dirname "$0")/upload-test-images.sh" e2e || fail_test "Error uploading test images" } # Tear down resources used in the eventing tests. @@ -146,7 +202,7 @@ function dump_extra_cluster_state() { # Collecting logs from all knative's eventing pods. echo "============================================================" local namespace="knative-eventing" - for pod in $(kubectl get pod -n $namespace | grep Running | awk '{print $1}' ); do + for pod in $(kubectl get pod -n $namespace | grep Running | awk '{print $1}'); do for container in $(kubectl get pod "${pod}" -n $namespace -ojsonpath='{.spec.containers[*].name}'); do echo "Namespace, Pod, Container: ${namespace}, ${pod}, ${container}" kubectl logs -n $namespace "${pod}" -c "${container}" || true @@ -157,3 +213,26 @@ function dump_extra_cluster_state() { done done } + +function wait_for_file() { + local file timeout waits + file="$1" + waits=300 + timeout=$waits + + echo "Waiting for existance of file: ${file}" + + while [ ! -f "${file}" ]; do + # When the timeout is equal to zero, show an error and leave the loop. + if [ "${timeout}" == 0 ]; then + echo "ERROR: Timeout (${waits}s) while waiting for the file ${file}." + return 1 + fi + + sleep 1 + + # Decrease the timeout of one + ((timeout--)) + done + return 0 +} diff --git a/test/e2e-tests.sh b/test/e2e-tests.sh index a3d218f0cc0..00ac825e3a3 100755 --- a/test/e2e-tests.sh +++ b/test/e2e-tests.sh @@ -26,7 +26,7 @@ export GO111MODULE=on -source $(dirname $0)/e2e-common.sh +source "$(dirname "$0")/e2e-common.sh" # Script entry point. diff --git a/test/e2e-upgrade-tests.sh b/test/e2e-upgrade-tests.sh new file mode 100755 index 00000000000..fdc55c205df --- /dev/null +++ b/test/e2e-upgrade-tests.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# Copyright 2020 The Knative Authors +# +# 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. + +# Docs -> file://./upgrade/README.md + +# Script entry point. + +export GO111MODULE=on + +source "$(dirname "$0")/e2e-common.sh" + +# Overrides + +function knative_setup { + install_latest_release || fail_test 'Installing latest release of Knative Eventing failed' +} + +function install_test_resources { + # Nothing to install before tests + true +} + +function uninstall_test_resources { + # Nothing to uninstall after tests + true +} + +initialize $@ --skip-istio-addon + +TIMEOUT=${TIMEOUT:-30m} + +header "Running preupgrade tests" + +go_test_e2e -tags=preupgrade -timeout="${TIMEOUT}" ./test/upgrade || fail_test + +header "Starting prober test" +rm -fv /tmp/prober-ready +go_test_e2e -tags=probe -timeout="${TIMEOUT}" ./test/upgrade & +PROBER_PID=$! +echo "Prober PID is ${PROBER_PID}" + +wait_for_file /tmp/prober-ready || fail_test + +header "Performing upgrade to HEAD" +install_head || fail_test 'Installing HEAD version of eventing failed' +install_channel_crds || fail_test 'Installing HEAD channel CRDs failed' +install_broker || fail_test 'Installing HEAD Broker failed' + +header "Running postupgrade tests" +go_test_e2e -tags=postupgrade -timeout="${TIMEOUT}" ./test/upgrade || fail_test + +header "Performing downgrade to latest release" +install_latest_release || fail_test 'Installing latest release of Knative Eventing failed' + +header "Running postdowngrade tests" +go_test_e2e -tags=postdowngrade -timeout="${TIMEOUT}" ./test/upgrade || fail_test + +# The prober is blocking on /tmp/prober-signal to know when it should exit. +echo "done" > /tmp/prober-signal + +header "Waiting for prober test" +wait ${PROBER_PID} || fail_test "Prober failed" + +success diff --git a/test/e2e/channel_single_event_test.go b/test/e2e/channel_single_event_test.go index 4db274b8315..5562496c656 100644 --- a/test/e2e/channel_single_event_test.go +++ b/test/e2e/channel_single_event_test.go @@ -37,7 +37,7 @@ func TestSingleBinaryEventForChannel(t *testing.T) { helpers.SingleEventForChannelTestHelper( t, cloudevents.Binary, - "v1alpha1", + helpers.SubscriptionV1alpha1, "", channelTestRunner, ) @@ -47,7 +47,7 @@ func TestSingleStructuredEventForChannel(t *testing.T) { helpers.SingleEventForChannelTestHelper( t, cloudevents.Structured, - "v1alpha1", + helpers.SubscriptionV1alpha1, "", channelTestRunner, ) @@ -57,7 +57,7 @@ func TestSingleBinaryEventForChannelV1Beta1(t *testing.T) { helpers.SingleEventForChannelTestHelper( t, cloudevents.Binary, - "v1beta1", + helpers.SubscriptionV1beta1, "", channelTestRunner, ) @@ -67,7 +67,7 @@ func TestSingleBinaryEventForChannelV1Beta1SubscribeToV1Alpha1(t *testing.T) { helpers.SingleEventForChannelTestHelper( t, cloudevents.Binary, - "v1beta1", + helpers.SubscriptionV1beta1, "messaging.knative.dev/v1alpha1", channelTestRunner, ) @@ -77,7 +77,7 @@ func TestSingleStructuredEventForChannelV1Beta1(t *testing.T) { helpers.SingleEventForChannelTestHelper( t, cloudevents.Structured, - "v1beta1", + helpers.SubscriptionV1beta1, "", channelTestRunner, ) diff --git a/test/e2e/helpers/channel_single_event_helper.go b/test/e2e/helpers/channel_single_event_helper.go index b2210a7f715..80883b34853 100644 --- a/test/e2e/helpers/channel_single_event_helper.go +++ b/test/e2e/helpers/channel_single_event_helper.go @@ -28,11 +28,11 @@ import ( "knative.dev/eventing/test/lib/resources" ) -type subscriptionVersion string +type SubscriptionVersion string const ( - subscriptionV1alpha1 subscriptionVersion = "v1alpha1" - subscriptionV1beta1 subscriptionVersion = "v1beta1" + SubscriptionV1alpha1 SubscriptionVersion = "v1alpha1" + SubscriptionV1beta1 SubscriptionVersion = "v1beta1" ) // SingleEventForChannelTestHelper is the helper function for channel_single_event_test @@ -42,7 +42,7 @@ const ( // channelVersion == "" means that the version of the channel subscribed to is not // modified. func SingleEventForChannelTestHelper(t *testing.T, encoding string, - subscriptionVersion subscriptionVersion, + subscriptionVersion SubscriptionVersion, channelVersion string, channelTestRunner lib.ChannelTestRunner, options ...lib.SetupClientOption) { @@ -70,14 +70,14 @@ func SingleEventForChannelTestHelper(t *testing.T, encoding string, } // create subscription to subscribe the channel, and forward the received events to the logger service switch subscriptionVersion { - case subscriptionV1alpha1: + case SubscriptionV1alpha1: client.CreateSubscriptionOrFail( subscriptionName, channelName, &channel, resources.WithSubscriberForSubscription(loggerPodName), ) - case subscriptionV1beta1: + case SubscriptionV1beta1: client.CreateSubscriptionOrFailV1Beta1( subscriptionName, channelName, diff --git a/test/lib/await.go b/test/lib/await.go new file mode 100644 index 00000000000..b02da0d4241 --- /dev/null +++ b/test/lib/await.go @@ -0,0 +1,89 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package lib + +import ( + "fmt" + "io/ioutil" + "net/http" + "time" + + "go.uber.org/zap" + "golang.org/x/sync/errgroup" + "k8s.io/apimachinery/pkg/util/wait" +) + +// WaitFor will register a wait routine to be resolved later +func WaitFor(name string, routine AwaitRoutine) { + w := &namedAwait{ + name: name, + routine: routine, + } + waits = append(waits, w) +} + +// AwaitForAll will wait until all registered wait routines resolves +func AwaitForAll(log *zap.SugaredLogger) error { + var g errgroup.Group + for _, w := range waits { + w := w // https://golang.org/doc/faq#closures_and_goroutines + g.Go(func() error { + log.Infof("Wait for %s", w.name) + before := time.Now() + err := w.routine() + took := time.Now().Sub(before) + if err != nil { + log.Errorf("Error while waiting for %s: %v", w.name, err) + } else { + log.Infof("Successful wait for %s, took %v to complete", w.name, took) + } + return err + }) + } + waits = nil + // Wait for all waits to complete. + if err := g.Wait(); err != nil { + return err + } + return nil +} + +// WaitForReadiness will wait until readiness endpoint reports OK +func WaitForReadiness(port int) error { + return wait.Poll(25*time.Millisecond, 5*time.Minute, func() (done bool, err error) { + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/healthz", port)) + if err != nil { + return false, err + } + defer func() { + _ = resp.Body.Close() + }() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return false, err + } + return resp.StatusCode == 200 && string(body) == "OK", nil + }) +} + +type AwaitRoutine func() error + +type namedAwait struct { + name string + routine AwaitRoutine +} + +var waits []*namedAwait diff --git a/test/lib/duck/resource_checks.go b/test/lib/duck/resource_checks.go index 7bdf25efbf3..32a51cc56fd 100644 --- a/test/lib/duck/resource_checks.go +++ b/test/lib/duck/resource_checks.go @@ -33,7 +33,6 @@ import ( ) const ( - // The interval and timeout used for polling in checking resource states. interval = 1 * time.Second timeout = 2 * time.Minute ) diff --git a/test/lib/duck/serving_checks.go b/test/lib/duck/serving_checks.go new file mode 100644 index 00000000000..123e6241c41 --- /dev/null +++ b/test/lib/duck/serving_checks.go @@ -0,0 +1,60 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package duck + +import ( + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/util/wait" + "knative.dev/eventing/test/lib/resources" + "knative.dev/pkg/test" +) + +// WaitForKServiceReady will wait until ksvc reports that's ready +func WaitForKServiceReady(client resources.ServingClient, name, namespace string) error { + meta := resources.NewMetaResource(name, namespace, &resources.KServiceType) + return WaitForResourceReady(client.Dynamic, meta) +} + +// WaitForKServiceScales will wait until ksvc scale is satisfied +func WaitForKServiceScales(client resources.ServingClient, name, namespace string, satisfyScale func(int) bool) error { + err := WaitForKServiceReady(client, name, namespace) + if err != nil { + return err + } + deploymentName, err := waitForKServiceDeploymentName(client, name, namespace) + if err != nil { + return err + } + inState := func(dep *appsv1.Deployment) (bool, error) { + return satisfyScale(int(dep.Status.ReadyReplicas)), nil + } + return test.WaitForDeploymentState( + client.Kube, deploymentName, inState, "scales", namespace, timeout, + ) +} + +func waitForKServiceDeploymentName(client resources.ServingClient, name, namespace string) (string, error) { + var deploymentName string + err := wait.PollImmediate(interval, timeout, func() (bool, error) { + dn, found, err := resources.KServiceDeploymentName(client, name, namespace) + if found { + deploymentName = dn + } + return found, err + }) + + return deploymentName, err +} diff --git a/test/lib/nodes/address.go b/test/lib/nodes/address.go new file mode 100644 index 00000000000..6512ac9e36b --- /dev/null +++ b/test/lib/nodes/address.go @@ -0,0 +1,68 @@ +/* + * Copyright 2020 The Knative Authors + * + * 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. + */ + +// TODO(ksuszyns): remove the whole package after knative/pkg#1001 is merged + +package nodes + +import ( + "sort" + + corev1 "k8s.io/api/core/v1" +) + +// GuessNodeExternalAddress tries to guess external address of a node +func (n *NodesClient) GuessNodeExternalAddress(node *corev1.Node) *corev1.NodeAddress { + sorted := make([]*corev1.NodeAddress, len(node.Status.Addresses)) + for i := 0; i < len(node.Status.Addresses); i++ { + sorted[i] = &node.Status.Addresses[i] + } + sort.Sort(byAddressType{sorted}) + first := sorted[0] + priority := addressTypePriority[first.Type] + if priority >= 2 { + n.logger.Warnf("Chosen address is probably an internal type: %s, "+ + "and might be unaccessible", first.Type) + } + return first +} + +type nodeAddresses []*corev1.NodeAddress + +type byAddressType struct { + nodeAddresses +} + +func (s byAddressType) Len() int { + return len(s.nodeAddresses) +} + +func (s byAddressType) Swap(i, j int) { + s.nodeAddresses[i], s.nodeAddresses[j] = s.nodeAddresses[j], s.nodeAddresses[i] +} + +func (s byAddressType) Less(i, j int) bool { + return addressTypePriority[s.nodeAddresses[i].Type] < + addressTypePriority[s.nodeAddresses[j].Type] +} + +var addressTypePriority = map[corev1.NodeAddressType]int{ + corev1.NodeExternalDNS: 0, + corev1.NodeExternalIP: 1, + corev1.NodeInternalIP: 2, + corev1.NodeInternalDNS: 3, + corev1.NodeHostName: 4, +} diff --git a/test/lib/nodes/address_test.go b/test/lib/nodes/address_test.go new file mode 100644 index 00000000000..1718c59fa6f --- /dev/null +++ b/test/lib/nodes/address_test.go @@ -0,0 +1,79 @@ +/* + * Copyright 2020 The Knative Authors + * + * 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. + */ + +// TODO(ksuszyns): remove the whole package after knative/pkg#1001 is merged + +package nodes + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + kubernetesfake "k8s.io/client-go/kubernetes/fake" +) + +func TestNodesClientGuessNodeExternalAddress(t *testing.T) { + clientset := kubernetesfake.NewSimpleClientset() + c := Client(clientset, newLogger()) + node := &corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{{ + Type: corev1.NodeInternalIP, + Address: "10.0.2.17", + }, { + Type: corev1.NodeHostName, + Address: "node-17", + }, { + Type: corev1.NodeInternalDNS, + Address: "node-17.europe3.internal", + }, { + Type: corev1.NodeExternalIP, + Address: "35.123.11.234", + }}, + }, + } + + address := c.GuessNodeExternalAddress(node) + + if address.Address != "35.123.11.234" { + t.Errorf("Address: %s want: 35.123.11.234", address.Address) + } +} + +func TestNodesClientGuessNodeExternalAddress_PrivateOnly(t *testing.T) { + clientset := kubernetesfake.NewSimpleClientset() + c := Client(clientset, newLogger()) + node := &corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{{ + Type: corev1.NodeInternalIP, + Address: "10.0.2.17", + }, { + Type: corev1.NodeHostName, + Address: "node-17", + }, { + Type: corev1.NodeInternalDNS, + Address: "node-17.europe3.internal", + }}, + }, + } + + address := c.GuessNodeExternalAddress(node) + + if address.Address != "10.0.2.17" { + t.Errorf("Address: %s want: 10.0.2.17", address.Address) + } +} diff --git a/test/lib/nodes/kind.go b/test/lib/nodes/kind.go new file mode 100644 index 00000000000..7cbe5a34527 --- /dev/null +++ b/test/lib/nodes/kind.go @@ -0,0 +1,47 @@ +/* + * Copyright 2020 The Knative Authors + * + * 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. + */ + +// TODO(ksuszyns): remove the whole package after knative/pkg#1001 is merged + +package nodes + +import ( + corev1 "k8s.io/api/core/v1" +) + +const ( + roleLabelFormat = "node-role.kubernetes.io/" +) + +// FilterOutByRole returns a new slice without nodes that have given role +func FilterOutByRole(nodes []corev1.Node, role string) []corev1.Node { + result := make([]corev1.Node, 0, len(nodes)) + for _, node := range nodes { + if !hasRole(role, node) { + result = append(result, node) + } + } + return result +} + +func hasRole(role string, node corev1.Node) bool { + if node.Labels == nil { + return false + } + label := roleLabelFormat + role + _, has := node.Labels[label] + return has +} diff --git a/test/lib/nodes/logger_test.go b/test/lib/nodes/logger_test.go new file mode 100644 index 00000000000..18928392fdf --- /dev/null +++ b/test/lib/nodes/logger_test.go @@ -0,0 +1,32 @@ +/* + * Copyright 2020 The Knative Authors + * + * 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. + */ + +// TODO(ksuszyns): remove the whole package after knative/pkg#1001 is merged + +package nodes + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func newLogger() *zap.SugaredLogger { + z, err := zap.NewDevelopment(zap.AddStacktrace(zapcore.ErrorLevel)) + if err != nil { + panic(err) + } + return z.Sugar() +} diff --git a/test/lib/nodes/nodes.go b/test/lib/nodes/nodes.go new file mode 100644 index 00000000000..a4259eca7b2 --- /dev/null +++ b/test/lib/nodes/nodes.go @@ -0,0 +1,69 @@ +/* + * Copyright 2020 The Knative Authors + * + * 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. + */ + +// TODO(ksuszyns): remove the whole package after knative/pkg#1001 is merged + +package nodes + +import ( + "errors" + "math/rand" + + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// NodesClient contains interface for making requests to kubernetes client. +type NodesClient struct { + kube kubernetes.Interface + logger *zap.SugaredLogger +} + +// Client creates a new nodes client +func Client(kube kubernetes.Interface, logger *zap.SugaredLogger) *NodesClient { + return &NodesClient{ + kube: kube, + logger: logger, + } +} + +// RandomWorkerNode gets a worker node randomly +func (n *NodesClient) RandomWorkerNode() (*corev1.Node, error) { + nodes, err := n.kube.CoreV1().Nodes().List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + nodesCount := len(nodes.Items) + if nodesCount == 0 { + return nil, errors.New("fetched 0 nodes, so can't chose a worker") + } + if nodesCount == 1 { + node := nodes.Items[0] + n.logger.Infof("Only one node found (named: %s), returning it as"+ + " it must be a worker (Minikube, CRC)", node.Name) + return &node, nil + } else { + role := "master" + n.logger.Infof("Filtering %d nodes, to not contain role: %s", nodesCount, role) + workers := FilterOutByRole(nodes.Items, role) + n.logger.Infof("Found %d worker(s)", len(workers)) + worker := workers[rand.Intn(len(workers))] + n.logger.Infof("Chosen node: %s", worker.Name) + return &worker, nil + } +} diff --git a/test/lib/nodes/nodes_test.go b/test/lib/nodes/nodes_test.go new file mode 100644 index 00000000000..2d4c49417a1 --- /dev/null +++ b/test/lib/nodes/nodes_test.go @@ -0,0 +1,99 @@ +/* + * Copyright 2020 The Knative Authors + * + * 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. + */ + +// TODO(ksuszyns): remove the whole package after knative/pkg#1001 is merged + +package nodes + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kubernetesfake "k8s.io/client-go/kubernetes/fake" +) + +func TestNodesClientRandomWorkerNode(t *testing.T) { + masterNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "master-1", + Labels: map[string]string{ + roleLabelFormat + "master": "", + }, + }, + } + workerNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "worker-1", + }, + } + clientset := kubernetesfake.NewSimpleClientset(masterNode, workerNode) + c := Client(clientset, newLogger()) + + node, err := c.RandomWorkerNode() + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + nodeName := workerNode.Name + assertProperNodeIsReturned(t, node, nodeName) +} + +func TestNodesClientRandomWorkerNode_OneNode(t *testing.T) { + nodeName := "minikube" + minikube := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName, + Labels: map[string]string{ + roleLabelFormat + "master": "", + }, + }, + } + clientset := kubernetesfake.NewSimpleClientset(minikube) + c := Client(clientset, newLogger()) + + node, err := c.RandomWorkerNode() + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + assertProperNodeIsReturned(t, node, nodeName) +} + +func TestNodesClientRandomWorkerNode_NoNode(t *testing.T) { + clientset := kubernetesfake.NewSimpleClientset() + c := Client(clientset, newLogger()) + + _, err := c.RandomWorkerNode() + + if err == nil { + t.Fatal("Expected error, but not received") + } + actual := err.Error() + if actual != "fetched 0 nodes, so can't chose a worker" { + t.Errorf("Unexpected error: %s", actual) + } +} + +func assertProperNodeIsReturned(t *testing.T, node *corev1.Node, nodeName string) { + t.Helper() + if node == nil { + t.Fatal("Expect not to be nil") + } + if node.Name != nodeName { + t.Errorf("Got = %v, want: %s", node, nodeName) + } +} diff --git a/test/lib/resources/constants.go b/test/lib/resources/constants.go index e1021813933..6d64baa7807 100644 --- a/test/lib/resources/constants.go +++ b/test/lib/resources/constants.go @@ -16,6 +16,11 @@ limitations under the License. package resources +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + // SystemNamespace is the namespace where Eventing is installed, it's default to be knative-eventing. const SystemNamespace = "knative-eventing" @@ -33,6 +38,20 @@ const ( KServiceKind string = "Service" ) +var ( + // KServicesGVR is GroupVersionResource for Knative Service + KServicesGVR = schema.GroupVersionResource{ + Group: "serving.knative.dev", + Version: "v1alpha1", + Resource: "services", + } + // KServiceType is type of Knative Service + KServiceType = metav1.TypeMeta{ + Kind: "Service", + APIVersion: KServicesGVR.GroupVersion().String(), + } +) + // Kind for core Kubernetes resources. const ( ServiceKind string = "Service" diff --git a/test/lib/resources/serving.go b/test/lib/resources/serving.go index 86bd110a137..11cbab1800a 100644 --- a/test/lib/resources/serving.go +++ b/test/lib/resources/serving.go @@ -19,11 +19,118 @@ package resources // This file contains functions that construct Serving resources. import ( + "fmt" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" + eventingv1alpha1 "knative.dev/eventing/pkg/apis/eventing/v1alpha1" + duckv1 "knative.dev/pkg/apis/duck/v1" pkgTest "knative.dev/pkg/test" ) +// ServingClient holds clients required to get serving resources +type ServingClient struct { + Kube *pkgTest.KubeClient + Dynamic dynamic.Interface +} + +// KServiceRoute represents ksvc route, so how much traffic is routed to given deployment +type KServiceRoute struct { + TrafficPercent uint8 + DeploymentName string +} + +// WithSubscriberKServiceRefForTrigger returns an option that adds a Subscriber +// Knative Service Ref for the given Trigger. +func WithSubscriberKServiceRefForTrigger(name string) TriggerOption { + return func(t *eventingv1alpha1.Trigger) { + if name != "" { + t.Spec.Subscriber = duckv1.Destination{ + Ref: KnativeRefForKservice(name, t.Namespace), + } + } + } +} + +// KnativeRefForKservice return a duck reference for Knative Service +func KnativeRefForKservice(name, namespace string) *duckv1.KReference { + return &duckv1.KReference{ + Kind: KServiceKind, + APIVersion: ServingAPIVersion, + Name: name, + Namespace: namespace, + } +} + // KServiceRef returns a Knative Service ObjectReference for a given Service name. func KServiceRef(name string) *corev1.ObjectReference { return pkgTest.CoreV1ObjectReference(KServiceKind, ServingAPIVersion, name) } + +// KServiceRoutes gets routes of given ksvc. +// If ksvc isn't ready yet second return value will be false. +func KServiceRoutes(client ServingClient, name, namespace string) ([]KServiceRoute, bool, error) { + serving := client.Dynamic.Resource(KServicesGVR).Namespace(namespace) + unstruct, err := serving.Get(name, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + // Return false as we are not done yet. + // We swallow the error to keep on polling. + // It should only happen if we wait for the auto-created resources, like default Broker. + return nil, false, nil + } else if err != nil { + // Return error to stop the polling. + return nil, false, err + } + + routes, ready := ksvcRoutes(unstruct) + return routes, ready, nil +} + +// KServiceDeploymentName returns a name of deployment of Knative Service that +// receives 100% of traffic. +// If ksvc isn't ready yet second return value will be false. +func KServiceDeploymentName(client ServingClient, name, namespace string) (string, bool, error) { + routes, ready, err := KServiceRoutes(client, name, namespace) + if ready { + if len(routes) > 1 { + return "", false, fmt.Errorf("traffic shouldn't be split to more then 1 revision: %v", routes) + } + r := routes[0] + return r.DeploymentName, true, nil + } + + return "", ready, err +} + +func ksvcRoutes(un *unstructured.Unstructured) ([]KServiceRoute, bool) { + routes := make([]KServiceRoute, 0) + content := un.UnstructuredContent() + maybeStatus, ok := content["status"] + if !ok { + return routes, false + } + status := maybeStatus.(map[string]interface{}) + maybeTraffic, ok := status["traffic"] + if !ok { + return routes, false + } + traffic := maybeTraffic.([]interface{}) + if len(traffic) == 0 { + // continue to wait + return routes, false + } + for _, uRoute := range traffic { + route := uRoute.(map[string]interface{}) + revisionName := route["revisionName"].(string) + percent := uint8(route["percent"].(int64)) + deploymentName := fmt.Sprintf("%s-deployment", revisionName) + routes = append(routes, KServiceRoute{ + TrafficPercent: percent, + DeploymentName: deploymentName, + }) + } + return routes, true +} diff --git a/test/test_images/wathola-forwarder/main.go b/test/test_images/wathola-forwarder/main.go new file mode 100644 index 00000000000..5f45b2be67e --- /dev/null +++ b/test/test_images/wathola-forwarder/main.go @@ -0,0 +1,25 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package main + +import "knative.dev/eventing/test/upgrade/prober/wathola/forwarder" + +var instance forwarder.Forwarder + +func main() { + instance = forwarder.New() + instance.Forward() +} diff --git a/test/test_images/wathola-forwarder/main_test.go b/test/test_images/wathola-forwarder/main_test.go new file mode 100644 index 00000000000..28ffffa3f79 --- /dev/null +++ b/test/test_images/wathola-forwarder/main_test.go @@ -0,0 +1,39 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package main + +import ( + "testing" + + "github.com/phayes/freeport" + "github.com/stretchr/testify/assert" + "go.uber.org/zap/zapcore" + "knative.dev/eventing/test/lib" + "knative.dev/eventing/test/upgrade/prober/wathola/config" + "knative.dev/eventing/test/upgrade/prober/wathola/forwarder" +) + +func TestForwarderMain(t *testing.T) { + port := freeport.GetPort() + config.Instance.LogLevel = zapcore.DebugLevel + config.Instance.Forwarder.Port = port + go main() + cancel := <-forwarder.Canceling + err := lib.WaitForReadiness(port) + assert.NoError(t, err) + assert.NotNil(t, instance) + cancel() +} diff --git a/test/test_images/wathola-forwarder/pod.yaml b/test/test_images/wathola-forwarder/pod.yaml new file mode 100644 index 00000000000..33e872919fe --- /dev/null +++ b/test/test_images/wathola-forwarder/pod.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Pod +metadata: + name: wathola-forwarder +spec: + containers: + - name: wathola-forwarder + image: ko://knative.dev/eventing/test/test_images/wathola-forwarder diff --git a/test/test_images/wathola-receiver/main.go b/test/test_images/wathola-receiver/main.go new file mode 100644 index 00000000000..719946cf360 --- /dev/null +++ b/test/test_images/wathola-receiver/main.go @@ -0,0 +1,25 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package main + +import "knative.dev/eventing/test/upgrade/prober/wathola/receiver" + +var instance receiver.Receiver + +func main() { + instance = receiver.New() + instance.Receive() +} diff --git a/test/test_images/wathola-receiver/main_test.go b/test/test_images/wathola-receiver/main_test.go new file mode 100644 index 00000000000..98116ccb946 --- /dev/null +++ b/test/test_images/wathola-receiver/main_test.go @@ -0,0 +1,41 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package main + +import ( + "testing" + + "github.com/phayes/freeport" + "go.uber.org/zap/zapcore" + "knative.dev/eventing/test/lib" + "knative.dev/eventing/test/upgrade/prober/wathola/config" + "knative.dev/eventing/test/upgrade/prober/wathola/receiver" + + "github.com/stretchr/testify/assert" +) + +func TestReceiverMain(t *testing.T) { + port := freeport.GetPort() + config.Instance.LogLevel = zapcore.DebugLevel + config.Instance.Receiver.Port = port + go main() + cancel := <-receiver.Canceling + err := lib.WaitForReadiness(port) + assert.NoError(t, err) + assert.NotNil(t, instance) + + cancel() +} diff --git a/test/test_images/wathola-receiver/pod.yaml b/test/test_images/wathola-receiver/pod.yaml new file mode 100644 index 00000000000..860e97a4c36 --- /dev/null +++ b/test/test_images/wathola-receiver/pod.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Pod +metadata: + name: wathola-receiver +spec: + containers: + - name: wathola-receiver + image: ko://knative.dev/eventing/test/test_images/wathola-receiver diff --git a/test/test_images/wathola-sender/main.go b/test/test_images/wathola-sender/main.go new file mode 100644 index 00000000000..21e97c42fa6 --- /dev/null +++ b/test/test_images/wathola-sender/main.go @@ -0,0 +1,22 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package main + +import "knative.dev/eventing/test/upgrade/prober/wathola/sender" + +func main() { + sender.New().SendContinually() +} diff --git a/test/test_images/wathola-sender/main_test.go b/test/test_images/wathola-sender/main_test.go new file mode 100644 index 00000000000..0aa3bc8fc7e --- /dev/null +++ b/test/test_images/wathola-sender/main_test.go @@ -0,0 +1,58 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package main + +import ( + "fmt" + "net" + "net/http" + "net/http/httptest" + "syscall" + "testing" + "time" + + "github.com/wavesoftware/go-ensure" + "knative.dev/eventing/test/upgrade/prober/wathola/config" +) + +func TestSenderMain(t *testing.T) { + ts := createMockServer() + defer ts.Close() + + p := syscall.Getpid() + go main() + time.Sleep(500 * time.Millisecond) + err := syscall.Kill(p, syscall.SIGTERM) + ensure.NoError(err) + time.Sleep(500 * time.Millisecond) +} + +func createMockServer() *httptest.Server { + l, err := net.Listen("tcp", + fmt.Sprintf(":%d", config.DefaultForwarderPort), + ) + ensure.NoError(err) + ts := &httptest.Server{ + Listener: l, + Config: &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + }), + }, + } + ts.Start() + return ts +} diff --git a/test/test_images/wathola-sender/pod.yaml b/test/test_images/wathola-sender/pod.yaml new file mode 100644 index 00000000000..cd9faf120e2 --- /dev/null +++ b/test/test_images/wathola-sender/pod.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Pod +metadata: + name: wathola-sender +spec: + containers: + - name: wathola-sender + image: ko://knative.dev/eventing/test/test_images/wathola-sender diff --git a/test/upgrade/README.md b/test/upgrade/README.md new file mode 100644 index 00000000000..c7a0c391124 --- /dev/null +++ b/test/upgrade/README.md @@ -0,0 +1,83 @@ +# Upgrade Tests + +In order to get coverage for the upgrade process from an operator’s perspective, +we need an additional suite of tests that perform a complete knative upgrade. +Running these tests on every commit will ensure that we don’t introduce any +non-upgradeable changes, so every commit should be releasable. + +This is inspired by kubernetes +[upgrade testing](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-testing/e2e-tests.md#version-skewed-and-upgrade-testing). + +These tests are a pretty big hammer in that they cover more than just version +changes, but it’s one of the only ways to make sure we don’t accidentally make +breaking changes for now. + +## Flow + +We’d like to validate that the upgrade doesn’t break any resources (they still +propagate events) and doesn't break our installation (we can still update +resources). + +At a high level, we want to do this: + +1. Install the latest knative release. +1. Create some resources. +1. Install knative at HEAD. +1. Test those resources, verify that we didn’t break anything. + +To achieve that, we just have three separate build tags: + +1. Install the latest release from GitHub. +1. Run the `preupgrade` tests in this directory. +1. Install at HEAD (`ko apply -f config/`). +1. Run the `postupgrade` tests in this directory. +1. Install the latest release from GitHub. +1. Run the `postdowngrade` tests in this directory. + +## Tests + +### Smoke test + +This was stolen from the e2e tests as one of the simplest cases. + +#### preupgrade, postupgrade, postdowngrade + +Run the selected smoke test. + +### Probe test + +In order to verify that we don't have data-plane unavailability during our +control-plane outages (when we're upgrading the knative/eventing installation), +we run a prober test that continually sends events to a service during the +entire upgrade/downgrade process. When the upgrade completes, we make sure that +all of those events propagated just once. + +To achieve that a tool was prepared (https://github.com/cardil/wathola). It +consists of 3 components: _sender_, _forwarder_, and _receiver_. _Sender_ is the +usual Kubernetes pod that publishes events to the default `broker` with given +interval. When it closes (by either `SIGTERM`, or `SIGINT`), an `finished` event +is generated. _Forwarder_ is a knative serving service that scales from zero to +receive the requested traffic. _Forwarders_ receive events and forwards them to +given target. _Receiver_ is an ordinary pod that collects events from multiple +forwarders and has an endpoint `/report` that can be polled to get the status of +sent events. + +Diagram below describe the setup: + +``` + (pod) (ksvc) (pod) ++--------+ +-----------+ +----------+ +| | | ++ | | +| Sender | +-->| Forwarder ||----->+ Receiver | +| | | | || | | ++---+----+ | +------------| +----------+ + | | +-----------+ + | | + | | + | +--+-----+ +---------+ + +-----> | | +-+ + | Broker | < - - | Trigger | | + | | | | | + +--------+ +---------+ | + (default) +----------+ +``` diff --git a/test/upgrade/main_test.go b/test/upgrade/main_test.go new file mode 100644 index 00000000000..9990630cb1a --- /dev/null +++ b/test/upgrade/main_test.go @@ -0,0 +1,37 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package upgrade + +import ( + "os" + "testing" + + "knative.dev/eventing/test" + "knative.dev/eventing/test/lib" +) + +var setup = lib.Setup +var tearDown = lib.TearDown +var channelTestRunner lib.ChannelTestRunner + +func TestMain(m *testing.M) { + test.InitializeEventingFlags() + channelTestRunner = lib.ChannelTestRunner{ + ChannelFeatureMap: lib.ChannelFeatureMap, + ChannelsToTest: test.EventingFlags.Channels, + } + os.Exit(m.Run()) +} diff --git a/test/upgrade/postdowngrade_test.go b/test/upgrade/postdowngrade_test.go new file mode 100644 index 00000000000..25f70a2cb97 --- /dev/null +++ b/test/upgrade/postdowngrade_test.go @@ -0,0 +1,26 @@ +// +build postdowngrade + +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package upgrade + +import ( + "testing" +) + +func TestPostDowngrade(t *testing.T) { + runSmokeTest(t) +} diff --git a/test/upgrade/postupgrade_test.go b/test/upgrade/postupgrade_test.go new file mode 100644 index 00000000000..ed5351c2a3e --- /dev/null +++ b/test/upgrade/postupgrade_test.go @@ -0,0 +1,26 @@ +// +build postupgrade + +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package upgrade + +import ( + "testing" +) + +func TestPostUpgrade(t *testing.T) { + runSmokeTest(t) +} diff --git a/test/upgrade/preupgrade_test.go b/test/upgrade/preupgrade_test.go new file mode 100644 index 00000000000..144c41e35fe --- /dev/null +++ b/test/upgrade/preupgrade_test.go @@ -0,0 +1,26 @@ +// +build preupgrade + +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package upgrade + +import ( + "testing" +) + +func TestPreUpgrade(t *testing.T) { + runSmokeTest(t) +} diff --git a/test/upgrade/probe_test.go b/test/upgrade/probe_test.go new file mode 100644 index 00000000000..a8949855d52 --- /dev/null +++ b/test/upgrade/probe_test.go @@ -0,0 +1,84 @@ +// +build probe + +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package upgrade + +import ( + "io/ioutil" + "os" + "syscall" + "testing" + + "github.com/wavesoftware/go-ensure" + "go.uber.org/zap" + "knative.dev/eventing/test/upgrade/prober" +) + +const ( + pipe = "/tmp/prober-signal" + ready = "/tmp/prober-ready" + readyMessage = "prober ready" +) + +func TestProbe(t *testing.T) { + // We run the prober as a golang test because it fits in nicely with + // the rest of our integration tests, and AssertProberDefault needs + // a *testing.T. Unfortunately, "go test" intercepts signals, so we + // can't coordinate with the test by just sending e.g. SIGCONT, so we + // create a named pipe and wait for the upgrade script to write to it + // to signal that we should stop probing. + ensureTempFilesAreCleaned() + if err := syscall.Mkfifo(pipe, 0666); err != nil { + t.Fatalf("Failed to create pipe: %v", err) + } + defer ensureTempFilesAreCleaned() + client := setup(t, false) + defer tearDown(client) + + config := prober.NewConfig(client.Namespace) + + // FIXME: https://github.com/knative/eventing/issues/2665 + config.FailOnMissingEvents = false + + // Use zap.SugarLogger instead of t.Logf because we want to see failures + // inline with other logs instead of buffered until the end. + log := createLogger() + probe := prober.RunEventProber(log, client, config) + ensure.NoError(ioutil.WriteFile(ready, []byte(readyMessage), 0666)) + defer prober.AssertEventProber(t, probe) + + log.Infof("Waiting for file: %v as a signal that "+ + "upgrade/downgrade is over, at which point we will finish the test "+ + "and check the prober.", pipe) + _, _ = ioutil.ReadFile(pipe) +} + +func createLogger() *zap.SugaredLogger { + log, err := zap.NewDevelopment() + ensure.NoError(err) + return log.Sugar() +} + +func ensureTempFilesAreCleaned() { + filenames := []string{pipe, ready} + for _, filename := range filenames { + _, err := os.Stat(filename) + if err == nil { + ensure.NoError(os.Remove(filename)) + } + } +} diff --git a/test/upgrade/prober/config.toml b/test/upgrade/prober/config.toml new file mode 100644 index 00000000000..122ec034ff7 --- /dev/null +++ b/test/upgrade/prober/config.toml @@ -0,0 +1,6 @@ +# logLevel = 5 # DEBUG(5) +[sender] +address = 'http://default-broker.{{- .Namespace -}}.svc.cluster.local' +interval = {{ .Interval.Nanoseconds }} +[forwarder] +target = 'http://wathola-receiver.{{- .Namespace -}}.svc.cluster.local' diff --git a/test/upgrade/prober/configuration.go b/test/upgrade/prober/configuration.go new file mode 100644 index 00000000000..496cacdd042 --- /dev/null +++ b/test/upgrade/prober/configuration.go @@ -0,0 +1,145 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package prober + +import ( + "bytes" + "fmt" + "io/ioutil" + "path" + "runtime" + "text/template" + + "github.com/wavesoftware/go-ensure" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/eventing/pkg/apis/eventing/v1alpha1" + "knative.dev/eventing/test/lib" + "knative.dev/eventing/test/lib/duck" + "knative.dev/eventing/test/lib/resources" +) + +const ( + configName = "wathola-config" + configMountPoint = "/home/nonroot/.config/wathola" + configFilename = "config.toml" + watholaEventNs = "com.github.cardil.wathola" + healthEndpoint = "/healthz" +) + +var ( + eventTypes = []string{"step", "finished"} +) + +func (p *prober) deployConfiguration() { + p.annotateNamespace() + p.deployConfigMap() + p.deployTriggers() +} + +func (p *prober) annotateNamespace() { + ns, err := p.client.Kube.Kube.CoreV1().Namespaces(). + Get(p.client.Namespace, metav1.GetOptions{}) + ensure.NoError(err) + ns.Labels = map[string]string{ + "knative-eventing-injection": "enabled", + } + _, err = p.client.Kube.Kube.CoreV1().Namespaces(). + Update(ns) + ensure.NoError(err) +} + +func (p *prober) deployConfigMap() { + name := configName + p.log.Infof("Deploying config map: %v", name) + + configData := p.compileTemplate(configFilename) + data := make(map[string]string, 0) + data[configFilename] = configData + secret := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Data: data, + } + _, err := p.client.Kube.Kube.CoreV1().ConfigMaps(p.config.Namespace). + Create(secret) + ensure.NoError(err) +} + +func (p *prober) deployTriggers() { + for _, eventType := range eventTypes { + name := fmt.Sprintf("wathola-trigger-%v", eventType) + fullType := fmt.Sprintf("%v.%v", watholaEventNs, eventType) + subscriberOption := resources.WithSubscriberServiceRefForTrigger(receiverName) + if p.config.Serving.Use { + subscriberOption = resources.WithSubscriberKServiceRefForTrigger(forwarderName) + } + trigger := resources.Trigger( + name, + resources.WithBroker("default"), + resources.WithAttributesTriggerFilter( + v1alpha1.TriggerAnyFilter, + fullType, + map[string]interface{}{}, + ), + subscriberOption, + ) + triggers := p.client.Eventing.EventingV1alpha1().Triggers(p.config.Namespace) + p.log.Infof("Deploying trigger: %v", name) + // update trigger with the new reference + _, err := triggers.Create(trigger) + ensure.NoError(err) + lib.WaitFor(fmt.Sprintf("trigger be ready: %v", name), func() error { + meta := resources.NewMetaResource(name, p.config.Namespace, lib.TriggerTypeMeta) + return duck.WaitForResourceReady(p.client.Dynamic, meta) + }) + } +} + +func (p *prober) removeConfiguration() { + p.removeConfigMap() + p.removeTriggers() +} + +func (p *prober) removeConfigMap() { + p.log.Infof("Removing config map: %v", configName) + err := p.client.Kube.Kube.CoreV1().ConfigMaps(p.config.Namespace). + Delete(configName, &metav1.DeleteOptions{}) + ensure.NoError(err) +} + +func (p *prober) removeTriggers() { + for _, eventType := range eventTypes { + name := fmt.Sprintf("wathola-trigger-%v", eventType) + p.log.Infof("Removing trigger: %v", name) + err := p.client.Eventing.EventingV1alpha1().Triggers(p.config.Namespace). + Delete(name, &metav1.DeleteOptions{}) + ensure.NoError(err) + } +} + +func (p *prober) compileTemplate(templateName string) string { + _, filename, _, _ := runtime.Caller(0) + templateFilepath := path.Join(path.Dir(filename), templateName) + templateBytes, err := ioutil.ReadFile(templateFilepath) + ensure.NoError(err) + tmpl, err := template.New(templateName).Parse(string(templateBytes)) + ensure.NoError(err) + var buff bytes.Buffer + ensure.NoError(tmpl.Execute(&buff, p.config)) + return buff.String() +} diff --git a/test/upgrade/prober/forwarder.go b/test/upgrade/prober/forwarder.go new file mode 100644 index 00000000000..4268800ccac --- /dev/null +++ b/test/upgrade/prober/forwarder.go @@ -0,0 +1,101 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package prober + +import ( + "fmt" + + "github.com/wavesoftware/go-ensure" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "knative.dev/eventing/test/lib" + "knative.dev/eventing/test/lib/duck" + "knative.dev/eventing/test/lib/resources" + pkgTest "knative.dev/pkg/test" +) + +var ( + forwarderName = "wathola-forwarder" +) + +func (p *prober) deployForwarder() { + p.log.Infof("Deploy forwarder knative service: %v", forwarderName) + serving := p.client.Dynamic.Resource(resources.KServicesGVR).Namespace(p.client.Namespace) + service := forwarderKService(forwarderName, p.client.Namespace) + _, err := serving.Create(service, metav1.CreateOptions{}) + ensure.NoError(err) + + sc := p.servingClient() + lib.WaitFor(fmt.Sprintf("forwarder ksvc be ready: %v", forwarderName), func() error { + return duck.WaitForKServiceReady(sc, forwarderName, p.client.Namespace) + }) + + if p.config.Serving.ScaleToZero { + lib.WaitFor(fmt.Sprintf("forwarder scales to zero: %v", forwarderName), func() error { + return duck.WaitForKServiceScales(sc, forwarderName, p.client.Namespace, func(scale int) bool { + return scale == 0 + }) + }) + } +} + +func (p *prober) removeForwarder() { + p.log.Infof("Remove forwarder knative service: %v", forwarderName) + serving := p.client.Dynamic.Resource(resources.KServicesGVR).Namespace(p.client.Namespace) + err := serving.Delete(forwarderName, &metav1.DeleteOptions{}) + ensure.NoError(err) +} + +func forwarderKService(name, namespace string) *unstructured.Unstructured { + obj := map[string]interface{}{ + "apiVersion": resources.KServiceType.APIVersion, + "kind": resources.KServiceType.Kind, + "metadata": map[string]interface{}{ + "name": name, + "namespace": namespace, + "labels": map[string]string{ + "serving.knative.dev/visibility": "cluster-local", + }, + }, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []map[string]interface{}{{ + "name": "forwarder", + "image": pkgTest.ImagePath(forwarderName), + "volumeMounts": []map[string]interface{}{{ + "name": configName, + "mountPath": configMountPoint, + "readOnly": true, + }}, + "readinessProbe": map[string]interface{}{ + "httpGet": map[string]interface{}{ + "path": healthEndpoint, + }, + }, + }}, + "volumes": []map[string]interface{}{{ + "name": configName, + "configMap": map[string]interface{}{ + "name": configName, + }, + }}, + }, + }, + }, + } + return &unstructured.Unstructured{Object: obj} +} diff --git a/test/upgrade/prober/prober.go b/test/upgrade/prober/prober.go new file mode 100644 index 00000000000..3f7c1c536fa --- /dev/null +++ b/test/upgrade/prober/prober.go @@ -0,0 +1,163 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package prober + +import ( + "testing" + "time" + + "github.com/wavesoftware/go-ensure" + "go.uber.org/zap" + "knative.dev/eventing/test/lib" + "knative.dev/eventing/test/lib/resources" +) + +var ( + // FIXME: Interval is set to 200 msec, as lower values will result in errors: knative/eventing#2357 + // Interval = 10 * time.Millisecond + Interval = 200 * time.Millisecond +) + +// Prober is the interface for a prober, which checks the result of the probes when stopped. +type Prober interface { + // Verify will verify prober state after finished has been send + Verify() ([]error, int) + + // Finish send finished event + Finish() + + // ReportError will reports found errors in proper way + ReportError(t *testing.T, err error) + + // deploy a prober to a cluster + deploy() + // remove a prober from cluster + remove() +} + +// Config represents a configuration for prober +type Config struct { + Namespace string + Interval time.Duration + Serving ServingConfig + FinishedSleep time.Duration + FailOnMissingEvents bool +} + +type ServingConfig struct { + Use bool + ScaleToZero bool +} + +func NewConfig(namespace string) *Config { + return &Config{ + Namespace: namespace, + Interval: Interval, + FinishedSleep: 5 * time.Second, + FailOnMissingEvents: true, + Serving: ServingConfig{ + Use: false, + ScaleToZero: true, + }, + } +} + +// RunEventProber starts a single Prober of the given domain. +func RunEventProber(log *zap.SugaredLogger, client *lib.Client, config *Config) Prober { + pm := newProber(log, client, config) + pm.deploy() + return pm +} + +// AssertEventProber will send finish event and then verify if all events propagated well +func AssertEventProber(t *testing.T, prober Prober) { + prober.Finish() + + waitAfterFinished(prober) + + errors, events := prober.Verify() + if len(errors) == 0 { + t.Logf("All %d events propagated well", events) + } else { + t.Logf("There ware %v errors. Listing them below.", len(errors)) + } + for _, err := range errors { + prober.ReportError(t, err) + } + + prober.remove() +} + +type prober struct { + log *zap.SugaredLogger + client *lib.Client + config *Config +} + +func (p *prober) servingClient() resources.ServingClient { + return resources.ServingClient{ + Kube: p.client.Kube, + Dynamic: p.client.Dynamic, + } +} + +func (p *prober) ReportError(t *testing.T, err error) { + if p.config.FailOnMissingEvents { + t.Error(err) + } else { + p.log.Warnf("Silenced FAIL: %v", err) + } +} + +func (p *prober) deploy() { + p.log.Infof("Using namespace for probe testing: %v", p.client.Namespace) + p.deployConfiguration() + p.deployReceiver() + if p.config.Serving.Use { + p.deployForwarder() + } + ensure.NoError(lib.AwaitForAll(p.log)) + + p.deploySender() + ensure.NoError(lib.AwaitForAll(p.log)) + // allow sender to send at least some events, 2 sec wait + time.Sleep(2 * time.Second) + p.log.Infof("Prober is now sending events with interval of %v in "+ + "namespace: %v", p.config.Interval, p.client.Namespace) +} + +func (p *prober) remove() { + if p.config.Serving.Use { + p.removeForwarder() + } + p.removeReceiver() + p.removeConfiguration() +} + +func newProber(log *zap.SugaredLogger, client *lib.Client, config *Config) Prober { + return &prober{ + log: log, + client: client, + config: config, + } +} + +func waitAfterFinished(p Prober) { + s := p.(*prober) + cfg := s.config + s.log.Infof("Waiting %v after sender finished...", cfg.FinishedSleep) + time.Sleep(cfg.FinishedSleep) +} diff --git a/test/upgrade/prober/receiver.go b/test/upgrade/prober/receiver.go new file mode 100644 index 00000000000..aa6a3ea44f1 --- /dev/null +++ b/test/upgrade/prober/receiver.go @@ -0,0 +1,149 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package prober + +import ( + "fmt" + + "github.com/wavesoftware/go-ensure" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "knative.dev/eventing/test/lib" + watholaconfig "knative.dev/eventing/test/upgrade/prober/wathola/config" + pkgTest "knative.dev/pkg/test" +) + +var ( + receiverName = "wathola-receiver" + receiverNodePort int32 = -1 +) + +func (p *prober) deployReceiver() { + p.deployReceiverPod() + p.deployReceiverService() +} + +func (p *prober) removeReceiver() { + p.removeReceiverService() + p.removeReceiverPod() +} + +func (p *prober) deployReceiverPod() { + p.log.Infof("Deploy of receiver pod: %v", receiverName) + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: receiverName, + Namespace: p.config.Namespace, + Labels: map[string]string{ + "app": receiverName, + }, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: configName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: configName}, + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "receiver", + Image: pkgTest.ImagePath(receiverName), + VolumeMounts: []corev1.VolumeMount{ + { + Name: configName, + ReadOnly: true, + MountPath: configMountPoint, + }, + }, + ReadinessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: healthEndpoint, + Port: intstr.FromInt(watholaconfig.DefaultReceiverPort), + }, + }, + }, + }, + }, + }, + } + _, err := p.client.Kube.CreatePod(pod) + ensure.NoError(err) + + lib.WaitFor(fmt.Sprintf("receiver be ready: %v", receiverName), func() error { + return pkgTest.WaitForPodRunning(p.client.Kube, receiverName, p.client.Namespace) + }) +} + +func (p *prober) deployReceiverService() { + p.log.Infof("Deploy of receiver service: %v", receiverName) + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: receiverName, + Namespace: p.config.Namespace, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "http", + Protocol: "TCP", + Port: 80, + TargetPort: intstr.IntOrString{ + Type: intstr.Int, + IntVal: watholaconfig.DefaultReceiverPort, + }, + }, + }, + Selector: map[string]string{ + "app": receiverName, + }, + Type: corev1.ServiceTypeNodePort, + }, + } + created, err := p.client.Kube.Kube.CoreV1().Services(p.config.Namespace). + Create(service) + ensure.NoError(err) + for _, portSpec := range created.Spec.Ports { + if portSpec.Port == 80 { + receiverNodePort = portSpec.NodePort + } + } + if receiverNodePort == -1 { + panic(fmt.Errorf("couldn't find a node port for service: %v", receiverName)) + } else { + p.log.Debugf("Node port for service: %v is %v", receiverName, receiverNodePort) + } +} + +func (p *prober) removeReceiverPod() { + p.log.Infof("Remove of receiver pod: %v", receiverName) + err := p.client.Kube.Kube.CoreV1().Pods(p.config.Namespace). + Delete(receiverName, &metav1.DeleteOptions{}) + ensure.NoError(err) +} + +func (p *prober) removeReceiverService() { + p.log.Infof("Remove of receiver service: %v", receiverName) + err := p.client.Kube.Kube.CoreV1().Services(p.config.Namespace). + Delete(receiverName, &metav1.DeleteOptions{}) + ensure.NoError(err) +} diff --git a/test/upgrade/prober/sender.go b/test/upgrade/prober/sender.go new file mode 100644 index 00000000000..c261f136ad4 --- /dev/null +++ b/test/upgrade/prober/sender.go @@ -0,0 +1,78 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package prober + +import ( + "fmt" + + "github.com/wavesoftware/go-ensure" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/eventing/test/lib" + pkgTest "knative.dev/pkg/test" +) + +var senderName = "wathola-sender" + +func (p *prober) deploySender() { + p.log.Infof("Deploy sender pod: %v", senderName) + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: senderName, + Namespace: p.config.Namespace, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: configName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: configName}, + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "sender", + Image: pkgTest.ImagePath(senderName), + VolumeMounts: []corev1.VolumeMount{ + { + Name: configName, + ReadOnly: true, + MountPath: configMountPoint, + }, + }, + }, + }, + }, + } + _, err := p.client.Kube.Kube.CoreV1().Pods(p.client.Namespace). + Create(pod) + ensure.NoError(err) + + lib.WaitFor(fmt.Sprintf("sender pod be ready: %v", senderName), func() error { + return pkgTest.WaitForPodRunning(p.client.Kube, senderName, p.client.Namespace) + }) +} + +func (p *prober) removeSender() { + p.log.Infof("Remove of sender pod: %v", senderName) + + err := p.client.Kube.Kube.CoreV1().Pods(p.client.Namespace). + Delete(senderName, &metav1.DeleteOptions{}) + ensure.NoError(err) +} diff --git a/test/upgrade/prober/verify.go b/test/upgrade/prober/verify.go new file mode 100644 index 00000000000..7db5f69a434 --- /dev/null +++ b/test/upgrade/prober/verify.go @@ -0,0 +1,82 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package prober + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/wavesoftware/go-ensure" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + "knative.dev/eventing/test/lib/nodes" +) + +func (p *prober) Verify() ([]error, int) { + nc := nodes.Client(p.client.Kube.Kube, p.log) + node, err := nc.RandomWorkerNode() + ensure.NoError(err) + address := nc.GuessNodeExternalAddress(node) + p.log.Debugf("Address resolved: %v, type: %v", address.Address, address.Type) + report := fetchReceiverReport(address, p.log) + p.log.Infof("Fetched receiver report. Events propagated: %v. "+ + "State: %v", report.Events, report.State) + if report.State == "active" { + panic(errors.New("report fetched to early, receiver is in active state")) + } + errs := make([]error, 0) + for _, t := range report.Thrown { + errs = append(errs, errors.New(t)) + } + return errs, report.Events +} + +func (p *prober) Finish() { + p.removeSender() +} + +func fetchReceiverReport(address *corev1.NodeAddress, log *zap.SugaredLogger) *Report { + u := fmt.Sprintf("http://%s:%d/report", address.Address, receiverNodePort) + log.Infof("Fetching receiver report from: %v", u) + resp, err := http.Get(u) + ensure.NoError(err) + if resp.StatusCode != 200 { + var b strings.Builder + ensure.NoError(resp.Header.Write(&b)) + headers := b.String() + panic(fmt.Errorf("could not get receiver report at %v, "+ + "status code: %v, headers: %v", u, resp.StatusCode, headers)) + } + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(resp.Body) + ensure.NoError(err) + ensure.NoError(resp.Body.Close()) + jsonBytes := buf.Bytes() + var report Report + ensure.NoError(json.Unmarshal(jsonBytes, &report)) + return &report +} + +// Report represents a receiver JSON report +type Report struct { + State string `json:"state"` + Events int `json:"events"` + Thrown []string `json:"thrown"` +} diff --git a/test/upgrade/prober/wathola/README.md b/test/upgrade/prober/wathola/README.md new file mode 100644 index 00000000000..a1b1dbf7fb4 --- /dev/null +++ b/test/upgrade/prober/wathola/README.md @@ -0,0 +1,3 @@ +# Wathola + +A cloudevents mass assurer tool (wathola - "he receive" in zulu) diff --git a/test/upgrade/prober/wathola/client/receiver.go b/test/upgrade/prober/wathola/client/receiver.go new file mode 100644 index 00000000000..0571abfada9 --- /dev/null +++ b/test/upgrade/prober/wathola/client/receiver.go @@ -0,0 +1,104 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package client + +import ( + "context" + nethttp "net/http" + "strings" + + cloudevents "github.com/cloudevents/sdk-go/v2" + cloudeventshttp "github.com/cloudevents/sdk-go/v2/protocol/http" + "github.com/wavesoftware/go-ensure" + "knative.dev/eventing/test/upgrade/prober/wathola/config" +) + +var log = config.Log + +// ReceiveEvent represents a function that receive event +type ReceiveEvent func(e cloudevents.Event) + +// Receive events and push then to passed fn +func Receive( + port int, + canceling chan context.CancelFunc, + receiveEvent ReceiveEvent, + middlewares ...cloudeventshttp.Middleware, +) { + portOpt := cloudevents.WithPort(port) + opts := make([]cloudeventshttp.Option, 0) + opts = append(opts, portOpt) + if config.Instance.Readiness.Enabled { + readyOpt := cloudevents.WithMiddleware(readinessMiddleware) + opts = append(opts, readyOpt) + } + for _, m := range middlewares { + opt := cloudevents.WithMiddleware(m) + opts = append(opts, opt) + } + http, err := cloudevents.NewHTTP(opts...) + if err != nil { + log.Fatalf("failed to create http transport, %v", err) + } + c, err := cloudevents.NewClient(http) + if err != nil { + log.Fatalf("failed to create client, %v", err) + } + log.Infof("Listening for events on port %v", port) + ctx, cancel := context.WithCancel(context.Background()) + cancelFunc := func() { + log.Infof("Stopping event receiver on port %v", port) + cancel() + } + // https://gobyexample.com/non-blocking-channel-operations + select { + case canceling <- cancelFunc: + default: + } + err = c.StartReceiver(ctx, receiveEvent) + if err != nil { + log.Fatal(err) + } +} + +func readinessMiddleware(next nethttp.Handler) nethttp.Handler { + log.Debugf("Using readiness probe: %v", config.Instance.Readiness.URI) + return &readinessProbe{ + next: next, + } +} + +type readinessProbe struct { + next nethttp.Handler +} + +func (r readinessProbe) ServeHTTP(rw nethttp.ResponseWriter, req *nethttp.Request) { + if req.RequestURI == config.Instance.Readiness.URI { + rw.WriteHeader(config.Instance.Readiness.Status) + _, err := rw.Write([]byte(config.Instance.Readiness.Message)) + ensure.NoError(err) + log.Debugf("Received ready check. Headers: %v", headersOf(req)) + } else { + r.next.ServeHTTP(rw, req) + } +} + +func headersOf(req *nethttp.Request) string { + var b strings.Builder + ensure.NoError(req.Header.Write(&b)) + headers := b.String() + return strings.ReplaceAll(headers, "\r\n", "; ") +} diff --git a/test/upgrade/prober/wathola/config/defaults.go b/test/upgrade/prober/wathola/config/defaults.go new file mode 100644 index 00000000000..ff28864a429 --- /dev/null +++ b/test/upgrade/prober/wathola/config/defaults.go @@ -0,0 +1,82 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package config + +import ( + "fmt" + nethttp "net/http" + "os" + "strconv" + "time" + + "go.uber.org/zap" +) + +const ( + // DefaultReceiverPort point to a default port of receiver component, and is + // unique so that components can be easily run on localhost for easy debugging + DefaultReceiverPort = 22111 + // DefaultForwarderPort point to a default port of forwarder component + DefaultForwarderPort = 22110 +) + +// Instance holds configuration values +var Instance = defaultValues() + +var port = envint("PORT", DefaultReceiverPort) +var forwarderPort = envint("PORT", DefaultForwarderPort) + +func envint(envKey string, defaultValue int) int { + val, ok := os.LookupEnv(envKey) + if !ok { + return defaultValue + } + result, err := strconv.Atoi(val) + if err != nil { + panic(err) + } + return result +} + +func defaultValues() *Config { + return &Config{ + Receiver: ReceiverConfig{ + Port: port, + Teardown: ReceiverTeardownConfig{ + Duration: 3 * time.Second, + }, + Progress: ReceiverProgressConfig{ + Duration: time.Second, + }, + }, + Forwarder: ForwarderConfig{ + Target: fmt.Sprintf("http://localhost:%v/", port), + Port: forwarderPort, + }, + Sender: SenderConfig{ + Address: fmt.Sprintf("http://localhost:%v/", forwarderPort), + Interval: 10 * time.Millisecond, + Cooldown: time.Second, + }, + Readiness: ReadinessConfig{ + Enabled: true, + URI: "/healthz", + Message: "OK", + Status: nethttp.StatusOK, + }, + LogLevel: zap.InfoLevel, + } +} diff --git a/test/upgrade/prober/wathola/config/defaults_test.go b/test/upgrade/prober/wathola/config/defaults_test.go new file mode 100644 index 00000000000..00b4fd86e9e --- /dev/null +++ b/test/upgrade/prober/wathola/config/defaults_test.go @@ -0,0 +1,29 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultValues(t *testing.T) { + assert.NotNil(t, Instance) + assert.Condition(t, func() (success bool) { + return Instance.Receiver.Teardown.Duration.Seconds() >= 1 + }) +} diff --git a/test/upgrade/prober/wathola/config/logger.go b/test/upgrade/prober/wathola/config/logger.go new file mode 100644 index 00000000000..a933bd63610 --- /dev/null +++ b/test/upgrade/prober/wathola/config/logger.go @@ -0,0 +1,32 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package config + +import ( + "go.uber.org/zap" +) + +// Log is a default logger for wathola +var Log = newLogger() +var logConfig = zap.NewProductionConfig() + +func newLogger() *zap.SugaredLogger { + z, err := logConfig.Build() + if err != nil { + panic(err) + } + return z.Sugar() +} diff --git a/test/upgrade/prober/wathola/config/reader.go b/test/upgrade/prober/wathola/config/reader.go new file mode 100644 index 00000000000..efb7f563fd2 --- /dev/null +++ b/test/upgrade/prober/wathola/config/reader.go @@ -0,0 +1,64 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package config + +import ( + "github.com/mitchellh/go-homedir" + "github.com/pelletier/go-toml" + "github.com/wavesoftware/go-ensure" + + "os" +) + +var location = "~/.config/wathola/config.toml" +var logFatal = Log.Fatal + +// ReadIfPresent read a configuration file if it exists +func ReadIfPresent() { + configFile, err := homedir.Expand(location) + ensure.NoError(err) + if fileExists(configFile) { + Log.Infof("Reading config file: %v", configFile) + err := Read(configFile) + if err != nil { + logFatal(err) + } + } else { + Log.Infof("Define config file to be taken into account: %v", configFile) + } +} + +// Read a config file and update configuration object +func Read(configFile string) error { + r, err := os.Open(configFile) + if err != nil { + return err + } + d := toml.NewDecoder(r) + err = d.Decode(Instance) + if err == nil { + logConfig.Level.SetLevel(Instance.LogLevel) + } + return err +} + +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} diff --git a/test/upgrade/prober/wathola/config/reader_test.go b/test/upgrade/prober/wathola/config/reader_test.go new file mode 100644 index 00000000000..fff158b8a41 --- /dev/null +++ b/test/upgrade/prober/wathola/config/reader_test.go @@ -0,0 +1,103 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package config + +import ( + "fmt" + + "github.com/google/uuid" + "github.com/mitchellh/go-homedir" + "github.com/stretchr/testify/assert" + "github.com/wavesoftware/go-ensure" + + "io/ioutil" + "os" + "path" + "testing" +) + +var id = uuid.New() + +func TestReadIfPresent(t *testing.T) { + // given + expanded := ensureConfigFileNotPresent() + data := []byte(`[sender] +address = 'http://default-broker.event-example.svc.cluster.local/' +`) + ensure.NoError(ioutil.WriteFile(expanded, data, 0644)) + defer func() { ensure.NoError(os.Remove(expanded)) }() + + // when + ReadIfPresent() + + // then + assert.Equal(t, + "http://default-broker.event-example.svc.cluster.local/", + Instance.Sender.Address) + assert.Equal(t, DefaultReceiverPort, Instance.Receiver.Port) + assert.Equal(t, DefaultForwarderPort, Instance.Forwarder.Port) +} + +func TestReadIfPresentAndInvalid(t *testing.T) { + // given + origLogFatal := logFatal + defer func() { logFatal = origLogFatal }() + expanded := ensureConfigFileNotPresent() + data := []byte(`[sender] +address = 'http://default-broker.event-example.svc.cluster.local/ +`) + ensure.NoError(ioutil.WriteFile(expanded, data, 0644)) + defer func() { ensure.NoError(os.Remove(expanded)) }() + var errors []string + logFatal = func(args ...interface{}) { + errors = append(errors, fmt.Sprint(args)) + } + + // when + ReadIfPresent() + + // then + assert.Contains(t, errors, "[(2, 12): unclosed string]") +} + +func TestReadIfNotPresent(t *testing.T) { + // given + ensureConfigFileNotPresent() + + // when + ReadIfPresent() + + // then + assert.Equal(t, + fmt.Sprintf("http://localhost:%d/", DefaultForwarderPort), + Instance.Sender.Address) + assert.Equal(t, DefaultReceiverPort, Instance.Receiver.Port) + assert.Equal(t, DefaultForwarderPort, Instance.Forwarder.Port) +} + +func ensureConfigFileNotPresent() string { + Instance = defaultValues() + location = fmt.Sprintf("~/tmp/wathola-%v/config.toml", id.String()) + expanded, err := homedir.Expand(location) + ensure.NoError(err) + dir := path.Dir(expanded) + ensure.NoError(os.MkdirAll(dir, os.ModePerm)) + if _, err := os.Stat(expanded); err == nil { + ensure.NoError(os.Remove(expanded)) + } + + return expanded +} diff --git a/test/upgrade/prober/wathola/config/structure.go b/test/upgrade/prober/wathola/config/structure.go new file mode 100644 index 00000000000..cd6af611a43 --- /dev/null +++ b/test/upgrade/prober/wathola/config/structure.go @@ -0,0 +1,69 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package config + +import ( + "time" + + "go.uber.org/zap/zapcore" +) + +// ReceiverTeardownConfig holds config receiver teardown +type ReceiverTeardownConfig struct { + Duration time.Duration +} + +// ReceiverProgressConfig holds config receiver progress reporting +type ReceiverProgressConfig struct { + Duration time.Duration +} + +// ReceiverConfig hold configuration for receiver +type ReceiverConfig struct { + Teardown ReceiverTeardownConfig + Progress ReceiverProgressConfig + Port int +} + +// SenderConfig hold configuration for sender +type SenderConfig struct { + Address string + Interval time.Duration + Cooldown time.Duration +} + +// ForwarderConfig holds configuration for forwarder +type ForwarderConfig struct { + Target string + Port int +} + +// ReadinessConfig holds a readiness configuration +type ReadinessConfig struct { + Enabled bool + URI string + Message string + Status int +} + +// Config hold complete configuration +type Config struct { + Sender SenderConfig + Forwarder ForwarderConfig + Receiver ReceiverConfig + Readiness ReadinessConfig + LogLevel zapcore.Level +} diff --git a/test/upgrade/prober/wathola/event/operations.go b/test/upgrade/prober/wathola/event/operations.go new file mode 100644 index 00000000000..c28cba92180 --- /dev/null +++ b/test/upgrade/prober/wathola/event/operations.go @@ -0,0 +1,34 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package event + +// StepsStore contains methods that register step event type +type StepsStore interface { + RegisterStep(step *Step) + Count() int +} + +// FinishedStore registers a finished event type +type FinishedStore interface { + RegisterFinished(finished *Finished) + State() State + Thrown() []string +} + +// Typed says a type of an event +type Typed interface { + Type() string +} diff --git a/test/upgrade/prober/wathola/event/services.go b/test/upgrade/prober/wathola/event/services.go new file mode 100644 index 00000000000..61e3c2c8635 --- /dev/null +++ b/test/upgrade/prober/wathola/event/services.go @@ -0,0 +1,162 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package event + +import ( + "fmt" + "sync" + "time" + + "knative.dev/eventing/test/upgrade/prober/wathola/config" +) + +var mutex = sync.RWMutex{} +var lastProgressReport = time.Now() + +// ErrorStore contains errors that was thrown +type ErrorStore struct { + state State + thrown []thrown +} + +// NewErrorStore creates a new error store +func NewErrorStore() *ErrorStore { + return &ErrorStore{ + state: Active, + thrown: make([]thrown, 0), + } +} + +// NewStepsStore creates StepsStore +func NewStepsStore(errors *ErrorStore) StepsStore { + return &stepStore{ + store: make(map[int]int), + errors: errors, + } +} + +// NewFinishedStore creates FinishedStore +func NewFinishedStore(steps StepsStore, errors *ErrorStore) FinishedStore { + return &finishedStore{ + received: 0, + count: -1, + steps: steps, + errors: errors, + } +} + +func (s *stepStore) RegisterStep(step *Step) { + mutex.Lock() + if times, found := s.store[step.Number]; found { + s.errors.throw( + "event #%d received %d times, but should be received only once", + step.Number, times+1) + } else { + s.store[step.Number] = 0 + } + s.store[step.Number]++ + mutex.Unlock() + log.Debugf("event #%d received", step.Number) + s.reportProgress() +} + +func (s *stepStore) Count() int { + return len(s.store) +} + +func (f *finishedStore) RegisterFinished(finished *Finished) { + if f.received > 0 { + f.errors.throw( + "finish event should be received only once, received %d", + f.received+1) + } + f.received++ + f.count = finished.Count + log.Infof("finish event received, expecting %d event ware propagated", finished.Count) + d := config.Instance.Receiver.Teardown.Duration + log.Infof("waiting additional %v to be sure all events came", d) + time.Sleep(d) + receivedEvents := f.steps.Count() + if receivedEvents != finished.Count { + f.errors.throw("expecting to have %v unique events received, "+ + "but received %v unique events", finished.Count, receivedEvents) + f.reportViolations(finished) + f.errors.state = Failed + } else { + log.Infof("properly received %d unique events", receivedEvents) + f.errors.state = Success + } +} + +func (f *finishedStore) State() State { + return f.errors.state +} + +func (f *finishedStore) Thrown() []string { + msgs := make([]string, 0) + for _, t := range f.errors.thrown { + errMsg := fmt.Sprintf(t.format, t.args...) + msgs = append(msgs, errMsg) + } + return msgs +} + +func (f *finishedStore) reportViolations(finished *Finished) { + steps := f.steps.(*stepStore) + for eventNo := 1; eventNo <= finished.Count; eventNo++ { + times, ok := steps.store[eventNo] + if !ok { + times = 0 + } + if times != 1 { + f.errors.throw("event #%v should be received once, but was received %v times", + eventNo, times) + } + } +} + +func (s *stepStore) reportProgress() { + if lastProgressReport.Add(config.Instance.Receiver.Progress.Duration).Before(time.Now()) { + lastProgressReport = time.Now() + log.Infof("collected %v unique events", s.Count()) + } +} + +func (e *ErrorStore) throw(format string, args ...interface{}) { + t := thrown{ + format: format, + args: args, + } + e.thrown = append(e.thrown, t) + log.Errorf(t.format, t.args...) +} + +type stepStore struct { + store map[int]int + errors *ErrorStore +} + +type finishedStore struct { + received int + count int + errors *ErrorStore + steps StepsStore +} + +type thrown struct { + format string + args []interface{} +} diff --git a/test/upgrade/prober/wathola/event/services_test.go b/test/upgrade/prober/wathola/event/services_test.go new file mode 100644 index 00000000000..5b9f963533d --- /dev/null +++ b/test/upgrade/prober/wathola/event/services_test.go @@ -0,0 +1,79 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package event + +import ( + "github.com/stretchr/testify/assert" + "knative.dev/eventing/test/upgrade/prober/wathola/config" + + "os" + "testing" + "time" +) + +func TestProperEventsPropagation(t *testing.T) { + // given + errors := NewErrorStore() + stepsStore := NewStepsStore(errors) + finishedStore := NewFinishedStore(stepsStore, errors) + + // when + stepsStore.RegisterStep(&Step{Number: 1}) + stepsStore.RegisterStep(&Step{Number: 3}) + stepsStore.RegisterStep(&Step{Number: 2}) + finishedStore.RegisterFinished(&Finished{Count: 3}) + + // then + assert.Empty(t, errors.thrown) +} + +func TestMissingAndDoubleEvent(t *testing.T) { + // given + errors := NewErrorStore() + stepsStore := NewStepsStore(errors) + finishedStore := NewFinishedStore(stepsStore, errors) + + // when + stepsStore.RegisterStep(&Step{Number: 1}) + stepsStore.RegisterStep(&Step{Number: 2}) + stepsStore.RegisterStep(&Step{Number: 2}) + finishedStore.RegisterFinished(&Finished{Count: 3}) + + // then + assert.NotEmpty(t, errors.thrown) +} + +func TestDoubleFinished(t *testing.T) { + // given + errors := NewErrorStore() + stepsStore := NewStepsStore(errors) + finishedStore := NewFinishedStore(stepsStore, errors) + + // when + stepsStore.RegisterStep(&Step{Number: 1}) + stepsStore.RegisterStep(&Step{Number: 2}) + finishedStore.RegisterFinished(&Finished{Count: 2}) + finishedStore.RegisterFinished(&Finished{Count: 2}) + + // then + assert.NotEmpty(t, errors.thrown) +} + +func TestMain(m *testing.M) { + config.Instance.Receiver.Teardown.Duration = 20 * time.Millisecond + exitcode := m.Run() + os.Exit(exitcode) +} diff --git a/test/upgrade/prober/wathola/event/types.go b/test/upgrade/prober/wathola/event/types.go new file mode 100644 index 00000000000..377f50ceafb --- /dev/null +++ b/test/upgrade/prober/wathola/event/types.go @@ -0,0 +1,59 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package event + +import "knative.dev/eventing/test/upgrade/prober/wathola/config" + +const ( + // StepType is a string type representation of step event + StepType = "com.github.cardil.wathola.step" + // FinishedType os a string type representation of finished event + FinishedType = "com.github.cardil.wathola.finished" +) + +// Step is a event call at each step of verification +type Step struct { + Number int +} + +// Finished is step call after verification finishes +type Finished struct { + Count int +} + +// Type returns a type of a event +func (s Step) Type() string { + return StepType +} + +// Type returns a type of a event +func (f Finished) Type() string { + return FinishedType +} + +// State defines a state of event store +type State int + +const ( + // Active == 1 (iota has been reset) + Active State = 1 << iota + // Success == 2 + Success State = 1 << iota + // Failed == 4 + Failed State = 1 << iota +) + +var log = config.Log diff --git a/test/upgrade/prober/wathola/event/types_test.go b/test/upgrade/prober/wathola/event/types_test.go new file mode 100644 index 00000000000..c73049b3e89 --- /dev/null +++ b/test/upgrade/prober/wathola/event/types_test.go @@ -0,0 +1,34 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package event + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStepType(t *testing.T) { + s1 := Step{Number: 1} + + assert.Equal(t, StepType, s1.Type()) +} + +func TestFinishedType(t *testing.T) { + f1 := Finished{Count: 441} + + assert.Equal(t, FinishedType, f1.Type()) +} diff --git a/test/upgrade/prober/wathola/forwarder/operations.go b/test/upgrade/prober/wathola/forwarder/operations.go new file mode 100644 index 00000000000..4ee037fde30 --- /dev/null +++ b/test/upgrade/prober/wathola/forwarder/operations.go @@ -0,0 +1,21 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package forwarder + +// Forwarder perform waiting and receiving of events and forwarding them to other place +type Forwarder interface { + Forward() +} diff --git a/test/upgrade/prober/wathola/forwarder/services.go b/test/upgrade/prober/wathola/forwarder/services.go new file mode 100644 index 00000000000..1e37cc980b6 --- /dev/null +++ b/test/upgrade/prober/wathola/forwarder/services.go @@ -0,0 +1,69 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package forwarder + +import ( + "context" + + cloudevents "github.com/cloudevents/sdk-go/v2" + "knative.dev/eventing/test/upgrade/prober/wathola/client" + "knative.dev/eventing/test/upgrade/prober/wathola/config" + "knative.dev/eventing/test/upgrade/prober/wathola/sender" + + "time" +) + +var ( + log = config.Log + lastProgressReport = time.Now() + Canceling = make(chan context.CancelFunc) +) + +// New creates new forwarder +func New() Forwarder { + config.ReadIfPresent() + f := &forwarder{ + count: 0, + } + return f +} + +func (f *forwarder) Forward() { + port := config.Instance.Forwarder.Port + client.Receive(port, Canceling, f.forwardEvent) +} + +func (f *forwarder) forwardEvent(e cloudevents.Event) { + target := config.Instance.Forwarder.Target + log.Debugf("Forwarding event %v to %v", e.ID(), target) + err := sender.SendEvent(e, target) + if err != nil { + log.Error(err) + } + f.count++ + f.reportProgress() +} + +func (f *forwarder) reportProgress() { + if lastProgressReport.Add(config.Instance.Receiver.Progress.Duration).Before(time.Now()) { + lastProgressReport = time.Now() + log.Infof("forwarded %v events", f.count) + } +} + +type forwarder struct { + count int +} diff --git a/test/upgrade/prober/wathola/receiver/operations.go b/test/upgrade/prober/wathola/receiver/operations.go new file mode 100644 index 00000000000..b188cc3afab --- /dev/null +++ b/test/upgrade/prober/wathola/receiver/operations.go @@ -0,0 +1,21 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package receiver + +// Receiver perform waiting and receiving of events +type Receiver interface { + Receive() +} diff --git a/test/upgrade/prober/wathola/receiver/services.go b/test/upgrade/prober/wathola/receiver/services.go new file mode 100644 index 00000000000..3e047d802f3 --- /dev/null +++ b/test/upgrade/prober/wathola/receiver/services.go @@ -0,0 +1,137 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package receiver + +import ( + "context" + "encoding/json" + "fmt" + + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/wavesoftware/go-ensure" + "knative.dev/eventing/test/upgrade/prober/wathola/client" + "knative.dev/eventing/test/upgrade/prober/wathola/config" + "knative.dev/eventing/test/upgrade/prober/wathola/event" + + "net/http" +) + +var ( + log = config.Log + Canceling = make(chan context.CancelFunc) +) + +// New creates new Receiver +func New() Receiver { + config.ReadIfPresent() + errors := event.NewErrorStore() + stepsStore := event.NewStepsStore(errors) + finishedStore := event.NewFinishedStore(stepsStore, errors) + r := newReceiver(stepsStore, finishedStore) + return r +} + +func (r receiver) Receive() { + port := config.Instance.Receiver.Port + client.Receive(port, Canceling, r.receiveEvent, r.reportMiddleware) +} + +func (r receiver) receiveEvent(e cloudevents.Event) { + // do something with event.Context and event.Data (via event.DataAs(foo) + t := e.Context.GetType() + if t == event.StepType { + step := &event.Step{} + err := e.DataAs(step) + if err != nil { + log.Fatal(err) + } + r.step.RegisterStep(step) + } + if t == event.FinishedType { + finished := &event.Finished{} + err := e.DataAs(finished) + if err != nil { + log.Fatal(err) + } + r.finished.RegisterFinished(finished) + } +} + +func (r *receiver) reportMiddleware(next http.Handler) http.Handler { + return &reportHandler{ + next: next, + receiver: r, + } +} + +type receiver struct { + step event.StepsStore + finished event.FinishedStore +} + +func newReceiver(step event.StepsStore, finished event.FinishedStore) *receiver { + r := &receiver{ + step: step, + finished: finished, + } + return r +} + +type reportHandler struct { + next http.Handler + receiver *receiver +} + +func (r reportHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if req.RequestURI == "/report" { + s := r.receiver.finished.State() + errs := r.receiver.finished.Thrown() + events := r.receiver.step.Count() + sj := &StateJSON{ + State: stateToString(s), + Events: events, + Thrown: errs, + } + b, err := json.Marshal(sj) + ensure.NoError(err) + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(http.StatusOK) + _, err = rw.Write(b) + ensure.NoError(err) + } else { + r.next.ServeHTTP(rw, req) + } +} + +func stateToString(state event.State) string { + switch state { + case event.Active: + return "active" + case event.Success: + return "success" + case event.Failed: + return "failed" + default: + panic(fmt.Sprintf("unknown state: %v", state)) + } +} + +// StateJSON represents state as JSON +type StateJSON struct { + State string `json:"state"` + Events int `json:"events"` + Thrown []string `json:"thrown"` +} diff --git a/test/upgrade/prober/wathola/receiver/services_test.go b/test/upgrade/prober/wathola/receiver/services_test.go new file mode 100644 index 00000000000..cc7d7d7df88 --- /dev/null +++ b/test/upgrade/prober/wathola/receiver/services_test.go @@ -0,0 +1,65 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package receiver + +import ( + "fmt" + + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/phayes/freeport" + "github.com/stretchr/testify/assert" + "knative.dev/eventing/test/upgrade/prober/wathola/config" + "knative.dev/eventing/test/upgrade/prober/wathola/event" + "knative.dev/eventing/test/upgrade/prober/wathola/sender" + + "os" + "testing" + "time" +) + +func TestReceiverReceive(t *testing.T) { + // given + e := sender.NewCloudEvent(event.Step{Number: 42}, event.StepType) + f := sender.NewCloudEvent(event.Finished{Count: 1}, event.FinishedType) + + instance := New() + port := freeport.GetPort() + config.Instance.Receiver.Port = port + go instance.Receive() + cancel := <-Canceling + defer cancel() + + // when + sendEvent(t, e, port) + sendEvent(t, f, port) + + // then + rr := instance.(*receiver) + assert.Equal(t, 1, rr.step.Count()) + assert.Equal(t, event.Success, rr.finished.State()) +} + +func TestMain(m *testing.M) { + config.Instance.Receiver.Teardown.Duration = 20 * time.Millisecond + exitcode := m.Run() + os.Exit(exitcode) +} + +func sendEvent(t *testing.T, e cloudevents.Event, port int) { + url := fmt.Sprintf("http://localhost:%v/", port) + err := sender.SendEvent(e, url) + assert.NoError(t, err) +} diff --git a/test/upgrade/prober/wathola/sender/operations.go b/test/upgrade/prober/wathola/sender/operations.go new file mode 100644 index 00000000000..05f77fb65bb --- /dev/null +++ b/test/upgrade/prober/wathola/sender/operations.go @@ -0,0 +1,54 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package sender + +import ( + "math/rand" + "time" + + "knative.dev/eventing/test/upgrade/prober/wathola/config" +) + +// New creates new Sender +func New() Sender { + config.ReadIfPresent() + return &sender{ + counter: 0, + } +} + +// NewEventID creates new event ID +func NewEventID() string { + return randString(16) +} + +const charset = "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +var seededRand = rand.New( + rand.NewSource(time.Now().UnixNano())) + +func randStringWithCharset(length int, charset string) string { + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) +} + +func randString(length int) string { + return randStringWithCharset(length, charset) +} diff --git a/test/upgrade/prober/wathola/sender/services.go b/test/upgrade/prober/wathola/sender/services.go new file mode 100644 index 00000000000..8db8837a342 --- /dev/null +++ b/test/upgrade/prober/wathola/sender/services.go @@ -0,0 +1,125 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package sender + +import ( + "context" + "fmt" + + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/wavesoftware/go-ensure" + "knative.dev/eventing/test/upgrade/prober/wathola/config" + "knative.dev/eventing/test/upgrade/prober/wathola/event" + + "os" + "os/signal" + "syscall" + "time" +) + +var log = config.Log +var senderConfig = &config.Instance.Sender + +type sender struct { + counter int +} + +func (s *sender) SendContinually() { + var shutdownCh = make(chan struct{}) + defer s.sendFinished() + + go func() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + sig := <-c + // sig is a ^C or term, handle it + log.Infof("%v signal received, closing", sig.String()) + close(shutdownCh) + }() + + for { + select { + case <-shutdownCh: + return + default: + } + err := s.sendStep() + if err != nil { + log.Warnf("Could not send step event, retry in %v", senderConfig.Cooldown) + time.Sleep(senderConfig.Cooldown) + } else { + time.Sleep(senderConfig.Interval) + } + } +} + +// NewCloudEvent creates a new cloud event +func NewCloudEvent(data interface{}, typ string) cloudevents.Event { + e := cloudevents.NewEvent() + e.SetDataContentType("application/json") + e.SetType(typ) + host, err := os.Hostname() + ensure.NoError(err) + e.SetSource(fmt.Sprintf("knative://%s/wathola/sender", host)) + e.SetID(NewEventID()) + e.SetTime(time.Now()) + err = e.SetData(cloudevents.ApplicationJSON, data) + ensure.NoError(err) + errs := e.Validate() + if errs != nil { + ensure.NoError(errs) + } + return e +} + +// SendEvent will send cloud event to given url +func SendEvent(e cloudevents.Event, url string) error { + c, err := cloudevents.NewDefaultClient() + if err != nil { + return err + } + ctx := cloudevents.ContextWithTarget(context.Background(), url) + + result := c.Send(ctx, e) + if cloudevents.IsACK(result) { + return nil + } + return result +} + +func (s *sender) sendStep() error { + step := event.Step{Number: s.counter + 1} + ce := NewCloudEvent(step, event.StepType) + url := senderConfig.Address + log.Infof("Sending step event #%v to %s", step.Number, url) + err := SendEvent(ce, url) + if err != nil { + return err + } + s.counter++ + return nil +} + +func (s *sender) sendFinished() { + if s.counter == 0 { + return + } + finished := event.Finished{Count: s.counter} + url := senderConfig.Address + ce := NewCloudEvent(finished, event.FinishedType) + log.Infof("Sending finished event (count: %v) to %s", finished.Count, url) + ensure.NoError(SendEvent(ce, url)) +} diff --git a/test/upgrade/prober/wathola/sender/types.go b/test/upgrade/prober/wathola/sender/types.go new file mode 100644 index 00000000000..3b78c465724 --- /dev/null +++ b/test/upgrade/prober/wathola/sender/types.go @@ -0,0 +1,21 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package sender + +// Sender will send messages continuously until process receives a SIGINT +type Sender interface { + SendContinually() +} diff --git a/test/upgrade/smoke_test.go b/test/upgrade/smoke_test.go new file mode 100644 index 00000000000..858d45373d5 --- /dev/null +++ b/test/upgrade/smoke_test.go @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Knative Authors + * 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. + */ + +package upgrade + +import ( + "testing" + + cloudevents "github.com/cloudevents/sdk-go" + "knative.dev/eventing/test/e2e/helpers" +) + +func runSmokeTest(t *testing.T) { + helpers.SingleEventForChannelTestHelper( + t, + cloudevents.Binary, + helpers.SubscriptionV1alpha1, + "", + channelTestRunner, + ) +} diff --git a/third_party/VENDOR-LICENSE/github.com/mitchellh/go-homedir/LICENSE b/third_party/VENDOR-LICENSE/github.com/mitchellh/go-homedir/LICENSE new file mode 100644 index 00000000000..f9c841a51e0 --- /dev/null +++ b/third_party/VENDOR-LICENSE/github.com/mitchellh/go-homedir/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/VENDOR-LICENSE/github.com/pelletier/go-toml/LICENSE b/third_party/VENDOR-LICENSE/github.com/pelletier/go-toml/LICENSE new file mode 100644 index 00000000000..583bdae6282 --- /dev/null +++ b/third_party/VENDOR-LICENSE/github.com/pelletier/go-toml/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 - 2017 Thomas Pelletier, Eric Anderton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/VENDOR-LICENSE/github.com/wavesoftware/go-ensure/LICENSE b/third_party/VENDOR-LICENSE/github.com/wavesoftware/go-ensure/LICENSE new file mode 100644 index 00000000000..6c180a6f23d --- /dev/null +++ b/third_party/VENDOR-LICENSE/github.com/wavesoftware/go-ensure/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Wave Software + + 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. diff --git a/vendor/github.com/mitchellh/go-homedir/LICENSE b/vendor/github.com/mitchellh/go-homedir/LICENSE new file mode 100644 index 00000000000..f9c841a51e0 --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/mitchellh/go-homedir/README.md b/vendor/github.com/mitchellh/go-homedir/README.md new file mode 100644 index 00000000000..d70706d5b35 --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/README.md @@ -0,0 +1,14 @@ +# go-homedir + +This is a Go library for detecting the user's home directory without +the use of cgo, so the library can be used in cross-compilation environments. + +Usage is incredibly simple, just call `homedir.Dir()` to get the home directory +for a user, and `homedir.Expand()` to expand the `~` in a path to the home +directory. + +**Why not just use `os/user`?** The built-in `os/user` package requires +cgo on Darwin systems. This means that any Go code that uses that package +cannot cross compile. But 99% of the time the use for `os/user` is just to +retrieve the home directory, which we can do for the current user without +cgo. This library does that, enabling cross-compilation. diff --git a/vendor/github.com/mitchellh/go-homedir/go.mod b/vendor/github.com/mitchellh/go-homedir/go.mod new file mode 100644 index 00000000000..7efa09a0432 --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/go.mod @@ -0,0 +1 @@ +module github.com/mitchellh/go-homedir diff --git a/vendor/github.com/mitchellh/go-homedir/homedir.go b/vendor/github.com/mitchellh/go-homedir/homedir.go new file mode 100644 index 00000000000..25378537ead --- /dev/null +++ b/vendor/github.com/mitchellh/go-homedir/homedir.go @@ -0,0 +1,167 @@ +package homedir + +import ( + "bytes" + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" +) + +// DisableCache will disable caching of the home directory. Caching is enabled +// by default. +var DisableCache bool + +var homedirCache string +var cacheLock sync.RWMutex + +// Dir returns the home directory for the executing user. +// +// This uses an OS-specific method for discovering the home directory. +// An error is returned if a home directory cannot be detected. +func Dir() (string, error) { + if !DisableCache { + cacheLock.RLock() + cached := homedirCache + cacheLock.RUnlock() + if cached != "" { + return cached, nil + } + } + + cacheLock.Lock() + defer cacheLock.Unlock() + + var result string + var err error + if runtime.GOOS == "windows" { + result, err = dirWindows() + } else { + // Unix-like system, so just assume Unix + result, err = dirUnix() + } + + if err != nil { + return "", err + } + homedirCache = result + return result, nil +} + +// Expand expands the path to include the home directory if the path +// is prefixed with `~`. If it isn't prefixed with `~`, the path is +// returned as-is. +func Expand(path string) (string, error) { + if len(path) == 0 { + return path, nil + } + + if path[0] != '~' { + return path, nil + } + + if len(path) > 1 && path[1] != '/' && path[1] != '\\' { + return "", errors.New("cannot expand user-specific home dir") + } + + dir, err := Dir() + if err != nil { + return "", err + } + + return filepath.Join(dir, path[1:]), nil +} + +// Reset clears the cache, forcing the next call to Dir to re-detect +// the home directory. This generally never has to be called, but can be +// useful in tests if you're modifying the home directory via the HOME +// env var or something. +func Reset() { + cacheLock.Lock() + defer cacheLock.Unlock() + homedirCache = "" +} + +func dirUnix() (string, error) { + homeEnv := "HOME" + if runtime.GOOS == "plan9" { + // On plan9, env vars are lowercase. + homeEnv = "home" + } + + // First prefer the HOME environmental variable + if home := os.Getenv(homeEnv); home != "" { + return home, nil + } + + var stdout bytes.Buffer + + // If that fails, try OS specific commands + if runtime.GOOS == "darwin" { + cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`) + cmd.Stdout = &stdout + if err := cmd.Run(); err == nil { + result := strings.TrimSpace(stdout.String()) + if result != "" { + return result, nil + } + } + } else { + cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid())) + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + // If the error is ErrNotFound, we ignore it. Otherwise, return it. + if err != exec.ErrNotFound { + return "", err + } + } else { + if passwd := strings.TrimSpace(stdout.String()); passwd != "" { + // username:password:uid:gid:gecos:home:shell + passwdParts := strings.SplitN(passwd, ":", 7) + if len(passwdParts) > 5 { + return passwdParts[5], nil + } + } + } + } + + // If all else fails, try the shell + stdout.Reset() + cmd := exec.Command("sh", "-c", "cd && pwd") + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return "", err + } + + result := strings.TrimSpace(stdout.String()) + if result == "" { + return "", errors.New("blank output when reading home directory") + } + + return result, nil +} + +func dirWindows() (string, error) { + // First prefer the HOME environmental variable + if home := os.Getenv("HOME"); home != "" { + return home, nil + } + + // Prefer standard environment variable USERPROFILE + if home := os.Getenv("USERPROFILE"); home != "" { + return home, nil + } + + drive := os.Getenv("HOMEDRIVE") + path := os.Getenv("HOMEPATH") + home := drive + path + if drive == "" || path == "" { + return "", errors.New("HOMEDRIVE, HOMEPATH, or USERPROFILE are blank") + } + + return home, nil +} diff --git a/vendor/github.com/pelletier/go-toml/.dockerignore b/vendor/github.com/pelletier/go-toml/.dockerignore new file mode 100644 index 00000000000..7b5883475df --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/.dockerignore @@ -0,0 +1,2 @@ +cmd/tomll/tomll +cmd/tomljson/tomljson diff --git a/vendor/github.com/pelletier/go-toml/.gitignore b/vendor/github.com/pelletier/go-toml/.gitignore new file mode 100644 index 00000000000..e6ba63a5c5c --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/.gitignore @@ -0,0 +1,5 @@ +test_program/test_program_bin +fuzz/ +cmd/tomll/tomll +cmd/tomljson/tomljson +cmd/tomltestgen/tomltestgen diff --git a/vendor/github.com/pelletier/go-toml/CONTRIBUTING.md b/vendor/github.com/pelletier/go-toml/CONTRIBUTING.md new file mode 100644 index 00000000000..405c911c903 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/CONTRIBUTING.md @@ -0,0 +1,132 @@ +## Contributing + +Thank you for your interest in go-toml! We appreciate you considering +contributing to go-toml! + +The main goal is the project is to provide an easy-to-use TOML +implementation for Go that gets the job done and gets out of your way – +dealing with TOML is probably not the central piece of your project. + +As the single maintainer of go-toml, time is scarce. All help, big or +small, is more than welcomed! + +### Ask questions + +Any question you may have, somebody else might have it too. Always feel +free to ask them on the [issues tracker][issues-tracker]. We will try to +answer them as clearly and quickly as possible, time permitting. + +Asking questions also helps us identify areas where the documentation needs +improvement, or new features that weren't envisioned before. Sometimes, a +seemingly innocent question leads to the fix of a bug. Don't hesitate and +ask away! + +### Improve the documentation + +The best way to share your knowledge and experience with go-toml is to +improve the documentation. Fix a typo, clarify an interface, add an +example, anything goes! + +The documentation is present in the [README][readme] and thorough the +source code. On release, it gets updated on [GoDoc][godoc]. To make a +change to the documentation, create a pull request with your proposed +changes. For simple changes like that, the easiest way to go is probably +the "Fork this project and edit the file" button on Github, displayed at +the top right of the file. Unless it's a trivial change (for example a +typo), provide a little bit of context in your pull request description or +commit message. + +### Report a bug + +Found a bug! Sorry to hear that :(. Help us and other track them down and +fix by reporting it. [File a new bug report][bug-report] on the [issues +tracker][issues-tracker]. The template should provide enough guidance on +what to include. When in doubt: add more details! By reducing ambiguity and +providing more information, it decreases back and forth and saves everyone +time. + +### Code changes + +Want to contribute a patch? Very happy to hear that! + +First, some high-level rules: + +* A short proposal with some POC code is better than a lengthy piece of + text with no code. Code speaks louder than words. +* No backward-incompatible patch will be accepted unless discussed. + Sometimes it's hard, and Go's lack of versioning by default does not + help, but we try not to break people's programs unless we absolutely have + to. +* If you are writing a new feature or extending an existing one, make sure + to write some documentation. +* Bug fixes need to be accompanied with regression tests. +* New code needs to be tested. +* Your commit messages need to explain why the change is needed, even if + already included in the PR description. + +It does sound like a lot, but those best practices are here to save time +overall and continuously improve the quality of the project, which is +something everyone benefits from. + +#### Get started + +The fairly standard code contribution process looks like that: + +1. [Fork the project][fork]. +2. Make your changes, commit on any branch you like. +3. [Open up a pull request][pull-request] +4. Review, potential ask for changes. +5. Merge. You're in! + +Feel free to ask for help! You can create draft pull requests to gather +some early feedback! + +#### Run the tests + +You can run tests for go-toml using Go's test tool: `go test ./...`. +When creating a pull requests, all tests will be ran on Linux on a few Go +versions (Travis CI), and on Windows using the latest Go version +(AppVeyor). + +#### Style + +Try to look around and follow the same format and structure as the rest of +the code. We enforce using `go fmt` on the whole code base. + +--- + +### Maintainers-only + +#### Merge pull request + +Checklist: + +* Passing CI. +* Does not introduce backward-incompatible changes (unless discussed). +* Has relevant doc changes. +* Has relevant unit tests. + +1. Merge using "squash and merge". +2. Make sure to edit the commit message to keep all the useful information + nice and clean. +3. Make sure the commit title is clear and contains the PR number (#123). + +#### New release + +1. Go to [releases][releases]. Click on "X commits to master since this + release". +2. Make note of all the changes. Look for backward incompatible changes, + new features, and bug fixes. +3. Pick the new version using the above and semver. +4. Create a [new release][new-release]. +5. Follow the same format as [1.1.0][release-110]. + +[issues-tracker]: https://github.com/pelletier/go-toml/issues +[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md +[godoc]: https://godoc.org/github.com/pelletier/go-toml +[readme]: ./README.md +[fork]: https://help.github.com/articles/fork-a-repo +[pull-request]: https://help.github.com/en/articles/creating-a-pull-request +[releases]: https://github.com/pelletier/go-toml/releases +[new-release]: https://github.com/pelletier/go-toml/releases/new +[release-110]: https://github.com/pelletier/go-toml/releases/tag/v1.1.0 diff --git a/vendor/github.com/pelletier/go-toml/Dockerfile b/vendor/github.com/pelletier/go-toml/Dockerfile new file mode 100644 index 00000000000..fffdb016668 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.12-alpine3.9 as builder +WORKDIR /go/src/github.com/pelletier/go-toml +COPY . . +ENV CGO_ENABLED=0 +ENV GOOS=linux +RUN go install ./... + +FROM scratch +COPY --from=builder /go/bin/tomll /usr/bin/tomll +COPY --from=builder /go/bin/tomljson /usr/bin/tomljson +COPY --from=builder /go/bin/jsontoml /usr/bin/jsontoml diff --git a/vendor/github.com/pelletier/go-toml/LICENSE b/vendor/github.com/pelletier/go-toml/LICENSE new file mode 100644 index 00000000000..583bdae6282 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 - 2017 Thomas Pelletier, Eric Anderton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/pelletier/go-toml/Makefile b/vendor/github.com/pelletier/go-toml/Makefile new file mode 100644 index 00000000000..9e4503aea65 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/Makefile @@ -0,0 +1,29 @@ +export CGO_ENABLED=0 +go := go +go.goos ?= $(shell echo `go version`|cut -f4 -d ' '|cut -d '/' -f1) +go.goarch ?= $(shell echo `go version`|cut -f4 -d ' '|cut -d '/' -f2) + +out.tools := tomll tomljson jsontoml +out.dist := $(out.tools:=_$(go.goos)_$(go.goarch).tar.xz) +sources := $(wildcard **/*.go) + + +.PHONY: +tools: $(out.tools) + +$(out.tools): $(sources) + GOOS=$(go.goos) GOARCH=$(go.goarch) $(go) build ./cmd/$@ + +.PHONY: +dist: $(out.dist) + +$(out.dist):%_$(go.goos)_$(go.goarch).tar.xz: % + if [ "$(go.goos)" = "windows" ]; then \ + tar -cJf $@ $^.exe; \ + else \ + tar -cJf $@ $^; \ + fi + +.PHONY: +clean: + rm -rf $(out.tools) $(out.dist) diff --git a/vendor/github.com/pelletier/go-toml/PULL_REQUEST_TEMPLATE.md b/vendor/github.com/pelletier/go-toml/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..041cdc4a2f1 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ +**Issue:** add link to pelletier/go-toml issue here + +Explanation of what this pull request does. + +More detailed description of the decisions being made and the reasons why (if the patch is non-trivial). diff --git a/vendor/github.com/pelletier/go-toml/README.md b/vendor/github.com/pelletier/go-toml/README.md new file mode 100644 index 00000000000..6831deb5bd1 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/README.md @@ -0,0 +1,151 @@ +# go-toml + +Go library for the [TOML](https://github.com/mojombo/toml) format. + +This library supports TOML version +[v1.0.0-rc.1](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v1.0.0-rc.1.md) + +[![GoDoc](https://godoc.org/github.com/pelletier/go-toml?status.svg)](http://godoc.org/github.com/pelletier/go-toml) +[![license](https://img.shields.io/github/license/pelletier/go-toml.svg)](https://github.com/pelletier/go-toml/blob/master/LICENSE) +[![Build Status](https://dev.azure.com/pelletierthomas/go-toml-ci/_apis/build/status/pelletier.go-toml?branchName=master)](https://dev.azure.com/pelletierthomas/go-toml-ci/_build/latest?definitionId=1&branchName=master) +[![codecov](https://codecov.io/gh/pelletier/go-toml/branch/master/graph/badge.svg)](https://codecov.io/gh/pelletier/go-toml) +[![Go Report Card](https://goreportcard.com/badge/github.com/pelletier/go-toml)](https://goreportcard.com/report/github.com/pelletier/go-toml) +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml?ref=badge_shield) + +## Features + +Go-toml provides the following features for using data parsed from TOML documents: + +* Load TOML documents from files and string data +* Easily navigate TOML structure using Tree +* Marshaling and unmarshaling to and from data structures +* Line & column position data for all parsed elements +* [Query support similar to JSON-Path](query/) +* Syntax errors contain line and column numbers + +## Import + +```go +import "github.com/pelletier/go-toml" +``` + +## Usage example + +Read a TOML document: + +```go +config, _ := toml.Load(` +[postgres] +user = "pelletier" +password = "mypassword"`) +// retrieve data directly +user := config.Get("postgres.user").(string) + +// or using an intermediate object +postgresConfig := config.Get("postgres").(*toml.Tree) +password := postgresConfig.Get("password").(string) +``` + +Or use Unmarshal: + +```go +type Postgres struct { + User string + Password string +} +type Config struct { + Postgres Postgres +} + +doc := []byte(` +[Postgres] +User = "pelletier" +Password = "mypassword"`) + +config := Config{} +toml.Unmarshal(doc, &config) +fmt.Println("user=", config.Postgres.User) +``` + +Or use a query: + +```go +// use a query to gather elements without walking the tree +q, _ := query.Compile("$..[user,password]") +results := q.Execute(config) +for ii, item := range results.Values() { + fmt.Printf("Query result %d: %v\n", ii, item) +} +``` + +## Documentation + +The documentation and additional examples are available at +[godoc.org](http://godoc.org/github.com/pelletier/go-toml). + +## Tools + +Go-toml provides two handy command line tools: + +* `tomll`: Reads TOML files and lints them. + + ``` + go install github.com/pelletier/go-toml/cmd/tomll + tomll --help + ``` +* `tomljson`: Reads a TOML file and outputs its JSON representation. + + ``` + go install github.com/pelletier/go-toml/cmd/tomljson + tomljson --help + ``` + + * `jsontoml`: Reads a JSON file and outputs a TOML representation. + + ``` + go install github.com/pelletier/go-toml/cmd/jsontoml + jsontoml --help + ``` + +### Docker image + +Those tools are also availble as a Docker image from +[dockerhub](https://hub.docker.com/r/pelletier/go-toml). For example, to +use `tomljson`: + +``` +docker run -v $PWD:/workdir pelletier/go-toml tomljson /workdir/example.toml +``` + +Only master (`latest`) and tagged versions are published to dockerhub. You +can build your own image as usual: + +``` +docker build -t go-toml . +``` + +## Contribute + +Feel free to report bugs and patches using GitHub's pull requests system on +[pelletier/go-toml](https://github.com/pelletier/go-toml). Any feedback would be +much appreciated! + +### Run tests + +`go test ./...` + +### Fuzzing + +The script `./fuzz.sh` is available to +run [go-fuzz](https://github.com/dvyukov/go-fuzz) on go-toml. + +## Versioning + +Go-toml follows [Semantic Versioning](http://semver.org/). The supported version +of [TOML](https://github.com/toml-lang/toml) is indicated at the beginning of +this document. The last two major versions of Go are supported +(see [Go Release Policy](https://golang.org/doc/devel/release.html#policy)). + +## License + +The MIT License (MIT). Read [LICENSE](LICENSE). diff --git a/vendor/github.com/pelletier/go-toml/azure-pipelines.yml b/vendor/github.com/pelletier/go-toml/azure-pipelines.yml new file mode 100644 index 00000000000..242b5b5403b --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/azure-pipelines.yml @@ -0,0 +1,230 @@ +trigger: +- master + +stages: +- stage: fuzzit + displayName: "Run Fuzzit" + dependsOn: [] + condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master')) + jobs: + - job: submit + displayName: "Submit" + pool: + vmImage: ubuntu-latest + steps: + - task: GoTool@0 + displayName: "Install Go 1.14" + inputs: + version: "1.14" + - script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/" + - script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml + - script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml + - task: Bash@3 + inputs: + filePath: './fuzzit.sh' + env: + TYPE: fuzzing + FUZZIT_API_KEY: $(FUZZIT_API_KEY) + +- stage: run_checks + displayName: "Check" + dependsOn: [] + jobs: + - job: fmt + displayName: "fmt" + pool: + vmImage: ubuntu-latest + steps: + - task: GoTool@0 + displayName: "Install Go 1.14" + inputs: + version: "1.14" + - task: Go@0 + displayName: "go fmt ./..." + inputs: + command: 'custom' + customCommand: 'fmt' + arguments: './...' + - job: coverage + displayName: "coverage" + pool: + vmImage: ubuntu-latest + steps: + - task: GoTool@0 + displayName: "Install Go 1.14" + inputs: + version: "1.14" + - task: Go@0 + displayName: "Generate coverage" + inputs: + command: 'test' + arguments: "-race -coverprofile=coverage.txt -covermode=atomic" + - task: Bash@3 + inputs: + targetType: 'inline' + script: 'bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN}' + env: + CODECOV_TOKEN: $(CODECOV_TOKEN) + - job: benchmark + displayName: "benchmark" + pool: + vmImage: ubuntu-latest + steps: + - task: GoTool@0 + displayName: "Install Go 1.14" + inputs: + version: "1.14" + - script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/" + - task: Bash@3 + inputs: + filePath: './benchmark.sh' + arguments: "master $(Build.Repository.Uri)" + + - job: fuzzing + displayName: "fuzzing" + pool: + vmImage: ubuntu-latest + steps: + - task: GoTool@0 + displayName: "Install Go 1.14" + inputs: + version: "1.14" + - script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/" + - script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml + - script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml + - task: Bash@3 + inputs: + filePath: './fuzzit.sh' + env: + TYPE: local-regression + + - job: go_unit_tests + displayName: "unit tests" + strategy: + matrix: + linux 1.14: + goVersion: '1.14' + imageName: 'ubuntu-latest' + mac 1.14: + goVersion: '1.14' + imageName: 'macOS-latest' + windows 1.14: + goVersion: '1.14' + imageName: 'windows-latest' + linux 1.13: + goVersion: '1.13' + imageName: 'ubuntu-latest' + mac 1.13: + goVersion: '1.13' + imageName: 'macOS-latest' + windows 1.13: + goVersion: '1.13' + imageName: 'windows-latest' + pool: + vmImage: $(imageName) + steps: + - task: GoTool@0 + displayName: "Install Go $(goVersion)" + inputs: + version: $(goVersion) + - task: Go@0 + displayName: "go test ./..." + inputs: + command: 'test' + arguments: './...' +- stage: build_binaries + displayName: "Build binaries" + dependsOn: run_checks + jobs: + - job: build_binary + displayName: "Build binary" + strategy: + matrix: + linux_amd64: + GOOS: linux + GOARCH: amd64 + darwin_amd64: + GOOS: darwin + GOARCH: amd64 + windows_amd64: + GOOS: windows + GOARCH: amd64 + pool: + vmImage: ubuntu-latest + steps: + - task: GoTool@0 + displayName: "Install Go" + inputs: + version: 1.14 + - task: Bash@3 + inputs: + targetType: inline + script: "make dist" + env: + go.goos: $(GOOS) + go.goarch: $(GOARCH) + - task: CopyFiles@2 + inputs: + sourceFolder: '$(Build.SourcesDirectory)' + contents: '*.tar.xz' + TargetFolder: '$(Build.ArtifactStagingDirectory)' + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: '$(Build.ArtifactStagingDirectory)' + artifactName: binaries +- stage: build_binaries_manifest + displayName: "Build binaries manifest" + dependsOn: build_binaries + jobs: + - job: build_manifest + displayName: "Build binaries manifest" + steps: + - task: DownloadBuildArtifacts@0 + inputs: + buildType: 'current' + downloadType: 'single' + artifactName: 'binaries' + downloadPath: '$(Build.SourcesDirectory)' + - task: Bash@3 + inputs: + targetType: inline + script: "cd binaries && sha256sum --binary *.tar.xz | tee $(Build.ArtifactStagingDirectory)/sha256sums.txt" + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: '$(Build.ArtifactStagingDirectory)' + artifactName: manifest + +- stage: build_docker_image + displayName: "Build Docker image" + dependsOn: run_checks + jobs: + - job: build + displayName: "Build" + pool: + vmImage: ubuntu-latest + steps: + - task: Docker@2 + inputs: + command: 'build' + Dockerfile: 'Dockerfile' + buildContext: '.' + addPipelineData: false + +- stage: publish_docker_image + displayName: "Publish Docker image" + dependsOn: build_docker_image + condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master')) + jobs: + - job: publish + displayName: "Publish" + pool: + vmImage: ubuntu-latest + steps: + - task: Docker@2 + inputs: + containerRegistry: 'DockerHub' + repository: 'pelletier/go-toml' + command: 'buildAndPush' + Dockerfile: 'Dockerfile' + buildContext: '.' + tags: 'latest' diff --git a/vendor/github.com/pelletier/go-toml/benchmark.json b/vendor/github.com/pelletier/go-toml/benchmark.json new file mode 100644 index 00000000000..86f99c6a877 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/benchmark.json @@ -0,0 +1,164 @@ +{ + "array": { + "key1": [ + 1, + 2, + 3 + ], + "key2": [ + "red", + "yellow", + "green" + ], + "key3": [ + [ + 1, + 2 + ], + [ + 3, + 4, + 5 + ] + ], + "key4": [ + [ + 1, + 2 + ], + [ + "a", + "b", + "c" + ] + ], + "key5": [ + 1, + 2, + 3 + ], + "key6": [ + 1, + 2 + ] + }, + "boolean": { + "False": false, + "True": true + }, + "datetime": { + "key1": "1979-05-27T07:32:00Z", + "key2": "1979-05-27T00:32:00-07:00", + "key3": "1979-05-27T00:32:00.999999-07:00" + }, + "float": { + "both": { + "key": 6.626e-34 + }, + "exponent": { + "key1": 5e+22, + "key2": 1000000, + "key3": -0.02 + }, + "fractional": { + "key1": 1, + "key2": 3.1415, + "key3": -0.01 + }, + "underscores": { + "key1": 9224617.445991227, + "key2": 1e+100 + } + }, + "fruit": [{ + "name": "apple", + "physical": { + "color": "red", + "shape": "round" + }, + "variety": [{ + "name": "red delicious" + }, + { + "name": "granny smith" + } + ] + }, + { + "name": "banana", + "variety": [{ + "name": "plantain" + }] + } + ], + "integer": { + "key1": 99, + "key2": 42, + "key3": 0, + "key4": -17, + "underscores": { + "key1": 1000, + "key2": 5349221, + "key3": 12345 + } + }, + "products": [{ + "name": "Hammer", + "sku": 738594937 + }, + {}, + { + "color": "gray", + "name": "Nail", + "sku": 284758393 + } + ], + "string": { + "basic": { + "basic": "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF." + }, + "literal": { + "multiline": { + "lines": "The first newline is\ntrimmed in raw strings.\n All other whitespace\n is preserved.\n", + "regex2": "I [dw]on't need \\d{2} apples" + }, + "quoted": "Tom \"Dubs\" Preston-Werner", + "regex": "\u003c\\i\\c*\\s*\u003e", + "winpath": "C:\\Users\\nodejs\\templates", + "winpath2": "\\\\ServerX\\admin$\\system32\\" + }, + "multiline": { + "continued": { + "key1": "The quick brown fox jumps over the lazy dog.", + "key2": "The quick brown fox jumps over the lazy dog.", + "key3": "The quick brown fox jumps over the lazy dog." + }, + "key1": "One\nTwo", + "key2": "One\nTwo", + "key3": "One\nTwo" + } + }, + "table": { + "inline": { + "name": { + "first": "Tom", + "last": "Preston-Werner" + }, + "point": { + "x": 1, + "y": 2 + } + }, + "key": "value", + "subtable": { + "key": "another value" + } + }, + "x": { + "y": { + "z": { + "w": {} + } + } + } +} diff --git a/vendor/github.com/pelletier/go-toml/benchmark.sh b/vendor/github.com/pelletier/go-toml/benchmark.sh new file mode 100644 index 00000000000..7914fff49c9 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/benchmark.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -ex + +reference_ref=${1:-master} +reference_git=${2:-.} + +if ! `hash benchstat 2>/dev/null`; then + echo "Installing benchstat" + go get golang.org/x/perf/cmd/benchstat +fi + +tempdir=`mktemp -d /tmp/go-toml-benchmark-XXXXXX` +ref_tempdir="${tempdir}/ref" +ref_benchmark="${ref_tempdir}/benchmark-`echo -n ${reference_ref}|tr -s '/' '-'`.txt" +local_benchmark="`pwd`/benchmark-local.txt" + +echo "=== ${reference_ref} (${ref_tempdir})" +git clone ${reference_git} ${ref_tempdir} >/dev/null 2>/dev/null +pushd ${ref_tempdir} >/dev/null +git checkout ${reference_ref} >/dev/null 2>/dev/null +go test -bench=. -benchmem | tee ${ref_benchmark} +popd >/dev/null + +echo "" +echo "=== local" +go test -bench=. -benchmem | tee ${local_benchmark} + +echo "" +echo "=== diff" +benchstat -delta-test=none ${ref_benchmark} ${local_benchmark} diff --git a/vendor/github.com/pelletier/go-toml/benchmark.toml b/vendor/github.com/pelletier/go-toml/benchmark.toml new file mode 100644 index 00000000000..dfd77e09622 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/benchmark.toml @@ -0,0 +1,244 @@ +################################################################################ +## Comment + +# Speak your mind with the hash symbol. They go from the symbol to the end of +# the line. + + +################################################################################ +## Table + +# Tables (also known as hash tables or dictionaries) are collections of +# key/value pairs. They appear in square brackets on a line by themselves. + +[table] + +key = "value" # Yeah, you can do this. + +# Nested tables are denoted by table names with dots in them. Name your tables +# whatever crap you please, just don't use #, ., [ or ]. + +[table.subtable] + +key = "another value" + +# You don't need to specify all the super-tables if you don't want to. TOML +# knows how to do it for you. + +# [x] you +# [x.y] don't +# [x.y.z] need these +[x.y.z.w] # for this to work + + +################################################################################ +## Inline Table + +# Inline tables provide a more compact syntax for expressing tables. They are +# especially useful for grouped data that can otherwise quickly become verbose. +# Inline tables are enclosed in curly braces `{` and `}`. No newlines are +# allowed between the curly braces unless they are valid within a value. + +[table.inline] + +name = { first = "Tom", last = "Preston-Werner" } +point = { x = 1, y = 2 } + + +################################################################################ +## String + +# There are four ways to express strings: basic, multi-line basic, literal, and +# multi-line literal. All strings must contain only valid UTF-8 characters. + +[string.basic] + +basic = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF." + +[string.multiline] + +# The following strings are byte-for-byte equivalent: +key1 = "One\nTwo" +key2 = """One\nTwo""" +key3 = """ +One +Two""" + +[string.multiline.continued] + +# The following strings are byte-for-byte equivalent: +key1 = "The quick brown fox jumps over the lazy dog." + +key2 = """ +The quick brown \ + + + fox jumps over \ + the lazy dog.""" + +key3 = """\ + The quick brown \ + fox jumps over \ + the lazy dog.\ + """ + +[string.literal] + +# What you see is what you get. +winpath = 'C:\Users\nodejs\templates' +winpath2 = '\\ServerX\admin$\system32\' +quoted = 'Tom "Dubs" Preston-Werner' +regex = '<\i\c*\s*>' + + +[string.literal.multiline] + +regex2 = '''I [dw]on't need \d{2} apples''' +lines = ''' +The first newline is +trimmed in raw strings. + All other whitespace + is preserved. +''' + + +################################################################################ +## Integer + +# Integers are whole numbers. Positive numbers may be prefixed with a plus sign. +# Negative numbers are prefixed with a minus sign. + +[integer] + +key1 = +99 +key2 = 42 +key3 = 0 +key4 = -17 + +[integer.underscores] + +# For large numbers, you may use underscores to enhance readability. Each +# underscore must be surrounded by at least one digit. +key1 = 1_000 +key2 = 5_349_221 +key3 = 1_2_3_4_5 # valid but inadvisable + + +################################################################################ +## Float + +# A float consists of an integer part (which may be prefixed with a plus or +# minus sign) followed by a fractional part and/or an exponent part. + +[float.fractional] + +key1 = +1.0 +key2 = 3.1415 +key3 = -0.01 + +[float.exponent] + +key1 = 5e+22 +key2 = 1e6 +key3 = -2E-2 + +[float.both] + +key = 6.626e-34 + +[float.underscores] + +key1 = 9_224_617.445_991_228_313 +key2 = 1e1_00 + + +################################################################################ +## Boolean + +# Booleans are just the tokens you're used to. Always lowercase. + +[boolean] + +True = true +False = false + + +################################################################################ +## Datetime + +# Datetimes are RFC 3339 dates. + +[datetime] + +key1 = 1979-05-27T07:32:00Z +key2 = 1979-05-27T00:32:00-07:00 +key3 = 1979-05-27T00:32:00.999999-07:00 + + +################################################################################ +## Array + +# Arrays are square brackets with other primitives inside. Whitespace is +# ignored. Elements are separated by commas. Data types may not be mixed. + +[array] + +key1 = [ 1, 2, 3 ] +key2 = [ "red", "yellow", "green" ] +key3 = [ [ 1, 2 ], [3, 4, 5] ] +#key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok + +# Arrays can also be multiline. So in addition to ignoring whitespace, arrays +# also ignore newlines between the brackets. Terminating commas are ok before +# the closing bracket. + +key5 = [ + 1, 2, 3 +] +key6 = [ + 1, + 2, # this is ok +] + + +################################################################################ +## Array of Tables + +# These can be expressed by using a table name in double brackets. Each table +# with the same double bracketed name will be an element in the array. The +# tables are inserted in the order encountered. + +[[products]] + +name = "Hammer" +sku = 738594937 + +[[products]] + +[[products]] + +name = "Nail" +sku = 284758393 +color = "gray" + + +# You can create nested arrays of tables as well. + +[[fruit]] + name = "apple" + + [fruit.physical] + color = "red" + shape = "round" + + [[fruit.variety]] + name = "red delicious" + + [[fruit.variety]] + name = "granny smith" + +[[fruit]] + name = "banana" + + [[fruit.variety]] + name = "plantain" diff --git a/vendor/github.com/pelletier/go-toml/benchmark.yml b/vendor/github.com/pelletier/go-toml/benchmark.yml new file mode 100644 index 00000000000..0bd19f08a69 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/benchmark.yml @@ -0,0 +1,121 @@ +--- +array: + key1: + - 1 + - 2 + - 3 + key2: + - red + - yellow + - green + key3: + - - 1 + - 2 + - - 3 + - 4 + - 5 + key4: + - - 1 + - 2 + - - a + - b + - c + key5: + - 1 + - 2 + - 3 + key6: + - 1 + - 2 +boolean: + 'False': false + 'True': true +datetime: + key1: '1979-05-27T07:32:00Z' + key2: '1979-05-27T00:32:00-07:00' + key3: '1979-05-27T00:32:00.999999-07:00' +float: + both: + key: 6.626e-34 + exponent: + key1: 5.0e+22 + key2: 1000000 + key3: -0.02 + fractional: + key1: 1 + key2: 3.1415 + key3: -0.01 + underscores: + key1: 9224617.445991227 + key2: 1.0e+100 +fruit: +- name: apple + physical: + color: red + shape: round + variety: + - name: red delicious + - name: granny smith +- name: banana + variety: + - name: plantain +integer: + key1: 99 + key2: 42 + key3: 0 + key4: -17 + underscores: + key1: 1000 + key2: 5349221 + key3: 12345 +products: +- name: Hammer + sku: 738594937 +- {} +- color: gray + name: Nail + sku: 284758393 +string: + basic: + basic: "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF." + literal: + multiline: + lines: | + The first newline is + trimmed in raw strings. + All other whitespace + is preserved. + regex2: I [dw]on't need \d{2} apples + quoted: Tom "Dubs" Preston-Werner + regex: "<\\i\\c*\\s*>" + winpath: C:\Users\nodejs\templates + winpath2: "\\\\ServerX\\admin$\\system32\\" + multiline: + continued: + key1: The quick brown fox jumps over the lazy dog. + key2: The quick brown fox jumps over the lazy dog. + key3: The quick brown fox jumps over the lazy dog. + key1: |- + One + Two + key2: |- + One + Two + key3: |- + One + Two +table: + inline: + name: + first: Tom + last: Preston-Werner + point: + x: 1 + y: 2 + key: value + subtable: + key: another value +x: + y: + z: + w: {} diff --git a/vendor/github.com/pelletier/go-toml/doc.go b/vendor/github.com/pelletier/go-toml/doc.go new file mode 100644 index 00000000000..a1406a32b38 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/doc.go @@ -0,0 +1,23 @@ +// Package toml is a TOML parser and manipulation library. +// +// This version supports the specification as described in +// https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md +// +// Marshaling +// +// Go-toml can marshal and unmarshal TOML documents from and to data +// structures. +// +// TOML document as a tree +// +// Go-toml can operate on a TOML document as a tree. Use one of the Load* +// functions to parse TOML data and obtain a Tree instance, then one of its +// methods to manipulate the tree. +// +// JSONPath-like queries +// +// The package github.com/pelletier/go-toml/query implements a system +// similar to JSONPath to quickly retrieve elements of a TOML document using a +// single expression. See the package documentation for more information. +// +package toml diff --git a/vendor/github.com/pelletier/go-toml/example-crlf.toml b/vendor/github.com/pelletier/go-toml/example-crlf.toml new file mode 100644 index 00000000000..780d9c68f2d --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/example-crlf.toml @@ -0,0 +1,30 @@ +# This is a TOML document. Boom. + +title = "TOML Example" + +[owner] +name = "Tom Preston-Werner" +organization = "GitHub" +bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." +dob = 1979-05-27T07:32:00Z # First class dates? Why not? + +[database] +server = "192.168.1.1" +ports = [ 8001, 8001, 8002 ] +connection_max = 5000 +enabled = true + +[servers] + + # You can indent as you please. Tabs or spaces. TOML don't care. + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10" + +[clients] +data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it +score = 4e-08 # to make sure leading zeroes in exponent parts of floats are supported \ No newline at end of file diff --git a/vendor/github.com/pelletier/go-toml/example.toml b/vendor/github.com/pelletier/go-toml/example.toml new file mode 100644 index 00000000000..f45bf88b8f6 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/example.toml @@ -0,0 +1,30 @@ +# This is a TOML document. Boom. + +title = "TOML Example" + +[owner] +name = "Tom Preston-Werner" +organization = "GitHub" +bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." +dob = 1979-05-27T07:32:00Z # First class dates? Why not? + +[database] +server = "192.168.1.1" +ports = [ 8001, 8001, 8002 ] +connection_max = 5000 +enabled = true + +[servers] + + # You can indent as you please. Tabs or spaces. TOML don't care. + [servers.alpha] + ip = "10.0.0.1" + dc = "eqdc10" + + [servers.beta] + ip = "10.0.0.2" + dc = "eqdc10" + +[clients] +data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it +score = 4e-08 # to make sure leading zeroes in exponent parts of floats are supported \ No newline at end of file diff --git a/vendor/github.com/pelletier/go-toml/fuzz.go b/vendor/github.com/pelletier/go-toml/fuzz.go new file mode 100644 index 00000000000..14570c8d357 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/fuzz.go @@ -0,0 +1,31 @@ +// +build gofuzz + +package toml + +func Fuzz(data []byte) int { + tree, err := LoadBytes(data) + if err != nil { + if tree != nil { + panic("tree must be nil if there is an error") + } + return 0 + } + + str, err := tree.ToTomlString() + if err != nil { + if str != "" { + panic(`str must be "" if there is an error`) + } + panic(err) + } + + tree, err = Load(str) + if err != nil { + if tree != nil { + panic("tree must be nil if there is an error") + } + return 0 + } + + return 1 +} diff --git a/vendor/github.com/pelletier/go-toml/fuzz.sh b/vendor/github.com/pelletier/go-toml/fuzz.sh new file mode 100644 index 00000000000..3204b4c4463 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/fuzz.sh @@ -0,0 +1,15 @@ +#! /bin/sh +set -eu + +go get github.com/dvyukov/go-fuzz/go-fuzz +go get github.com/dvyukov/go-fuzz/go-fuzz-build + +if [ ! -e toml-fuzz.zip ]; then + go-fuzz-build github.com/pelletier/go-toml +fi + +rm -fr fuzz +mkdir -p fuzz/corpus +cp *.toml fuzz/corpus + +go-fuzz -bin=toml-fuzz.zip -workdir=fuzz diff --git a/vendor/github.com/pelletier/go-toml/fuzzit.sh b/vendor/github.com/pelletier/go-toml/fuzzit.sh new file mode 100644 index 00000000000..b575a6081f0 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/fuzzit.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -xe + +# go-fuzz doesn't support modules yet, so ensure we do everything +# in the old style GOPATH way +export GO111MODULE="off" + +# install go-fuzz +go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build + +# target name can only contain lower-case letters (a-z), digits (0-9) and a dash (-) +# to add another target, make sure to create it with `fuzzit create target` +# before using `fuzzit create job` +TARGET=toml-fuzzer + +go-fuzz-build -libfuzzer -o ${TARGET}.a github.com/pelletier/go-toml +clang -fsanitize=fuzzer ${TARGET}.a -o ${TARGET} + +# install fuzzit for talking to fuzzit.dev service +# or latest version: +# https://github.com/fuzzitdev/fuzzit/releases/latest/download/fuzzit_Linux_x86_64 +wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.52/fuzzit_Linux_x86_64 +chmod a+x fuzzit + +# TODO: change kkowalczyk to go-toml and create toml-fuzzer target there +./fuzzit create job --type $TYPE go-toml/${TARGET} ${TARGET} diff --git a/vendor/github.com/pelletier/go-toml/go.mod b/vendor/github.com/pelletier/go-toml/go.mod new file mode 100644 index 00000000000..c7faa6b3e11 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/go.mod @@ -0,0 +1,9 @@ +module github.com/pelletier/go-toml + +go 1.12 + +require ( + github.com/BurntSushi/toml v0.3.1 + github.com/davecgh/go-spew v1.1.1 + gopkg.in/yaml.v2 v2.3.0 +) diff --git a/vendor/github.com/pelletier/go-toml/go.sum b/vendor/github.com/pelletier/go-toml/go.sum new file mode 100644 index 00000000000..6f356470d7c --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/go.sum @@ -0,0 +1,19 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/github.com/pelletier/go-toml/keysparsing.go b/vendor/github.com/pelletier/go-toml/keysparsing.go new file mode 100644 index 00000000000..e091500b246 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/keysparsing.go @@ -0,0 +1,112 @@ +// Parsing keys handling both bare and quoted keys. + +package toml + +import ( + "errors" + "fmt" +) + +// Convert the bare key group string to an array. +// The input supports double quotation and single quotation, +// but escape sequences are not supported. Lexers must unescape them beforehand. +func parseKey(key string) ([]string, error) { + runes := []rune(key) + var groups []string + + if len(key) == 0 { + return nil, errors.New("empty key") + } + + idx := 0 + for idx < len(runes) { + for ; idx < len(runes) && isSpace(runes[idx]); idx++ { + // skip leading whitespace + } + if idx >= len(runes) { + break + } + r := runes[idx] + if isValidBareChar(r) { + // parse bare key + startIdx := idx + endIdx := -1 + idx++ + for idx < len(runes) { + r = runes[idx] + if isValidBareChar(r) { + idx++ + } else if r == '.' { + endIdx = idx + break + } else if isSpace(r) { + endIdx = idx + for ; idx < len(runes) && isSpace(runes[idx]); idx++ { + // skip trailing whitespace + } + if idx < len(runes) && runes[idx] != '.' { + return nil, fmt.Errorf("invalid key character after whitespace: %c", runes[idx]) + } + break + } else { + return nil, fmt.Errorf("invalid bare key character: %c", r) + } + } + if endIdx == -1 { + endIdx = idx + } + groups = append(groups, string(runes[startIdx:endIdx])) + } else if r == '\'' { + // parse single quoted key + idx++ + startIdx := idx + for { + if idx >= len(runes) { + return nil, fmt.Errorf("unclosed single-quoted key") + } + r = runes[idx] + if r == '\'' { + groups = append(groups, string(runes[startIdx:idx])) + idx++ + break + } + idx++ + } + } else if r == '"' { + // parse double quoted key + idx++ + startIdx := idx + for { + if idx >= len(runes) { + return nil, fmt.Errorf("unclosed double-quoted key") + } + r = runes[idx] + if r == '"' { + groups = append(groups, string(runes[startIdx:idx])) + idx++ + break + } + idx++ + } + } else if r == '.' { + idx++ + if idx >= len(runes) { + return nil, fmt.Errorf("unexpected end of key") + } + r = runes[idx] + if !isValidBareChar(r) && r != '\'' && r != '"' && r != ' ' { + return nil, fmt.Errorf("expecting key part after dot") + } + } else { + return nil, fmt.Errorf("invalid key character: %c", r) + } + } + if len(groups) == 0 { + return nil, fmt.Errorf("empty key") + } + return groups, nil +} + +func isValidBareChar(r rune) bool { + return isAlphanumeric(r) || r == '-' || isDigit(r) +} diff --git a/vendor/github.com/pelletier/go-toml/lexer.go b/vendor/github.com/pelletier/go-toml/lexer.go new file mode 100644 index 00000000000..425e847a7aa --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/lexer.go @@ -0,0 +1,801 @@ +// TOML lexer. +// +// Written using the principles developed by Rob Pike in +// http://www.youtube.com/watch?v=HxaD_trXwRE + +package toml + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +var dateRegexp *regexp.Regexp + +// Define state functions +type tomlLexStateFn func() tomlLexStateFn + +// Define lexer +type tomlLexer struct { + inputIdx int + input []rune // Textual source + currentTokenStart int + currentTokenStop int + tokens []token + brackets []rune + line int + col int + endbufferLine int + endbufferCol int +} + +// Basic read operations on input + +func (l *tomlLexer) read() rune { + r := l.peek() + if r == '\n' { + l.endbufferLine++ + l.endbufferCol = 1 + } else { + l.endbufferCol++ + } + l.inputIdx++ + return r +} + +func (l *tomlLexer) next() rune { + r := l.read() + + if r != eof { + l.currentTokenStop++ + } + return r +} + +func (l *tomlLexer) ignore() { + l.currentTokenStart = l.currentTokenStop + l.line = l.endbufferLine + l.col = l.endbufferCol +} + +func (l *tomlLexer) skip() { + l.next() + l.ignore() +} + +func (l *tomlLexer) fastForward(n int) { + for i := 0; i < n; i++ { + l.next() + } +} + +func (l *tomlLexer) emitWithValue(t tokenType, value string) { + l.tokens = append(l.tokens, token{ + Position: Position{l.line, l.col}, + typ: t, + val: value, + }) + l.ignore() +} + +func (l *tomlLexer) emit(t tokenType) { + l.emitWithValue(t, string(l.input[l.currentTokenStart:l.currentTokenStop])) +} + +func (l *tomlLexer) peek() rune { + if l.inputIdx >= len(l.input) { + return eof + } + return l.input[l.inputIdx] +} + +func (l *tomlLexer) peekString(size int) string { + maxIdx := len(l.input) + upperIdx := l.inputIdx + size // FIXME: potential overflow + if upperIdx > maxIdx { + upperIdx = maxIdx + } + return string(l.input[l.inputIdx:upperIdx]) +} + +func (l *tomlLexer) follow(next string) bool { + return next == l.peekString(len(next)) +} + +// Error management + +func (l *tomlLexer) errorf(format string, args ...interface{}) tomlLexStateFn { + l.tokens = append(l.tokens, token{ + Position: Position{l.line, l.col}, + typ: tokenError, + val: fmt.Sprintf(format, args...), + }) + return nil +} + +// State functions + +func (l *tomlLexer) lexVoid() tomlLexStateFn { + for { + next := l.peek() + switch next { + case '}': // after '{' + return l.lexRightCurlyBrace + case '[': + return l.lexTableKey + case '#': + return l.lexComment(l.lexVoid) + case '=': + return l.lexEqual + case '\r': + fallthrough + case '\n': + l.skip() + continue + } + + if isSpace(next) { + l.skip() + } + + if isKeyStartChar(next) { + return l.lexKey + } + + if next == eof { + l.next() + break + } + } + + l.emit(tokenEOF) + return nil +} + +func (l *tomlLexer) lexRvalue() tomlLexStateFn { + for { + next := l.peek() + switch next { + case '.': + return l.errorf("cannot start float with a dot") + case '=': + return l.lexEqual + case '[': + return l.lexLeftBracket + case ']': + return l.lexRightBracket + case '{': + return l.lexLeftCurlyBrace + case '}': + return l.lexRightCurlyBrace + case '#': + return l.lexComment(l.lexRvalue) + case '"': + return l.lexString + case '\'': + return l.lexLiteralString + case ',': + return l.lexComma + case '\r': + fallthrough + case '\n': + l.skip() + if len(l.brackets) > 0 && l.brackets[len(l.brackets)-1] == '[' { + return l.lexRvalue + } + return l.lexVoid + } + + if l.follow("true") { + return l.lexTrue + } + + if l.follow("false") { + return l.lexFalse + } + + if l.follow("inf") { + return l.lexInf + } + + if l.follow("nan") { + return l.lexNan + } + + if isSpace(next) { + l.skip() + continue + } + + if next == eof { + l.next() + break + } + + possibleDate := l.peekString(35) + dateSubmatches := dateRegexp.FindStringSubmatch(possibleDate) + if dateSubmatches != nil && dateSubmatches[0] != "" { + l.fastForward(len(dateSubmatches[0])) + if dateSubmatches[2] == "" { // no timezone information => local date + return l.lexLocalDate + } + return l.lexDate + } + + if next == '+' || next == '-' || isDigit(next) { + return l.lexNumber + } + + return l.errorf("no value can start with %c", next) + } + + l.emit(tokenEOF) + return nil +} + +func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn { + l.next() + l.emit(tokenLeftCurlyBrace) + l.brackets = append(l.brackets, '{') + return l.lexVoid +} + +func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn { + l.next() + l.emit(tokenRightCurlyBrace) + if len(l.brackets) == 0 || l.brackets[len(l.brackets)-1] != '{' { + return l.errorf("cannot have '}' here") + } + l.brackets = l.brackets[:len(l.brackets)-1] + return l.lexRvalue +} + +func (l *tomlLexer) lexDate() tomlLexStateFn { + l.emit(tokenDate) + return l.lexRvalue +} + +func (l *tomlLexer) lexLocalDate() tomlLexStateFn { + l.emit(tokenLocalDate) + return l.lexRvalue +} + +func (l *tomlLexer) lexTrue() tomlLexStateFn { + l.fastForward(4) + l.emit(tokenTrue) + return l.lexRvalue +} + +func (l *tomlLexer) lexFalse() tomlLexStateFn { + l.fastForward(5) + l.emit(tokenFalse) + return l.lexRvalue +} + +func (l *tomlLexer) lexInf() tomlLexStateFn { + l.fastForward(3) + l.emit(tokenInf) + return l.lexRvalue +} + +func (l *tomlLexer) lexNan() tomlLexStateFn { + l.fastForward(3) + l.emit(tokenNan) + return l.lexRvalue +} + +func (l *tomlLexer) lexEqual() tomlLexStateFn { + l.next() + l.emit(tokenEqual) + return l.lexRvalue +} + +func (l *tomlLexer) lexComma() tomlLexStateFn { + l.next() + l.emit(tokenComma) + if len(l.brackets) > 0 && l.brackets[len(l.brackets)-1] == '{' { + return l.lexVoid + } + return l.lexRvalue +} + +// Parse the key and emits its value without escape sequences. +// bare keys, basic string keys and literal string keys are supported. +func (l *tomlLexer) lexKey() tomlLexStateFn { + growingString := "" + + for r := l.peek(); isKeyChar(r) || r == '\n' || r == '\r'; r = l.peek() { + if r == '"' { + l.next() + str, err := l.lexStringAsString(`"`, false, true) + if err != nil { + return l.errorf(err.Error()) + } + growingString += "\"" + str + "\"" + l.next() + continue + } else if r == '\'' { + l.next() + str, err := l.lexLiteralStringAsString(`'`, false) + if err != nil { + return l.errorf(err.Error()) + } + growingString += "'" + str + "'" + l.next() + continue + } else if r == '\n' { + return l.errorf("keys cannot contain new lines") + } else if isSpace(r) { + str := " " + // skip trailing whitespace + l.next() + for r = l.peek(); isSpace(r); r = l.peek() { + str += string(r) + l.next() + } + // break loop if not a dot + if r != '.' { + break + } + str += "." + // skip trailing whitespace after dot + l.next() + for r = l.peek(); isSpace(r); r = l.peek() { + str += string(r) + l.next() + } + growingString += str + continue + } else if r == '.' { + // skip + } else if !isValidBareChar(r) { + return l.errorf("keys cannot contain %c character", r) + } + growingString += string(r) + l.next() + } + l.emitWithValue(tokenKey, growingString) + return l.lexVoid +} + +func (l *tomlLexer) lexComment(previousState tomlLexStateFn) tomlLexStateFn { + return func() tomlLexStateFn { + for next := l.peek(); next != '\n' && next != eof; next = l.peek() { + if next == '\r' && l.follow("\r\n") { + break + } + l.next() + } + l.ignore() + return previousState + } +} + +func (l *tomlLexer) lexLeftBracket() tomlLexStateFn { + l.next() + l.emit(tokenLeftBracket) + l.brackets = append(l.brackets, '[') + return l.lexRvalue +} + +func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNewLine bool) (string, error) { + growingString := "" + + if discardLeadingNewLine { + if l.follow("\r\n") { + l.skip() + l.skip() + } else if l.peek() == '\n' { + l.skip() + } + } + + // find end of string + for { + if l.follow(terminator) { + return growingString, nil + } + + next := l.peek() + if next == eof { + break + } + growingString += string(l.next()) + } + + return "", errors.New("unclosed string") +} + +func (l *tomlLexer) lexLiteralString() tomlLexStateFn { + l.skip() + + // handle special case for triple-quote + terminator := "'" + discardLeadingNewLine := false + if l.follow("''") { + l.skip() + l.skip() + terminator = "'''" + discardLeadingNewLine = true + } + + str, err := l.lexLiteralStringAsString(terminator, discardLeadingNewLine) + if err != nil { + return l.errorf(err.Error()) + } + + l.emitWithValue(tokenString, str) + l.fastForward(len(terminator)) + l.ignore() + return l.lexRvalue +} + +// Lex a string and return the results as a string. +// Terminator is the substring indicating the end of the token. +// The resulting string does not include the terminator. +func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine, acceptNewLines bool) (string, error) { + growingString := "" + + if discardLeadingNewLine { + if l.follow("\r\n") { + l.skip() + l.skip() + } else if l.peek() == '\n' { + l.skip() + } + } + + for { + if l.follow(terminator) { + return growingString, nil + } + + if l.follow("\\") { + l.next() + switch l.peek() { + case '\r': + fallthrough + case '\n': + fallthrough + case '\t': + fallthrough + case ' ': + // skip all whitespace chars following backslash + for strings.ContainsRune("\r\n\t ", l.peek()) { + l.next() + } + case '"': + growingString += "\"" + l.next() + case 'n': + growingString += "\n" + l.next() + case 'b': + growingString += "\b" + l.next() + case 'f': + growingString += "\f" + l.next() + case '/': + growingString += "/" + l.next() + case 't': + growingString += "\t" + l.next() + case 'r': + growingString += "\r" + l.next() + case '\\': + growingString += "\\" + l.next() + case 'u': + l.next() + code := "" + for i := 0; i < 4; i++ { + c := l.peek() + if !isHexDigit(c) { + return "", errors.New("unfinished unicode escape") + } + l.next() + code = code + string(c) + } + intcode, err := strconv.ParseInt(code, 16, 32) + if err != nil { + return "", errors.New("invalid unicode escape: \\u" + code) + } + growingString += string(rune(intcode)) + case 'U': + l.next() + code := "" + for i := 0; i < 8; i++ { + c := l.peek() + if !isHexDigit(c) { + return "", errors.New("unfinished unicode escape") + } + l.next() + code = code + string(c) + } + intcode, err := strconv.ParseInt(code, 16, 64) + if err != nil { + return "", errors.New("invalid unicode escape: \\U" + code) + } + growingString += string(rune(intcode)) + default: + return "", errors.New("invalid escape sequence: \\" + string(l.peek())) + } + } else { + r := l.peek() + + if 0x00 <= r && r <= 0x1F && r != '\t' && !(acceptNewLines && (r == '\n' || r == '\r')) { + return "", fmt.Errorf("unescaped control character %U", r) + } + l.next() + growingString += string(r) + } + + if l.peek() == eof { + break + } + } + + return "", errors.New("unclosed string") +} + +func (l *tomlLexer) lexString() tomlLexStateFn { + l.skip() + + // handle special case for triple-quote + terminator := `"` + discardLeadingNewLine := false + acceptNewLines := false + if l.follow(`""`) { + l.skip() + l.skip() + terminator = `"""` + discardLeadingNewLine = true + acceptNewLines = true + } + + str, err := l.lexStringAsString(terminator, discardLeadingNewLine, acceptNewLines) + if err != nil { + return l.errorf(err.Error()) + } + + l.emitWithValue(tokenString, str) + l.fastForward(len(terminator)) + l.ignore() + return l.lexRvalue +} + +func (l *tomlLexer) lexTableKey() tomlLexStateFn { + l.next() + + if l.peek() == '[' { + // token '[[' signifies an array of tables + l.next() + l.emit(tokenDoubleLeftBracket) + return l.lexInsideTableArrayKey + } + // vanilla table key + l.emit(tokenLeftBracket) + return l.lexInsideTableKey +} + +// Parse the key till "]]", but only bare keys are supported +func (l *tomlLexer) lexInsideTableArrayKey() tomlLexStateFn { + for r := l.peek(); r != eof; r = l.peek() { + switch r { + case ']': + if l.currentTokenStop > l.currentTokenStart { + l.emit(tokenKeyGroupArray) + } + l.next() + if l.peek() != ']' { + break + } + l.next() + l.emit(tokenDoubleRightBracket) + return l.lexVoid + case '[': + return l.errorf("table array key cannot contain ']'") + default: + l.next() + } + } + return l.errorf("unclosed table array key") +} + +// Parse the key till "]" but only bare keys are supported +func (l *tomlLexer) lexInsideTableKey() tomlLexStateFn { + for r := l.peek(); r != eof; r = l.peek() { + switch r { + case ']': + if l.currentTokenStop > l.currentTokenStart { + l.emit(tokenKeyGroup) + } + l.next() + l.emit(tokenRightBracket) + return l.lexVoid + case '[': + return l.errorf("table key cannot contain ']'") + default: + l.next() + } + } + return l.errorf("unclosed table key") +} + +func (l *tomlLexer) lexRightBracket() tomlLexStateFn { + l.next() + l.emit(tokenRightBracket) + if len(l.brackets) == 0 || l.brackets[len(l.brackets)-1] != '[' { + return l.errorf("cannot have ']' here") + } + l.brackets = l.brackets[:len(l.brackets)-1] + return l.lexRvalue +} + +type validRuneFn func(r rune) bool + +func isValidHexRune(r rune) bool { + return r >= 'a' && r <= 'f' || + r >= 'A' && r <= 'F' || + r >= '0' && r <= '9' || + r == '_' +} + +func isValidOctalRune(r rune) bool { + return r >= '0' && r <= '7' || r == '_' +} + +func isValidBinaryRune(r rune) bool { + return r == '0' || r == '1' || r == '_' +} + +func (l *tomlLexer) lexNumber() tomlLexStateFn { + r := l.peek() + + if r == '0' { + follow := l.peekString(2) + if len(follow) == 2 { + var isValidRune validRuneFn + switch follow[1] { + case 'x': + isValidRune = isValidHexRune + case 'o': + isValidRune = isValidOctalRune + case 'b': + isValidRune = isValidBinaryRune + default: + if follow[1] >= 'a' && follow[1] <= 'z' || follow[1] >= 'A' && follow[1] <= 'Z' { + return l.errorf("unknown number base: %s. possible options are x (hex) o (octal) b (binary)", string(follow[1])) + } + } + + if isValidRune != nil { + l.next() + l.next() + digitSeen := false + for { + next := l.peek() + if !isValidRune(next) { + break + } + digitSeen = true + l.next() + } + + if !digitSeen { + return l.errorf("number needs at least one digit") + } + + l.emit(tokenInteger) + + return l.lexRvalue + } + } + } + + if r == '+' || r == '-' { + l.next() + if l.follow("inf") { + return l.lexInf + } + if l.follow("nan") { + return l.lexNan + } + } + + pointSeen := false + expSeen := false + digitSeen := false + for { + next := l.peek() + if next == '.' { + if pointSeen { + return l.errorf("cannot have two dots in one float") + } + l.next() + if !isDigit(l.peek()) { + return l.errorf("float cannot end with a dot") + } + pointSeen = true + } else if next == 'e' || next == 'E' { + expSeen = true + l.next() + r := l.peek() + if r == '+' || r == '-' { + l.next() + } + } else if isDigit(next) { + digitSeen = true + l.next() + } else if next == '_' { + l.next() + } else { + break + } + if pointSeen && !digitSeen { + return l.errorf("cannot start float with a dot") + } + } + + if !digitSeen { + return l.errorf("no digit in that number") + } + if pointSeen || expSeen { + l.emit(tokenFloat) + } else { + l.emit(tokenInteger) + } + return l.lexRvalue +} + +func (l *tomlLexer) run() { + for state := l.lexVoid; state != nil; { + state = state() + } +} + +func init() { + // Regexp for all date/time formats supported by TOML. + // Group 1: nano precision + // Group 2: timezone + // + // /!\ also matches the empty string + // + // Example matches: + //1979-05-27T07:32:00Z + //1979-05-27T00:32:00-07:00 + //1979-05-27T00:32:00.999999-07:00 + //1979-05-27 07:32:00Z + //1979-05-27 00:32:00-07:00 + //1979-05-27 00:32:00.999999-07:00 + //1979-05-27T07:32:00 + //1979-05-27T00:32:00.999999 + //1979-05-27 07:32:00 + //1979-05-27 00:32:00.999999 + //1979-05-27 + //07:32:00 + //00:32:00.999999 + dateRegexp = regexp.MustCompile(`^(?:\d{1,4}-\d{2}-\d{2})?(?:[T ]?\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})?)?`) +} + +// Entry point +func lexToml(inputBytes []byte) []token { + runes := bytes.Runes(inputBytes) + l := &tomlLexer{ + input: runes, + tokens: make([]token, 0, 256), + line: 1, + col: 1, + endbufferLine: 1, + endbufferCol: 1, + } + l.run() + return l.tokens +} diff --git a/vendor/github.com/pelletier/go-toml/localtime.go b/vendor/github.com/pelletier/go-toml/localtime.go new file mode 100644 index 00000000000..a2149e9663a --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/localtime.go @@ -0,0 +1,281 @@ +// Implementation of TOML's local date/time. +// Copied over from https://github.com/googleapis/google-cloud-go/blob/master/civil/civil.go +// to avoid pulling all the Google dependencies. +// +// Copyright 2016 Google LLC +// +// 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. + +// Package civil implements types for civil time, a time-zone-independent +// representation of time that follows the rules of the proleptic +// Gregorian calendar with exactly 24-hour days, 60-minute hours, and 60-second +// minutes. +// +// Because they lack location information, these types do not represent unique +// moments or intervals of time. Use time.Time for that purpose. +package toml + +import ( + "fmt" + "time" +) + +// A LocalDate represents a date (year, month, day). +// +// This type does not include location information, and therefore does not +// describe a unique 24-hour timespan. +type LocalDate struct { + Year int // Year (e.g., 2014). + Month time.Month // Month of the year (January = 1, ...). + Day int // Day of the month, starting at 1. +} + +// LocalDateOf returns the LocalDate in which a time occurs in that time's location. +func LocalDateOf(t time.Time) LocalDate { + var d LocalDate + d.Year, d.Month, d.Day = t.Date() + return d +} + +// ParseLocalDate parses a string in RFC3339 full-date format and returns the date value it represents. +func ParseLocalDate(s string) (LocalDate, error) { + t, err := time.Parse("2006-01-02", s) + if err != nil { + return LocalDate{}, err + } + return LocalDateOf(t), nil +} + +// String returns the date in RFC3339 full-date format. +func (d LocalDate) String() string { + return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day) +} + +// IsValid reports whether the date is valid. +func (d LocalDate) IsValid() bool { + return LocalDateOf(d.In(time.UTC)) == d +} + +// In returns the time corresponding to time 00:00:00 of the date in the location. +// +// In is always consistent with time.LocalDate, even when time.LocalDate returns a time +// on a different day. For example, if loc is America/Indiana/Vincennes, then both +// time.LocalDate(1955, time.May, 1, 0, 0, 0, 0, loc) +// and +// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}.In(loc) +// return 23:00:00 on April 30, 1955. +// +// In panics if loc is nil. +func (d LocalDate) In(loc *time.Location) time.Time { + return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc) +} + +// AddDays returns the date that is n days in the future. +// n can also be negative to go into the past. +func (d LocalDate) AddDays(n int) LocalDate { + return LocalDateOf(d.In(time.UTC).AddDate(0, 0, n)) +} + +// DaysSince returns the signed number of days between the date and s, not including the end day. +// This is the inverse operation to AddDays. +func (d LocalDate) DaysSince(s LocalDate) (days int) { + // We convert to Unix time so we do not have to worry about leap seconds: + // Unix time increases by exactly 86400 seconds per day. + deltaUnix := d.In(time.UTC).Unix() - s.In(time.UTC).Unix() + return int(deltaUnix / 86400) +} + +// Before reports whether d1 occurs before d2. +func (d1 LocalDate) Before(d2 LocalDate) bool { + if d1.Year != d2.Year { + return d1.Year < d2.Year + } + if d1.Month != d2.Month { + return d1.Month < d2.Month + } + return d1.Day < d2.Day +} + +// After reports whether d1 occurs after d2. +func (d1 LocalDate) After(d2 LocalDate) bool { + return d2.Before(d1) +} + +// MarshalText implements the encoding.TextMarshaler interface. +// The output is the result of d.String(). +func (d LocalDate) MarshalText() ([]byte, error) { + return []byte(d.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// The date is expected to be a string in a format accepted by ParseLocalDate. +func (d *LocalDate) UnmarshalText(data []byte) error { + var err error + *d, err = ParseLocalDate(string(data)) + return err +} + +// A LocalTime represents a time with nanosecond precision. +// +// This type does not include location information, and therefore does not +// describe a unique moment in time. +// +// This type exists to represent the TIME type in storage-based APIs like BigQuery. +// Most operations on Times are unlikely to be meaningful. Prefer the LocalDateTime type. +type LocalTime struct { + Hour int // The hour of the day in 24-hour format; range [0-23] + Minute int // The minute of the hour; range [0-59] + Second int // The second of the minute; range [0-59] + Nanosecond int // The nanosecond of the second; range [0-999999999] +} + +// LocalTimeOf returns the LocalTime representing the time of day in which a time occurs +// in that time's location. It ignores the date. +func LocalTimeOf(t time.Time) LocalTime { + var tm LocalTime + tm.Hour, tm.Minute, tm.Second = t.Clock() + tm.Nanosecond = t.Nanosecond() + return tm +} + +// ParseLocalTime parses a string and returns the time value it represents. +// ParseLocalTime accepts an extended form of the RFC3339 partial-time format. After +// the HH:MM:SS part of the string, an optional fractional part may appear, +// consisting of a decimal point followed by one to nine decimal digits. +// (RFC3339 admits only one digit after the decimal point). +func ParseLocalTime(s string) (LocalTime, error) { + t, err := time.Parse("15:04:05.999999999", s) + if err != nil { + return LocalTime{}, err + } + return LocalTimeOf(t), nil +} + +// String returns the date in the format described in ParseLocalTime. If Nanoseconds +// is zero, no fractional part will be generated. Otherwise, the result will +// end with a fractional part consisting of a decimal point and nine digits. +func (t LocalTime) String() string { + s := fmt.Sprintf("%02d:%02d:%02d", t.Hour, t.Minute, t.Second) + if t.Nanosecond == 0 { + return s + } + return s + fmt.Sprintf(".%09d", t.Nanosecond) +} + +// IsValid reports whether the time is valid. +func (t LocalTime) IsValid() bool { + // Construct a non-zero time. + tm := time.Date(2, 2, 2, t.Hour, t.Minute, t.Second, t.Nanosecond, time.UTC) + return LocalTimeOf(tm) == t +} + +// MarshalText implements the encoding.TextMarshaler interface. +// The output is the result of t.String(). +func (t LocalTime) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// The time is expected to be a string in a format accepted by ParseLocalTime. +func (t *LocalTime) UnmarshalText(data []byte) error { + var err error + *t, err = ParseLocalTime(string(data)) + return err +} + +// A LocalDateTime represents a date and time. +// +// This type does not include location information, and therefore does not +// describe a unique moment in time. +type LocalDateTime struct { + Date LocalDate + Time LocalTime +} + +// Note: We deliberately do not embed LocalDate into LocalDateTime, to avoid promoting AddDays and Sub. + +// LocalDateTimeOf returns the LocalDateTime in which a time occurs in that time's location. +func LocalDateTimeOf(t time.Time) LocalDateTime { + return LocalDateTime{ + Date: LocalDateOf(t), + Time: LocalTimeOf(t), + } +} + +// ParseLocalDateTime parses a string and returns the LocalDateTime it represents. +// ParseLocalDateTime accepts a variant of the RFC3339 date-time format that omits +// the time offset but includes an optional fractional time, as described in +// ParseLocalTime. Informally, the accepted format is +// YYYY-MM-DDTHH:MM:SS[.FFFFFFFFF] +// where the 'T' may be a lower-case 't'. +func ParseLocalDateTime(s string) (LocalDateTime, error) { + t, err := time.Parse("2006-01-02T15:04:05.999999999", s) + if err != nil { + t, err = time.Parse("2006-01-02t15:04:05.999999999", s) + if err != nil { + return LocalDateTime{}, err + } + } + return LocalDateTimeOf(t), nil +} + +// String returns the date in the format described in ParseLocalDate. +func (dt LocalDateTime) String() string { + return dt.Date.String() + "T" + dt.Time.String() +} + +// IsValid reports whether the datetime is valid. +func (dt LocalDateTime) IsValid() bool { + return dt.Date.IsValid() && dt.Time.IsValid() +} + +// In returns the time corresponding to the LocalDateTime in the given location. +// +// If the time is missing or ambigous at the location, In returns the same +// result as time.LocalDate. For example, if loc is America/Indiana/Vincennes, then +// both +// time.LocalDate(1955, time.May, 1, 0, 30, 0, 0, loc) +// and +// civil.LocalDateTime{ +// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}}, +// civil.LocalTime{Minute: 30}}.In(loc) +// return 23:30:00 on April 30, 1955. +// +// In panics if loc is nil. +func (dt LocalDateTime) In(loc *time.Location) time.Time { + return time.Date(dt.Date.Year, dt.Date.Month, dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc) +} + +// Before reports whether dt1 occurs before dt2. +func (dt1 LocalDateTime) Before(dt2 LocalDateTime) bool { + return dt1.In(time.UTC).Before(dt2.In(time.UTC)) +} + +// After reports whether dt1 occurs after dt2. +func (dt1 LocalDateTime) After(dt2 LocalDateTime) bool { + return dt2.Before(dt1) +} + +// MarshalText implements the encoding.TextMarshaler interface. +// The output is the result of dt.String(). +func (dt LocalDateTime) MarshalText() ([]byte, error) { + return []byte(dt.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// The datetime is expected to be a string in a format accepted by ParseLocalDateTime +func (dt *LocalDateTime) UnmarshalText(data []byte) error { + var err error + *dt, err = ParseLocalDateTime(string(data)) + return err +} diff --git a/vendor/github.com/pelletier/go-toml/marshal.go b/vendor/github.com/pelletier/go-toml/marshal.go new file mode 100644 index 00000000000..db5a7b4f09a --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/marshal.go @@ -0,0 +1,1240 @@ +package toml + +import ( + "bytes" + "encoding" + "errors" + "fmt" + "io" + "reflect" + "sort" + "strconv" + "strings" + "time" +) + +const ( + tagFieldName = "toml" + tagFieldComment = "comment" + tagCommented = "commented" + tagMultiline = "multiline" + tagDefault = "default" +) + +type tomlOpts struct { + name string + nameFromTag bool + comment string + commented bool + multiline bool + include bool + omitempty bool + defaultValue string +} + +type encOpts struct { + quoteMapKeys bool + arraysOneElementPerLine bool +} + +var encOptsDefaults = encOpts{ + quoteMapKeys: false, +} + +type annotation struct { + tag string + comment string + commented string + multiline string + defaultValue string +} + +var annotationDefault = annotation{ + tag: tagFieldName, + comment: tagFieldComment, + commented: tagCommented, + multiline: tagMultiline, + defaultValue: tagDefault, +} + +type marshalOrder int + +// Orders the Encoder can write the fields to the output stream. +const ( + // Sort fields alphabetically. + OrderAlphabetical marshalOrder = iota + 1 + // Preserve the order the fields are encountered. For example, the order of fields in + // a struct. + OrderPreserve +) + +var timeType = reflect.TypeOf(time.Time{}) +var marshalerType = reflect.TypeOf(new(Marshaler)).Elem() +var unmarshalerType = reflect.TypeOf(new(Unmarshaler)).Elem() +var textMarshalerType = reflect.TypeOf(new(encoding.TextMarshaler)).Elem() +var textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem() +var localDateType = reflect.TypeOf(LocalDate{}) +var localTimeType = reflect.TypeOf(LocalTime{}) +var localDateTimeType = reflect.TypeOf(LocalDateTime{}) + +// Check if the given marshal type maps to a Tree primitive +func isPrimitive(mtype reflect.Type) bool { + switch mtype.Kind() { + case reflect.Ptr: + return isPrimitive(mtype.Elem()) + case reflect.Bool: + return true + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return true + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return true + case reflect.Float32, reflect.Float64: + return true + case reflect.String: + return true + case reflect.Struct: + return isTimeType(mtype) + default: + return false + } +} + +func isTimeType(mtype reflect.Type) bool { + return mtype == timeType || mtype == localDateType || mtype == localDateTimeType || mtype == localTimeType +} + +// Check if the given marshal type maps to a Tree slice or array +func isTreeSequence(mtype reflect.Type) bool { + switch mtype.Kind() { + case reflect.Ptr: + return isTreeSequence(mtype.Elem()) + case reflect.Slice, reflect.Array: + return isTree(mtype.Elem()) + default: + return false + } +} + +// Check if the given marshal type maps to a slice or array of a custom marshaler type +func isCustomMarshalerSequence(mtype reflect.Type) bool { + switch mtype.Kind() { + case reflect.Ptr: + return isCustomMarshalerSequence(mtype.Elem()) + case reflect.Slice, reflect.Array: + return isCustomMarshaler(mtype.Elem()) || isCustomMarshaler(reflect.New(mtype.Elem()).Type()) + default: + return false + } +} + +// Check if the given marshal type maps to a slice or array of a text marshaler type +func isTextMarshalerSequence(mtype reflect.Type) bool { + switch mtype.Kind() { + case reflect.Ptr: + return isTextMarshalerSequence(mtype.Elem()) + case reflect.Slice, reflect.Array: + return isTextMarshaler(mtype.Elem()) || isTextMarshaler(reflect.New(mtype.Elem()).Type()) + default: + return false + } +} + +// Check if the given marshal type maps to a non-Tree slice or array +func isOtherSequence(mtype reflect.Type) bool { + switch mtype.Kind() { + case reflect.Ptr: + return isOtherSequence(mtype.Elem()) + case reflect.Slice, reflect.Array: + return !isTreeSequence(mtype) + default: + return false + } +} + +// Check if the given marshal type maps to a Tree +func isTree(mtype reflect.Type) bool { + switch mtype.Kind() { + case reflect.Ptr: + return isTree(mtype.Elem()) + case reflect.Map: + return true + case reflect.Struct: + return !isPrimitive(mtype) + default: + return false + } +} + +func isCustomMarshaler(mtype reflect.Type) bool { + return mtype.Implements(marshalerType) +} + +func callCustomMarshaler(mval reflect.Value) ([]byte, error) { + return mval.Interface().(Marshaler).MarshalTOML() +} + +func isTextMarshaler(mtype reflect.Type) bool { + return mtype.Implements(textMarshalerType) && !isTimeType(mtype) +} + +func callTextMarshaler(mval reflect.Value) ([]byte, error) { + return mval.Interface().(encoding.TextMarshaler).MarshalText() +} + +func isCustomUnmarshaler(mtype reflect.Type) bool { + return mtype.Implements(unmarshalerType) +} + +func callCustomUnmarshaler(mval reflect.Value, tval interface{}) error { + return mval.Interface().(Unmarshaler).UnmarshalTOML(tval) +} + +func isTextUnmarshaler(mtype reflect.Type) bool { + return mtype.Implements(textUnmarshalerType) +} + +func callTextUnmarshaler(mval reflect.Value, text []byte) error { + return mval.Interface().(encoding.TextUnmarshaler).UnmarshalText(text) +} + +// Marshaler is the interface implemented by types that +// can marshal themselves into valid TOML. +type Marshaler interface { + MarshalTOML() ([]byte, error) +} + +// Unmarshaler is the interface implemented by types that +// can unmarshal a TOML description of themselves. +type Unmarshaler interface { + UnmarshalTOML(interface{}) error +} + +/* +Marshal returns the TOML encoding of v. Behavior is similar to the Go json +encoder, except that there is no concept of a Marshaler interface or MarshalTOML +function for sub-structs, and currently only definite types can be marshaled +(i.e. no `interface{}`). + +The following struct annotations are supported: + + toml:"Field" Overrides the field's name to output. + omitempty When set, empty values and groups are not emitted. + comment:"comment" Emits a # comment on the same line. This supports new lines. + commented:"true" Emits the value as commented. + +Note that pointers are automatically assigned the "omitempty" option, as TOML +explicitly does not handle null values (saying instead the label should be +dropped). + +Tree structural types and corresponding marshal types: + + *Tree (*)struct, (*)map[string]interface{} + []*Tree (*)[](*)struct, (*)[](*)map[string]interface{} + []interface{} (as interface{}) (*)[]primitive, (*)[]([]interface{}) + interface{} (*)primitive + +Tree primitive types and corresponding marshal types: + + uint64 uint, uint8-uint64, pointers to same + int64 int, int8-uint64, pointers to same + float64 float32, float64, pointers to same + string string, pointers to same + bool bool, pointers to same + time.LocalTime time.LocalTime{}, pointers to same + +For additional flexibility, use the Encoder API. +*/ +func Marshal(v interface{}) ([]byte, error) { + return NewEncoder(nil).marshal(v) +} + +// Encoder writes TOML values to an output stream. +type Encoder struct { + w io.Writer + encOpts + annotation + line int + col int + order marshalOrder + promoteAnon bool + indentation string +} + +// NewEncoder returns a new encoder that writes to w. +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{ + w: w, + encOpts: encOptsDefaults, + annotation: annotationDefault, + line: 0, + col: 1, + order: OrderAlphabetical, + indentation: " ", + } +} + +// Encode writes the TOML encoding of v to the stream. +// +// See the documentation for Marshal for details. +func (e *Encoder) Encode(v interface{}) error { + b, err := e.marshal(v) + if err != nil { + return err + } + if _, err := e.w.Write(b); err != nil { + return err + } + return nil +} + +// QuoteMapKeys sets up the encoder to encode +// maps with string type keys with quoted TOML keys. +// +// This relieves the character limitations on map keys. +func (e *Encoder) QuoteMapKeys(v bool) *Encoder { + e.quoteMapKeys = v + return e +} + +// ArraysWithOneElementPerLine sets up the encoder to encode arrays +// with more than one element on multiple lines instead of one. +// +// For example: +// +// A = [1,2,3] +// +// Becomes +// +// A = [ +// 1, +// 2, +// 3, +// ] +func (e *Encoder) ArraysWithOneElementPerLine(v bool) *Encoder { + e.arraysOneElementPerLine = v + return e +} + +// Order allows to change in which order fields will be written to the output stream. +func (e *Encoder) Order(ord marshalOrder) *Encoder { + e.order = ord + return e +} + +// Indentation allows to change indentation when marshalling. +func (e *Encoder) Indentation(indent string) *Encoder { + e.indentation = indent + return e +} + +// SetTagName allows changing default tag "toml" +func (e *Encoder) SetTagName(v string) *Encoder { + e.tag = v + return e +} + +// SetTagComment allows changing default tag "comment" +func (e *Encoder) SetTagComment(v string) *Encoder { + e.comment = v + return e +} + +// SetTagCommented allows changing default tag "commented" +func (e *Encoder) SetTagCommented(v string) *Encoder { + e.commented = v + return e +} + +// SetTagMultiline allows changing default tag "multiline" +func (e *Encoder) SetTagMultiline(v string) *Encoder { + e.multiline = v + return e +} + +// PromoteAnonymous allows to change how anonymous struct fields are marshaled. +// Usually, they are marshaled as if the inner exported fields were fields in +// the outer struct. However, if an anonymous struct field is given a name in +// its TOML tag, it is treated like a regular struct field with that name. +// rather than being anonymous. +// +// In case anonymous promotion is enabled, all anonymous structs are promoted +// and treated like regular struct fields. +func (e *Encoder) PromoteAnonymous(promote bool) *Encoder { + e.promoteAnon = promote + return e +} + +func (e *Encoder) marshal(v interface{}) ([]byte, error) { + // Check if indentation is valid + for _, char := range e.indentation { + if !isSpace(char) { + return []byte{}, fmt.Errorf("invalid indentation: must only contains space or tab characters") + } + } + + mtype := reflect.TypeOf(v) + if mtype == nil { + return []byte{}, errors.New("nil cannot be marshaled to TOML") + } + + switch mtype.Kind() { + case reflect.Struct, reflect.Map: + case reflect.Ptr: + if mtype.Elem().Kind() != reflect.Struct { + return []byte{}, errors.New("Only pointer to struct can be marshaled to TOML") + } + if reflect.ValueOf(v).IsNil() { + return []byte{}, errors.New("nil pointer cannot be marshaled to TOML") + } + default: + return []byte{}, errors.New("Only a struct or map can be marshaled to TOML") + } + + sval := reflect.ValueOf(v) + if isCustomMarshaler(mtype) { + return callCustomMarshaler(sval) + } + if isTextMarshaler(mtype) { + return callTextMarshaler(sval) + } + t, err := e.valueToTree(mtype, sval) + if err != nil { + return []byte{}, err + } + + var buf bytes.Buffer + _, err = t.writeToOrdered(&buf, "", "", 0, e.arraysOneElementPerLine, e.order, e.indentation, false) + + return buf.Bytes(), err +} + +// Create next tree with a position based on Encoder.line +func (e *Encoder) nextTree() *Tree { + return newTreeWithPosition(Position{Line: e.line, Col: 1}) +} + +// Convert given marshal struct or map value to toml tree +func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, error) { + if mtype.Kind() == reflect.Ptr { + return e.valueToTree(mtype.Elem(), mval.Elem()) + } + tval := e.nextTree() + switch mtype.Kind() { + case reflect.Struct: + switch mval.Interface().(type) { + case Tree: + reflect.ValueOf(tval).Elem().Set(mval) + default: + for i := 0; i < mtype.NumField(); i++ { + mtypef, mvalf := mtype.Field(i), mval.Field(i) + opts := tomlOptions(mtypef, e.annotation) + if opts.include && ((mtypef.Type.Kind() != reflect.Interface && !opts.omitempty) || !isZero(mvalf)) { + val, err := e.valueToToml(mtypef.Type, mvalf) + if err != nil { + return nil, err + } + if tree, ok := val.(*Tree); ok && mtypef.Anonymous && !opts.nameFromTag && !e.promoteAnon { + e.appendTree(tval, tree) + } else { + tval.SetPathWithOptions([]string{opts.name}, SetOptions{ + Comment: opts.comment, + Commented: opts.commented, + Multiline: opts.multiline, + }, val) + } + } + } + } + case reflect.Map: + keys := mval.MapKeys() + if e.order == OrderPreserve && len(keys) > 0 { + // Sorting []reflect.Value is not straight forward. + // + // OrderPreserve will support deterministic results when string is used + // as the key to maps. + typ := keys[0].Type() + kind := keys[0].Kind() + if kind == reflect.String { + ikeys := make([]string, len(keys)) + for i := range keys { + ikeys[i] = keys[i].Interface().(string) + } + sort.Strings(ikeys) + for i := range ikeys { + keys[i] = reflect.ValueOf(ikeys[i]).Convert(typ) + } + } + } + for _, key := range keys { + mvalf := mval.MapIndex(key) + if (mtype.Elem().Kind() == reflect.Ptr || mtype.Elem().Kind() == reflect.Interface) && mvalf.IsNil() { + continue + } + val, err := e.valueToToml(mtype.Elem(), mvalf) + if err != nil { + return nil, err + } + if e.quoteMapKeys { + keyStr, err := tomlValueStringRepresentation(key.String(), "", "", e.order, e.arraysOneElementPerLine) + if err != nil { + return nil, err + } + tval.SetPath([]string{keyStr}, val) + } else { + tval.SetPath([]string{key.String()}, val) + } + } + } + return tval, nil +} + +// Convert given marshal slice to slice of Toml trees +func (e *Encoder) valueToTreeSlice(mtype reflect.Type, mval reflect.Value) ([]*Tree, error) { + tval := make([]*Tree, mval.Len(), mval.Len()) + for i := 0; i < mval.Len(); i++ { + val, err := e.valueToTree(mtype.Elem(), mval.Index(i)) + if err != nil { + return nil, err + } + tval[i] = val + } + return tval, nil +} + +// Convert given marshal slice to slice of toml values +func (e *Encoder) valueToOtherSlice(mtype reflect.Type, mval reflect.Value) (interface{}, error) { + tval := make([]interface{}, mval.Len(), mval.Len()) + for i := 0; i < mval.Len(); i++ { + val, err := e.valueToToml(mtype.Elem(), mval.Index(i)) + if err != nil { + return nil, err + } + tval[i] = val + } + return tval, nil +} + +// Convert given marshal value to toml value +func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface{}, error) { + e.line++ + if mtype.Kind() == reflect.Ptr { + switch { + case isCustomMarshaler(mtype): + return callCustomMarshaler(mval) + case isTextMarshaler(mtype): + return callTextMarshaler(mval) + default: + return e.valueToToml(mtype.Elem(), mval.Elem()) + } + } + if mtype.Kind() == reflect.Interface { + return e.valueToToml(mval.Elem().Type(), mval.Elem()) + } + switch { + case isCustomMarshaler(mtype): + return callCustomMarshaler(mval) + case isTextMarshaler(mtype): + return callTextMarshaler(mval) + case isTree(mtype): + return e.valueToTree(mtype, mval) + case isOtherSequence(mtype), isCustomMarshalerSequence(mtype), isTextMarshalerSequence(mtype): + return e.valueToOtherSlice(mtype, mval) + case isTreeSequence(mtype): + return e.valueToTreeSlice(mtype, mval) + default: + switch mtype.Kind() { + case reflect.Bool: + return mval.Bool(), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if mtype.Kind() == reflect.Int64 && mtype == reflect.TypeOf(time.Duration(1)) { + return fmt.Sprint(mval), nil + } + return mval.Int(), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return mval.Uint(), nil + case reflect.Float32, reflect.Float64: + return mval.Float(), nil + case reflect.String: + return mval.String(), nil + case reflect.Struct: + return mval.Interface(), nil + default: + return nil, fmt.Errorf("Marshal can't handle %v(%v)", mtype, mtype.Kind()) + } + } +} + +func (e *Encoder) appendTree(t, o *Tree) error { + for key, value := range o.values { + if _, ok := t.values[key]; ok { + continue + } + if tomlValue, ok := value.(*tomlValue); ok { + tomlValue.position.Col = t.position.Col + } + t.values[key] = value + } + return nil +} + +// Unmarshal attempts to unmarshal the Tree into a Go struct pointed by v. +// Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for +// sub-structs, and only definite types can be unmarshaled. +func (t *Tree) Unmarshal(v interface{}) error { + d := Decoder{tval: t, tagName: tagFieldName} + return d.unmarshal(v) +} + +// Marshal returns the TOML encoding of Tree. +// See Marshal() documentation for types mapping table. +func (t *Tree) Marshal() ([]byte, error) { + var buf bytes.Buffer + _, err := t.WriteTo(&buf) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// Unmarshal parses the TOML-encoded data and stores the result in the value +// pointed to by v. Behavior is similar to the Go json encoder, except that there +// is no concept of an Unmarshaler interface or UnmarshalTOML function for +// sub-structs, and currently only definite types can be unmarshaled to (i.e. no +// `interface{}`). +// +// The following struct annotations are supported: +// +// toml:"Field" Overrides the field's name to map to. +// default:"foo" Provides a default value. +// +// For default values, only fields of the following types are supported: +// * string +// * bool +// * int +// * int64 +// * float64 +// +// See Marshal() documentation for types mapping table. +func Unmarshal(data []byte, v interface{}) error { + t, err := LoadReader(bytes.NewReader(data)) + if err != nil { + return err + } + return t.Unmarshal(v) +} + +// Decoder reads and decodes TOML values from an input stream. +type Decoder struct { + r io.Reader + tval *Tree + encOpts + tagName string + strict bool + visitor visitorState +} + +// NewDecoder returns a new decoder that reads from r. +func NewDecoder(r io.Reader) *Decoder { + return &Decoder{ + r: r, + encOpts: encOptsDefaults, + tagName: tagFieldName, + } +} + +// Decode reads a TOML-encoded value from it's input +// and unmarshals it in the value pointed at by v. +// +// See the documentation for Marshal for details. +func (d *Decoder) Decode(v interface{}) error { + var err error + d.tval, err = LoadReader(d.r) + if err != nil { + return err + } + return d.unmarshal(v) +} + +// SetTagName allows changing default tag "toml" +func (d *Decoder) SetTagName(v string) *Decoder { + d.tagName = v + return d +} + +// Strict allows changing to strict decoding. Any fields that are found in the +// input data and do not have a corresponding struct member cause an error. +func (d *Decoder) Strict(strict bool) *Decoder { + d.strict = strict + return d +} + +func (d *Decoder) unmarshal(v interface{}) error { + mtype := reflect.TypeOf(v) + if mtype == nil { + return errors.New("nil cannot be unmarshaled from TOML") + } + if mtype.Kind() != reflect.Ptr { + return errors.New("only a pointer to struct or map can be unmarshaled from TOML") + } + + elem := mtype.Elem() + + switch elem.Kind() { + case reflect.Struct, reflect.Map: + default: + return errors.New("only a pointer to struct or map can be unmarshaled from TOML") + } + + if reflect.ValueOf(v).IsNil() { + return errors.New("nil pointer cannot be unmarshaled from TOML") + } + + vv := reflect.ValueOf(v).Elem() + + if d.strict { + d.visitor = newVisitorState(d.tval) + } + + sval, err := d.valueFromTree(elem, d.tval, &vv) + if err != nil { + return err + } + if err := d.visitor.validate(); err != nil { + return err + } + reflect.ValueOf(v).Elem().Set(sval) + return nil +} + +// Convert toml tree to marshal struct or map, using marshal type. When mval1 +// is non-nil, merge fields into the given value instead of allocating a new one. +func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.Value) (reflect.Value, error) { + if mtype.Kind() == reflect.Ptr { + return d.unwrapPointer(mtype, tval, mval1) + } + + // Check if pointer to value implements the Unmarshaler interface. + if mvalPtr := reflect.New(mtype); isCustomUnmarshaler(mvalPtr.Type()) { + d.visitor.visitAll() + + if err := callCustomUnmarshaler(mvalPtr, tval.ToMap()); err != nil { + return reflect.ValueOf(nil), fmt.Errorf("unmarshal toml: %v", err) + } + return mvalPtr.Elem(), nil + } + + var mval reflect.Value + switch mtype.Kind() { + case reflect.Struct: + if mval1 != nil { + mval = *mval1 + } else { + mval = reflect.New(mtype).Elem() + } + + switch mval.Interface().(type) { + case Tree: + mval.Set(reflect.ValueOf(tval).Elem()) + default: + for i := 0; i < mtype.NumField(); i++ { + mtypef := mtype.Field(i) + an := annotation{tag: d.tagName} + opts := tomlOptions(mtypef, an) + if !opts.include { + continue + } + baseKey := opts.name + keysToTry := []string{ + baseKey, + strings.ToLower(baseKey), + strings.ToTitle(baseKey), + strings.ToLower(string(baseKey[0])) + baseKey[1:], + } + + found := false + if tval != nil { + for _, key := range keysToTry { + exists := tval.HasPath([]string{key}) + if !exists { + continue + } + + d.visitor.push(key) + val := tval.GetPath([]string{key}) + fval := mval.Field(i) + mvalf, err := d.valueFromToml(mtypef.Type, val, &fval) + if err != nil { + return mval, formatError(err, tval.GetPositionPath([]string{key})) + } + mval.Field(i).Set(mvalf) + found = true + d.visitor.pop() + break + } + } + + if !found && opts.defaultValue != "" { + mvalf := mval.Field(i) + var val interface{} + var err error + switch mvalf.Kind() { + case reflect.String: + val = opts.defaultValue + case reflect.Bool: + val, err = strconv.ParseBool(opts.defaultValue) + case reflect.Uint: + val, err = strconv.ParseUint(opts.defaultValue, 10, 0) + case reflect.Uint8: + val, err = strconv.ParseUint(opts.defaultValue, 10, 8) + case reflect.Uint16: + val, err = strconv.ParseUint(opts.defaultValue, 10, 16) + case reflect.Uint32: + val, err = strconv.ParseUint(opts.defaultValue, 10, 32) + case reflect.Uint64: + val, err = strconv.ParseUint(opts.defaultValue, 10, 64) + case reflect.Int: + val, err = strconv.ParseInt(opts.defaultValue, 10, 0) + case reflect.Int8: + val, err = strconv.ParseInt(opts.defaultValue, 10, 8) + case reflect.Int16: + val, err = strconv.ParseInt(opts.defaultValue, 10, 16) + case reflect.Int32: + val, err = strconv.ParseInt(opts.defaultValue, 10, 32) + case reflect.Int64: + val, err = strconv.ParseInt(opts.defaultValue, 10, 64) + case reflect.Float32: + val, err = strconv.ParseFloat(opts.defaultValue, 32) + case reflect.Float64: + val, err = strconv.ParseFloat(opts.defaultValue, 64) + default: + return mvalf, fmt.Errorf("unsupported field type for default option") + } + + if err != nil { + return mvalf, err + } + mvalf.Set(reflect.ValueOf(val).Convert(mvalf.Type())) + } + + // save the old behavior above and try to check structs + if !found && opts.defaultValue == "" && mtypef.Type.Kind() == reflect.Struct { + tmpTval := tval + if !mtypef.Anonymous { + tmpTval = nil + } + fval := mval.Field(i) + v, err := d.valueFromTree(mtypef.Type, tmpTval, &fval) + if err != nil { + return v, err + } + mval.Field(i).Set(v) + } + } + } + case reflect.Map: + mval = reflect.MakeMap(mtype) + for _, key := range tval.Keys() { + d.visitor.push(key) + // TODO: path splits key + val := tval.GetPath([]string{key}) + mvalf, err := d.valueFromToml(mtype.Elem(), val, nil) + if err != nil { + return mval, formatError(err, tval.GetPositionPath([]string{key})) + } + mval.SetMapIndex(reflect.ValueOf(key).Convert(mtype.Key()), mvalf) + d.visitor.pop() + } + } + return mval, nil +} + +// Convert toml value to marshal struct/map slice, using marshal type +func (d *Decoder) valueFromTreeSlice(mtype reflect.Type, tval []*Tree) (reflect.Value, error) { + mval, err := makeSliceOrArray(mtype, len(tval)) + if err != nil { + return mval, err + } + + for i := 0; i < len(tval); i++ { + d.visitor.push(strconv.Itoa(i)) + val, err := d.valueFromTree(mtype.Elem(), tval[i], nil) + if err != nil { + return mval, err + } + mval.Index(i).Set(val) + d.visitor.pop() + } + return mval, nil +} + +// Convert toml value to marshal primitive slice, using marshal type +func (d *Decoder) valueFromOtherSlice(mtype reflect.Type, tval []interface{}) (reflect.Value, error) { + mval, err := makeSliceOrArray(mtype, len(tval)) + if err != nil { + return mval, err + } + + for i := 0; i < len(tval); i++ { + val, err := d.valueFromToml(mtype.Elem(), tval[i], nil) + if err != nil { + return mval, err + } + mval.Index(i).Set(val) + } + return mval, nil +} + +// Convert toml value to marshal primitive slice, using marshal type +func (d *Decoder) valueFromOtherSliceI(mtype reflect.Type, tval interface{}) (reflect.Value, error) { + val := reflect.ValueOf(tval) + length := val.Len() + + mval, err := makeSliceOrArray(mtype, length) + if err != nil { + return mval, err + } + + for i := 0; i < length; i++ { + val, err := d.valueFromToml(mtype.Elem(), val.Index(i).Interface(), nil) + if err != nil { + return mval, err + } + mval.Index(i).Set(val) + } + return mval, nil +} + +// Create a new slice or a new array with specified length +func makeSliceOrArray(mtype reflect.Type, tLength int) (reflect.Value, error) { + var mval reflect.Value + switch mtype.Kind() { + case reflect.Slice: + mval = reflect.MakeSlice(mtype, tLength, tLength) + case reflect.Array: + mval = reflect.New(reflect.ArrayOf(mtype.Len(), mtype.Elem())).Elem() + if tLength > mtype.Len() { + return mval, fmt.Errorf("unmarshal: TOML array length (%v) exceeds destination array length (%v)", tLength, mtype.Len()) + } + } + return mval, nil +} + +// Convert toml value to marshal value, using marshal type. When mval1 is non-nil +// and the given type is a struct value, merge fields into it. +func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}, mval1 *reflect.Value) (reflect.Value, error) { + if mtype.Kind() == reflect.Ptr { + return d.unwrapPointer(mtype, tval, mval1) + } + + switch t := tval.(type) { + case *Tree: + var mval11 *reflect.Value + if mtype.Kind() == reflect.Struct { + mval11 = mval1 + } + + if isTree(mtype) { + return d.valueFromTree(mtype, t, mval11) + } + + if mtype.Kind() == reflect.Interface { + if mval1 == nil || mval1.IsNil() { + return d.valueFromTree(reflect.TypeOf(map[string]interface{}{}), t, nil) + } else { + return d.valueFromToml(mval1.Elem().Type(), t, nil) + } + } + + return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a tree", tval, tval) + case []*Tree: + if isTreeSequence(mtype) { + return d.valueFromTreeSlice(mtype, t) + } + if mtype.Kind() == reflect.Interface { + if mval1 == nil || mval1.IsNil() { + return d.valueFromTreeSlice(reflect.TypeOf([]map[string]interface{}{}), t) + } else { + ival := mval1.Elem() + return d.valueFromToml(mval1.Elem().Type(), t, &ival) + } + } + return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to trees", tval, tval) + case []interface{}: + d.visitor.visit() + if isOtherSequence(mtype) { + return d.valueFromOtherSlice(mtype, t) + } + if mtype.Kind() == reflect.Interface { + if mval1 == nil || mval1.IsNil() { + return d.valueFromOtherSlice(reflect.TypeOf([]interface{}{}), t) + } else { + ival := mval1.Elem() + return d.valueFromToml(mval1.Elem().Type(), t, &ival) + } + } + return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a slice", tval, tval) + default: + d.visitor.visit() + // Check if pointer to value implements the encoding.TextUnmarshaler. + if mvalPtr := reflect.New(mtype); isTextUnmarshaler(mvalPtr.Type()) && !isTimeType(mtype) { + if err := d.unmarshalText(tval, mvalPtr); err != nil { + return reflect.ValueOf(nil), fmt.Errorf("unmarshal text: %v", err) + } + return mvalPtr.Elem(), nil + } + + switch mtype.Kind() { + case reflect.Bool, reflect.Struct: + val := reflect.ValueOf(tval) + + switch val.Type() { + case localDateType: + localDate := val.Interface().(LocalDate) + switch mtype { + case timeType: + return reflect.ValueOf(time.Date(localDate.Year, localDate.Month, localDate.Day, 0, 0, 0, 0, time.Local)), nil + } + case localDateTimeType: + localDateTime := val.Interface().(LocalDateTime) + switch mtype { + case timeType: + return reflect.ValueOf(time.Date( + localDateTime.Date.Year, + localDateTime.Date.Month, + localDateTime.Date.Day, + localDateTime.Time.Hour, + localDateTime.Time.Minute, + localDateTime.Time.Second, + localDateTime.Time.Nanosecond, + time.Local)), nil + } + } + + // if this passes for when mtype is reflect.Struct, tval is a time.LocalTime + if !val.Type().ConvertibleTo(mtype) { + return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String()) + } + + return val.Convert(mtype), nil + case reflect.String: + val := reflect.ValueOf(tval) + // stupidly, int64 is convertible to string. So special case this. + if !val.Type().ConvertibleTo(mtype) || val.Kind() == reflect.Int64 { + return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String()) + } + + return val.Convert(mtype), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + val := reflect.ValueOf(tval) + if mtype.Kind() == reflect.Int64 && mtype == reflect.TypeOf(time.Duration(1)) && val.Kind() == reflect.String { + d, err := time.ParseDuration(val.String()) + if err != nil { + return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v. %s", tval, tval, mtype.String(), err) + } + return reflect.ValueOf(d), nil + } + if !val.Type().ConvertibleTo(mtype) || val.Kind() == reflect.Float64 { + return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String()) + } + if reflect.Indirect(reflect.New(mtype)).OverflowInt(val.Convert(reflect.TypeOf(int64(0))).Int()) { + return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String()) + } + + return val.Convert(mtype), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + val := reflect.ValueOf(tval) + if !val.Type().ConvertibleTo(mtype) || val.Kind() == reflect.Float64 { + return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String()) + } + + if val.Convert(reflect.TypeOf(int(1))).Int() < 0 { + return reflect.ValueOf(nil), fmt.Errorf("%v(%T) is negative so does not fit in %v", tval, tval, mtype.String()) + } + if reflect.Indirect(reflect.New(mtype)).OverflowUint(val.Convert(reflect.TypeOf(uint64(0))).Uint()) { + return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String()) + } + + return val.Convert(mtype), nil + case reflect.Float32, reflect.Float64: + val := reflect.ValueOf(tval) + if !val.Type().ConvertibleTo(mtype) || val.Kind() == reflect.Int64 { + return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String()) + } + if reflect.Indirect(reflect.New(mtype)).OverflowFloat(val.Convert(reflect.TypeOf(float64(0))).Float()) { + return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String()) + } + + return val.Convert(mtype), nil + case reflect.Interface: + if mval1 == nil || mval1.IsNil() { + return reflect.ValueOf(tval), nil + } else { + ival := mval1.Elem() + return d.valueFromToml(mval1.Elem().Type(), t, &ival) + } + case reflect.Slice, reflect.Array: + if isOtherSequence(mtype) && isOtherSequence(reflect.TypeOf(t)) { + return d.valueFromOtherSliceI(mtype, t) + } + return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v(%v)", tval, tval, mtype, mtype.Kind()) + default: + return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v(%v)", tval, tval, mtype, mtype.Kind()) + } + } +} + +func (d *Decoder) unwrapPointer(mtype reflect.Type, tval interface{}, mval1 *reflect.Value) (reflect.Value, error) { + var melem *reflect.Value + + if mval1 != nil && !mval1.IsNil() && (mtype.Elem().Kind() == reflect.Struct || mtype.Elem().Kind() == reflect.Interface) { + elem := mval1.Elem() + melem = &elem + } + + val, err := d.valueFromToml(mtype.Elem(), tval, melem) + if err != nil { + return reflect.ValueOf(nil), err + } + mval := reflect.New(mtype.Elem()) + mval.Elem().Set(val) + return mval, nil +} + +func (d *Decoder) unmarshalText(tval interface{}, mval reflect.Value) error { + var buf bytes.Buffer + fmt.Fprint(&buf, tval) + return callTextUnmarshaler(mval, buf.Bytes()) +} + +func tomlOptions(vf reflect.StructField, an annotation) tomlOpts { + tag := vf.Tag.Get(an.tag) + parse := strings.Split(tag, ",") + var comment string + if c := vf.Tag.Get(an.comment); c != "" { + comment = c + } + commented, _ := strconv.ParseBool(vf.Tag.Get(an.commented)) + multiline, _ := strconv.ParseBool(vf.Tag.Get(an.multiline)) + defaultValue := vf.Tag.Get(tagDefault) + result := tomlOpts{ + name: vf.Name, + nameFromTag: false, + comment: comment, + commented: commented, + multiline: multiline, + include: true, + omitempty: false, + defaultValue: defaultValue, + } + if parse[0] != "" { + if parse[0] == "-" && len(parse) == 1 { + result.include = false + } else { + result.name = strings.Trim(parse[0], " ") + result.nameFromTag = true + } + } + if vf.PkgPath != "" { + result.include = false + } + if len(parse) > 1 && strings.Trim(parse[1], " ") == "omitempty" { + result.omitempty = true + } + if vf.Type.Kind() == reflect.Ptr { + result.omitempty = true + } + return result +} + +func isZero(val reflect.Value) bool { + switch val.Type().Kind() { + case reflect.Slice, reflect.Array, reflect.Map: + return val.Len() == 0 + default: + return reflect.DeepEqual(val.Interface(), reflect.Zero(val.Type()).Interface()) + } +} + +func formatError(err error, pos Position) error { + if err.Error()[0] == '(' { // Error already contains position information + return err + } + return fmt.Errorf("%s: %s", pos, err) +} + +// visitorState keeps track of which keys were unmarshaled. +type visitorState struct { + tree *Tree + path []string + keys map[string]struct{} + active bool +} + +func newVisitorState(tree *Tree) visitorState { + path, result := []string{}, map[string]struct{}{} + insertKeys(path, result, tree) + return visitorState{ + tree: tree, + path: path[:0], + keys: result, + active: true, + } +} + +func (s *visitorState) push(key string) { + if s.active { + s.path = append(s.path, key) + } +} + +func (s *visitorState) pop() { + if s.active { + s.path = s.path[:len(s.path)-1] + } +} + +func (s *visitorState) visit() { + if s.active { + delete(s.keys, strings.Join(s.path, ".")) + } +} + +func (s *visitorState) visitAll() { + if s.active { + for k := range s.keys { + if strings.HasPrefix(k, strings.Join(s.path, ".")) { + delete(s.keys, k) + } + } + } +} + +func (s *visitorState) validate() error { + if !s.active { + return nil + } + undecoded := make([]string, 0, len(s.keys)) + for key := range s.keys { + undecoded = append(undecoded, key) + } + sort.Strings(undecoded) + if len(undecoded) > 0 { + return fmt.Errorf("undecoded keys: %q", undecoded) + } + return nil +} + +func insertKeys(path []string, m map[string]struct{}, tree *Tree) { + for k, v := range tree.values { + switch node := v.(type) { + case []*Tree: + for i, item := range node { + insertKeys(append(path, k, strconv.Itoa(i)), m, item) + } + case *Tree: + insertKeys(append(path, k), m, node) + case *tomlValue: + m[strings.Join(append(path, k), ".")] = struct{}{} + } + } +} diff --git a/vendor/github.com/pelletier/go-toml/marshal_OrderPreserve_test.toml b/vendor/github.com/pelletier/go-toml/marshal_OrderPreserve_test.toml new file mode 100644 index 00000000000..792b72ed721 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/marshal_OrderPreserve_test.toml @@ -0,0 +1,39 @@ +title = "TOML Marshal Testing" + +[basic_lists] + floats = [12.3,45.6,78.9] + bools = [true,false,true] + dates = [1979-05-27T07:32:00Z,1980-05-27T07:32:00Z] + ints = [8001,8001,8002] + uints = [5002,5003] + strings = ["One","Two","Three"] + +[[subdocptrs]] + name = "Second" + +[basic_map] + one = "one" + two = "two" + +[subdoc] + + [subdoc.second] + name = "Second" + + [subdoc.first] + name = "First" + +[basic] + uint = 5001 + bool = true + float = 123.4 + float64 = 123.456782132399 + int = 5000 + string = "Bite me" + date = 1979-05-27T07:32:00Z + +[[subdoclist]] + name = "List.First" + +[[subdoclist]] + name = "List.Second" diff --git a/vendor/github.com/pelletier/go-toml/marshal_test.toml b/vendor/github.com/pelletier/go-toml/marshal_test.toml new file mode 100644 index 00000000000..ba5e110bf04 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/marshal_test.toml @@ -0,0 +1,39 @@ +title = "TOML Marshal Testing" + +[basic] + bool = true + date = 1979-05-27T07:32:00Z + float = 123.4 + float64 = 123.456782132399 + int = 5000 + string = "Bite me" + uint = 5001 + +[basic_lists] + bools = [true,false,true] + dates = [1979-05-27T07:32:00Z,1980-05-27T07:32:00Z] + floats = [12.3,45.6,78.9] + ints = [8001,8001,8002] + strings = ["One","Two","Three"] + uints = [5002,5003] + +[basic_map] + one = "one" + two = "two" + +[subdoc] + + [subdoc.first] + name = "First" + + [subdoc.second] + name = "Second" + +[[subdoclist]] + name = "List.First" + +[[subdoclist]] + name = "List.Second" + +[[subdocptrs]] + name = "Second" diff --git a/vendor/github.com/pelletier/go-toml/parser.go b/vendor/github.com/pelletier/go-toml/parser.go new file mode 100644 index 00000000000..7bf40bbdc7e --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/parser.go @@ -0,0 +1,493 @@ +// TOML Parser. + +package toml + +import ( + "errors" + "fmt" + "math" + "reflect" + "regexp" + "strconv" + "strings" + "time" +) + +type tomlParser struct { + flowIdx int + flow []token + tree *Tree + currentTable []string + seenTableKeys []string +} + +type tomlParserStateFn func() tomlParserStateFn + +// Formats and panics an error message based on a token +func (p *tomlParser) raiseError(tok *token, msg string, args ...interface{}) { + panic(tok.Position.String() + ": " + fmt.Sprintf(msg, args...)) +} + +func (p *tomlParser) run() { + for state := p.parseStart; state != nil; { + state = state() + } +} + +func (p *tomlParser) peek() *token { + if p.flowIdx >= len(p.flow) { + return nil + } + return &p.flow[p.flowIdx] +} + +func (p *tomlParser) assume(typ tokenType) { + tok := p.getToken() + if tok == nil { + p.raiseError(tok, "was expecting token %s, but token stream is empty", tok) + } + if tok.typ != typ { + p.raiseError(tok, "was expecting token %s, but got %s instead", typ, tok) + } +} + +func (p *tomlParser) getToken() *token { + tok := p.peek() + if tok == nil { + return nil + } + p.flowIdx++ + return tok +} + +func (p *tomlParser) parseStart() tomlParserStateFn { + tok := p.peek() + + // end of stream, parsing is finished + if tok == nil { + return nil + } + + switch tok.typ { + case tokenDoubleLeftBracket: + return p.parseGroupArray + case tokenLeftBracket: + return p.parseGroup + case tokenKey: + return p.parseAssign + case tokenEOF: + return nil + case tokenError: + p.raiseError(tok, "parsing error: %s", tok.String()) + default: + p.raiseError(tok, "unexpected token %s", tok.typ) + } + return nil +} + +func (p *tomlParser) parseGroupArray() tomlParserStateFn { + startToken := p.getToken() // discard the [[ + key := p.getToken() + if key.typ != tokenKeyGroupArray { + p.raiseError(key, "unexpected token %s, was expecting a table array key", key) + } + + // get or create table array element at the indicated part in the path + keys, err := parseKey(key.val) + if err != nil { + p.raiseError(key, "invalid table array key: %s", err) + } + p.tree.createSubTree(keys[:len(keys)-1], startToken.Position) // create parent entries + destTree := p.tree.GetPath(keys) + var array []*Tree + if destTree == nil { + array = make([]*Tree, 0) + } else if target, ok := destTree.([]*Tree); ok && target != nil { + array = destTree.([]*Tree) + } else { + p.raiseError(key, "key %s is already assigned and not of type table array", key) + } + p.currentTable = keys + + // add a new tree to the end of the table array + newTree := newTree() + newTree.position = startToken.Position + array = append(array, newTree) + p.tree.SetPath(p.currentTable, array) + + // remove all keys that were children of this table array + prefix := key.val + "." + found := false + for ii := 0; ii < len(p.seenTableKeys); { + tableKey := p.seenTableKeys[ii] + if strings.HasPrefix(tableKey, prefix) { + p.seenTableKeys = append(p.seenTableKeys[:ii], p.seenTableKeys[ii+1:]...) + } else { + found = (tableKey == key.val) + ii++ + } + } + + // keep this key name from use by other kinds of assignments + if !found { + p.seenTableKeys = append(p.seenTableKeys, key.val) + } + + // move to next parser state + p.assume(tokenDoubleRightBracket) + return p.parseStart +} + +func (p *tomlParser) parseGroup() tomlParserStateFn { + startToken := p.getToken() // discard the [ + key := p.getToken() + if key.typ != tokenKeyGroup { + p.raiseError(key, "unexpected token %s, was expecting a table key", key) + } + for _, item := range p.seenTableKeys { + if item == key.val { + p.raiseError(key, "duplicated tables") + } + } + + p.seenTableKeys = append(p.seenTableKeys, key.val) + keys, err := parseKey(key.val) + if err != nil { + p.raiseError(key, "invalid table array key: %s", err) + } + if err := p.tree.createSubTree(keys, startToken.Position); err != nil { + p.raiseError(key, "%s", err) + } + destTree := p.tree.GetPath(keys) + if target, ok := destTree.(*Tree); ok && target != nil && target.inline { + p.raiseError(key, "could not re-define exist inline table or its sub-table : %s", + strings.Join(keys, ".")) + } + p.assume(tokenRightBracket) + p.currentTable = keys + return p.parseStart +} + +func (p *tomlParser) parseAssign() tomlParserStateFn { + key := p.getToken() + p.assume(tokenEqual) + + parsedKey, err := parseKey(key.val) + if err != nil { + p.raiseError(key, "invalid key: %s", err.Error()) + } + + value := p.parseRvalue() + var tableKey []string + if len(p.currentTable) > 0 { + tableKey = p.currentTable + } else { + tableKey = []string{} + } + + prefixKey := parsedKey[0 : len(parsedKey)-1] + tableKey = append(tableKey, prefixKey...) + + // find the table to assign, looking out for arrays of tables + var targetNode *Tree + switch node := p.tree.GetPath(tableKey).(type) { + case []*Tree: + targetNode = node[len(node)-1] + case *Tree: + targetNode = node + case nil: + // create intermediate + if err := p.tree.createSubTree(tableKey, key.Position); err != nil { + p.raiseError(key, "could not create intermediate group: %s", err) + } + targetNode = p.tree.GetPath(tableKey).(*Tree) + default: + p.raiseError(key, "Unknown table type for path: %s", + strings.Join(tableKey, ".")) + } + + if targetNode.inline { + p.raiseError(key, "could not add key or sub-table to exist inline table or its sub-table : %s", + strings.Join(tableKey, ".")) + } + + // assign value to the found table + keyVal := parsedKey[len(parsedKey)-1] + localKey := []string{keyVal} + finalKey := append(tableKey, keyVal) + if targetNode.GetPath(localKey) != nil { + p.raiseError(key, "The following key was defined twice: %s", + strings.Join(finalKey, ".")) + } + var toInsert interface{} + + switch value.(type) { + case *Tree, []*Tree: + toInsert = value + default: + toInsert = &tomlValue{value: value, position: key.Position} + } + targetNode.values[keyVal] = toInsert + return p.parseStart +} + +var numberUnderscoreInvalidRegexp *regexp.Regexp +var hexNumberUnderscoreInvalidRegexp *regexp.Regexp + +func numberContainsInvalidUnderscore(value string) error { + if numberUnderscoreInvalidRegexp.MatchString(value) { + return errors.New("invalid use of _ in number") + } + return nil +} + +func hexNumberContainsInvalidUnderscore(value string) error { + if hexNumberUnderscoreInvalidRegexp.MatchString(value) { + return errors.New("invalid use of _ in hex number") + } + return nil +} + +func cleanupNumberToken(value string) string { + cleanedVal := strings.Replace(value, "_", "", -1) + return cleanedVal +} + +func (p *tomlParser) parseRvalue() interface{} { + tok := p.getToken() + if tok == nil || tok.typ == tokenEOF { + p.raiseError(tok, "expecting a value") + } + + switch tok.typ { + case tokenString: + return tok.val + case tokenTrue: + return true + case tokenFalse: + return false + case tokenInf: + if tok.val[0] == '-' { + return math.Inf(-1) + } + return math.Inf(1) + case tokenNan: + return math.NaN() + case tokenInteger: + cleanedVal := cleanupNumberToken(tok.val) + var err error + var val int64 + if len(cleanedVal) >= 3 && cleanedVal[0] == '0' { + switch cleanedVal[1] { + case 'x': + err = hexNumberContainsInvalidUnderscore(tok.val) + if err != nil { + p.raiseError(tok, "%s", err) + } + val, err = strconv.ParseInt(cleanedVal[2:], 16, 64) + case 'o': + err = numberContainsInvalidUnderscore(tok.val) + if err != nil { + p.raiseError(tok, "%s", err) + } + val, err = strconv.ParseInt(cleanedVal[2:], 8, 64) + case 'b': + err = numberContainsInvalidUnderscore(tok.val) + if err != nil { + p.raiseError(tok, "%s", err) + } + val, err = strconv.ParseInt(cleanedVal[2:], 2, 64) + default: + panic("invalid base") // the lexer should catch this first + } + } else { + err = numberContainsInvalidUnderscore(tok.val) + if err != nil { + p.raiseError(tok, "%s", err) + } + val, err = strconv.ParseInt(cleanedVal, 10, 64) + } + if err != nil { + p.raiseError(tok, "%s", err) + } + return val + case tokenFloat: + err := numberContainsInvalidUnderscore(tok.val) + if err != nil { + p.raiseError(tok, "%s", err) + } + cleanedVal := cleanupNumberToken(tok.val) + val, err := strconv.ParseFloat(cleanedVal, 64) + if err != nil { + p.raiseError(tok, "%s", err) + } + return val + case tokenDate: + layout := time.RFC3339Nano + if !strings.Contains(tok.val, "T") { + layout = strings.Replace(layout, "T", " ", 1) + } + val, err := time.ParseInLocation(layout, tok.val, time.UTC) + if err != nil { + p.raiseError(tok, "%s", err) + } + return val + case tokenLocalDate: + v := strings.Replace(tok.val, " ", "T", -1) + isDateTime := false + isTime := false + for _, c := range v { + if c == 'T' || c == 't' { + isDateTime = true + break + } + if c == ':' { + isTime = true + break + } + } + + var val interface{} + var err error + + if isDateTime { + val, err = ParseLocalDateTime(v) + } else if isTime { + val, err = ParseLocalTime(v) + } else { + val, err = ParseLocalDate(v) + } + + if err != nil { + p.raiseError(tok, "%s", err) + } + return val + case tokenLeftBracket: + return p.parseArray() + case tokenLeftCurlyBrace: + return p.parseInlineTable() + case tokenEqual: + p.raiseError(tok, "cannot have multiple equals for the same key") + case tokenError: + p.raiseError(tok, "%s", tok) + } + + p.raiseError(tok, "never reached") + + return nil +} + +func tokenIsComma(t *token) bool { + return t != nil && t.typ == tokenComma +} + +func (p *tomlParser) parseInlineTable() *Tree { + tree := newTree() + var previous *token +Loop: + for { + follow := p.peek() + if follow == nil || follow.typ == tokenEOF { + p.raiseError(follow, "unterminated inline table") + } + switch follow.typ { + case tokenRightCurlyBrace: + p.getToken() + break Loop + case tokenKey, tokenInteger, tokenString: + if !tokenIsComma(previous) && previous != nil { + p.raiseError(follow, "comma expected between fields in inline table") + } + key := p.getToken() + p.assume(tokenEqual) + + parsedKey, err := parseKey(key.val) + if err != nil { + p.raiseError(key, "invalid key: %s", err) + } + + value := p.parseRvalue() + tree.SetPath(parsedKey, value) + case tokenComma: + if tokenIsComma(previous) { + p.raiseError(follow, "need field between two commas in inline table") + } + p.getToken() + default: + p.raiseError(follow, "unexpected token type in inline table: %s", follow.String()) + } + previous = follow + } + if tokenIsComma(previous) { + p.raiseError(previous, "trailing comma at the end of inline table") + } + tree.inline = true + return tree +} + +func (p *tomlParser) parseArray() interface{} { + var array []interface{} + arrayType := reflect.TypeOf(newTree()) + for { + follow := p.peek() + if follow == nil || follow.typ == tokenEOF { + p.raiseError(follow, "unterminated array") + } + if follow.typ == tokenRightBracket { + p.getToken() + break + } + val := p.parseRvalue() + if reflect.TypeOf(val) != arrayType { + arrayType = nil + } + array = append(array, val) + follow = p.peek() + if follow == nil || follow.typ == tokenEOF { + p.raiseError(follow, "unterminated array") + } + if follow.typ != tokenRightBracket && follow.typ != tokenComma { + p.raiseError(follow, "missing comma") + } + if follow.typ == tokenComma { + p.getToken() + } + } + + // if the array is a mixed-type array or its length is 0, + // don't convert it to a table array + if len(array) <= 0 { + arrayType = nil + } + // An array of Trees is actually an array of inline + // tables, which is a shorthand for a table array. If the + // array was not converted from []interface{} to []*Tree, + // the two notations would not be equivalent. + if arrayType == reflect.TypeOf(newTree()) { + tomlArray := make([]*Tree, len(array)) + for i, v := range array { + tomlArray[i] = v.(*Tree) + } + return tomlArray + } + return array +} + +func parseToml(flow []token) *Tree { + result := newTree() + result.position = Position{1, 1} + parser := &tomlParser{ + flowIdx: 0, + flow: flow, + tree: result, + currentTable: make([]string, 0), + seenTableKeys: make([]string, 0), + } + parser.run() + return result +} + +func init() { + numberUnderscoreInvalidRegexp = regexp.MustCompile(`([^\d]_|_[^\d])|_$|^_`) + hexNumberUnderscoreInvalidRegexp = regexp.MustCompile(`(^0x_)|([^\da-f]_|_[^\da-f])|_$|^_`) +} diff --git a/vendor/github.com/pelletier/go-toml/position.go b/vendor/github.com/pelletier/go-toml/position.go new file mode 100644 index 00000000000..c17bff87baa --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/position.go @@ -0,0 +1,29 @@ +// Position support for go-toml + +package toml + +import ( + "fmt" +) + +// Position of a document element within a TOML document. +// +// Line and Col are both 1-indexed positions for the element's line number and +// column number, respectively. Values of zero or less will cause Invalid(), +// to return true. +type Position struct { + Line int // line within the document + Col int // column within the line +} + +// String representation of the position. +// Displays 1-indexed line and column numbers. +func (p Position) String() string { + return fmt.Sprintf("(%d, %d)", p.Line, p.Col) +} + +// Invalid returns whether or not the position is valid (i.e. with negative or +// null values) +func (p Position) Invalid() bool { + return p.Line <= 0 || p.Col <= 0 +} diff --git a/vendor/github.com/pelletier/go-toml/token.go b/vendor/github.com/pelletier/go-toml/token.go new file mode 100644 index 00000000000..6af4ec46bcf --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/token.go @@ -0,0 +1,134 @@ +package toml + +import "fmt" + +// Define tokens +type tokenType int + +const ( + eof = -(iota + 1) +) + +const ( + tokenError tokenType = iota + tokenEOF + tokenComment + tokenKey + tokenString + tokenInteger + tokenTrue + tokenFalse + tokenFloat + tokenInf + tokenNan + tokenEqual + tokenLeftBracket + tokenRightBracket + tokenLeftCurlyBrace + tokenRightCurlyBrace + tokenLeftParen + tokenRightParen + tokenDoubleLeftBracket + tokenDoubleRightBracket + tokenDate + tokenLocalDate + tokenKeyGroup + tokenKeyGroupArray + tokenComma + tokenColon + tokenDollar + tokenStar + tokenQuestion + tokenDot + tokenDotDot + tokenEOL +) + +var tokenTypeNames = []string{ + "Error", + "EOF", + "Comment", + "Key", + "String", + "Integer", + "True", + "False", + "Float", + "Inf", + "NaN", + "=", + "[", + "]", + "{", + "}", + "(", + ")", + "]]", + "[[", + "LocalDate", + "LocalDate", + "KeyGroup", + "KeyGroupArray", + ",", + ":", + "$", + "*", + "?", + ".", + "..", + "EOL", +} + +type token struct { + Position + typ tokenType + val string +} + +func (tt tokenType) String() string { + idx := int(tt) + if idx < len(tokenTypeNames) { + return tokenTypeNames[idx] + } + return "Unknown" +} + +func (t token) String() string { + switch t.typ { + case tokenEOF: + return "EOF" + case tokenError: + return t.val + } + + return fmt.Sprintf("%q", t.val) +} + +func isSpace(r rune) bool { + return r == ' ' || r == '\t' +} + +func isAlphanumeric(r rune) bool { + return 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || r == '_' +} + +func isKeyChar(r rune) bool { + // Keys start with the first character that isn't whitespace or [ and end + // with the last non-whitespace character before the equals sign. Keys + // cannot contain a # character." + return !(r == '\r' || r == '\n' || r == eof || r == '=') +} + +func isKeyStartChar(r rune) bool { + return !(isSpace(r) || r == '\r' || r == '\n' || r == eof || r == '[') +} + +func isDigit(r rune) bool { + return '0' <= r && r <= '9' +} + +func isHexDigit(r rune) bool { + return isDigit(r) || + (r >= 'a' && r <= 'f') || + (r >= 'A' && r <= 'F') +} diff --git a/vendor/github.com/pelletier/go-toml/toml.go b/vendor/github.com/pelletier/go-toml/toml.go new file mode 100644 index 00000000000..d323c39bce9 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/toml.go @@ -0,0 +1,399 @@ +package toml + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "runtime" + "strings" +) + +type tomlValue struct { + value interface{} // string, int64, uint64, float64, bool, time.Time, [] of any of this list + comment string + commented bool + multiline bool + position Position +} + +// Tree is the result of the parsing of a TOML file. +type Tree struct { + values map[string]interface{} // string -> *tomlValue, *Tree, []*Tree + comment string + commented bool + inline bool + position Position +} + +func newTree() *Tree { + return newTreeWithPosition(Position{}) +} + +func newTreeWithPosition(pos Position) *Tree { + return &Tree{ + values: make(map[string]interface{}), + position: pos, + } +} + +// TreeFromMap initializes a new Tree object using the given map. +func TreeFromMap(m map[string]interface{}) (*Tree, error) { + result, err := toTree(m) + if err != nil { + return nil, err + } + return result.(*Tree), nil +} + +// Position returns the position of the tree. +func (t *Tree) Position() Position { + return t.position +} + +// Has returns a boolean indicating if the given key exists. +func (t *Tree) Has(key string) bool { + if key == "" { + return false + } + return t.HasPath(strings.Split(key, ".")) +} + +// HasPath returns true if the given path of keys exists, false otherwise. +func (t *Tree) HasPath(keys []string) bool { + return t.GetPath(keys) != nil +} + +// Keys returns the keys of the toplevel tree (does not recurse). +func (t *Tree) Keys() []string { + keys := make([]string, len(t.values)) + i := 0 + for k := range t.values { + keys[i] = k + i++ + } + return keys +} + +// Get the value at key in the Tree. +// Key is a dot-separated path (e.g. a.b.c) without single/double quoted strings. +// If you need to retrieve non-bare keys, use GetPath. +// Returns nil if the path does not exist in the tree. +// If keys is of length zero, the current tree is returned. +func (t *Tree) Get(key string) interface{} { + if key == "" { + return t + } + return t.GetPath(strings.Split(key, ".")) +} + +// GetPath returns the element in the tree indicated by 'keys'. +// If keys is of length zero, the current tree is returned. +func (t *Tree) GetPath(keys []string) interface{} { + if len(keys) == 0 { + return t + } + subtree := t + for _, intermediateKey := range keys[:len(keys)-1] { + value, exists := subtree.values[intermediateKey] + if !exists { + return nil + } + switch node := value.(type) { + case *Tree: + subtree = node + case []*Tree: + // go to most recent element + if len(node) == 0 { + return nil + } + subtree = node[len(node)-1] + default: + return nil // cannot navigate through other node types + } + } + // branch based on final node type + switch node := subtree.values[keys[len(keys)-1]].(type) { + case *tomlValue: + return node.value + default: + return node + } +} + +// GetPosition returns the position of the given key. +func (t *Tree) GetPosition(key string) Position { + if key == "" { + return t.position + } + return t.GetPositionPath(strings.Split(key, ".")) +} + +// GetPositionPath returns the element in the tree indicated by 'keys'. +// If keys is of length zero, the current tree is returned. +func (t *Tree) GetPositionPath(keys []string) Position { + if len(keys) == 0 { + return t.position + } + subtree := t + for _, intermediateKey := range keys[:len(keys)-1] { + value, exists := subtree.values[intermediateKey] + if !exists { + return Position{0, 0} + } + switch node := value.(type) { + case *Tree: + subtree = node + case []*Tree: + // go to most recent element + if len(node) == 0 { + return Position{0, 0} + } + subtree = node[len(node)-1] + default: + return Position{0, 0} + } + } + // branch based on final node type + switch node := subtree.values[keys[len(keys)-1]].(type) { + case *tomlValue: + return node.position + case *Tree: + return node.position + case []*Tree: + // go to most recent element + if len(node) == 0 { + return Position{0, 0} + } + return node[len(node)-1].position + default: + return Position{0, 0} + } +} + +// GetDefault works like Get but with a default value +func (t *Tree) GetDefault(key string, def interface{}) interface{} { + val := t.Get(key) + if val == nil { + return def + } + return val +} + +// SetOptions arguments are supplied to the SetWithOptions and SetPathWithOptions functions to modify marshalling behaviour. +// The default values within the struct are valid default options. +type SetOptions struct { + Comment string + Commented bool + Multiline bool +} + +// SetWithOptions is the same as Set, but allows you to provide formatting +// instructions to the key, that will be used by Marshal(). +func (t *Tree) SetWithOptions(key string, opts SetOptions, value interface{}) { + t.SetPathWithOptions(strings.Split(key, "."), opts, value) +} + +// SetPathWithOptions is the same as SetPath, but allows you to provide +// formatting instructions to the key, that will be reused by Marshal(). +func (t *Tree) SetPathWithOptions(keys []string, opts SetOptions, value interface{}) { + subtree := t + for i, intermediateKey := range keys[:len(keys)-1] { + nextTree, exists := subtree.values[intermediateKey] + if !exists { + nextTree = newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col}) + subtree.values[intermediateKey] = nextTree // add new element here + } + switch node := nextTree.(type) { + case *Tree: + subtree = node + case []*Tree: + // go to most recent element + if len(node) == 0 { + // create element if it does not exist + subtree.values[intermediateKey] = append(node, newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col})) + } + subtree = node[len(node)-1] + } + } + + var toInsert interface{} + + switch v := value.(type) { + case *Tree: + v.comment = opts.Comment + v.commented = opts.Commented + toInsert = value + case []*Tree: + for i := range v { + v[i].commented = opts.Commented + } + toInsert = value + case *tomlValue: + v.comment = opts.Comment + toInsert = v + default: + toInsert = &tomlValue{value: value, + comment: opts.Comment, + commented: opts.Commented, + multiline: opts.Multiline, + position: Position{Line: subtree.position.Line + len(subtree.values) + 1, Col: subtree.position.Col}} + } + + subtree.values[keys[len(keys)-1]] = toInsert +} + +// Set an element in the tree. +// Key is a dot-separated path (e.g. a.b.c). +// Creates all necessary intermediate trees, if needed. +func (t *Tree) Set(key string, value interface{}) { + t.SetWithComment(key, "", false, value) +} + +// SetWithComment is the same as Set, but allows you to provide comment +// information to the key, that will be reused by Marshal(). +func (t *Tree) SetWithComment(key string, comment string, commented bool, value interface{}) { + t.SetPathWithComment(strings.Split(key, "."), comment, commented, value) +} + +// SetPath sets an element in the tree. +// Keys is an array of path elements (e.g. {"a","b","c"}). +// Creates all necessary intermediate trees, if needed. +func (t *Tree) SetPath(keys []string, value interface{}) { + t.SetPathWithComment(keys, "", false, value) +} + +// SetPathWithComment is the same as SetPath, but allows you to provide comment +// information to the key, that will be reused by Marshal(). +func (t *Tree) SetPathWithComment(keys []string, comment string, commented bool, value interface{}) { + t.SetPathWithOptions(keys, SetOptions{Comment: comment, Commented: commented}, value) +} + +// Delete removes a key from the tree. +// Key is a dot-separated path (e.g. a.b.c). +func (t *Tree) Delete(key string) error { + keys, err := parseKey(key) + if err != nil { + return err + } + return t.DeletePath(keys) +} + +// DeletePath removes a key from the tree. +// Keys is an array of path elements (e.g. {"a","b","c"}). +func (t *Tree) DeletePath(keys []string) error { + keyLen := len(keys) + if keyLen == 1 { + delete(t.values, keys[0]) + return nil + } + tree := t.GetPath(keys[:keyLen-1]) + item := keys[keyLen-1] + switch node := tree.(type) { + case *Tree: + delete(node.values, item) + return nil + } + return errors.New("no such key to delete") +} + +// createSubTree takes a tree and a key and create the necessary intermediate +// subtrees to create a subtree at that point. In-place. +// +// e.g. passing a.b.c will create (assuming tree is empty) tree[a], tree[a][b] +// and tree[a][b][c] +// +// Returns nil on success, error object on failure +func (t *Tree) createSubTree(keys []string, pos Position) error { + subtree := t + for i, intermediateKey := range keys { + nextTree, exists := subtree.values[intermediateKey] + if !exists { + tree := newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col}) + tree.position = pos + tree.inline = subtree.inline + subtree.values[intermediateKey] = tree + nextTree = tree + } + + switch node := nextTree.(type) { + case []*Tree: + subtree = node[len(node)-1] + case *Tree: + subtree = node + default: + return fmt.Errorf("unknown type for path %s (%s): %T (%#v)", + strings.Join(keys, "."), intermediateKey, nextTree, nextTree) + } + } + return nil +} + +// LoadBytes creates a Tree from a []byte. +func LoadBytes(b []byte) (tree *Tree, err error) { + defer func() { + if r := recover(); r != nil { + if _, ok := r.(runtime.Error); ok { + panic(r) + } + err = errors.New(r.(string)) + } + }() + + if len(b) >= 4 && (hasUTF32BigEndianBOM4(b) || hasUTF32LittleEndianBOM4(b)) { + b = b[4:] + } else if len(b) >= 3 && hasUTF8BOM3(b) { + b = b[3:] + } else if len(b) >= 2 && (hasUTF16BigEndianBOM2(b) || hasUTF16LittleEndianBOM2(b)) { + b = b[2:] + } + + tree = parseToml(lexToml(b)) + return +} + +func hasUTF16BigEndianBOM2(b []byte) bool { + return b[0] == 0xFE && b[1] == 0xFF +} + +func hasUTF16LittleEndianBOM2(b []byte) bool { + return b[0] == 0xFF && b[1] == 0xFE +} + +func hasUTF8BOM3(b []byte) bool { + return b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF +} + +func hasUTF32BigEndianBOM4(b []byte) bool { + return b[0] == 0x00 && b[1] == 0x00 && b[2] == 0xFE && b[3] == 0xFF +} + +func hasUTF32LittleEndianBOM4(b []byte) bool { + return b[0] == 0xFF && b[1] == 0xFE && b[2] == 0x00 && b[3] == 0x00 +} + +// LoadReader creates a Tree from any io.Reader. +func LoadReader(reader io.Reader) (tree *Tree, err error) { + inputBytes, err := ioutil.ReadAll(reader) + if err != nil { + return + } + tree, err = LoadBytes(inputBytes) + return +} + +// Load creates a Tree from a string. +func Load(content string) (tree *Tree, err error) { + return LoadBytes([]byte(content)) +} + +// LoadFile creates a Tree from a file. +func LoadFile(path string) (tree *Tree, err error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + return LoadReader(file) +} diff --git a/vendor/github.com/pelletier/go-toml/tomltree_create.go b/vendor/github.com/pelletier/go-toml/tomltree_create.go new file mode 100644 index 00000000000..79610e9b340 --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/tomltree_create.go @@ -0,0 +1,142 @@ +package toml + +import ( + "fmt" + "reflect" + "time" +) + +var kindToType = [reflect.String + 1]reflect.Type{ + reflect.Bool: reflect.TypeOf(true), + reflect.String: reflect.TypeOf(""), + reflect.Float32: reflect.TypeOf(float64(1)), + reflect.Float64: reflect.TypeOf(float64(1)), + reflect.Int: reflect.TypeOf(int64(1)), + reflect.Int8: reflect.TypeOf(int64(1)), + reflect.Int16: reflect.TypeOf(int64(1)), + reflect.Int32: reflect.TypeOf(int64(1)), + reflect.Int64: reflect.TypeOf(int64(1)), + reflect.Uint: reflect.TypeOf(uint64(1)), + reflect.Uint8: reflect.TypeOf(uint64(1)), + reflect.Uint16: reflect.TypeOf(uint64(1)), + reflect.Uint32: reflect.TypeOf(uint64(1)), + reflect.Uint64: reflect.TypeOf(uint64(1)), +} + +// typeFor returns a reflect.Type for a reflect.Kind, or nil if none is found. +// supported values: +// string, bool, int64, uint64, float64, time.Time, int, int8, int16, int32, uint, uint8, uint16, uint32, float32 +func typeFor(k reflect.Kind) reflect.Type { + if k > 0 && int(k) < len(kindToType) { + return kindToType[k] + } + return nil +} + +func simpleValueCoercion(object interface{}) (interface{}, error) { + switch original := object.(type) { + case string, bool, int64, uint64, float64, time.Time: + return original, nil + case int: + return int64(original), nil + case int8: + return int64(original), nil + case int16: + return int64(original), nil + case int32: + return int64(original), nil + case uint: + return uint64(original), nil + case uint8: + return uint64(original), nil + case uint16: + return uint64(original), nil + case uint32: + return uint64(original), nil + case float32: + return float64(original), nil + case fmt.Stringer: + return original.String(), nil + default: + return nil, fmt.Errorf("cannot convert type %T to Tree", object) + } +} + +func sliceToTree(object interface{}) (interface{}, error) { + // arrays are a bit tricky, since they can represent either a + // collection of simple values, which is represented by one + // *tomlValue, or an array of tables, which is represented by an + // array of *Tree. + + // holding the assumption that this function is called from toTree only when value.Kind() is Array or Slice + value := reflect.ValueOf(object) + insideType := value.Type().Elem() + length := value.Len() + if length > 0 { + insideType = reflect.ValueOf(value.Index(0).Interface()).Type() + } + if insideType.Kind() == reflect.Map { + // this is considered as an array of tables + tablesArray := make([]*Tree, 0, length) + for i := 0; i < length; i++ { + table := value.Index(i) + tree, err := toTree(table.Interface()) + if err != nil { + return nil, err + } + tablesArray = append(tablesArray, tree.(*Tree)) + } + return tablesArray, nil + } + + sliceType := typeFor(insideType.Kind()) + if sliceType == nil { + sliceType = insideType + } + + arrayValue := reflect.MakeSlice(reflect.SliceOf(sliceType), 0, length) + + for i := 0; i < length; i++ { + val := value.Index(i).Interface() + simpleValue, err := simpleValueCoercion(val) + if err != nil { + return nil, err + } + arrayValue = reflect.Append(arrayValue, reflect.ValueOf(simpleValue)) + } + return &tomlValue{value: arrayValue.Interface(), position: Position{}}, nil +} + +func toTree(object interface{}) (interface{}, error) { + value := reflect.ValueOf(object) + + if value.Kind() == reflect.Map { + values := map[string]interface{}{} + keys := value.MapKeys() + for _, key := range keys { + if key.Kind() != reflect.String { + if _, ok := key.Interface().(string); !ok { + return nil, fmt.Errorf("map key needs to be a string, not %T (%v)", key.Interface(), key.Kind()) + } + } + + v := value.MapIndex(key) + newValue, err := toTree(v.Interface()) + if err != nil { + return nil, err + } + values[key.String()] = newValue + } + return &Tree{values: values, position: Position{}}, nil + } + + if value.Kind() == reflect.Array || value.Kind() == reflect.Slice { + return sliceToTree(object) + } + + simpleValue, err := simpleValueCoercion(object) + if err != nil { + return nil, err + } + return &tomlValue{value: simpleValue, position: Position{}}, nil +} diff --git a/vendor/github.com/pelletier/go-toml/tomltree_write.go b/vendor/github.com/pelletier/go-toml/tomltree_write.go new file mode 100644 index 00000000000..2d6487ede4a --- /dev/null +++ b/vendor/github.com/pelletier/go-toml/tomltree_write.go @@ -0,0 +1,517 @@ +package toml + +import ( + "bytes" + "fmt" + "io" + "math" + "math/big" + "reflect" + "sort" + "strconv" + "strings" + "time" +) + +type valueComplexity int + +const ( + valueSimple valueComplexity = iota + 1 + valueComplex +) + +type sortNode struct { + key string + complexity valueComplexity +} + +// Encodes a string to a TOML-compliant multi-line string value +// This function is a clone of the existing encodeTomlString function, except that whitespace characters +// are preserved. Quotation marks and backslashes are also not escaped. +func encodeMultilineTomlString(value string, commented string) string { + var b bytes.Buffer + adjacentQuoteCount := 0 + + b.WriteString(commented) + for i, rr := range value { + if rr != '"' { + adjacentQuoteCount = 0 + } else { + adjacentQuoteCount++ + } + switch rr { + case '\b': + b.WriteString(`\b`) + case '\t': + b.WriteString("\t") + case '\n': + b.WriteString("\n" + commented) + case '\f': + b.WriteString(`\f`) + case '\r': + b.WriteString("\r") + case '"': + if adjacentQuoteCount >= 3 || i == len(value)-1 { + adjacentQuoteCount = 0 + b.WriteString(`\"`) + } else { + b.WriteString(`"`) + } + case '\\': + b.WriteString(`\`) + default: + intRr := uint16(rr) + if intRr < 0x001F { + b.WriteString(fmt.Sprintf("\\u%0.4X", intRr)) + } else { + b.WriteRune(rr) + } + } + } + return b.String() +} + +// Encodes a string to a TOML-compliant string value +func encodeTomlString(value string) string { + var b bytes.Buffer + + for _, rr := range value { + switch rr { + case '\b': + b.WriteString(`\b`) + case '\t': + b.WriteString(`\t`) + case '\n': + b.WriteString(`\n`) + case '\f': + b.WriteString(`\f`) + case '\r': + b.WriteString(`\r`) + case '"': + b.WriteString(`\"`) + case '\\': + b.WriteString(`\\`) + default: + intRr := uint16(rr) + if intRr < 0x001F { + b.WriteString(fmt.Sprintf("\\u%0.4X", intRr)) + } else { + b.WriteRune(rr) + } + } + } + return b.String() +} + +func tomlTreeStringRepresentation(t *Tree, ord marshalOrder) (string, error) { + var orderedVals []sortNode + switch ord { + case OrderPreserve: + orderedVals = sortByLines(t) + default: + orderedVals = sortAlphabetical(t) + } + + var values []string + for _, node := range orderedVals { + k := node.key + v := t.values[k] + + repr, err := tomlValueStringRepresentation(v, "", "", ord, false) + if err != nil { + return "", err + } + values = append(values, quoteKeyIfNeeded(k)+" = "+repr) + } + return "{ " + strings.Join(values, ", ") + " }", nil +} + +func tomlValueStringRepresentation(v interface{}, commented string, indent string, ord marshalOrder, arraysOneElementPerLine bool) (string, error) { + // this interface check is added to dereference the change made in the writeTo function. + // That change was made to allow this function to see formatting options. + tv, ok := v.(*tomlValue) + if ok { + v = tv.value + } else { + tv = &tomlValue{} + } + + switch value := v.(type) { + case uint64: + return strconv.FormatUint(value, 10), nil + case int64: + return strconv.FormatInt(value, 10), nil + case float64: + // Default bit length is full 64 + bits := 64 + // Float panics if nan is used + if !math.IsNaN(value) { + // if 32 bit accuracy is enough to exactly show, use 32 + _, acc := big.NewFloat(value).Float32() + if acc == big.Exact { + bits = 32 + } + } + if math.Trunc(value) == value { + return strings.ToLower(strconv.FormatFloat(value, 'f', 1, bits)), nil + } + return strings.ToLower(strconv.FormatFloat(value, 'f', -1, bits)), nil + case string: + if tv.multiline { + return "\"\"\"\n" + encodeMultilineTomlString(value, commented) + "\"\"\"", nil + } + return "\"" + encodeTomlString(value) + "\"", nil + case []byte: + b, _ := v.([]byte) + return tomlValueStringRepresentation(string(b), commented, indent, ord, arraysOneElementPerLine) + case bool: + if value { + return "true", nil + } + return "false", nil + case time.Time: + return value.Format(time.RFC3339), nil + case LocalDate: + return value.String(), nil + case LocalDateTime: + return value.String(), nil + case LocalTime: + return value.String(), nil + case *Tree: + return tomlTreeStringRepresentation(value, ord) + case nil: + return "", nil + } + + rv := reflect.ValueOf(v) + + if rv.Kind() == reflect.Slice { + var values []string + for i := 0; i < rv.Len(); i++ { + item := rv.Index(i).Interface() + itemRepr, err := tomlValueStringRepresentation(item, commented, indent, ord, arraysOneElementPerLine) + if err != nil { + return "", err + } + values = append(values, itemRepr) + } + if arraysOneElementPerLine && len(values) > 1 { + stringBuffer := bytes.Buffer{} + valueIndent := indent + ` ` // TODO: move that to a shared encoder state + + stringBuffer.WriteString("[\n") + + for _, value := range values { + stringBuffer.WriteString(valueIndent) + stringBuffer.WriteString(commented + value) + stringBuffer.WriteString(`,`) + stringBuffer.WriteString("\n") + } + + stringBuffer.WriteString(indent + commented + "]") + + return stringBuffer.String(), nil + } + return "[" + strings.Join(values, ", ") + "]", nil + } + return "", fmt.Errorf("unsupported value type %T: %v", v, v) +} + +func getTreeArrayLine(trees []*Tree) (line int) { + // get lowest line number that is not 0 + for _, tv := range trees { + if tv.position.Line < line || line == 0 { + line = tv.position.Line + } + } + return +} + +func sortByLines(t *Tree) (vals []sortNode) { + var ( + line int + lines []int + tv *Tree + tom *tomlValue + node sortNode + ) + vals = make([]sortNode, 0) + m := make(map[int]sortNode) + + for k := range t.values { + v := t.values[k] + switch v.(type) { + case *Tree: + tv = v.(*Tree) + line = tv.position.Line + node = sortNode{key: k, complexity: valueComplex} + case []*Tree: + line = getTreeArrayLine(v.([]*Tree)) + node = sortNode{key: k, complexity: valueComplex} + default: + tom = v.(*tomlValue) + line = tom.position.Line + node = sortNode{key: k, complexity: valueSimple} + } + lines = append(lines, line) + vals = append(vals, node) + m[line] = node + } + sort.Ints(lines) + + for i, line := range lines { + vals[i] = m[line] + } + + return vals +} + +func sortAlphabetical(t *Tree) (vals []sortNode) { + var ( + node sortNode + simpVals []string + compVals []string + ) + vals = make([]sortNode, 0) + m := make(map[string]sortNode) + + for k := range t.values { + v := t.values[k] + switch v.(type) { + case *Tree, []*Tree: + node = sortNode{key: k, complexity: valueComplex} + compVals = append(compVals, node.key) + default: + node = sortNode{key: k, complexity: valueSimple} + simpVals = append(simpVals, node.key) + } + vals = append(vals, node) + m[node.key] = node + } + + // Simples first to match previous implementation + sort.Strings(simpVals) + i := 0 + for _, key := range simpVals { + vals[i] = m[key] + i++ + } + + sort.Strings(compVals) + for _, key := range compVals { + vals[i] = m[key] + i++ + } + + return vals +} + +func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool) (int64, error) { + return t.writeToOrdered(w, indent, keyspace, bytesCount, arraysOneElementPerLine, OrderAlphabetical, " ", false) +} + +func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool, ord marshalOrder, indentString string, parentCommented bool) (int64, error) { + var orderedVals []sortNode + + switch ord { + case OrderPreserve: + orderedVals = sortByLines(t) + default: + orderedVals = sortAlphabetical(t) + } + + for _, node := range orderedVals { + switch node.complexity { + case valueComplex: + k := node.key + v := t.values[k] + + combinedKey := quoteKeyIfNeeded(k) + if keyspace != "" { + combinedKey = keyspace + "." + combinedKey + } + + switch node := v.(type) { + // node has to be of those two types given how keys are sorted above + case *Tree: + tv, ok := t.values[k].(*Tree) + if !ok { + return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k]) + } + if tv.comment != "" { + comment := strings.Replace(tv.comment, "\n", "\n"+indent+"#", -1) + start := "# " + if strings.HasPrefix(comment, "#") { + start = "" + } + writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment) + bytesCount += int64(writtenBytesCountComment) + if errc != nil { + return bytesCount, errc + } + } + + var commented string + if parentCommented || t.commented || tv.commented { + commented = "# " + } + writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[", combinedKey, "]\n") + bytesCount += int64(writtenBytesCount) + if err != nil { + return bytesCount, err + } + bytesCount, err = node.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, parentCommented || t.commented || tv.commented) + if err != nil { + return bytesCount, err + } + case []*Tree: + for _, subTree := range node { + var commented string + if parentCommented || t.commented || subTree.commented { + commented = "# " + } + writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[[", combinedKey, "]]\n") + bytesCount += int64(writtenBytesCount) + if err != nil { + return bytesCount, err + } + + bytesCount, err = subTree.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, parentCommented || t.commented || subTree.commented) + if err != nil { + return bytesCount, err + } + } + } + default: // Simple + k := node.key + v, ok := t.values[k].(*tomlValue) + if !ok { + return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k]) + } + + var commented string + if parentCommented || t.commented || v.commented { + commented = "# " + } + repr, err := tomlValueStringRepresentation(v, commented, indent, ord, arraysOneElementPerLine) + if err != nil { + return bytesCount, err + } + + if v.comment != "" { + comment := strings.Replace(v.comment, "\n", "\n"+indent+"#", -1) + start := "# " + if strings.HasPrefix(comment, "#") { + start = "" + } + writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment, "\n") + bytesCount += int64(writtenBytesCountComment) + if errc != nil { + return bytesCount, errc + } + } + + quotedKey := quoteKeyIfNeeded(k) + writtenBytesCount, err := writeStrings(w, indent, commented, quotedKey, " = ", repr, "\n") + bytesCount += int64(writtenBytesCount) + if err != nil { + return bytesCount, err + } + } + } + + return bytesCount, nil +} + +// quote a key if it does not fit the bare key format (A-Za-z0-9_-) +// quoted keys use the same rules as strings +func quoteKeyIfNeeded(k string) string { + // when encoding a map with the 'quoteMapKeys' option enabled, the tree will contain + // keys that have already been quoted. + // not an ideal situation, but good enough of a stop gap. + if len(k) >= 2 && k[0] == '"' && k[len(k)-1] == '"' { + return k + } + isBare := true + for _, r := range k { + if !isValidBareChar(r) { + isBare = false + break + } + } + if isBare { + return k + } + return quoteKey(k) +} + +func quoteKey(k string) string { + return "\"" + encodeTomlString(k) + "\"" +} + +func writeStrings(w io.Writer, s ...string) (int, error) { + var n int + for i := range s { + b, err := io.WriteString(w, s[i]) + n += b + if err != nil { + return n, err + } + } + return n, nil +} + +// WriteTo encode the Tree as Toml and writes it to the writer w. +// Returns the number of bytes written in case of success, or an error if anything happened. +func (t *Tree) WriteTo(w io.Writer) (int64, error) { + return t.writeTo(w, "", "", 0, false) +} + +// ToTomlString generates a human-readable representation of the current tree. +// Output spans multiple lines, and is suitable for ingest by a TOML parser. +// If the conversion cannot be performed, ToString returns a non-nil error. +func (t *Tree) ToTomlString() (string, error) { + b, err := t.Marshal() + if err != nil { + return "", err + } + return string(b), nil +} + +// String generates a human-readable representation of the current tree. +// Alias of ToString. Present to implement the fmt.Stringer interface. +func (t *Tree) String() string { + result, _ := t.ToTomlString() + return result +} + +// ToMap recursively generates a representation of the tree using Go built-in structures. +// The following types are used: +// +// * bool +// * float64 +// * int64 +// * string +// * uint64 +// * time.Time +// * map[string]interface{} (where interface{} is any of this list) +// * []interface{} (where interface{} is any of this list) +func (t *Tree) ToMap() map[string]interface{} { + result := map[string]interface{}{} + + for k, v := range t.values { + switch node := v.(type) { + case []*Tree: + var array []interface{} + for _, item := range node { + array = append(array, item.ToMap()) + } + result[k] = array + case *Tree: + result[k] = node.ToMap() + case *tomlValue: + result[k] = node.value + } + } + return result +} diff --git a/vendor/github.com/phayes/freeport/.gitignore b/vendor/github.com/phayes/freeport/.gitignore new file mode 100644 index 00000000000..1521c8b7652 --- /dev/null +++ b/vendor/github.com/phayes/freeport/.gitignore @@ -0,0 +1 @@ +dist diff --git a/vendor/github.com/phayes/freeport/.goreleaser.yml b/vendor/github.com/phayes/freeport/.goreleaser.yml new file mode 100644 index 00000000000..48044ea396b --- /dev/null +++ b/vendor/github.com/phayes/freeport/.goreleaser.yml @@ -0,0 +1,134 @@ +project_name: freeport + +release: + github: + owner: phayes + name: freeport + +builds: + - binary: freeport + goos: + - linux + - darwin + goarch: + - amd64 + - "386" + goarm: + - "6" + main: ./cmd/freeport + ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} + +archive: + format: tar.gz + name_template: '{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ + .Arm }}{{ end }}' + files: + - licence* + - LICENCE* + - license* + - LICENSE* + - readme* + - README* + - changelog* + - CHANGELOG* + +snapshot: + name_template: SNAPSHOT-{{ .Commit }} + +checksum: + name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt' + +# Create RPM and .DEB files +fpm: + vendor: Patrick Hayes + + # Your app's homepage. + #homepage: https://example.com/ + + # Your app's maintainer + maintainer: Patrick Hayes + + # Your app's description. + description: Get a free open TCP port that is ready to use. + + # Your app's license. + # Default is empty. + license: BSD + + # Formats to be generated. + formats: + - deb + - rpm + + # Packages your package depends on. + #dependencies: + # - git + # - zsh + + # Packages that conflict with your package. + #conflicts: + # - svn + # - bash + + # Files or directories to add to your package (beyond the binary). + # Keys are source paths to get the files from. + # Values are the destination locations of the files in the package. + #files: + # "scripts/etc/init.d/": "/etc/init.d" + +# Homebrew repos +brew: + # Reporitory to push the tap to. + github: + owner: phayes + name: homebrew-repo + + # Git author used to commit to the repository. + # Defaults are shown. + commit_author: + name: goreleaserbot + email: goreleaser@carlosbecker.com + + # Folder inside the repository to put the formula. + # Default is the root folder. + # folder: . + + # Caveats for the user of your binary. + # Default is empty. + # caveats: "How to use this binary" + + # Your app's homepage. + # Default is empty. + # homepage: "https://example.com/" + + # Your app's description. + # Default is empty. + description: "Get a free open TCP port that is ready to use." + + # Packages your package depends on. + #dependencies: + # - git + # - zsh + + # Packages that conflict with your package. + #conflicts: + # - svn + # - bash + + # Specify for packages that run as a service. + # Default is empty. + #plist: | + # + # ... + + # So you can `brew test` your formula. + # Default is empty. + #test: | + # system "#{bin}/program --version" + # ... + + # Custom install script for brew. + # Default is 'bin.install "program"'. + #install: | + # bin.install "program" + # ... \ No newline at end of file diff --git a/vendor/github.com/phayes/freeport/LICENSE.md b/vendor/github.com/phayes/freeport/LICENSE.md new file mode 100644 index 00000000000..d9882e595e4 --- /dev/null +++ b/vendor/github.com/phayes/freeport/LICENSE.md @@ -0,0 +1,15 @@ +Open Source License (BSD 3-Clause) +---------------------------------- + +Copyright (c) 2014, Patrick Hayes / HighWire Press +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/phayes/freeport/README.md b/vendor/github.com/phayes/freeport/README.md new file mode 100644 index 00000000000..1665ccf4d4f --- /dev/null +++ b/vendor/github.com/phayes/freeport/README.md @@ -0,0 +1,58 @@ +FreePort +======== + +Get a free open TCP port that is ready to use. + +## Command Line Example: +```bash +# Ask the kernel to give us an open port. +export port=$(freeport) + +# Start standalone httpd server for testing +httpd -X -c "Listen $port" & + +# Curl local server on the selected port +curl localhost:$port +``` + +## Golang example: +```go +package main + +import "github.com/phayes/freeport" + +func main() { + port, err := freeport.GetFreePort() + if err != nil { + log.Fatal(err) + } + // port is ready to listen on +} + +``` + +## Installation + +#### Mac OSX +```bash +brew install phayes/repo/freeport +``` + + +#### CentOS and other RPM based systems +```bash +wget https://github.com/phayes/freeport/releases/download/1.0.2/freeport_1.0.2_linux_386.rpm +rpm -Uvh freeport_1.0.2_linux_386.rpm +``` + +#### Ubuntu and other DEB based systems +```bash +wget wget https://github.com/phayes/freeport/releases/download/1.0.2/freeport_1.0.2_linux_amd64.deb +dpkg -i freeport_1.0.2_linux_amd64.deb +``` + +#### Building From Source +```bash +sudo apt-get install golang # Download go. Alternativly build from source: https://golang.org/doc/install/source +go get github.com/phayes/freeport +``` diff --git a/vendor/github.com/phayes/freeport/freeport.go b/vendor/github.com/phayes/freeport/freeport.go new file mode 100644 index 00000000000..fcfb6bab036 --- /dev/null +++ b/vendor/github.com/phayes/freeport/freeport.go @@ -0,0 +1,49 @@ +package freeport + +import ( + "net" +) + +// GetFreePort asks the kernel for a free open port that is ready to use. +func GetFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} + +// GetPort is deprecated, use GetFreePort instead +// Ask the kernel for a free open port that is ready to use +func GetPort() int { + port, err := GetFreePort() + if err != nil { + panic(err) + } + return port +} + +// GetFreePort asks the kernel for free open ports that are ready to use. +func GetFreePorts(count int) ([]int, error) { + var ports []int + for i := 0; i < count; i++ { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return nil, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return nil, err + } + defer l.Close() + ports = append(ports, l.Addr().(*net.TCPAddr).Port) + } + return ports, nil +} diff --git a/vendor/github.com/wavesoftware/go-ensure/.editorconfig b/vendor/github.com/wavesoftware/go-ensure/.editorconfig new file mode 100644 index 00000000000..25d2a11a993 --- /dev/null +++ b/vendor/github.com/wavesoftware/go-ensure/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 + +[*.go] +indent_style = tab diff --git a/vendor/github.com/wavesoftware/go-ensure/.gitignore b/vendor/github.com/wavesoftware/go-ensure/.gitignore new file mode 100644 index 00000000000..e67b8a3c6f4 --- /dev/null +++ b/vendor/github.com/wavesoftware/go-ensure/.gitignore @@ -0,0 +1,17 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +build/ diff --git a/vendor/github.com/wavesoftware/go-ensure/.travis.yml b/vendor/github.com/wavesoftware/go-ensure/.travis.yml new file mode 100644 index 00000000000..2f4b5c2f147 --- /dev/null +++ b/vendor/github.com/wavesoftware/go-ensure/.travis.yml @@ -0,0 +1,15 @@ +--- +language: go +sudo: false +dist: bionic +go: + - 1.13.x +script: + - make clean test +branches: + only: + - master + - develop + - "/^v\\d/" +notifications: + email: onchange diff --git a/vendor/github.com/wavesoftware/go-ensure/LICENSE b/vendor/github.com/wavesoftware/go-ensure/LICENSE new file mode 100644 index 00000000000..6c180a6f23d --- /dev/null +++ b/vendor/github.com/wavesoftware/go-ensure/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Wave Software + + 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. diff --git a/vendor/github.com/wavesoftware/go-ensure/Makefile b/vendor/github.com/wavesoftware/go-ensure/Makefile new file mode 100644 index 00000000000..afa4896e3a7 --- /dev/null +++ b/vendor/github.com/wavesoftware/go-ensure/Makefile @@ -0,0 +1,32 @@ +PROJECT_DIR = $(shell readlink -f .) +BUILD_DIR = "$(PROJECT_DIR)/build" + +GO ?= go +RICHGO ?= rich$(GO) + +.PHONY: default +default: binaries + +.PHONY: builddeps +builddeps: + @GO111MODULE=off $(GO) get github.com/kyoh86/richgo + @GO111MODULE=off $(GO) get github.com/mgechev/revive + +.PHONY: builddir +builddir: + @mkdir -p build + +.PHONY: clean +clean: builddeps + @echo "🛁 Cleaning" + @rm -frv $(BUILD_DIR) + +.PHONY: check +check: builddeps + @echo "🛂 Checking" + revive -config revive.toml -formatter stylish ./... + +.PHONY: test +test: builddir check + @echo "✔️ Testing" + $(RICHGO) test -v -covermode=count -coverprofile=build/coverage.out ./... diff --git a/vendor/github.com/wavesoftware/go-ensure/README.md b/vendor/github.com/wavesoftware/go-ensure/README.md new file mode 100644 index 00000000000..82e73e4525d --- /dev/null +++ b/vendor/github.com/wavesoftware/go-ensure/README.md @@ -0,0 +1,55 @@ +# Ensure for Go + +A simple ensure package for Golang + +## Use case + +Writing a Go code makes a lot of repetition, regarding error checking. That's +especially true for end user code like e2e tests when we expect that something +will work. In other cases that's a bug and should code should panic then. + +## Usage + +Instead of writing: + +```go +func DeployAllComponents() error { + alpha, err := deployAlpha() + if err != nil { + return errors.WithMessage(err, "unexpected error") + } + beta, err := deployBeta(alpha) + if err != nil { + return errors.WithMessage(err, "unexpected error") + } + _, err := deployGamma(beta) + if err != nil { + return errors.WithMessage(err, "unexpected error") + } + return nil +} + +// execution isn't simple +err = DeployAllComponents() +if err != nil { + panic(err) +} +``` + +with this PR I can write it like: + +```go +func DeployAllComponents() { + alpha, err := deployAlpha() + ensure.NoError(err) + beta, err := deployBeta(alpha) + ensure.NoError(err) + _, err := deployGamma(beta) + ensure.NoError(err) +} + +// execution is simple +DeployAllComponents() +``` + +Above is much more readable and pleasant to see. diff --git a/vendor/github.com/wavesoftware/go-ensure/errors.go b/vendor/github.com/wavesoftware/go-ensure/errors.go new file mode 100644 index 00000000000..9da1e2e860a --- /dev/null +++ b/vendor/github.com/wavesoftware/go-ensure/errors.go @@ -0,0 +1,33 @@ +package ensure + +import ( + "fmt" + "github.com/pkg/errors" + "regexp" +) + +// NoError will panic if given an error, as it was unexpected +func NoError(err error) { + if err != nil { + panic(errors.WithMessage(err, "unexpected error")) + } +} + +// Error will panic if given no error, as it expected one +func Error(err error) { + if err == nil { + panic(errors.New("expecting error, but none given")) + } +} + +// ErrorWithMessage will panic if given no error, or error message don't match provided regexp +func ErrorWithMessage(err error, messageRegexp string) { + Error(err) + validErrorMessage := regexp.MustCompile(messageRegexp) + if !validErrorMessage.MatchString(err.Error()) { + panic(errors.WithMessage( + err, + fmt.Sprintf("given error doesn't match given regexp (%s)", messageRegexp), + )) + } +} diff --git a/vendor/github.com/wavesoftware/go-ensure/go.mod b/vendor/github.com/wavesoftware/go-ensure/go.mod new file mode 100644 index 00000000000..06dbff11848 --- /dev/null +++ b/vendor/github.com/wavesoftware/go-ensure/go.mod @@ -0,0 +1,5 @@ +module github.com/wavesoftware/go-ensure + +go 1.13 + +require github.com/pkg/errors v0.9.1 diff --git a/vendor/github.com/wavesoftware/go-ensure/go.sum b/vendor/github.com/wavesoftware/go-ensure/go.sum new file mode 100644 index 00000000000..7c401c3f58b --- /dev/null +++ b/vendor/github.com/wavesoftware/go-ensure/go.sum @@ -0,0 +1,2 @@ +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/vendor/github.com/wavesoftware/go-ensure/revive.toml b/vendor/github.com/wavesoftware/go-ensure/revive.toml new file mode 100644 index 00000000000..a4cb9ecd72e --- /dev/null +++ b/vendor/github.com/wavesoftware/go-ensure/revive.toml @@ -0,0 +1,35 @@ +# When set to false, ignores files with "GENERATED" header, similar to golint +ignoreGeneratedHeader = true + +# Sets the default severity to "warning" +severity = "warning" + +# Sets the default failure confidence. This means that linting errors +# with less than 0.8 confidence will be ignored. +confidence = 0.8 + +# Sets the error code for failures with severity "error" +errorCode = 102 + +# Sets the error code for failures with severity "warning" +warningCode = 102 + +[rule.blank-imports] +[rule.context-as-argument] +[rule.context-keys-type] +[rule.dot-imports] +[rule.error-return] +[rule.error-strings] +[rule.error-naming] +[rule.exported] +[rule.if-return] +[rule.increment-decrement] +[rule.var-naming] +[rule.var-declaration] +[rule.package-comments] +[rule.range] +[rule.receiver-naming] +[rule.time-naming] +[rule.unexported-return] +[rule.indent-error-flow] +[rule.errorf] diff --git a/vendor/gopkg.in/yaml.v2/apic.go b/vendor/gopkg.in/yaml.v2/apic.go index 1f7e87e6727..d2c2308f1f4 100644 --- a/vendor/gopkg.in/yaml.v2/apic.go +++ b/vendor/gopkg.in/yaml.v2/apic.go @@ -86,6 +86,7 @@ func yaml_emitter_initialize(emitter *yaml_emitter_t) { raw_buffer: make([]byte, 0, output_raw_buffer_size), states: make([]yaml_emitter_state_t, 0, initial_stack_size), events: make([]yaml_event_t, 0, initial_queue_size), + best_width: -1, } } diff --git a/vendor/modules.txt b/vendor/modules.txt index c9a80b459ce..8458b395a96 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -218,6 +218,8 @@ github.com/mailru/easyjson/jwriter github.com/markbates/inflect # github.com/matttproud/golang_protobuf_extensions v1.0.1 github.com/matttproud/golang_protobuf_extensions/pbutil +# github.com/mitchellh/go-homedir v1.1.0 +github.com/mitchellh/go-homedir # github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd github.com/modern-go/concurrent # github.com/modern-go/reflect2 v1.0.1 @@ -229,6 +231,10 @@ github.com/openzipkin/zipkin-go/model github.com/openzipkin/zipkin-go/propagation github.com/openzipkin/zipkin-go/reporter github.com/openzipkin/zipkin-go/reporter/http +# github.com/pelletier/go-toml v1.8.0 +github.com/pelletier/go-toml +# github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 +github.com/phayes/freeport # github.com/pkg/errors v0.9.1 github.com/pkg/errors # github.com/pmezard/go-difflib v1.0.0 @@ -264,6 +270,8 @@ github.com/stretchr/testify/require github.com/tsenart/vegeta/lib # github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/bytebufferpool +# github.com/wavesoftware/go-ensure v1.0.0 +github.com/wavesoftware/go-ensure # go.opencensus.io v0.22.3 go.opencensus.io go.opencensus.io/internal @@ -497,7 +505,7 @@ google.golang.org/grpc/status google.golang.org/grpc/tap # gopkg.in/inf.v0 v0.9.1 gopkg.in/inf.v0 -# gopkg.in/yaml.v2 v2.2.8 +# gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v2 # honnef.co/go/tools v0.0.1-2020.1.3 honnef.co/go/tools/arg