├── .circleci └── config.yml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── doc.go ├── go.mod ├── go.sum ├── network ├── address.go ├── client.go └── types.go ├── ptypes ├── doc.go ├── duration.go ├── ipnet.go ├── ipnet_test.go ├── rate.go └── size.go ├── run ├── init_context.go ├── invoker.go ├── invoker_test.go ├── panic.go └── panic_test.go ├── runtime ├── doc.go ├── env.go ├── influxdb_batch.go ├── influxdb_batch_test.go ├── influxdb_client.go ├── influxdb_client_test.go ├── metrics.go ├── metrics_api.go ├── metrics_types.go ├── resetting_counter.go ├── resetting_histogram.go ├── resetting_histogram_test.go ├── runenv.go ├── runenv_assets.go ├── runenv_events.go ├── runenv_http.go ├── runenv_logger.go ├── runenv_test.go ├── runparams.go └── test_utils.go ├── sync ├── client.go ├── client_conn.go ├── client_inmem.go ├── client_pubsub.go ├── client_state.go ├── client_sugar.go ├── client_topic.go ├── context.go ├── doc.go ├── interface.go └── types.go └── verbose.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | 4 | jobs: 5 | build: 6 | docker: 7 | - image: circleci/golang:1.14 8 | - image: circleci/redis:latest 9 | steps: 10 | - checkout 11 | - restore_cache: 12 | keys: 13 | - v1-pkg-cache 14 | - run: 15 | name: Tidy 16 | command: go mod tidy && git diff --exit-code 17 | - run: 18 | name: Test 19 | # command: go test -v -race ./... 20 | command: go test -v ./... 21 | # - run: 22 | #name: Vet 23 | # command: go vet ./... 24 | - save_cache: 25 | key: v1-pkg-cache 26 | paths: 27 | - "/go/pkg" 28 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 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 2 | 3 | http://www.apache.org/licenses/LICENSE-2.0 4 | 5 | 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. 6 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Testground SDK 2 | 3 | ![Testground logo](https://raw.githubusercontent.com/testground/pm/master/logo/TG_Banner_GitHub.jpg) 4 | 5 | [![Made by Protocol Labs](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) 6 | ![Go version](https://img.shields.io/badge/go-%3E%3D1.14.0-blue.svg?style=flat-square) 7 | [![GoDoc](https://img.shields.io/badge/godoc-reference-5272B4.svg?style=flat-square)](https://pkg.go.dev/github.com/testground/sdk-go) 8 | [![CircleCI](https://circleci.com/gh/testground/sdk-go.svg?style=svg)](https://circleci.com/gh/testground/sdk-go) 9 | 10 | This repository contains the Golang SDK for developing [Testground](https://github.com/testground/testground) test plans. 11 | 12 | ## API docs 13 | 14 | See [Godocs](https://pkg.go.dev/github.com/testground/sdk-go). 15 | 16 | ## Issues 17 | 18 | Please report issues in the [`testground/testground` repo](https://github.com/testground/testground/issues). 19 | 20 | ## License 21 | 22 | Dual-licensed: [MIT](./LICENSE-MIT), [Apache Software License v2](./LICENSE-APACHE), by way of the 23 | [Permissive License Stack](https://protocol.ai/blog/announcing-the-permissive-license-stack/). 24 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // This module contains the Go SDK for developing Testground test plans. 2 | package sdk 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/testground/sdk-go 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/avast/retry-go v2.6.0+incompatible 7 | github.com/dustin/go-humanize v1.0.0 8 | github.com/hashicorp/go-multierror v1.1.0 9 | github.com/influxdata/influxdb1-client v0.0.0-20200515024757-02f0bf5dbca3 10 | github.com/prometheus/client_golang v1.7.1 11 | github.com/raulk/clock v1.1.0 12 | github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 13 | github.com/stretchr/testify v1.5.1 14 | github.com/testground/sync-service v0.1.0 15 | go.uber.org/zap v1.16.0 16 | nhooyr.io/websocket v1.8.6 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= 2 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= 5 | github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 6 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 7 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 8 | github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= 9 | github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= 10 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 11 | github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 12 | github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 13 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 14 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 15 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 16 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 17 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 18 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 19 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 20 | github.com/avast/retry-go v2.6.0+incompatible h1:FelcMrm7Bxacr1/RM8+/eqkDkmVN7tjlsy51dOzB3LI= 21 | github.com/avast/retry-go v2.6.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= 22 | github.com/aws/aws-sdk-go v1.28.9/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 23 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 24 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 25 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 26 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 27 | github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= 28 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 29 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 30 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 31 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 32 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 33 | github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= 34 | github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= 35 | github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= 36 | github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= 37 | github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= 38 | github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY= 39 | github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= 40 | github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= 41 | github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= 42 | github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= 43 | github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= 44 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 45 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 46 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 47 | github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 48 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 49 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 50 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 51 | github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 52 | github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 53 | github.com/docker/docker v1.4.2-0.20200206084213-b5fc6ea92cde/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 54 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 55 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 56 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 57 | github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= 58 | github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= 59 | github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 60 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 61 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 62 | github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 63 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 64 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 65 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 66 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 67 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 68 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 69 | github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 70 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 71 | github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= 72 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 73 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 74 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 75 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 76 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 77 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 78 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 79 | github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= 80 | github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 81 | github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= 82 | github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA= 83 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 84 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 85 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 86 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 87 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 88 | github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= 89 | github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= 90 | github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= 91 | github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= 92 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 93 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 94 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 95 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 96 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 97 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 98 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 99 | github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o= 100 | github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 101 | github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= 102 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 103 | github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= 104 | github.com/gobuffalo/here v0.6.2/go.mod h1:D75Sq0p2BVHdgQu3vCRsXbg85rx943V19urJpqAVWjI= 105 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= 106 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 107 | github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= 108 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 109 | github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= 110 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 111 | github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= 112 | github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 113 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 114 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 115 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 116 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 117 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 118 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 119 | github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 120 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 121 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 122 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 123 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 124 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 125 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 126 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 127 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 128 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 129 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 130 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 131 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 132 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 133 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 134 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 135 | github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 136 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 137 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 138 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 139 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 140 | github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= 141 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 142 | github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 143 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 144 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 145 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 146 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 147 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 148 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 149 | github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= 150 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 151 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 152 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 153 | github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 154 | github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 155 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 156 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 157 | github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= 158 | github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= 159 | github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= 160 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 161 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 162 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 163 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 164 | github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 165 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 166 | github.com/influxdata/influxdb1-client v0.0.0-20200515024757-02f0bf5dbca3 h1:k3/6a1Shi7GGCp9QpyYuXsMM6ncTOjCzOE9Fd6CDA+Q= 167 | github.com/influxdata/influxdb1-client v0.0.0-20200515024757-02f0bf5dbca3/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= 168 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 169 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 170 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 171 | github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 172 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 173 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 174 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 175 | github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= 176 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 177 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 178 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 179 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 180 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 181 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 182 | github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 183 | github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= 184 | github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 185 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 186 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 187 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 188 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 189 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 190 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 191 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 192 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 193 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 194 | github.com/kubernetes/client-go v11.0.0+incompatible/go.mod h1:kszVi2i+FeqECZHhjpkV5h5zM0GnURfJv897YzgoAQ8= 195 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 196 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 197 | github.com/logrusorgru/aurora v0.0.0-20191017060258-dc85c304c434/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 198 | github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 199 | github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= 200 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 201 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 202 | github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= 203 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 204 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 205 | github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU= 206 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 207 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 208 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 209 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 210 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 211 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 212 | github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 213 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 214 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 215 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 216 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 217 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 218 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 219 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 220 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 221 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 222 | github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= 223 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 224 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 225 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 226 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 227 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 228 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 229 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 230 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 231 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 232 | github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 233 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 234 | github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 235 | github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= 236 | github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= 237 | github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 238 | github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= 239 | github.com/otiai10/copy v1.0.2/go.mod h1:c7RpqBkwMom4bYTSkLSym4VSJz/XtncWRAj/J4PEIMY= 240 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= 241 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= 242 | github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 243 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 244 | github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 245 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 246 | github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 247 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 248 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 249 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 250 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 251 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 252 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 253 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 254 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 255 | github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= 256 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 257 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 258 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 259 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 260 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 261 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 262 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 263 | github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= 264 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 265 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 266 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 267 | github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= 268 | github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= 269 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 270 | github.com/raulk/clock v1.1.0 h1:dpb29+UKMbLqiU/jqIJptgLR1nn23HLgMY0sTCDza5Y= 271 | github.com/raulk/clock v1.1.0/go.mod h1:3MpVxdZ/ODBQDxbN+kzshf5OSZwPjtMDx6BBXBmOeY0= 272 | github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= 273 | github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 274 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 275 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 276 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 277 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 278 | github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= 279 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 280 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 281 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 282 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 283 | github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 284 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 285 | github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 286 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 287 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 288 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 289 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 290 | github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 291 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 292 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 293 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 294 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 295 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 296 | github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= 297 | github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= 298 | github.com/testground/plan-templates/templates v0.0.0-20200429051153-b24fdc73e401/go.mod h1:MT3F6oeXhaO0bwhclY7dbOxKVfuDuWuO9YHy+TZvgNc= 299 | github.com/testground/sdk-go v0.2.4/go.mod h1:3ewI3dydDseP7eCO1MHGh+67simvbkcUnguPYssFqiA= 300 | github.com/testground/sync-service v0.1.0 h1:FmG5F426wyLufcFlBnV/qSDn0BvsqLCOm34wnAL0/Rg= 301 | github.com/testground/sync-service v0.1.0/go.mod h1:UxLxsGjkZPqY1TtlusUu8xO0CS97eQ2PfIVF5O9JETA= 302 | github.com/testground/testground v0.5.3 h1:WSfB6njhk0IGyMKr7Ky4jBa2k1rct4Oen11bio1fHFI= 303 | github.com/testground/testground v0.5.3/go.mod h1:o7jPX2o6B1HmK6UFvPfVcGypNjTZa4yUjfOVsQRtb5s= 304 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 305 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 306 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 307 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 308 | github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= 309 | github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 310 | github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 311 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 312 | github.com/vishvananda/netlink v1.0.0/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= 313 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= 314 | github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0/go.mod h1:2rx5KE5FLD0HRfkkpyn8JwbVLBdhgeiOb2D2D9LLKM4= 315 | github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 316 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 317 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 318 | github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= 319 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 320 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 321 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 322 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 323 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 324 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 325 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 326 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 327 | go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 328 | go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= 329 | go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 330 | golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 331 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 332 | golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 333 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 334 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 335 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 336 | golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 337 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 338 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 339 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 340 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 341 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 342 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 343 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 344 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 345 | golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 346 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 347 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 348 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 349 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 350 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 351 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 352 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 353 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 354 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 355 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 356 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 357 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 358 | golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 359 | golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 360 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 361 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 362 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 363 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 364 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 365 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 366 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 367 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 368 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 369 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= 370 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 371 | golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 372 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 373 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 374 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 375 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 376 | golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 377 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 378 | golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 379 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 380 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 381 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 382 | golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 383 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 384 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 385 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 386 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 387 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 388 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 389 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 390 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 391 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= 392 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 393 | golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 394 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 395 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 396 | golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 397 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 398 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 399 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 400 | golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 401 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 402 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 403 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 404 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 405 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 406 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 407 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 408 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 409 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f h1:kDxGY2VmgABOe55qheT/TFqUMtcTHnomIPS1iv3G4Ms= 410 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 411 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 412 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 413 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 414 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 415 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 416 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 417 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 418 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 419 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 420 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 421 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 422 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 423 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 424 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 425 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 426 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 427 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 428 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 429 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 430 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 431 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 432 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 433 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 434 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 435 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 436 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 437 | gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= 438 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 439 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 440 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 441 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 442 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 443 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 444 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 445 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 446 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= 447 | gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 448 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 449 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 450 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 451 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 452 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 453 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 454 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 455 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 456 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 457 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 458 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 459 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 460 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 461 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 462 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 463 | k8s.io/api v0.0.0-20190703205437-39734b2a72fe/go.mod h1:J5EZ0KSEjvyKOBy5BDHSF3zn82madLLWg7nUKaOHZKU= 464 | k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= 465 | k8s.io/apimachinery v0.0.0-20190703205208-4cfb76a8bf76/go.mod h1:M2fZgZL9DbLfeJaPBCDqSqNsdsmLN+V29knYJnIXlMA= 466 | k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= 467 | k8s.io/client-go v0.0.0-20190706005506-4ed54556a14a/go.mod h1:vn7Y34rpPc8EO7qSbsZ7JCxA3ujt/wnQozW3RfYdT/E= 468 | k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 469 | k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 470 | k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 471 | k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 472 | k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= 473 | k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= 474 | k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= 475 | k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= 476 | k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= 477 | nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= 478 | nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= 479 | sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= 480 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 481 | -------------------------------------------------------------------------------- /network/address.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | // GetDataNetworkIP examines the local network interfaces, and tries to find our 9 | // assigned IP within the data network. 10 | // 11 | // This function returns the IP and a nil error if found. If running in 12 | // a sidecar-less environment, the error ErrNoTrafficShaping is returned. 13 | func (c *Client) GetDataNetworkIP() (net.IP, error) { 14 | re := c.runenv 15 | if !re.TestSidecar { 16 | // this must be a local:exec runner and we currently don't support 17 | // traffic shaping on it for now, just return the loopback address 18 | return net.ParseIP("127.0.0.1"), nil 19 | } 20 | 21 | ifaces, err := net.Interfaces() 22 | if err != nil { 23 | return nil, fmt.Errorf("unable to get local network interfaces: %s", err) 24 | } 25 | 26 | for _, i := range ifaces { 27 | addrs, err := i.Addrs() 28 | if err != nil { 29 | re.RecordMessage("error getting addrs for interface: %s", err) 30 | continue 31 | } 32 | for _, a := range addrs { 33 | switch v := a.(type) { 34 | case *net.IPNet: 35 | ip := v.IP.To4() 36 | if ip == nil { 37 | re.RecordMessage("ignoring non ip4 addr %s", v) 38 | continue 39 | } 40 | if re.TestSubnet.Contains(ip) { 41 | re.RecordMessage("detected data network IP: %s", v) 42 | return v.IP, nil 43 | } else { 44 | re.RecordMessage("%s not in data subnet %s, ignoring", ip, re.TestSubnet.String()) 45 | } 46 | } 47 | } 48 | } 49 | return nil, fmt.Errorf("unable to determine data network IP. no interface found with IP in %s", re.TestSubnet.String()) 50 | } 51 | 52 | // MustGetDataNetworkIP calls GetDataNetworkIP, and panics if it 53 | // errors. It is suitable to use with runner.Invoke/InvokeMap, as long as 54 | // this method is called from the main goroutine of the test plan. 55 | func (c *Client) MustGetDataNetworkIP() net.IP { 56 | ip, err := c.GetDataNetworkIP() 57 | if err != nil { 58 | panic(err) 59 | } 60 | return ip 61 | } 62 | -------------------------------------------------------------------------------- /network/client.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/testground/sdk-go/runtime" 9 | "github.com/testground/sdk-go/sync" 10 | ) 11 | 12 | const ( 13 | // magic values that we monitor on the Testground runner side to detect when Testground 14 | // testplan instances are initialised and at the stage of actually running a test 15 | // check cluster_k8s.go for more information 16 | InitialisationSuccessful = "network initialisation successful" 17 | InitialisationFailed = "network initialisation failed" 18 | ) 19 | 20 | type Client struct { 21 | runenv *runtime.RunEnv 22 | syncClient sync.Client 23 | } 24 | 25 | // NewClient returns a new network client. Use this client to request network 26 | // changes, such as setting latencies, jitter, packet loss, connectedness, etc. 27 | func NewClient(syncClient sync.Client, runenv *runtime.RunEnv) *Client { 28 | return &Client{ 29 | runenv: runenv, 30 | syncClient: syncClient, 31 | } 32 | } 33 | 34 | // WaitNetworkInitialized waits for the sidecar to initialize the network, if 35 | // the sidecar is enabled. If not, it returns immediately. 36 | func (c *Client) WaitNetworkInitialized(ctx context.Context) error { 37 | se := &runtime.Event{StageStartEvent: &runtime.StageStartEvent{ 38 | Name: "network-initialized", 39 | TestGroupID: c.runenv.TestGroupID, 40 | }} 41 | if err := c.syncClient.SignalEvent(ctx, se); err != nil { 42 | return err 43 | } 44 | 45 | if c.runenv.TestSidecar { 46 | err := <-c.syncClient.MustBarrier(ctx, "network-initialized", c.runenv.TestInstanceCount).C 47 | if err != nil { 48 | c.runenv.RecordMessage(InitialisationFailed) 49 | return fmt.Errorf("failed to initialize network: %w", err) 50 | } 51 | } 52 | c.runenv.RecordMessage(InitialisationSuccessful) 53 | 54 | ee := &runtime.Event{StageEndEvent: &runtime.StageEndEvent{ 55 | Name: "network-initialized", 56 | TestGroupID: c.runenv.TestGroupID, 57 | }} 58 | if err := c.syncClient.SignalEvent(ctx, ee); err != nil { 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // MustWaitNetworkInitialized calls WaitNetworkInitialized, and panics if it 66 | // errors. It is suitable to use with runner.Invoke/InvokeMap, as long as 67 | // this method is called from the main goroutine of the test plan. 68 | func (c *Client) MustWaitNetworkInitialized(ctx context.Context) { 69 | err := c.WaitNetworkInitialized(ctx) 70 | if err != nil { 71 | panic(err) 72 | } 73 | } 74 | 75 | // ConfigureNetwork asks the sidecar to configure the network, and returns 76 | // either when the sidecar signals back to us, or when the context expires. 77 | func (c *Client) ConfigureNetwork(ctx context.Context, config *Config) (err error) { 78 | if !c.runenv.TestSidecar { 79 | msg := "ignoring network change request; running in a sidecar-less environment" 80 | c.runenv.SLogger().Named("netclient").Warn(msg) 81 | return nil 82 | } 83 | 84 | hostname, err := os.Hostname() 85 | if err != nil { 86 | return fmt.Errorf("failed to configure network; could not obtain hostname: %w", err) 87 | } 88 | 89 | if config.CallbackState == "" { 90 | return fmt.Errorf("failed to configure network; no callback state provided") 91 | } 92 | 93 | topic := sync.NewTopic("network:"+hostname, &Config{}) 94 | 95 | target := config.CallbackTarget 96 | if target == 0 { 97 | // Fall back to instance count on zero value. 98 | target = c.runenv.TestInstanceCount 99 | } 100 | 101 | _, err = c.syncClient.PublishAndWait(ctx, topic, config, config.CallbackState, target) 102 | if err != nil { 103 | err = fmt.Errorf("failed to configure network: %w", err) 104 | } 105 | return err 106 | } 107 | 108 | // MustConfigureNetwork calls ConfigureNetwork, and panics if it 109 | // errors. It is suitable to use with runner.Invoke/InvokeMap, as long as 110 | // this method is called from the main goroutine of the test plan. 111 | func (c *Client) MustConfigureNetwork(ctx context.Context, config *Config) { 112 | err := c.ConfigureNetwork(ctx, config) 113 | if err != nil { 114 | panic(err) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /network/types.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/testground/sdk-go/ptypes" 8 | "github.com/testground/sdk-go/sync" 9 | ) 10 | 11 | // ErrNoTrafficShaping is returned from functions in this package when traffic 12 | // shaping is not available, such as when using the local:exec runner. 13 | var ErrNoTrafficShaping = fmt.Errorf("no traffic shaping available with this runner") 14 | 15 | type FilterAction int 16 | 17 | const ( 18 | Accept FilterAction = iota 19 | Reject 20 | Drop 21 | ) 22 | 23 | const ( 24 | // The `data` network that Testground currently configures. In the future we might want to have 25 | // multiple `data` networks, and shape the network traffic on them differently. 26 | DefaultDataNetwork = "default" 27 | ) 28 | 29 | // LinkShape defines how traffic should be shaped. 30 | type LinkShape struct { 31 | // Latency is the egress latency 32 | Latency time.Duration `json:"latency"` 33 | 34 | // Jitter is the egress jitter 35 | Jitter time.Duration `json:"jitter"` 36 | 37 | // Bandwidth is egress bytes per second 38 | Bandwidth uint64 `json:"bandwidth"` 39 | 40 | // Drop all inbound traffic. 41 | // TODO: Not implemented 42 | Filter FilterAction `json:"filter"` 43 | 44 | // Loss is the egress packet loss (%) 45 | Loss float32 `json:"loss"` 46 | 47 | // Corrupt is the egress packet corruption probability (%) 48 | Corrupt float32 `json:"corrupt"` 49 | 50 | // Corrupt is the egress packet corruption correlation (%) 51 | CorruptCorr float32 `json:"corrupt_corr"` 52 | 53 | // Reorder is the probability that an egress packet will be reordered (%) 54 | // 55 | // Reordered packets will skip the latency delay and be sent 56 | // immediately. You must specify a non-zero Latency for this option to 57 | // make sense. 58 | Reorder float32 `json:"reorder"` 59 | 60 | // ReorderCorr is the egress packet reordering correlation (%) 61 | ReorderCorr float32 `json:"reorder_corr"` 62 | 63 | // Duplicate is the percentage of packets that are duplicated (%) 64 | Duplicate float32 `json:"duplicate"` 65 | 66 | // DuplicateCorr is the correlation between egress packet duplication (%) 67 | DuplicateCorr float32 `json:"duplicate_corr"` 68 | } 69 | 70 | // LinkRule applies a LinkShape to a subnet. 71 | type LinkRule struct { 72 | LinkShape 73 | Subnet ptypes.IPNet `json:"subnet"` 74 | } 75 | 76 | // RoutingPolicyType defines a certain routing policy to a network. 77 | type RoutingPolicyType string 78 | 79 | const ( 80 | AllowAll = RoutingPolicyType("allow_all") 81 | DenyAll = RoutingPolicyType("deny_all") 82 | ) 83 | 84 | // NetworkConfig specifies how a node's network should be configured. 85 | type Config struct { 86 | // Network is the name of the network to configure 87 | Network string `json:"network"` 88 | 89 | // IPv4 and IPv6 set the IP addresses of this network device. If 90 | // unspecified, the sidecar will leave them alone. 91 | // 92 | // Your test-case will be assigned a B block in the range 93 | // 16.0.0.1-32.0.0.0. X.Y.0.1 will always be reserved for the gateway 94 | // and shouldn't be used by the test. 95 | // 96 | // TODO: IPv6 is currently not supported. 97 | IPv4, IPv6 *ptypes.IPNet 98 | 99 | // Enable enables this network device. 100 | Enable bool `json:"enable"` 101 | 102 | // Default is the default link shaping rule. 103 | Default LinkShape `json:"default"` 104 | 105 | // Rules defines how traffic should be shaped to different subnets. 106 | // 107 | // TODO: This is not implemented. 108 | Rules []LinkRule `json:"rules"` 109 | 110 | // CallbackState will be signalled when the link changes are applied. 111 | // 112 | // Nodes can use the same state to wait for _all_ or a subset of nodes to 113 | // enter the desired network state. See CallbackTarget. 114 | CallbackState sync.State `json:"callback_state"` 115 | 116 | // CallbackTarget is the amount of instances that will have needed to signal 117 | // on the Callback state to consider the configuration operation a success. 118 | // 119 | // A zero value falls back to runenv.TestInstanceCount (i.e. all instances 120 | // participating in the test run). 121 | CallbackTarget int `json:"-"` 122 | 123 | // RoutingPolicy defines the data routing policy of a certain node. This affects 124 | // external networks other than the network 'Default', e.g., external Internet 125 | // access. 126 | RoutingPolicy RoutingPolicyType `json:"routing_policy"` 127 | } 128 | -------------------------------------------------------------------------------- /ptypes/doc.go: -------------------------------------------------------------------------------- 1 | // Package ptypes contains types that are commonplace in test plan parameters. 2 | package ptypes 3 | -------------------------------------------------------------------------------- /ptypes/duration.go: -------------------------------------------------------------------------------- 1 | package ptypes 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "time" 7 | ) 8 | 9 | // Duration wraps a time.Duration and provides JSON marshal logic. 10 | type Duration struct { 11 | time.Duration 12 | } 13 | 14 | var ( 15 | _ json.Marshaler = Duration{} 16 | _ json.Unmarshaler = &Duration{} 17 | ) 18 | 19 | func (d Duration) MarshalJSON() ([]byte, error) { 20 | return json.Marshal(d.String()) 21 | } 22 | 23 | func (d *Duration) UnmarshalJSON(b []byte) error { 24 | var v interface{} 25 | if err := json.Unmarshal(b, &v); err != nil { 26 | return err 27 | } 28 | switch value := v.(type) { 29 | case float64: 30 | d.Duration = time.Duration(value) 31 | return nil 32 | case string: 33 | var err error 34 | d.Duration, err = time.ParseDuration(value) 35 | if err != nil { 36 | return err 37 | } 38 | return nil 39 | default: 40 | return errors.New("invalid duration") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ptypes/ipnet.go: -------------------------------------------------------------------------------- 1 | package ptypes 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | ) 7 | 8 | type IPNet struct { 9 | net.IPNet 10 | } 11 | 12 | func (i IPNet) MarshalJSON() ([]byte, error) { 13 | if len(i.IPNet.IP) == 0 { 14 | return json.Marshal("") 15 | } 16 | return json.Marshal(i.String()) 17 | } 18 | 19 | func (i *IPNet) UnmarshalJSON(data []byte) error { 20 | var s string 21 | if err := json.Unmarshal(data, &s); err != nil { 22 | return err 23 | } 24 | 25 | if s == "" { 26 | return nil 27 | } 28 | 29 | ip, ipnet, err := net.ParseCIDR(s) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | ipv4 := ip.To4() 35 | if ip != nil { 36 | ip = ipv4 37 | } 38 | 39 | ipnet.IP = ip 40 | i.IPNet = *ipnet 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /ptypes/ipnet_test.go: -------------------------------------------------------------------------------- 1 | package ptypes 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net" 7 | "testing" 8 | ) 9 | 10 | func TestIPNetMarshalUnmarshal(t *testing.T) { 11 | tests := []struct { 12 | input IPNet 13 | expectedJSON string 14 | }{ 15 | { 16 | input: IPNet{net.IPNet{IP: net.IP{127, 0, 0, 1}, Mask: net.IPMask{255, 255, 255, 0}}}, 17 | expectedJSON: `"127.0.0.1/24"`, 18 | }, 19 | { 20 | input: IPNet{net.IPNet{IP: net.IP{192, 168, 1, 2}, Mask: net.IPMask{255, 255, 255, 255}}}, 21 | expectedJSON: `"192.168.1.2/32"`, 22 | }, 23 | } 24 | 25 | for _, tt := range tests { 26 | output, err := json.Marshal(tt.input) 27 | if err != nil { 28 | t.Fatal("couldnt marshal input") 29 | } 30 | 31 | if string(output) != tt.expectedJSON { 32 | t.Fatalf("expectedJSON mismatch output, expected %s, got %s", tt.expectedJSON, string(output)) 33 | } 34 | 35 | var revert IPNet 36 | 37 | err = json.Unmarshal(output, &revert) 38 | if err != nil { 39 | t.Fatal("couldnt unmarshal input") 40 | } 41 | 42 | if !bytes.Equal(revert.IPNet.IP, tt.input.IPNet.IP) { 43 | t.Fatal("original IPNet IP is different to marshal/unmarshalled input IP") 44 | } 45 | 46 | if !bytes.Equal(revert.IPNet.Mask, tt.input.IPNet.Mask) { 47 | t.Fatal("original IPNet Mask is different to marshal/unmarshalled input Mask") 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ptypes/rate.go: -------------------------------------------------------------------------------- 1 | package ptypes 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "time" 10 | "unicode" 11 | ) 12 | 13 | // Rate is a param that's parsed as "quantity/interval", where `quantity` is a 14 | // float and `interval` is a string parsable by time.ParseDuration, e.g. "1s". 15 | // 16 | // You can omit the numeric component of the interval to default to 1, e.g. 17 | // "100/s" is the same as "100/1s". 18 | // 19 | // Examples of valid Rate strings include: "100/s", "0.5/m", "500/5m". 20 | type Rate struct { 21 | Quantity float64 22 | Interval time.Duration 23 | } 24 | 25 | var ( 26 | _ json.Marshaler = Duration{} 27 | _ json.Unmarshaler = &Duration{} 28 | ) 29 | 30 | func (r Rate) MarshalJSON() ([]byte, error) { 31 | return nil, nil 32 | } 33 | func (r *Rate) UnmarshalJSON(b []byte) error { 34 | var v interface{} 35 | if err := json.Unmarshal(b, &v); err != nil { 36 | return err 37 | } 38 | 39 | str, ok := v.(string) 40 | if !ok { 41 | return errors.New("invalid rate param, must be string") 42 | } 43 | 44 | strs := strings.Split(str, "/") 45 | if len(strs) != 2 { 46 | return errors.New("invalid rate param. Must be in format 'quantity / interval'") 47 | } 48 | 49 | q, err := strconv.ParseFloat(strs[0], 64) 50 | if err != nil { 51 | return fmt.Errorf("error parsing quantity portion of rate: %s", err) 52 | } 53 | intervalStr := strings.TrimSpace(strs[1]) 54 | if !unicode.IsDigit(rune(intervalStr[0])) { 55 | intervalStr = "1" + intervalStr 56 | } 57 | 58 | i, err := time.ParseDuration(intervalStr) 59 | if err != nil { 60 | return fmt.Errorf("error parsing interval portion of rate: %s", err) 61 | } 62 | 63 | r.Quantity = q 64 | r.Interval = i 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /ptypes/size.go: -------------------------------------------------------------------------------- 1 | package ptypes 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "github.com/dustin/go-humanize" 8 | ) 9 | 10 | // Size is a type that unmarshals human-readable binary sizes like "100 KB" 11 | // into an uint64, where the unit is bytes. 12 | type Size uint64 13 | 14 | var ( 15 | _ json.Marshaler = Size(0) 16 | _ json.Unmarshaler = (*Size)(nil) 17 | ) 18 | 19 | func (s Size) MarshalJSON() ([]byte, error) { 20 | return nil, nil 21 | } 22 | 23 | func (s *Size) UnmarshalJSON(b []byte) error { 24 | var v interface{} 25 | if err := json.Unmarshal(b, &v); err != nil { 26 | return err 27 | } 28 | str, ok := v.(string) 29 | if !ok { 30 | return errors.New("invalid size param, must be string") 31 | } 32 | n, err := humanize.ParseBytes(str) 33 | if err != nil { 34 | return err 35 | } 36 | *s = Size(n) 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /run/init_context.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/testground/sdk-go/network" 8 | "github.com/testground/sdk-go/runtime" 9 | "github.com/testground/sdk-go/sync" 10 | ) 11 | 12 | const ( 13 | StateInitializedGlobal = sync.State("initialized_global") 14 | StateInitializedGroupFmt = "initialized_group_%s" 15 | ) 16 | 17 | // InitSyncClientFactory is the function that will be called to initialize a 18 | // sync client as part of an InitContext. 19 | // 20 | // Replaced in testing. 21 | var InitSyncClientFactory = func(ctx context.Context, env *runtime.RunEnv) sync.Client { 22 | // cannot assign sync.MustBoundClient directly because Go can't infer the contravariance 23 | // in the return type (i.e. that *sync.DefaultClient satisfies sync.Client). 24 | return sync.MustBoundClient(ctx, env) 25 | } 26 | 27 | // InitContext encapsulates a sync client, a net client, and global and 28 | // group-scoped seq numbers assigned to this test instance by the sync service. 29 | // 30 | // The states we signal to acquire the global and group-scoped seq numbers are: 31 | // - initialized_global 32 | // - initialized_group_ 33 | type InitContext struct { 34 | SyncClient sync.Client 35 | NetClient *network.Client 36 | GlobalSeq int64 37 | GroupSeq int64 38 | 39 | runenv *runtime.RunEnv 40 | } 41 | 42 | // init can be safely invoked on a nil reference. 43 | func (ic *InitContext) init(runenv *runtime.RunEnv) { 44 | var ( 45 | grpstate = sync.State(fmt.Sprintf(StateInitializedGroupFmt, runenv.TestGroupID)) 46 | client = InitSyncClientFactory(context.Background(), runenv) 47 | netclient = network.NewClient(client, runenv) 48 | ) 49 | 50 | runenv.RecordMessage("waiting for network to initialize") 51 | netclient.MustWaitNetworkInitialized(context.Background()) 52 | runenv.RecordMessage("network initialization complete") 53 | 54 | *ic = InitContext{ 55 | SyncClient: client, 56 | NetClient: netclient, 57 | GlobalSeq: client.MustSignalEntry(context.Background(), StateInitializedGlobal), 58 | GroupSeq: client.MustSignalEntry(context.Background(), grpstate), 59 | runenv: runenv, 60 | } 61 | 62 | runenv.AttachSyncClient(client) 63 | 64 | runenv.RecordMessage("claimed sequence numbers; global=%d, group(%s)=%d", ic.GlobalSeq, runenv.TestGroupID, ic.GroupSeq) 65 | } 66 | 67 | func (ic *InitContext) close() { 68 | if err := ic.SyncClient.Close(); err != nil { 69 | panic(err) 70 | } 71 | } 72 | 73 | // WaitAllInstancesInitialized waits for all instances to initialize. 74 | func (ic *InitContext) WaitAllInstancesInitialized(ctx context.Context) error { 75 | return <-ic.SyncClient.MustBarrier(ctx, StateInitializedGlobal, ic.runenv.TestInstanceCount).C 76 | } 77 | 78 | // MustWaitAllInstancesInitialized calls WaitAllInstancesInitialized, and 79 | // panics if it errors. 80 | func (ic *InitContext) MustWaitAllInstancesInitialized(ctx context.Context) { 81 | if err := ic.WaitAllInstancesInitialized(ctx); err != nil { 82 | panic(err) 83 | } 84 | } 85 | 86 | // WaitGroupInstancesInitialized waits for all group instances to initialize. 87 | func (ic *InitContext) WaitGroupInstancesInitialized(ctx context.Context) error { 88 | grpstate := sync.State(fmt.Sprintf(StateInitializedGroupFmt, ic.runenv.TestGroupID)) 89 | return <-ic.SyncClient.MustBarrier(ctx, grpstate, ic.runenv.TestGroupInstanceCount).C 90 | } 91 | 92 | // MustWaitGroupInstancesInitialized calls WaitGroupInstancesInitialized, and 93 | // panics if it errors. 94 | func (ic *InitContext) MustWaitGroupInstancesInitialized(ctx context.Context) { 95 | if err := ic.WaitGroupInstancesInitialized(ctx); err != nil { 96 | panic(err) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /run/invoker.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net" 8 | "net/http" 9 | _ "net/http/pprof" 10 | "os" 11 | "path/filepath" 12 | "runtime/debug" 13 | "runtime/pprof" 14 | "strings" 15 | "sync" 16 | "time" 17 | 18 | "github.com/hashicorp/go-multierror" 19 | "github.com/prometheus/client_golang/prometheus/promhttp" 20 | "github.com/raulk/clock" 21 | 22 | "github.com/testground/sdk-go" 23 | "github.com/testground/sdk-go/runtime" 24 | ) 25 | 26 | var ( 27 | // _clk can be overridden with a mock clock for test purposes. 28 | _clk = clock.New() 29 | ) 30 | 31 | const ( 32 | // These ports are the HTTP ports we'll attempt to bind to. If this instance 33 | // is running in a Docker container, binding to 6060 is safe. If it's a 34 | // local:exec run, these ports belong to the host, so starting more than one 35 | // instance will lead to a collision. Therefore we fallback to 0. 36 | HTTPPort = 6060 37 | HTTPPortFallback = 0 38 | ) 39 | 40 | // HTTPListenAddr will be set to the listener address _before_ the test case is 41 | // invoked. If we were unable to start the listener, this value will be "". 42 | var HTTPListenAddr string 43 | 44 | type TestCaseFn = func(env *runtime.RunEnv) error 45 | 46 | // InitializedTestCaseFn allows users to indicate they want a basic 47 | // initialization routine to be run before yielding control to the test case 48 | // function itself. 49 | // 50 | // The initialization routine is common scaffolding that gets repeated across 51 | // the test plans we've seen. We package it here in an attempt to keep your 52 | // code DRY. 53 | // 54 | // It consists of: 55 | // 56 | // 1. Initializing a sync client, bound to the runenv. 57 | // 2. Initializing a net client. 58 | // 3. Waiting for the network to initialize. 59 | // 4. Claiming a global sequence number. 60 | // 5. Claiming a group-scoped sequence number. 61 | // 62 | // The injected InitContext is a bundle containing the result, and you can use 63 | // its objects in your test logic. In fact, you don't need to close them 64 | // (sync client, net client), as the SDK manages that for you. 65 | type InitializedTestCaseFn = func(env *runtime.RunEnv, initCtx *InitContext) error 66 | 67 | // InvokeMap takes a map of test case names and their functions, and calls the 68 | // matched test case, or panics if the name is unrecognised. 69 | // 70 | // Supported function signatures are TestCaseFn and InitializedTestCaseFn. 71 | // Refer to their respective godocs for more info. 72 | func InvokeMap(cases map[string]interface{}) { 73 | runenv := runtime.CurrentRunEnv() 74 | defer runenv.Close() 75 | 76 | if fn, ok := cases[runenv.TestCase]; ok { 77 | invoke(runenv, fn) 78 | } else { 79 | msg := fmt.Sprintf("unrecognized test case: %s", runenv.TestCase) 80 | panic(msg) 81 | } 82 | } 83 | 84 | // Invoke runs the passed test-case and reports the result. 85 | // 86 | // Supported function signatures are TestCaseFn and InitializedTestCaseFn. 87 | // Refer to their respective godocs for more info. 88 | func Invoke(fn interface{}) { 89 | runenv := runtime.CurrentRunEnv() 90 | defer runenv.Close() 91 | 92 | invoke(runenv, fn) 93 | } 94 | 95 | func invoke(runenv *runtime.RunEnv, fn interface{}) { 96 | maybeSetupHTTPListener(runenv) 97 | 98 | runenv.RecordStart() 99 | 100 | var closer func() 101 | defer func() { 102 | if closer != nil { 103 | closer() 104 | } 105 | }() 106 | 107 | var err error 108 | errfile, err := runenv.CreateRawAsset("run.err") 109 | if err != nil { 110 | runenv.RecordCrash(err) 111 | return 112 | } 113 | 114 | rd, wr, err := os.Pipe() 115 | if err != nil { 116 | runenv.RecordCrash(err) 117 | return 118 | } 119 | 120 | w := io.MultiWriter(errfile, os.Stderr) 121 | os.Stderr = wr 122 | 123 | // handle the copying of stderr into run.err. 124 | go func() { 125 | defer func() { 126 | _ = rd.Close() 127 | if sdk.Verbose { 128 | runenv.RecordMessage("io closed") 129 | } 130 | }() 131 | 132 | _, err := io.Copy(w, rd) 133 | if err != nil && !strings.Contains(err.Error(), "file already closed") { 134 | runenv.RecordCrash(fmt.Errorf("stderr copy failed: %w", err)) 135 | return 136 | } 137 | 138 | if err = errfile.Sync(); err != nil { 139 | runenv.RecordCrash(fmt.Errorf("stderr file tee sync failed failed: %w", err)) 140 | } 141 | }() 142 | 143 | // Prepare the event. 144 | defer func() { 145 | if err := recover(); err != nil { 146 | // Handle panics by recording them in the runenv output. 147 | runenv.RecordCrash(err) 148 | 149 | // Developers expect panics to be recorded in run.err too. 150 | _, _ = fmt.Fprintln(os.Stderr, err) 151 | debug.PrintStack() 152 | } 153 | }() 154 | 155 | closeProfiles, err := captureProfiles(runenv) 156 | if err != nil { 157 | runenv.SLogger().Warnw("some or all profile captures failed to initialize", "error", err) 158 | } 159 | defer closeProfiles() 160 | 161 | errCh := make(chan error) 162 | go func() { 163 | defer close(errCh) 164 | defer HandlePanics() 165 | 166 | switch f := fn.(type) { 167 | case TestCaseFn: 168 | errCh <- f(runenv) 169 | case InitializedTestCaseFn: 170 | ic := new(InitContext) 171 | ic.init(runenv) 172 | closer = ic.close // we want to close the InitContext after having calld RecordSuccess or RecordFailure 173 | errCh <- f(runenv, ic) 174 | default: 175 | msg := fmt.Sprintf("unexpected function passed to Invoke*; expected types: TestCaseFn, InitializedTestCaseFn; was: %T", f) 176 | panic(msg) 177 | } 178 | }() 179 | 180 | select { 181 | case err := <-errCh: 182 | switch err { 183 | case nil: 184 | runenv.RecordSuccess() 185 | default: 186 | runenv.RecordFailure(err) 187 | } 188 | case p := <-panicHandler: 189 | // propagate the panic. 190 | runenv.RecordCrash(p.DebugStacktrace) 191 | panic(p.RecoverObj) 192 | } 193 | } 194 | 195 | type ProfilesCloseFn = func() error 196 | 197 | func captureProfiles(runenv *runtime.RunEnv) (ProfilesCloseFn, error) { 198 | outDir := runenv.TestOutputsPath 199 | 200 | var ( 201 | merr *multierror.Error 202 | wg sync.WaitGroup 203 | ctx, cancel = context.WithCancel(context.Background()) 204 | ) 205 | 206 | ret := func() error { 207 | // cancel all other profiles, and wait until they have yielded. 208 | cancel() 209 | wg.Wait() 210 | return nil 211 | } 212 | 213 | for kind, value := range runenv.TestCaptureProfiles { 214 | switch kind { 215 | case "cpu": 216 | runenv.SLogger().Infof("writing cpu profile") 217 | 218 | path := filepath.Join(outDir, "cpu.prof") 219 | f, err := os.Create(path) 220 | if err != nil { 221 | err = fmt.Errorf("failed to create CPU profile output file: %w", err) 222 | merr = multierror.Append(merr, err) 223 | continue 224 | } 225 | if err = pprof.StartCPUProfile(f); err != nil { 226 | err = fmt.Errorf("failed to start capturing CPU profile: %w", err) 227 | merr = multierror.Append(merr, err) 228 | continue 229 | } 230 | 231 | wg.Add(1) 232 | go func() { 233 | defer wg.Done() 234 | 235 | <-ctx.Done() 236 | // stop the CPU profile. 237 | pprof.StopCPUProfile() 238 | _ = f.Close() 239 | }() 240 | 241 | default: 242 | prof := pprof.Lookup(kind) 243 | if prof == nil { 244 | merr = multierror.Append(merr, fmt.Errorf("profile of kind %s not recognized; skipped", kind)) 245 | continue 246 | } 247 | freq, err := time.ParseDuration(value) 248 | if err != nil { 249 | merr = multierror.Append(merr, fmt.Errorf("unparseable duration for profile of kind %s: %s", kind, value)) 250 | continue 251 | } 252 | 253 | runenv.SLogger().Infof("writing %s profile every %s", kind, freq) 254 | 255 | kind := kind 256 | wg.Add(1) 257 | go func() { 258 | defer wg.Done() 259 | 260 | ticker := _clk.Ticker(freq) 261 | for { 262 | select { 263 | case <-ticker.C: 264 | path := filepath.Join(outDir, fmt.Sprintf("%s.%s.prof", kind, _clk.Now().Format(time.RFC3339))) 265 | f, err := os.Create(path) 266 | if err != nil { 267 | runenv.SLogger().Warnw("failed to create output file for profile", "kind", kind, "path", path, "error", err) 268 | continue 269 | } 270 | runenv.SLogger().Debugf("writing profile: %s", path) 271 | if err = prof.WriteTo(f, 0); err != nil { 272 | runenv.SLogger().Warnw("failed to write profile", "kind", kind, "path", path, "error", err) 273 | continue 274 | } 275 | _ = f.Close() 276 | case <-ctx.Done(): 277 | return // exiting 278 | } 279 | } 280 | }() 281 | 282 | } 283 | } 284 | 285 | return ret, merr.ErrorOrNil() 286 | } 287 | 288 | func maybeSetupHTTPListener(runenv *runtime.RunEnv) { 289 | if HTTPListenAddr != "" { 290 | // already set up. 291 | return 292 | } 293 | 294 | addr := fmt.Sprintf("0.0.0.0:%d", HTTPPort) 295 | l, err := net.Listen("tcp", addr) 296 | if err != nil { 297 | addr = fmt.Sprintf("0.0.0.0:%d", HTTPPortFallback) 298 | if l, err = net.Listen("tcp", addr); err != nil { 299 | runenv.RecordMessage("error registering default http handler at: %s: %s", addr, err) 300 | return 301 | } 302 | } 303 | 304 | // DefaultServeMux already includes the pprof handler, add the 305 | // Prometheus handler. 306 | http.DefaultServeMux.Handle("/metrics", promhttp.Handler()) 307 | 308 | HTTPListenAddr = l.Addr().String() 309 | 310 | runenv.RecordMessage("registering default http handler at: http://%s/ (pprof: http://%s/debug/pprof/)", HTTPListenAddr, HTTPListenAddr) 311 | 312 | go func() { 313 | _ = http.Serve(l, nil) 314 | }() 315 | } 316 | -------------------------------------------------------------------------------- /run/invoker_test.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | 10 | "github.com/hashicorp/go-multierror" 11 | "github.com/raulk/clock" 12 | 13 | "github.com/testground/sdk-go/runtime" 14 | "github.com/testground/sdk-go/sync" 15 | 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func init() { 20 | syncClient := sync.NewInmemClient() 21 | InitSyncClientFactory = func(_ context.Context, _ *runtime.RunEnv) sync.Client { 22 | return syncClient 23 | } 24 | } 25 | 26 | func TestInitializedInvoke(t *testing.T) { 27 | nextGlobalSeq := 0 28 | nextGroupSeq := make(map[string]int, 2) 29 | var test InitializedTestCaseFn = func(env *runtime.RunEnv, initCtx *InitContext) error { 30 | // the test case performs asserts on the RunEnv and InitContext. 31 | require.NotNil(t, env) 32 | require.NotNil(t, initCtx) 33 | require.NotNil(t, initCtx.SyncClient) 34 | require.NotNil(t, initCtx.NetClient) 35 | 36 | // keep track of the expected seq numbers. 37 | nextGlobalSeq++ 38 | nextGroupSeq[env.TestGroupID] = nextGroupSeq[env.TestGroupID] + 1 39 | 40 | require.EqualValues(t, int64(nextGlobalSeq), initCtx.GlobalSeq) 41 | require.EqualValues(t, int64(nextGroupSeq[env.TestGroupID]), initCtx.GroupSeq) 42 | return nil 43 | } 44 | 45 | env, cleanup := runtime.RandomTestRunEnv(t) 46 | t.Cleanup(cleanup) 47 | 48 | for k, v := range env.ToEnvVars() { 49 | _ = os.Setenv(k, v) 50 | } 51 | 52 | // we simulate starting many instances by calling invoke multiple times. 53 | // all invocations are backed by the same inmem sync service instance. 54 | Invoke(test) 55 | Invoke(test) 56 | Invoke(test) 57 | } 58 | 59 | func TestUninitializedInvoke(t *testing.T) { 60 | env, cleanup := runtime.RandomTestRunEnv(t) 61 | t.Cleanup(cleanup) 62 | 63 | for k, v := range env.ToEnvVars() { 64 | _ = os.Setenv(k, v) 65 | } 66 | 67 | await := func(ch chan struct{}) { 68 | select { 69 | case <-ch: 70 | case <-time.After(1 * time.Second): 71 | t.Fatal("test function not invoked") 72 | } 73 | } 74 | 75 | // not using type alias. 76 | ch := make(chan struct{}) 77 | Invoke(func(runenv *runtime.RunEnv) error { 78 | close(ch) 79 | return nil 80 | }) 81 | await(ch) 82 | 83 | // using type alias. 84 | ch = make(chan struct{}) 85 | Invoke(TestCaseFn(func(runenv *runtime.RunEnv) error { 86 | close(ch) 87 | return nil 88 | })) 89 | await(ch) 90 | 91 | } 92 | 93 | func TestProfiles(t *testing.T) { 94 | clk := clock.NewMock() 95 | _clk = clk 96 | 97 | env, cleanup := runtime.RandomTestRunEnv(t) 98 | t.Cleanup(cleanup) 99 | 100 | env.TestCaptureProfiles = map[string]string{ 101 | "cpu": "foo", // disregarded 102 | "heap": "5s", 103 | "allocs": "10s", 104 | "goroutine": "30s", 105 | "block": "1h", // will never be written 106 | "unsupported": "foo", 107 | } 108 | 109 | stop, err := captureProfiles(env) 110 | require.Error(t, err) // error due to "unsupported" profile. 111 | require.Equal(t, 1, err.(*multierror.Error).Len()) 112 | defer stop() 113 | 114 | time.Sleep(500 * time.Millisecond) // allow goroutines to start. 115 | 116 | for i := 1; i <= 6; i++ { 117 | // advance the clock 5 seconds; sleep 500ms for goroutines to execute. 118 | clk.Add(5 * time.Second) 119 | time.Sleep(500 * time.Millisecond) 120 | } 121 | 122 | // stop. 123 | require.NoError(t, stop()) 124 | 125 | // 1 cpu profile. 126 | matches, _ := filepath.Glob(filepath.Join(env.TestOutputsPath, "cpu.prof")) 127 | require.Len(t, matches, 1) 128 | 129 | // 6 heap profiles. 130 | matches, _ = filepath.Glob(filepath.Join(env.TestOutputsPath, "heap.*.prof")) 131 | require.Len(t, matches, 6) 132 | 133 | // 3 allocs profiles. 134 | matches, _ = filepath.Glob(filepath.Join(env.TestOutputsPath, "allocs.*.prof")) 135 | require.Len(t, matches, 3) 136 | 137 | // 1 goroutine profiles. 138 | matches, _ = filepath.Glob(filepath.Join(env.TestOutputsPath, "goroutine.*.prof")) 139 | require.Len(t, matches, 1) 140 | 141 | // no more profiles. 142 | matches, _ = filepath.Glob(filepath.Join(env.TestOutputsPath, "*.prof")) 143 | require.Len(t, matches, 11) 144 | } 145 | -------------------------------------------------------------------------------- /run/panic.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import "runtime/debug" 4 | 5 | type PanicPayload struct { 6 | RecoverObj interface{} 7 | DebugStacktrace string 8 | } 9 | 10 | // panicHandler is where the top-level main goroutine panic handler is 11 | // listening for panics. 12 | var panicHandler = make(chan PanicPayload) 13 | 14 | // HandlePanics should be called in a defer at the top of any goroutine that 15 | // the test plan spawns, so that panics from children goroutine are propagated 16 | // to the main goroutine, where they will be handled by run.Invoke and recorded 17 | // as a CRASH event. The test will end immediately. 18 | func HandlePanics() { 19 | obj := recover() 20 | if obj == nil { 21 | return 22 | } 23 | panicHandler <- PanicPayload{obj, string(debug.Stack())} 24 | } 25 | -------------------------------------------------------------------------------- /run/panic_test.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "bufio" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/testground/sdk-go/runtime" 13 | ) 14 | 15 | func runWithTempDir(t *testing.T, f func()) *os.File { 16 | t.Helper() 17 | 18 | tmpdir, err := ioutil.TempDir("", "") 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | t.Cleanup(func() { _ = os.RemoveAll(tmpdir) }) 23 | 24 | _ = os.Setenv(runtime.EnvTestOutputsPath, tmpdir) 25 | f() 26 | 27 | runout, err := os.Open(filepath.Join(tmpdir, "run.out")) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | return runout 33 | } 34 | 35 | func TestPanicFromMain(t *testing.T) { 36 | var tc TestCaseFn = func(runenv *runtime.RunEnv) error { 37 | panic("bang!") 38 | } 39 | 40 | runout := runWithTempDir(t, func() { Invoke(tc) }) 41 | 42 | var last string 43 | for scanner := bufio.NewScanner(runout); scanner.Scan(); { 44 | last = scanner.Text() 45 | } 46 | 47 | if !strings.Contains(last, "\"crash_event\"") { 48 | t.Fatalf("expected crashed event; got: %s", last) 49 | } 50 | } 51 | 52 | func TestPanicFromChildGoroutine(t *testing.T) { 53 | var tc TestCaseFn = func(runenv *runtime.RunEnv) error { 54 | go func() { 55 | defer HandlePanics() 56 | panic("bang!") 57 | }() 58 | 59 | // test case hangs. 60 | time.Sleep(1 * time.Hour) 61 | return nil 62 | } 63 | 64 | runout := runWithTempDir(t, func() { Invoke(tc) }) 65 | 66 | var last string 67 | for scanner := bufio.NewScanner(runout); scanner.Scan(); { 68 | last = scanner.Text() 69 | } 70 | 71 | if !strings.Contains(last, "\"crash_event\"") { 72 | t.Fatalf("expected crashed event; got: %s", last) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /runtime/doc.go: -------------------------------------------------------------------------------- 1 | // Package runtime contains functions and types to interact with the test 2 | // runtime environment, as formally defined in the system specification. 3 | package runtime 4 | -------------------------------------------------------------------------------- /runtime/env.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | const ( 4 | EnvTestBranch = "TEST_BRANCH" 5 | EnvTestCase = "TEST_CASE" 6 | EnvTestGroupID = "TEST_GROUP_ID" 7 | EnvTestGroupInstanceCount = "TEST_GROUP_INSTANCE_COUNT" 8 | EnvTestInstanceCount = "TEST_INSTANCE_COUNT" 9 | EnvTestInstanceParams = "TEST_INSTANCE_PARAMS" 10 | EnvTestInstanceRole = "TEST_INSTANCE_ROLE" 11 | EnvTestOutputsPath = "TEST_OUTPUTS_PATH" 12 | EnvTestPlan = "TEST_PLAN" 13 | EnvTestRepo = "TEST_REPO" 14 | EnvTestRun = "TEST_RUN" 15 | EnvTestSidecar = "TEST_SIDECAR" 16 | EnvTestStartTime = "TEST_START_TIME" 17 | EnvTestSubnet = "TEST_SUBNET" 18 | EnvTestTag = "TEST_TAG" 19 | EnvTestCaptureProfiles = "TEST_CAPTURE_PROFILES" 20 | EnvTestTempPath = "TEST_TEMP_PATH" 21 | EnvTestDisableMetrics = "TEST_DISABLE_METRICS" 22 | ) 23 | -------------------------------------------------------------------------------- /runtime/influxdb_batch.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "io" 5 | "time" 6 | 7 | "github.com/avast/retry-go" 8 | _ "github.com/influxdata/influxdb1-client" 9 | client "github.com/influxdata/influxdb1-client/v2" 10 | 11 | "github.com/testground/sdk-go" 12 | ) 13 | 14 | type Batcher interface { 15 | io.Closer 16 | 17 | WritePoint(p *client.Point) 18 | } 19 | 20 | type batcher struct { 21 | re *RunEnv 22 | client client.Client 23 | length int 24 | interval time.Duration 25 | retryOpts []retry.Option 26 | 27 | writeCh chan *client.Point 28 | flushCh chan struct{} 29 | doneCh chan struct{} 30 | 31 | pending []*client.Point 32 | sending []*client.Point 33 | sendRes chan error 34 | doneErr chan error 35 | } 36 | 37 | func newBatcher(re *RunEnv, cli client.Client, length int, interval time.Duration, retry ...retry.Option) *batcher { 38 | b := &batcher{ 39 | re: re, 40 | client: cli, 41 | length: length, 42 | interval: interval, 43 | retryOpts: retry, 44 | 45 | writeCh: make(chan *client.Point), 46 | flushCh: make(chan struct{}, 1), 47 | sendRes: make(chan error, 1), 48 | doneCh: make(chan struct{}), 49 | doneErr: make(chan error), 50 | 51 | pending: nil, 52 | sending: nil, 53 | } 54 | 55 | go b.background() 56 | 57 | return b 58 | } 59 | 60 | func (b *batcher) background() { 61 | tick := time.NewTicker(b.interval) 62 | defer tick.Stop() 63 | 64 | attemptFlush := func() { 65 | if b.sending != nil { 66 | // there's already a flush taking place. 67 | return 68 | } 69 | select { 70 | case b.flushCh <- struct{}{}: 71 | default: 72 | // there's a flush queued to be accepted. 73 | } 74 | } 75 | 76 | for { 77 | select { 78 | case p := <-b.writeCh: 79 | b.pending = append(b.pending, p) 80 | if len(b.pending) >= b.length { 81 | attemptFlush() 82 | } 83 | 84 | case err := <-b.sendRes: 85 | if err == nil { 86 | b.pending = b.pending[len(b.sending):] 87 | if sdk.Verbose { 88 | b.re.RecordMessage("influxdb: uploaded %d points", len(b.sending)) 89 | } 90 | } else { 91 | b.re.RecordMessage("influxdb: failed to upload %d points; err: %s", len(b.sending), err) 92 | } 93 | b.sending = nil 94 | if len(b.pending) >= b.length { 95 | attemptFlush() 96 | } 97 | 98 | case <-tick.C: 99 | attemptFlush() 100 | 101 | case <-b.flushCh: 102 | if b.sending != nil { 103 | continue 104 | } 105 | l := len(b.pending) 106 | if l == 0 { 107 | continue 108 | } 109 | if l > b.length { 110 | l = b.length 111 | } 112 | b.sending = b.pending[:l] 113 | go b.send() 114 | 115 | case <-b.doneCh: 116 | if b.sending != nil { 117 | // we are currently sending, wait for the send to finish first. 118 | if err := <-b.sendRes; err == nil { 119 | b.pending = b.pending[len(b.sending):] 120 | if sdk.Verbose { 121 | b.re.RecordMessage("influxdb: uploaded %d points", len(b.sending)) 122 | } 123 | } else { 124 | b.re.RecordMessage("influxdb: failed to upload %d points; err: %s", len(b.sending), err) 125 | } 126 | } 127 | 128 | var err error 129 | if len(b.pending) > 0 { 130 | // send all remaining data at once. 131 | b.sending = b.pending 132 | go b.send() 133 | err = <-b.sendRes 134 | if err == nil { 135 | if sdk.Verbose { 136 | b.re.RecordMessage("influxdb: uploaded %d points", len(b.sending)) 137 | } 138 | } else { 139 | b.re.RecordMessage("influxdb: failed to upload %d points; err: %s", len(b.sending), err) 140 | } 141 | b.sending = nil 142 | } 143 | b.doneErr <- err 144 | return 145 | } 146 | } 147 | } 148 | 149 | func (b *batcher) WritePoint(p *client.Point) { 150 | b.writeCh <- p 151 | } 152 | 153 | // Close flushes any remaining points and returns any errors from the final flush. 154 | func (b *batcher) Close() error { 155 | select { 156 | case _, ok := <-b.doneCh: 157 | if !ok { 158 | return nil 159 | } 160 | default: 161 | } 162 | close(b.doneCh) 163 | return <-b.doneErr 164 | } 165 | 166 | func (b *batcher) send() { 167 | points, err := client.NewBatchPoints(client.BatchPointsConfig{Database: "testground"}) 168 | if err != nil { 169 | b.sendRes <- err 170 | return 171 | } 172 | 173 | for _, p := range b.sending { 174 | points.AddPoint(p) 175 | } 176 | 177 | err = retry.Do(func() error { return b.client.Write(points) }, b.retryOpts...) 178 | b.sendRes <- err 179 | } 180 | 181 | type nilBatcher struct { 182 | client.Client 183 | } 184 | 185 | func (n *nilBatcher) WritePoint(p *client.Point) { 186 | bp, _ := client.NewBatchPoints(client.BatchPointsConfig{Database: "testground"}) 187 | bp.AddPoint(p) 188 | _ = n.Write(bp) 189 | } 190 | 191 | func (n *nilBatcher) Close() error { 192 | return nil 193 | } 194 | 195 | var _ Batcher = (*nilBatcher)(nil) 196 | -------------------------------------------------------------------------------- /runtime/influxdb_batch_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/avast/retry-go" 8 | client "github.com/influxdata/influxdb1-client/v2" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestLengthBatching(t *testing.T) { 13 | runenv, cleanup := RandomTestRunEnv(t) 14 | t.Cleanup(cleanup) 15 | defer runenv.Close() 16 | 17 | tc := &testClient{} 18 | b := newBatcher(runenv, tc, 16, 24*time.Hour) 19 | 20 | writePoints(t, b, 0, 36) 21 | 22 | time.Sleep(1 * time.Second) 23 | 24 | require := require.New(t) 25 | 26 | // we should've received two batches. 27 | tc.RLock() 28 | require.Len(tc.batchPoints, 2) 29 | require.Len(tc.batchPoints[0].Points(), 16) 30 | require.Len(tc.batchPoints[1].Points(), 16) 31 | tc.RUnlock() 32 | 33 | require.NoError(b.Close()) 34 | tc.RLock() 35 | require.Len(tc.batchPoints, 3) 36 | require.Len(tc.batchPoints[2].Points(), 4) 37 | tc.RUnlock() 38 | } 39 | 40 | func TestIntervalBatching(t *testing.T) { 41 | runenv, cleanup := RandomTestRunEnv(t) 42 | t.Cleanup(cleanup) 43 | defer runenv.Close() 44 | 45 | tc := &testClient{} 46 | b := newBatcher(runenv, tc, 1000, 500*time.Millisecond) 47 | 48 | writePoints(t, b, 0, 10) 49 | 50 | time.Sleep(2 * time.Second) 51 | 52 | require := require.New(t) 53 | 54 | // we should've received two batches. 55 | tc.RLock() 56 | require.Len(tc.batchPoints, 1) 57 | require.Len(tc.batchPoints[0].Points(), 10) 58 | tc.RUnlock() 59 | 60 | require.NoError(b.Close()) 61 | tc.RLock() 62 | require.Len(tc.batchPoints, 1) 63 | tc.RUnlock() 64 | } 65 | 66 | func TestBatchFailure(t *testing.T) { 67 | runenv, cleanup := RandomTestRunEnv(t) 68 | t.Cleanup(cleanup) 69 | defer runenv.Close() 70 | 71 | test := func(b *batcher) func(t *testing.T) { 72 | tc := &testClient{} 73 | b.client = tc 74 | 75 | return func(t *testing.T) { 76 | 77 | // Enable failures. 78 | tc.EnableFail(true) 79 | 80 | // Write three batches of 10 points each. 81 | writePoints(t, b, 0, 10) 82 | writePoints(t, b, 10, 10) 83 | writePoints(t, b, 20, 10) 84 | 85 | time.Sleep(2 * time.Second) 86 | 87 | require := require.New(t) 88 | 89 | // we should've received the same batch many times. 90 | tc.RLock() 91 | require.Greater(len(tc.batchPoints), 1) 92 | assertPointsExactly(t, tc.batchPoints[0], 0, 10) 93 | tc.RUnlock() 94 | 95 | // get out of failure mode. 96 | tc.EnableFail(false) 97 | 98 | // wait for the retries to be done. 99 | time.Sleep(2 * time.Second) 100 | 101 | // now the last four elements should be: 102 | // batch(0-9) (failed), batch(0-9) (ok), batch(10-19) (ok), batch(20-29) (ok) 103 | tc.RLock() 104 | require.Greater(len(tc.batchPoints), 1) 105 | assertPointsExactly(t, tc.batchPoints[len(tc.batchPoints)-4], 0, 10) 106 | assertPointsExactly(t, tc.batchPoints[len(tc.batchPoints)-3], 0, 10) 107 | assertPointsExactly(t, tc.batchPoints[len(tc.batchPoints)-2], 10, 10) 108 | assertPointsExactly(t, tc.batchPoints[len(tc.batchPoints)-1], 20, 10) 109 | tc.RUnlock() 110 | } 111 | } 112 | 113 | t.Run("batches_by_length", test(newBatcher(runenv, nil, 10, 24*time.Hour, 114 | retry.Attempts(3), 115 | retry.Delay(100*time.Millisecond), 116 | ))) 117 | 118 | t.Run("batches_by_time", test(newBatcher(runenv, nil, 10, 100*time.Millisecond, 119 | retry.Attempts(3), 120 | retry.Delay(100*time.Millisecond), 121 | ))) 122 | 123 | } 124 | 125 | func writePoints(t *testing.T, b *batcher, offset, count int) { 126 | t.Helper() 127 | 128 | for i := offset; i < offset+count; i++ { 129 | tags := map[string]string{} 130 | fields := map[string]interface{}{ 131 | "i": i, 132 | } 133 | p, err := client.NewPoint("point", tags, fields) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | b.WritePoint(p) 138 | } 139 | } 140 | 141 | func assertPointsExactly(t *testing.T, bp client.BatchPoints, offset, length int) { 142 | t.Helper() 143 | 144 | if l := len(bp.Points()); l != length { 145 | t.Fatalf("length did not match; expected: %d, got %d", length, l) 146 | } 147 | 148 | for i, p := range bp.Points() { 149 | f, _ := p.Fields() 150 | if actual := f["i"].(int64); int64(i+offset) != actual { 151 | t.Fatalf("comparison failed; expected: %d, got %d", i+offset, actual) 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /runtime/influxdb_client.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | _ "github.com/influxdata/influxdb1-client" // this is important because of the bug in go mod 9 | client "github.com/influxdata/influxdb1-client/v2" 10 | ) 11 | 12 | const EnvInfluxDBURL = "INFLUXDB_URL" 13 | 14 | var ( 15 | // TestInfluxDBClient sets a client for testing. If this value is set, 16 | // NewInfluxDBClient will always return it. 17 | TestInfluxDBClient client.Client 18 | ) 19 | 20 | func NewInfluxDBClient(re *RunEnv) (client.Client, error) { 21 | if TestInfluxDBClient != nil { 22 | return TestInfluxDBClient, nil 23 | } 24 | 25 | addr := os.Getenv(EnvInfluxDBURL) 26 | if addr == "" { 27 | return nil, fmt.Errorf("no InfluxDB URL in $%s env var", EnvInfluxDBURL) 28 | } 29 | 30 | cfg := client.HTTPConfig{Addr: addr, Timeout: 5 * time.Second} 31 | return client.NewHTTPClient(cfg) 32 | } 33 | -------------------------------------------------------------------------------- /runtime/influxdb_client_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | _ "github.com/influxdata/influxdb1-client" 9 | client "github.com/influxdata/influxdb1-client/v2" 10 | ) 11 | 12 | type testClient struct { 13 | sync.RWMutex 14 | 15 | fail bool 16 | batchPoints []client.BatchPoints 17 | } 18 | 19 | var _ client.Client = (*testClient)(nil) 20 | 21 | func (t *testClient) EnableFail(fail bool) { 22 | t.Lock() 23 | defer t.Unlock() 24 | 25 | t.fail = fail 26 | } 27 | 28 | func (t *testClient) Ping(_ time.Duration) (time.Duration, string, error) { 29 | return 0, "", nil 30 | } 31 | 32 | func (t *testClient) Write(bp client.BatchPoints) error { 33 | t.Lock() 34 | defer t.Unlock() 35 | 36 | t.batchPoints = append(t.batchPoints, bp) 37 | 38 | var err error 39 | if t.fail { 40 | err = fmt.Errorf("error") 41 | } 42 | return err 43 | } 44 | 45 | func (t *testClient) Query(_ client.Query) (*client.Response, error) { 46 | return nil, nil 47 | } 48 | 49 | func (t *testClient) QueryAsChunk(_ client.Query) (*client.ChunkedResponse, error) { 50 | return nil, nil 51 | } 52 | 53 | func (t *testClient) Close() error { 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /runtime/metrics.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "time" 10 | 11 | "github.com/hashicorp/go-multierror" 12 | _ "github.com/influxdata/influxdb1-client" 13 | client "github.com/influxdata/influxdb1-client/v2" 14 | "github.com/rcrowley/go-metrics" 15 | ) 16 | 17 | type Metrics struct { 18 | re *RunEnv 19 | diagnostics *MetricsApi 20 | results *MetricsApi 21 | influxdb client.Client 22 | batcher Batcher 23 | tags map[string]string 24 | } 25 | 26 | func newMetrics(re *RunEnv) *Metrics { 27 | m := &Metrics{re: re} 28 | 29 | var dsinks = []MetricSinkFn{m.logSinkJSON("diagnostics.out")} 30 | 31 | if re.TestDisableMetrics { 32 | re.RecordMessage("InfluxDB batching disabled by test; no metrics will be dispatched") 33 | } else if client, err := NewInfluxDBClient(re); err == nil { 34 | m.tags = map[string]string{ 35 | "run": re.TestRun, 36 | "group_id": re.TestGroupID, 37 | } 38 | 39 | m.influxdb = client 40 | if InfluxTestBatcher { 41 | m.batcher = &nilBatcher{client} 42 | } else { 43 | m.batcher = newBatcher(re, client, InfluxBatchLength, InfluxBatchInterval, InfluxBatchRetryOpts(re)...) 44 | } 45 | 46 | dsinks = append(dsinks, m.writeToInfluxDBSink("diagnostics")) 47 | } else { 48 | re.RecordMessage("InfluxDB unavailable; no metrics will be dispatched: %s", err) 49 | } 50 | 51 | m.diagnostics = newMetricsApi(re, metricsApiOpts{ 52 | freq: 5 * time.Second, 53 | preregister: metrics.RegisterRuntimeMemStats, 54 | callbacks: []func(metrics.Registry){metrics.CaptureRuntimeMemStatsOnce}, 55 | sinks: dsinks, 56 | }) 57 | 58 | m.results = newMetricsApi(re, metricsApiOpts{ 59 | freq: 1 * time.Second, 60 | sinks: []MetricSinkFn{m.logSinkJSON("results.out")}, 61 | }) 62 | 63 | return m 64 | } 65 | 66 | func (m *Metrics) R() *MetricsApi { 67 | return m.results 68 | } 69 | 70 | func (m *Metrics) D() *MetricsApi { 71 | return m.diagnostics 72 | } 73 | 74 | func (m *Metrics) Close() error { 75 | var err *multierror.Error 76 | 77 | // close diagnostics; this stops the ticker and any further observations on 78 | // runenv.D() will fail/panic. 79 | err = multierror.Append(err, m.diagnostics.Close()) 80 | 81 | // close results; no more results via runenv.R() can be recorded. 82 | err = multierror.Append(err, m.results.Close()) 83 | 84 | if m.influxdb != nil { 85 | // Next, we reopen the results.out file, and write all points to InfluxDB. 86 | results := filepath.Join(m.re.TestOutputsPath, "results.out") 87 | if file, errf := os.OpenFile(results, os.O_RDONLY, 0666); errf == nil { 88 | err = multierror.Append(err, m.batchInsertInfluxDB(file)) 89 | } else { 90 | err = multierror.Append(err, errf) 91 | } 92 | } 93 | 94 | // Flush the immediate InfluxDB writer. 95 | if m.batcher != nil { 96 | err = multierror.Append(err, m.batcher.Close()) 97 | } 98 | 99 | // Now we're ready to close InfluxDB. 100 | if m.influxdb != nil { 101 | err = multierror.Append(err, m.influxdb.Close()) 102 | } 103 | 104 | return err.ErrorOrNil() 105 | } 106 | 107 | func (m *Metrics) batchInsertInfluxDB(results *os.File) error { 108 | sink := m.writeToInfluxDBSink("results") 109 | 110 | for dec := json.NewDecoder(results); dec.More(); { 111 | var me Metric 112 | if err := dec.Decode(&me); err != nil { 113 | m.re.RecordMessage("failed to decode Metric from results.out: %s", err) 114 | continue 115 | } 116 | 117 | if err := sink(&me); err != nil { 118 | m.re.RecordMessage("failed to process Metric from results.out: %s", err) 119 | } 120 | } 121 | return nil 122 | } 123 | 124 | func (m *Metrics) logSinkJSON(filename string) MetricSinkFn { 125 | f, err := m.re.CreateRawAsset(filename) 126 | if err != nil { 127 | panic(err) 128 | } 129 | 130 | enc := json.NewEncoder(f) 131 | return func(m *Metric) error { 132 | return enc.Encode(m) 133 | } 134 | } 135 | 136 | func (m *Metrics) computeTags(name string, customtags []string) map[string]string { 137 | ret := make(map[string]string, len(m.tags)+len(customtags)) 138 | 139 | // copy global tags. 140 | for k, v := range m.tags { 141 | ret[k] = v 142 | } 143 | 144 | // process custom tags. 145 | for _, t := range customtags { 146 | kv := strings.Split(t, "=") 147 | if len(kv) != 2 { 148 | m.re.SLogger().Warnf("skipping invalid tag for metric; name: %s, tag: %s", name, t) 149 | continue 150 | } 151 | ret[kv[0]] = kv[1] 152 | } 153 | return ret 154 | } 155 | 156 | func (m *Metrics) writeToInfluxDBSink(measurementType string) MetricSinkFn { 157 | return func(metric *Metric) error { 158 | fields := make(map[string]interface{}, len(metric.Measures)) 159 | for k, v := range metric.Measures { 160 | fields[k] = v 161 | 162 | var tags map[string]string 163 | vals := strings.Split(metric.Name, ",") 164 | if len(vals) > 1 { 165 | // we have custom metric tags; inject global tags + provided tags. 166 | tags = m.computeTags(vals[0], vals[1:]) 167 | } else { 168 | // we have no custom metric tags; inject global tags only. 169 | tags = m.tags 170 | } 171 | 172 | prefix := fmt.Sprintf("%s.%s-%s", measurementType, m.re.TestPlan, m.re.TestCase) 173 | measurementName := fmt.Sprintf("%s.%s.%s", prefix, vals[0], metric.Type.String()) 174 | 175 | p, err := client.NewPoint(measurementName, tags, fields, time.Unix(0, metric.Timestamp)) 176 | if err != nil { 177 | return err 178 | } 179 | m.batcher.WritePoint(p) 180 | } 181 | return nil 182 | } 183 | } 184 | 185 | func (m *Metrics) recordEvent(evt *Event) { 186 | if m.influxdb == nil { 187 | return 188 | } 189 | 190 | // this map copy is terrible; the influxdb v2 SDK makes points mutable. 191 | tags := make(map[string]string, len(m.tags)+2) 192 | for k, v := range m.tags { 193 | tags[k] = v 194 | } 195 | 196 | fields := map[string]interface{}{ 197 | "count": 1, 198 | } 199 | 200 | tags["event_type"] = evt.Type() 201 | 202 | p, err := client.NewPoint("events", tags, fields) 203 | if err != nil { 204 | m.re.RecordMessage("failed to create InfluxDB point: %s", err) 205 | } 206 | 207 | m.batcher.WritePoint(p) 208 | } 209 | -------------------------------------------------------------------------------- /runtime/metrics_api.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/rcrowley/go-metrics" 8 | ) 9 | 10 | // Type aliases to hide implementation details in the APIs. 11 | type ( 12 | Counter = metrics.Counter 13 | EWMA = metrics.EWMA 14 | Gauge = metrics.GaugeFloat64 15 | Histogram = metrics.Histogram 16 | Meter = metrics.Meter 17 | Sample = metrics.Sample 18 | Timer = metrics.Timer 19 | Point float64 20 | ) 21 | 22 | type MetricSinkFn func(m *Metric) error 23 | 24 | type MetricsApi struct { 25 | // re is the RunEnv this MetricsApi object is attached to. 26 | re *RunEnv 27 | 28 | // reg is the go-metrics Registry this MetricsApi object creates metrics under. 29 | reg metrics.Registry 30 | 31 | // sinks to invoke when a new observation has been made. 32 | // 1) data points are sent immediately. 33 | // 2) aggregated metrics are sent periodically, based on freq. 34 | sinks []MetricSinkFn 35 | 36 | // freq is the frequency with which to materialize aggregated metrics. 37 | freq time.Duration 38 | 39 | // callbacks are callbacks functions to call on every tick. 40 | callbacks []func(registry metrics.Registry) 41 | 42 | wg sync.WaitGroup 43 | freqChangeCh chan time.Duration 44 | doneCh chan struct{} 45 | } 46 | 47 | type metricsApiOpts struct { 48 | freq time.Duration 49 | preregister func(registry metrics.Registry) 50 | callbacks []func(registry metrics.Registry) 51 | sinks []MetricSinkFn 52 | } 53 | 54 | func newMetricsApi(re *RunEnv, opts metricsApiOpts) *MetricsApi { 55 | m := &MetricsApi{ 56 | re: re, 57 | reg: metrics.NewRegistry(), 58 | sinks: opts.sinks, 59 | freq: opts.freq, 60 | callbacks: opts.callbacks, 61 | freqChangeCh: make(chan time.Duration), 62 | doneCh: make(chan struct{}), 63 | } 64 | 65 | if opts.preregister != nil { 66 | opts.preregister(m.reg) 67 | } 68 | 69 | m.wg.Add(1) 70 | go m.background() 71 | return m 72 | } 73 | 74 | func (m *MetricsApi) background() { 75 | var ( 76 | tick *time.Ticker 77 | c <-chan time.Time 78 | ) 79 | 80 | defer m.wg.Done() 81 | 82 | // resetTicker resets the ticker to a new frequency. 83 | resetTicker := func(d time.Duration) { 84 | if tick != nil { 85 | tick.Stop() 86 | tick = nil 87 | c = nil 88 | } 89 | if d <= 0 { 90 | return 91 | } 92 | tick = time.NewTicker(d) 93 | c = tick.C 94 | } 95 | 96 | // Will stop and nullify the ticker. 97 | defer resetTicker(0) 98 | 99 | // Set the initial tick frequency. 100 | resetTicker(m.freq) 101 | 102 | for { 103 | select { 104 | case <-c: 105 | for _, a := range m.callbacks { 106 | a(m.reg) 107 | } 108 | m.reg.Each(m.broadcast) 109 | 110 | case f := <-m.freqChangeCh: 111 | m.freq = f 112 | resetTicker(f) 113 | 114 | case <-m.doneCh: 115 | return 116 | } 117 | } 118 | } 119 | 120 | // broadcast sends an observation to all emitters. 121 | func (m *MetricsApi) broadcast(name string, obj interface{}) { 122 | metric := NewMetric(name, obj) 123 | defer metric.Release() 124 | 125 | for _, sink := range m.sinks { 126 | if err := sink(metric); err != nil { 127 | m.re.RecordMessage("failed to emit metric: %s", err) 128 | } 129 | } 130 | } 131 | 132 | func (m *MetricsApi) Close() error { 133 | close(m.doneCh) 134 | m.wg.Wait() 135 | 136 | return nil 137 | } 138 | 139 | func (m *MetricsApi) SetFrequency(freq time.Duration) { 140 | m.freqChangeCh <- freq 141 | } 142 | 143 | // RecordPoint records a float64 point under the provided metric name + tags. 144 | // 145 | // The format of the metric name is a comma-separated list, where the first 146 | // element is the metric name, and optionally, an unbounded list of 147 | // key-value pairs. Example: 148 | // 149 | // requests_received,tag1=value1,tag2=value2,tag3=value3 150 | func (m *MetricsApi) RecordPoint(name string, value float64) { 151 | m.broadcast(name, Point(value)) 152 | } 153 | 154 | // Counter creates a measurement of counter type. The returned type is an 155 | // alias of go-metrics' Counter type. Refer to godocs there for details. 156 | // 157 | // The format of the metric name is a comma-separated list, where the first 158 | // element is the metric name, and optionally, an unbounded list of 159 | // key-value pairs. Example: 160 | // 161 | // requests_received,tag1=value1,tag2=value2,tag3=value3 162 | func (m *MetricsApi) Counter(name string) Counter { 163 | return m.reg.GetOrRegister(name, newResettingCounter()).(metrics.Counter) 164 | } 165 | 166 | // EWMA creates a measurement of exponential-weighted moving average type. 167 | // The returned type is an alias of go-metrics' EWMA type. Refer to godocs 168 | // there for details. 169 | // 170 | // The format of the metric name is a comma-separated list, where the first 171 | // element is the metric name, and optionally, an unbounded list of 172 | // key-value pairs. Example: 173 | // 174 | // requests_received,tag1=value1,tag2=value2,tag3=value3 175 | func (m *MetricsApi) EWMA(name string, alpha float64) EWMA { 176 | return m.reg.GetOrRegister(name, metrics.NewEWMA(alpha)).(metrics.EWMA) 177 | } 178 | 179 | // Gauge creates a measurement of gauge type (float64). 180 | // The returned type is an alias of go-metrics' GaugeFloat64 type. Refer to 181 | // godocs there for details. 182 | // 183 | // The format of the metric name is a comma-separated list, where the first 184 | // element is the metric name, and optionally, an unbounded list of 185 | // key-value pairs. Example: 186 | // 187 | // requests_received,tag1=value1,tag2=value2,tag3=value3 188 | func (m *MetricsApi) Gauge(name string) Gauge { 189 | return m.reg.GetOrRegister(name, metrics.NewGaugeFloat64()).(metrics.GaugeFloat64) 190 | } 191 | 192 | // GaugeF creates a measurement of functional gauge type (float64). 193 | // The returned type is an alias of go-metrics' GaugeFloat64 type. Refer to 194 | // godocs there for details. 195 | // 196 | // The format of the metric name is a comma-separated list, where the first 197 | // element is the metric name, and optionally, an unbounded list of 198 | // key-value pairs. Example: 199 | // 200 | // requests_received,tag1=value1,tag2=value2,tag3=value3 201 | func (m *MetricsApi) GaugeF(name string, f func() float64) Gauge { 202 | return m.reg.GetOrRegister(name, metrics.NewFunctionalGaugeFloat64(f)).(metrics.GaugeFloat64) 203 | } 204 | 205 | // Histogram creates a measurement of histogram type. 206 | // The returned type is an alias of go-metrics' Histogram type. Refer to 207 | // godocs there for details. 208 | // 209 | // The format of the metric name is a comma-separated list, where the first 210 | // element is the metric name, and optionally, an unbounded list of 211 | // key-value pairs. Example: 212 | // 213 | // requests_received,tag1=value1,tag2=value2,tag3=value3 214 | func (m *MetricsApi) Histogram(name string, s Sample) Histogram { 215 | return m.reg.GetOrRegister(name, metrics.NewHistogram(s)).(metrics.Histogram) 216 | } 217 | 218 | // Meter creates a measurement of meter type. 219 | // The returned type is an alias of go-metrics' Meter type. Refer to 220 | // godocs there for details. 221 | // 222 | // The format of the metric name is a comma-separated list, where the first 223 | // element is the metric name, and optionally, an unbounded list of 224 | // key-value pairs. Example: 225 | // 226 | // requests_received,tag1=value1,tag2=value2,tag3=value3 227 | func (m *MetricsApi) Meter(name string) Meter { 228 | return m.reg.GetOrRegister(name, metrics.NewMeter()).(metrics.Meter) 229 | } 230 | 231 | // Timer creates a measurement of timer type. 232 | // The returned type is an alias of go-metrics' Timer type. Refer to 233 | // godocs there for details. 234 | // 235 | // The format of the metric name is a comma-separated list, where the first 236 | // element is the metric name, and optionally, an unbounded list of 237 | // key-value pairs. Example: 238 | // 239 | // requests_received,tag1=value1,tag2=value2,tag3=value3 240 | func (m *MetricsApi) Timer(name string) Timer { 241 | return m.reg.GetOrRegister(name, metrics.NewTimer()).(metrics.Timer) 242 | } 243 | 244 | // ResettingHistogram creates a measurement of histogram type, which cyclically 245 | // resets to zero when its values are harvested. 246 | // 247 | // The returned type is an alias of go-metrics' Histogram type. Refer to 248 | // godocs there for details. 249 | // 250 | // The format of the metric name is a comma-separated list, where the first 251 | // element is the metric name, and optionally, an unbounded list of 252 | // key-value pairs. Example: 253 | // 254 | // requests_received,tag1=value1,tag2=value2,tag3=value3 255 | func (m *MetricsApi) ResettingHistogram(name string) Histogram { 256 | return m.reg.GetOrRegister(name, newResettingHistogram()).(Histogram) 257 | } 258 | 259 | func (m *MetricsApi) NewExpDecaySample(reservoirSize int, alpha float64) Sample { 260 | return metrics.NewExpDecaySample(reservoirSize, alpha) 261 | } 262 | 263 | func (m *MetricsApi) NewUniformSample(reservoirSize int) Sample { 264 | return metrics.NewUniformSample(reservoirSize) 265 | } 266 | -------------------------------------------------------------------------------- /runtime/metrics_types.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "sync" 8 | "time" 9 | 10 | "github.com/rcrowley/go-metrics" 11 | ) 12 | 13 | var pools [7]sync.Pool 14 | 15 | func init() { 16 | for i := range pools { 17 | pools[i].New = func() interface{} { 18 | return &Metric{ 19 | Type: MetricType(i), 20 | Measures: make(map[string]interface{}, 1), 21 | } 22 | } 23 | } 24 | } 25 | 26 | type MetricType int 27 | 28 | const ( 29 | MetricPoint MetricType = iota 30 | MetricCounter 31 | MetricEWMA 32 | MetricGauge 33 | MetricHistogram 34 | MetricMeter 35 | MetricTimer 36 | ) 37 | 38 | var typeMappings = [...]string{"point", "counter", "ewma", "gauge", "histogram", "meter", "timer"} 39 | 40 | func (mt MetricType) String() string { 41 | return typeMappings[mt] 42 | } 43 | 44 | func (mt MetricType) MarshalJSON() ([]byte, error) { 45 | return json.Marshal(mt.String()) 46 | } 47 | 48 | // UnmarshalJSON is only used for testing; it's inefficient but not relevant. 49 | func (mt *MetricType) UnmarshalJSON(b []byte) error { 50 | var s string 51 | if err := json.Unmarshal(b, &s); err != nil { 52 | return nil 53 | } 54 | for i, m := range typeMappings { 55 | if m == s { 56 | *mt = MetricType(i) 57 | return nil 58 | } 59 | } 60 | return fmt.Errorf("invalid metric type") 61 | } 62 | 63 | type Metric struct { 64 | Timestamp int64 `json:"ts"` 65 | Type MetricType `json:"type"` 66 | Name string `json:"name"` 67 | Measures map[string]interface{} `json:"measures"` 68 | } 69 | 70 | func (m *Metric) Release() { 71 | pools[m.Type].Put(m) 72 | } 73 | 74 | func NewMetric(name string, i interface{}) *Metric { 75 | var ( 76 | m *Metric 77 | t MetricType 78 | ts = time.Now().UnixNano() 79 | ) 80 | 81 | switch v := i.(type) { 82 | case Point: 83 | t = MetricPoint 84 | m = pools[t].Get().(*Metric) 85 | m.Measures["value"] = float64(v) 86 | 87 | case Counter: 88 | t = MetricCounter 89 | m = pools[t].Get().(*Metric) 90 | s := v.Snapshot() 91 | m.Measures["count"] = s.Count() 92 | 93 | case EWMA: 94 | t = MetricEWMA 95 | m = pools[t].Get().(*Metric) 96 | s := v.Snapshot() 97 | m.Measures["rate"] = s.Rate() 98 | 99 | case Gauge: // float64 gauge, aliased in our SDK 100 | t = MetricGauge 101 | m = pools[t].Get().(*Metric) 102 | s := v.Snapshot() 103 | m.Measures["value"] = s.Value() 104 | 105 | case metrics.Gauge: // int64 gauge, used by go runtime metrics 106 | t = MetricGauge 107 | m = pools[t].Get().(*Metric) 108 | s := v.Snapshot() 109 | m.Measures["value"] = float64(s.Value()) 110 | 111 | case Histogram: 112 | t = MetricHistogram 113 | m = pools[t].Get().(*Metric) 114 | s := v.Snapshot() 115 | p := s.Percentiles([]float64{0.5, 0.75, 0.95, 0.99, 0.999, 0.9999}) 116 | m.Measures["count"] = float64(s.Count()) 117 | m.Measures["max"] = float64(s.Max()) 118 | m.Measures["mean"] = s.Mean() 119 | m.Measures["min"] = float64(s.Min()) 120 | m.Measures["stddev"] = s.StdDev() 121 | m.Measures["variance"] = s.Variance() 122 | m.Measures["p50"] = p[0] 123 | m.Measures["p75"] = p[1] 124 | m.Measures["p95"] = p[2] 125 | m.Measures["p99"] = p[3] 126 | m.Measures["p999"] = p[4] 127 | m.Measures["p9999"] = p[5] 128 | 129 | case Meter: 130 | t = MetricMeter 131 | m = pools[t].Get().(*Metric) 132 | s := v.Snapshot() 133 | m.Measures["count"] = float64(s.Count()) 134 | m.Measures["m1"] = s.Rate1() 135 | m.Measures["m5"] = s.Rate5() 136 | m.Measures["m15"] = s.Rate15() 137 | m.Measures["mean"] = s.RateMean() 138 | 139 | case Timer: 140 | t = MetricTimer 141 | m = pools[t].Get().(*Metric) 142 | s := v.Snapshot() 143 | p := s.Percentiles([]float64{0.5, 0.75, 0.95, 0.99, 0.999, 0.9999}) 144 | m.Measures["count"] = float64(s.Count()) 145 | m.Measures["max"] = float64(s.Max()) 146 | m.Measures["mean"] = s.Mean() 147 | m.Measures["min"] = float64(s.Min()) 148 | m.Measures["stddev"] = s.StdDev() 149 | m.Measures["variance"] = s.Variance() 150 | m.Measures["p50"] = p[0] 151 | m.Measures["p75"] = p[1] 152 | m.Measures["p95"] = p[2] 153 | m.Measures["p99"] = p[3] 154 | m.Measures["p999"] = p[4] 155 | m.Measures["p9999"] = p[5] 156 | m.Measures["m1"] = s.Rate1() 157 | m.Measures["m5"] = s.Rate5() 158 | m.Measures["m15"] = s.Rate15() 159 | m.Measures["meanrate"] = s.RateMean() 160 | 161 | default: 162 | panic(fmt.Sprintf("unexpected metric type: %v", reflect.TypeOf(v))) 163 | 164 | } 165 | 166 | m.Timestamp = ts 167 | m.Type = t 168 | m.Name = name 169 | return m 170 | } 171 | -------------------------------------------------------------------------------- /runtime/resetting_counter.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "sync/atomic" 5 | 6 | "github.com/rcrowley/go-metrics" 7 | ) 8 | 9 | func newResettingCounter() Counter { 10 | if metrics.UseNilMetrics { 11 | return metrics.NilCounter{} 12 | } 13 | return &standardResettingCounter{0} 14 | } 15 | 16 | // StandardResettingCounter is the standard implementation of a Counter and uses the 17 | // sync/atomic package to manage a single int64 value. It resets when Snapshot() is called. 18 | type standardResettingCounter struct { 19 | count int64 20 | } 21 | 22 | // Clear sets the counter to zero. 23 | func (c *standardResettingCounter) Clear() { 24 | atomic.StoreInt64(&c.count, 0) 25 | } 26 | 27 | // Count returns the current count. 28 | func (c *standardResettingCounter) Count() int64 { 29 | return atomic.LoadInt64(&c.count) 30 | } 31 | 32 | // Dec decrements the counter by the given amount. 33 | func (c *standardResettingCounter) Dec(i int64) { 34 | atomic.AddInt64(&c.count, -i) 35 | } 36 | 37 | // Inc increments the counter by the given amount. 38 | func (c *standardResettingCounter) Inc(i int64) { 39 | atomic.AddInt64(&c.count, i) 40 | } 41 | 42 | // Snapshot returns a read-only copy of the counter, and resets it. 43 | func (c *standardResettingCounter) Snapshot() Counter { 44 | currentValue := atomic.SwapInt64(&c.count, 0) 45 | return metrics.CounterSnapshot(currentValue) 46 | } 47 | -------------------------------------------------------------------------------- /runtime/resetting_histogram.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | "sync" 7 | 8 | "github.com/rcrowley/go-metrics" 9 | ) 10 | 11 | // Initial slice capacity for the values stored in a ResettingHistogram 12 | const InitialResettingHistogramSliceCap = 10 13 | 14 | // newResettingHistogram constructs a new StandardResettingHistogram 15 | func newResettingHistogram() Histogram { 16 | if metrics.UseNilMetrics { 17 | return nilResettingHistogram{} 18 | } 19 | return &standardResettingHistogram{ 20 | values: make([]int64, 0, InitialResettingHistogramSliceCap), 21 | } 22 | } 23 | 24 | // nilResettingHistogram is a no-op ResettingHistogram. 25 | type nilResettingHistogram struct { 26 | } 27 | 28 | // Values is a no-op. 29 | func (nilResettingHistogram) Values() []int64 { return nil } 30 | 31 | // Snapshot is a no-op. 32 | func (nilResettingHistogram) Snapshot() Histogram { 33 | return &resettingHistogramSnapshot{ 34 | values: []int64{}, 35 | } 36 | } 37 | 38 | // Update is a no-op. 39 | func (nilResettingHistogram) Update(int64) {} 40 | 41 | // Clear is a no-op. 42 | func (nilResettingHistogram) Clear() {} 43 | 44 | func (nilResettingHistogram) Count() int64 { 45 | return 0 46 | } 47 | 48 | func (nilResettingHistogram) Variance() float64 { 49 | return 0.0 50 | } 51 | 52 | func (nilResettingHistogram) Min() int64 { 53 | return 0 54 | } 55 | 56 | func (nilResettingHistogram) Max() int64 { 57 | return 0 58 | } 59 | 60 | func (nilResettingHistogram) Sum() int64 { 61 | return 0 62 | } 63 | 64 | func (nilResettingHistogram) StdDev() float64 { 65 | return 0.0 66 | } 67 | 68 | func (nilResettingHistogram) Sample() Sample { 69 | return metrics.NilSample{} 70 | } 71 | 72 | func (nilResettingHistogram) Percentiles([]float64) []float64 { 73 | return nil 74 | } 75 | 76 | func (nilResettingHistogram) Percentile(float64) float64 { 77 | return 0.0 78 | } 79 | 80 | func (nilResettingHistogram) Mean() float64 { 81 | return 0.0 82 | } 83 | 84 | // standardResettingHistogram is used for storing aggregated values for timers, which are reset on every flush interval. 85 | type standardResettingHistogram struct { 86 | values []int64 87 | mutex sync.Mutex 88 | } 89 | 90 | func (t *standardResettingHistogram) Count() int64 { 91 | panic("Count called on a resetting histogram; capture a snapshot first") 92 | } 93 | 94 | func (t *standardResettingHistogram) Max() int64 { 95 | panic("Max called on a resetting histogram; capture a snapshot first") 96 | } 97 | 98 | func (t *standardResettingHistogram) Min() int64 { 99 | panic("Min called on a resetting histogram; capture a snapshot first") 100 | } 101 | 102 | func (t *standardResettingHistogram) StdDev() float64 { 103 | panic("StdDev called on a resetting histogram; capture a snapshot first") 104 | } 105 | 106 | func (t *standardResettingHistogram) Variance() float64 { 107 | panic("Variance called on a resetting histogram; capture a snapshot first") 108 | } 109 | 110 | func (t *standardResettingHistogram) Sum() int64 { 111 | panic("Sum called on a resetting histogram; capture a snapshot first") 112 | } 113 | 114 | func (t *standardResettingHistogram) Sample() Sample { 115 | panic("Sample called on a resetting histogram; capture a snapshot first") 116 | } 117 | 118 | func (t *standardResettingHistogram) Percentiles([]float64) []float64 { 119 | panic("Percentiles called on a resetting histogram; capture a snapshot first") 120 | } 121 | 122 | func (t *standardResettingHistogram) Percentile(float64) float64 { 123 | panic("Percentile called on a resetting histogram; capture a snapshot first") 124 | } 125 | 126 | func (t *standardResettingHistogram) Mean() float64 { 127 | panic("Mean called on a resetting histogram; capture a snapshot first") 128 | } 129 | 130 | // Values returns a slice with all measurements. 131 | func (t *standardResettingHistogram) Values() []int64 { 132 | return t.values 133 | } 134 | 135 | // Snapshot resets the timer and returns a read-only copy of its sorted contents. 136 | func (t *standardResettingHistogram) Snapshot() Histogram { 137 | t.mutex.Lock() 138 | defer t.mutex.Unlock() 139 | 140 | currentValues := t.values 141 | t.values = make([]int64, 0, InitialResettingHistogramSliceCap) 142 | 143 | sort.Slice(currentValues, func(i, j int) bool { return currentValues[i] < currentValues[j] }) 144 | 145 | return &resettingHistogramSnapshot{ 146 | values: currentValues, 147 | } 148 | } 149 | 150 | func (t *standardResettingHistogram) Clear() { 151 | t.mutex.Lock() 152 | defer t.mutex.Unlock() 153 | t.values = t.values[:0] 154 | } 155 | 156 | // Record the duration of an event. 157 | func (t *standardResettingHistogram) Update(d int64) { 158 | t.mutex.Lock() 159 | defer t.mutex.Unlock() 160 | t.values = append(t.values, d) 161 | } 162 | 163 | // resettingHistogramSnapshot is a point-in-time copy of another resettingHistogram. 164 | type resettingHistogramSnapshot struct { 165 | sync.Mutex 166 | 167 | values []int64 168 | mean float64 169 | calculated bool 170 | } 171 | 172 | // resettingHistogramSnapshot returns the snapshot. 173 | func (t *resettingHistogramSnapshot) Snapshot() Histogram { return t } 174 | 175 | func (*resettingHistogramSnapshot) Update(int64) { 176 | panic("Update called on a resetting histogram snapshot") 177 | } 178 | 179 | func (t *resettingHistogramSnapshot) Clear() { 180 | panic("Clear called on a resetting histogram snapshot") 181 | } 182 | 183 | func (t *resettingHistogramSnapshot) Sample() Sample { 184 | panic("Sample called on a resetting histogram snapshot") 185 | } 186 | 187 | func (t *resettingHistogramSnapshot) Count() int64 { 188 | t.Lock() 189 | defer t.Unlock() 190 | 191 | return int64(len(t.values)) 192 | } 193 | 194 | // Values returns all values from snapshot. 195 | func (t *resettingHistogramSnapshot) Values() []int64 { 196 | t.Lock() 197 | defer t.Unlock() 198 | 199 | return t.values 200 | } 201 | 202 | func (t *resettingHistogramSnapshot) Min() int64 { 203 | t.Lock() 204 | defer t.Unlock() 205 | 206 | if len(t.values) > 0 { 207 | return t.values[0] 208 | } 209 | return 0 210 | } 211 | 212 | func (t *resettingHistogramSnapshot) Variance() float64 { 213 | t.Lock() 214 | defer t.Unlock() 215 | 216 | if len(t.values) == 0 { 217 | return 0.0 218 | } 219 | 220 | m := t._mean() 221 | var sum float64 222 | for _, v := range t.values { 223 | d := float64(v) - m 224 | sum += d * d 225 | } 226 | return sum / float64(len(t.values)) 227 | } 228 | 229 | func (t *resettingHistogramSnapshot) Max() int64 { 230 | t.Lock() 231 | defer t.Unlock() 232 | 233 | if len(t.values) > 0 { 234 | return t.values[len(t.values)-1] 235 | } 236 | return 0 237 | } 238 | 239 | func (t *resettingHistogramSnapshot) StdDev() float64 { 240 | return math.Sqrt(t.Variance()) 241 | } 242 | 243 | func (t *resettingHistogramSnapshot) Sum() int64 { 244 | t.Lock() 245 | defer t.Unlock() 246 | 247 | var sum int64 248 | for _, v := range t.values { 249 | sum += v 250 | } 251 | return sum 252 | } 253 | 254 | // Percentile returns the boundaries for the input percentiles. 255 | func (t *resettingHistogramSnapshot) Percentile(percentile float64) float64 { 256 | t.Lock() 257 | defer t.Unlock() 258 | 259 | tb := t.calc([]float64{percentile}) 260 | 261 | return tb[0] 262 | } 263 | 264 | // Percentiles returns the boundaries for the input percentiles. 265 | func (t *resettingHistogramSnapshot) Percentiles(percentiles []float64) []float64 { 266 | t.Lock() 267 | defer t.Unlock() 268 | 269 | tb := t.calc(percentiles) 270 | 271 | return tb 272 | } 273 | 274 | // Mean returns the mean of the snapshotted values 275 | func (t *resettingHistogramSnapshot) Mean() float64 { 276 | t.Lock() 277 | defer t.Unlock() 278 | 279 | return t._mean() 280 | } 281 | 282 | func (t *resettingHistogramSnapshot) _mean() float64 { 283 | if !t.calculated { 284 | _ = t.calc([]float64{}) 285 | } 286 | 287 | return t.mean 288 | } 289 | 290 | func (t *resettingHistogramSnapshot) calc(percentiles []float64) (thresholdBoundaries []float64) { 291 | count := len(t.values) 292 | if count == 0 { 293 | thresholdBoundaries = make([]float64, len(percentiles)) 294 | t.mean = 0 295 | t.calculated = true 296 | return 297 | } 298 | 299 | min := t.values[0] 300 | max := t.values[count-1] 301 | 302 | cumulativeValues := make([]int64, count) 303 | cumulativeValues[0] = min 304 | for i := 1; i < count; i++ { 305 | cumulativeValues[i] = t.values[i] + cumulativeValues[i-1] 306 | } 307 | 308 | thresholdBoundaries = make([]float64, len(percentiles)) 309 | 310 | thresholdBoundary := max 311 | 312 | for i, pct := range percentiles { 313 | if count > 1 { 314 | var abs float64 315 | if pct >= 0 { 316 | abs = pct 317 | } else { 318 | abs = 100 + pct 319 | } 320 | // poor man's math.Round(x): 321 | // math.Floor(x + 0.5) 322 | indexOfPerc := int(math.Floor(((abs / 100.0) * float64(count)) + 0.5)) 323 | if pct >= 0 && indexOfPerc > 0 { 324 | indexOfPerc -= 1 // index offset=0 325 | } 326 | thresholdBoundary = t.values[indexOfPerc] 327 | } 328 | 329 | thresholdBoundaries[i] = float64(thresholdBoundary) 330 | } 331 | 332 | sum := cumulativeValues[count-1] 333 | t.mean = float64(sum) / float64(count) 334 | t.calculated = true 335 | return 336 | } 337 | -------------------------------------------------------------------------------- /runtime/resetting_histogram_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestResettingHistogram(t *testing.T) { 9 | tests := []struct { 10 | values []int64 11 | start int 12 | end int 13 | wantP50 float64 14 | wantP95 float64 15 | wantP99 float64 16 | wantMean float64 17 | wantMin int64 18 | wantMax int64 19 | }{ 20 | { 21 | values: []int64{}, 22 | start: 1, 23 | end: 11, 24 | wantP50: 5, wantP95: 10, wantP99: 10, 25 | wantMin: 1, wantMax: 10, wantMean: 5.5, 26 | }, 27 | { 28 | values: []int64{}, 29 | start: 1, 30 | end: 101, 31 | wantP50: 50, wantP95: 95, wantP99: 99, 32 | wantMin: 1, wantMax: 100, wantMean: 50.5, 33 | }, 34 | { 35 | values: []int64{1}, 36 | start: 0, 37 | end: 0, 38 | wantP50: 1, wantP95: 1, wantP99: 1, 39 | wantMin: 1, wantMax: 1, wantMean: 1, 40 | }, 41 | { 42 | values: []int64{0}, 43 | start: 0, 44 | end: 0, 45 | wantP50: 0, wantP95: 0, wantP99: 0, 46 | wantMin: 0, wantMax: 0, wantMean: 0, 47 | }, 48 | { 49 | values: []int64{}, 50 | start: 0, 51 | end: 0, 52 | wantP50: 0, wantP95: 0, wantP99: 0, 53 | wantMin: 0, wantMax: 0, wantMean: 0, 54 | }, 55 | { 56 | values: []int64{1, 10}, 57 | start: 0, 58 | end: 0, 59 | wantP50: 1, wantP95: 10, wantP99: 10, 60 | wantMin: 1, wantMax: 10, wantMean: 5.5, 61 | }, 62 | } 63 | for ind, tt := range tests { 64 | timer := newResettingHistogram() 65 | 66 | for i := tt.start; i < tt.end; i++ { 67 | tt.values = append(tt.values, int64(i)) 68 | } 69 | 70 | for _, v := range tt.values { 71 | timer.Update(int64(time.Duration(v))) 72 | } 73 | 74 | snap := timer.Snapshot() 75 | 76 | ps := snap.Percentiles([]float64{50, 95, 99}) 77 | 78 | val := tt.values 79 | 80 | if len(val) > 0 { 81 | if tt.wantMin != val[0] { 82 | t.Fatalf("%d: min: got %d, want %d", ind, val[0], tt.wantMin) 83 | } 84 | 85 | if tt.wantMax != val[len(val)-1] { 86 | t.Fatalf("%d: max: got %d, want %d", ind, val[len(val)-1], tt.wantMax) 87 | } 88 | } 89 | 90 | if tt.wantMean != snap.Mean() { 91 | t.Fatalf("%d: mean: got %.2f, want %.2f", ind, snap.Mean(), tt.wantMean) 92 | } 93 | 94 | if tt.wantP50 != ps[0] { 95 | t.Fatalf("%d: p50: got %v, want %v", ind, ps[0], tt.wantP50) 96 | } 97 | 98 | if tt.wantP95 != ps[1] { 99 | t.Fatalf("%d: p95: got %v, want %v", ind, ps[1], tt.wantP95) 100 | } 101 | 102 | if tt.wantP99 != ps[2] { 103 | t.Fatalf("%d: p99: got %v, want %v", ind, ps[2], tt.wantP99) 104 | } 105 | } 106 | } 107 | 108 | func TestResettingHistogramWithFivePercentiles(t *testing.T) { 109 | tests := []struct { 110 | values []int64 111 | start int 112 | end int 113 | wantP05 float64 114 | wantP20 float64 115 | wantP50 float64 116 | wantP95 float64 117 | wantP99 float64 118 | wantMean float64 119 | wantMin int64 120 | wantMax int64 121 | }{ 122 | { 123 | values: []int64{}, 124 | start: 1, 125 | end: 11, 126 | wantP05: 1, wantP20: 2, wantP50: 5, wantP95: 10, wantP99: 10, 127 | wantMin: 1, wantMax: 10, wantMean: 5.5, 128 | }, 129 | { 130 | values: []int64{}, 131 | start: 1, 132 | end: 101, 133 | wantP05: 5, wantP20: 20, wantP50: 50, wantP95: 95, wantP99: 99, 134 | wantMin: 1, wantMax: 100, wantMean: 50.5, 135 | }, 136 | { 137 | values: []int64{1}, 138 | start: 0, 139 | end: 0, 140 | wantP05: 1, wantP20: 1, wantP50: 1, wantP95: 1, wantP99: 1, 141 | wantMin: 1, wantMax: 1, wantMean: 1, 142 | }, 143 | { 144 | values: []int64{0}, 145 | start: 0, 146 | end: 0, 147 | wantP05: 0, wantP20: 0, wantP50: 0, wantP95: 0, wantP99: 0, 148 | wantMin: 0, wantMax: 0, wantMean: 0, 149 | }, 150 | { 151 | values: []int64{}, 152 | start: 0, 153 | end: 0, 154 | wantP05: 0, wantP20: 0, wantP50: 0, wantP95: 0, wantP99: 0, 155 | wantMin: 0, wantMax: 0, wantMean: 0, 156 | }, 157 | { 158 | values: []int64{1, 10}, 159 | start: 0, 160 | end: 0, 161 | wantP05: 1, wantP20: 1, wantP50: 1, wantP95: 10, wantP99: 10, 162 | wantMin: 1, wantMax: 10, wantMean: 5.5, 163 | }, 164 | } 165 | for ind, tt := range tests { 166 | timer := newResettingHistogram() 167 | 168 | for i := tt.start; i < tt.end; i++ { 169 | tt.values = append(tt.values, int64(i)) 170 | } 171 | 172 | for _, v := range tt.values { 173 | timer.Update(int64(time.Duration(v))) 174 | } 175 | 176 | snap := timer.Snapshot() 177 | 178 | ps := snap.Percentiles([]float64{5, 20, 50, 95, 99}) 179 | 180 | val := tt.values 181 | 182 | if len(val) > 0 { 183 | if tt.wantMin != val[0] { 184 | t.Fatalf("%d: min: got %d, want %d", ind, val[0], tt.wantMin) 185 | } 186 | 187 | if tt.wantMax != val[len(val)-1] { 188 | t.Fatalf("%d: max: got %d, want %d", ind, val[len(val)-1], tt.wantMax) 189 | } 190 | } 191 | 192 | if tt.wantMean != snap.Mean() { 193 | t.Fatalf("%d: mean: got %.2f, want %.2f", ind, snap.Mean(), tt.wantMean) 194 | } 195 | 196 | if tt.wantP05 != ps[0] { 197 | t.Fatalf("%d: p05: got %v, want %v", ind, ps[0], tt.wantP05) 198 | } 199 | 200 | if tt.wantP20 != ps[1] { 201 | t.Fatalf("%d: p20: got %v, want %v", ind, ps[1], tt.wantP20) 202 | } 203 | 204 | if tt.wantP50 != ps[2] { 205 | t.Fatalf("%d: p50: got %v, want %v", ind, ps[2], tt.wantP50) 206 | } 207 | 208 | if tt.wantP95 != ps[3] { 209 | t.Fatalf("%d: p95: got %v, want %v", ind, ps[3], tt.wantP95) 210 | } 211 | 212 | if tt.wantP99 != ps[4] { 213 | t.Fatalf("%d: p99: got %v, want %v", ind, ps[4], tt.wantP99) 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /runtime/runenv.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "os" 6 | gosync "sync" 7 | "time" 8 | 9 | "github.com/avast/retry-go" 10 | "github.com/hashicorp/go-multierror" 11 | _ "github.com/influxdata/influxdb1-client" // this is important because of the bug in go mod 12 | "go.uber.org/zap" 13 | ) 14 | 15 | var ( 16 | InfluxTestBatcher = true // used for testing purposes 17 | InfluxBatchLength = 128 18 | InfluxBatchInterval = 1 * time.Second 19 | InfluxBatchRetryOpts = func(re *RunEnv) []retry.Option { 20 | return []retry.Option{ 21 | retry.Attempts(5), 22 | retry.Delay(500 * time.Millisecond), 23 | retry.OnRetry(func(n uint, err error) { 24 | re.RecordMessage("failed to send batch to InfluxDB; attempt %d; err: %s", n, err) 25 | }), 26 | } 27 | } 28 | ) 29 | 30 | // RunEnv encapsulates the context for this test run. 31 | type RunEnv struct { 32 | RunParams 33 | 34 | logger *zap.Logger 35 | metrics *Metrics 36 | signalEmitter SignalEmitter 37 | 38 | wg gosync.WaitGroup 39 | closeCh chan struct{} 40 | assetsErr error 41 | 42 | unstructured struct { 43 | files []*os.File 44 | ch chan *os.File 45 | } 46 | structured struct { 47 | loggers []*zap.Logger 48 | ch chan *zap.Logger 49 | } 50 | } 51 | 52 | func (re *RunEnv) SLogger() *zap.SugaredLogger { 53 | return re.logger.Sugar() 54 | } 55 | 56 | // NewRunEnv constructs a runtime environment from the given runtime parameters. 57 | func NewRunEnv(params RunParams) *RunEnv { 58 | re := &RunEnv{ 59 | RunParams: params, 60 | closeCh: make(chan struct{}), 61 | } 62 | re.initLogger() 63 | 64 | re.structured.ch = make(chan *zap.Logger) 65 | re.unstructured.ch = make(chan *os.File) 66 | re.signalEmitter = &NilSignalEmitter{} 67 | 68 | re.wg.Add(1) 69 | go re.manageAssets() 70 | 71 | re.metrics = newMetrics(re) 72 | 73 | return re 74 | } 75 | 76 | type SignalEmitter interface { 77 | SignalEvent(context.Context, *Event) error 78 | } 79 | 80 | type NilSignalEmitter struct{} 81 | 82 | func (ne NilSignalEmitter) SignalEvent(ctx context.Context, event *Event) error { 83 | return nil 84 | } 85 | 86 | func (re *RunEnv) AttachSyncClient(se SignalEmitter) { 87 | re.signalEmitter = se 88 | } 89 | 90 | // R returns a metrics object for results. 91 | func (re *RunEnv) R() *MetricsApi { 92 | return re.metrics.R() 93 | } 94 | 95 | // D returns a metrics object for diagnostics. 96 | func (re *RunEnv) D() *MetricsApi { 97 | return re.metrics.D() 98 | } 99 | 100 | func (re *RunEnv) manageAssets() { 101 | defer re.wg.Done() 102 | 103 | var err *multierror.Error 104 | defer func() { re.assetsErr = err.ErrorOrNil() }() 105 | 106 | for { 107 | select { 108 | case f := <-re.unstructured.ch: 109 | re.unstructured.files = append(re.unstructured.files, f) 110 | case l := <-re.structured.ch: 111 | re.structured.loggers = append(re.structured.loggers, l) 112 | case <-re.closeCh: 113 | for _, f := range re.unstructured.files { 114 | err = multierror.Append(err, f.Close()) 115 | } 116 | for _, l := range re.structured.loggers { 117 | err = multierror.Append(err, l.Sync()) 118 | } 119 | return 120 | } 121 | } 122 | } 123 | 124 | func (re *RunEnv) Close() error { 125 | var err *multierror.Error 126 | 127 | // close metrics. 128 | err = multierror.Append(err, re.metrics.Close()) 129 | 130 | // This close stops monitoring the wapi errors channel, and closes assets. 131 | close(re.closeCh) 132 | re.wg.Wait() 133 | err = multierror.Append(err, re.assetsErr) 134 | 135 | if l := re.logger; l != nil { 136 | _ = l.Sync() 137 | } 138 | 139 | return err.ErrorOrNil() 140 | } 141 | 142 | // CurrentRunEnv populates a test context from environment vars. 143 | func CurrentRunEnv() *RunEnv { 144 | re, _ := ParseRunEnv(os.Environ()) 145 | return re 146 | } 147 | 148 | // ParseRunEnv parses a list of environment variables into a RunEnv. 149 | func ParseRunEnv(env []string) (*RunEnv, error) { 150 | p, err := ParseRunParams(env) 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | return NewRunEnv(*p), nil 156 | } 157 | -------------------------------------------------------------------------------- /runtime/runenv_assets.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "bufio" 5 | "crypto/rand" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | 11 | "go.uber.org/zap" 12 | "go.uber.org/zap/zapcore" 13 | ) 14 | 15 | // CreateRawAsset creates an output asset. 16 | // 17 | // Output assets will be saved when the test terminates and available for 18 | // further investigation. You can also manually create output assets/directories 19 | // under re.TestOutputsPath. 20 | func (re *RunEnv) CreateRawAsset(name string) (*os.File, error) { 21 | file, err := os.Create(filepath.Join(re.TestOutputsPath, name)) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | re.unstructured.ch <- file 27 | 28 | return file, nil 29 | } 30 | 31 | // CreateStructuredAsset creates an output asset and wraps it in zap loggers. 32 | func (re *RunEnv) CreateStructuredAsset(name string, config zap.Config) (*zap.Logger, *zap.SugaredLogger, error) { 33 | path := filepath.Join(re.TestOutputsPath, name) 34 | config.OutputPaths = []string{path} 35 | 36 | logger, err := config.Build() 37 | if err != nil { 38 | return nil, nil, err 39 | } 40 | 41 | re.structured.ch <- logger 42 | 43 | return logger, logger.Sugar(), nil 44 | } 45 | 46 | // StandardJSONConfig returns a zap.Config with JSON encoding, debug verbosity, 47 | // caller and stacktraces disabled, and with timestamps encoded as nanos after 48 | // epoch. 49 | func StandardJSONConfig() zap.Config { 50 | enc := zap.NewProductionEncoderConfig() 51 | enc.EncodeTime = zapcore.EpochNanosTimeEncoder 52 | 53 | return zap.Config{ 54 | Level: zap.NewAtomicLevelAt(zap.DebugLevel), 55 | Encoding: "json", 56 | EncoderConfig: enc, 57 | DisableCaller: true, 58 | DisableStacktrace: true, 59 | } 60 | } 61 | 62 | // CreateRandomFile creates a file of the specified size (in bytes) within the 63 | // specified directory path and returns its path. 64 | func (re *RunEnv) CreateRandomFile(directoryPath string, size int64) (string, error) { 65 | file, err := ioutil.TempFile(directoryPath, re.TestPlan) 66 | if err != nil { 67 | return "", err 68 | } 69 | defer file.Close() 70 | 71 | buf := bufio.NewWriter(file) 72 | var written int64 73 | for written < size { 74 | w, err := io.CopyN(buf, rand.Reader, size-written) 75 | if err != nil { 76 | return "", err 77 | } 78 | written += w 79 | } 80 | 81 | err = buf.Flush() 82 | if err != nil { 83 | return "", err 84 | } 85 | 86 | return file.Name(), file.Sync() 87 | } 88 | 89 | // CreateRandomDirectory creates a nested directory with the specified depth within the specified 90 | // directory path. If depth is zero, the directory path is returned. 91 | func (re *RunEnv) CreateRandomDirectory(directoryPath string, depth uint) (string, error) { 92 | if depth == 0 { 93 | return directoryPath, nil 94 | } 95 | 96 | base, err := ioutil.TempDir(directoryPath, re.TestPlan) 97 | if err != nil { 98 | return "", err 99 | } 100 | 101 | name := base 102 | var i uint 103 | for i = 1; i < depth; i++ { 104 | name, err = ioutil.TempDir(name, "tg") 105 | if err != nil { 106 | return "", err 107 | } 108 | } 109 | 110 | return base, nil 111 | } 112 | -------------------------------------------------------------------------------- /runtime/runenv_events.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "runtime/debug" 7 | 8 | "go.uber.org/zap" 9 | "go.uber.org/zap/zapcore" 10 | ) 11 | 12 | type Event struct { 13 | *StartEvent `json:"start_event,omitempty"` 14 | *MessageEvent `json:"message_event,omitempty"` 15 | *SuccessEvent `json:"success_event,omitempty"` 16 | *FailureEvent `json:"failure_event,omitempty"` 17 | *CrashEvent `json:"crash_event,omitempty"` 18 | *StageStartEvent `json:"stage_start_event,omitempty"` 19 | *StageEndEvent `json:"stage_end_event,omitempty"` 20 | } 21 | 22 | func (e *Event) Type() string { 23 | switch { 24 | case e.StartEvent != nil: 25 | return e.StartEvent.Type() 26 | case e.MessageEvent != nil: 27 | return e.MessageEvent.Type() 28 | case e.SuccessEvent != nil: 29 | return e.SuccessEvent.Type() 30 | case e.FailureEvent != nil: 31 | return e.FailureEvent.Type() 32 | case e.CrashEvent != nil: 33 | return e.CrashEvent.Type() 34 | case e.StageStartEvent != nil: 35 | return e.StageStartEvent.Type() 36 | case e.StageEndEvent != nil: 37 | return e.StageEndEvent.Type() 38 | default: 39 | panic("no such event") 40 | } 41 | } 42 | 43 | type StartEvent struct { 44 | Runenv *RunParams `json:"runenv"` 45 | } 46 | 47 | func (StartEvent) Type() string { 48 | return "start_event" 49 | } 50 | 51 | func (s StartEvent) MarshalLogObject(oe zapcore.ObjectEncoder) error { 52 | return oe.AddObject("runenv", s.Runenv) 53 | } 54 | 55 | type MessageEvent struct { 56 | Message string `json:"message"` 57 | } 58 | 59 | func (MessageEvent) Type() string { 60 | return "message_event" 61 | } 62 | 63 | func (m MessageEvent) MarshalLogObject(oe zapcore.ObjectEncoder) error { 64 | oe.AddString("message", m.Message) 65 | return nil 66 | } 67 | 68 | type SuccessEvent struct { 69 | TestGroupID string `json:"group"` 70 | } 71 | 72 | func (SuccessEvent) Type() string { 73 | return "SuccessEvent" 74 | } 75 | 76 | func (s SuccessEvent) MarshalLogObject(oe zapcore.ObjectEncoder) error { 77 | oe.AddString("group", s.TestGroupID) 78 | return nil 79 | } 80 | 81 | type FailureEvent struct { 82 | TestGroupID string `json:"group"` 83 | Error string `json:"error"` 84 | } 85 | 86 | func (FailureEvent) Type() string { 87 | return "failure_event" 88 | } 89 | 90 | func (f FailureEvent) MarshalLogObject(oe zapcore.ObjectEncoder) error { 91 | oe.AddString("group", f.TestGroupID) 92 | oe.AddString("error", f.Error) 93 | return nil 94 | } 95 | 96 | type CrashEvent struct { 97 | TestGroupID string `json:"group"` 98 | Error string `json:"error"` 99 | Stacktrace string `json:"stacktrace"` 100 | } 101 | 102 | func (CrashEvent) Type() string { 103 | return "crash_event" 104 | } 105 | 106 | func (c CrashEvent) MarshalLogObject(oe zapcore.ObjectEncoder) error { 107 | oe.AddString("group", c.TestGroupID) 108 | oe.AddString("error", c.Error) 109 | oe.AddString("stacktrace", c.Stacktrace) 110 | return nil 111 | } 112 | 113 | type StageStartEvent struct { 114 | Name string `json:"name"` 115 | TestGroupID string `json:"group"` 116 | } 117 | 118 | func (StageStartEvent) Type() string { 119 | return "stage_start_event" 120 | } 121 | 122 | func (s StageStartEvent) MarshalLogObject(oe zapcore.ObjectEncoder) error { 123 | oe.AddString("name", s.Name) 124 | oe.AddString("group", s.TestGroupID) 125 | return nil 126 | } 127 | 128 | type StageEndEvent struct { 129 | Name string `json:"name"` 130 | TestGroupID string `json:"group"` 131 | } 132 | 133 | func (StageEndEvent) Type() string { 134 | return "stage_end_event" 135 | } 136 | 137 | func (s StageEndEvent) MarshalLogObject(oe zapcore.ObjectEncoder) error { 138 | oe.AddString("name", s.Name) 139 | oe.AddString("group", s.TestGroupID) 140 | return nil 141 | } 142 | 143 | func (e Event) MarshalLogObject(oe zapcore.ObjectEncoder) error { 144 | switch { 145 | case e.StartEvent != nil: 146 | return oe.AddObject("start_event", e.StartEvent) 147 | case e.MessageEvent != nil: 148 | return oe.AddObject("message_event", e.MessageEvent) 149 | case e.SuccessEvent != nil: 150 | return oe.AddObject("success_event", e.SuccessEvent) 151 | case e.FailureEvent != nil: 152 | return oe.AddObject("failure_event", e.FailureEvent) 153 | case e.CrashEvent != nil: 154 | return oe.AddObject("crash_event", e.CrashEvent) 155 | case e.StageStartEvent != nil: 156 | return oe.AddObject("stage_start_event", e.StageStartEvent) 157 | case e.StageEndEvent != nil: 158 | return oe.AddObject("stage_end_event", e.StageEndEvent) 159 | default: 160 | panic("no such event") 161 | } 162 | } 163 | 164 | func (rp *RunParams) MarshalLogObject(oe zapcore.ObjectEncoder) error { 165 | oe.AddString("plan", rp.TestPlan) 166 | oe.AddString("case", rp.TestCase) 167 | oe.AddString("run", rp.TestRun) 168 | if err := oe.AddReflected("params", rp.TestInstanceParams); err != nil { 169 | return err 170 | } 171 | oe.AddInt("instances", rp.TestInstanceCount) 172 | oe.AddString("outputs_path", rp.TestOutputsPath) 173 | oe.AddString("temp_path", rp.TestTempPath) 174 | oe.AddString("network", func() string { 175 | if rp.TestSubnet == nil { 176 | return "" 177 | } 178 | return rp.TestSubnet.String() 179 | }()) 180 | 181 | oe.AddString("group", rp.TestGroupID) 182 | oe.AddInt("group_instances", rp.TestGroupInstanceCount) 183 | 184 | if rp.TestRepo != "" { 185 | oe.AddString("repo", rp.TestRepo) 186 | } 187 | if rp.TestCommit != "" { 188 | oe.AddString("commit", rp.TestCommit) 189 | } 190 | if rp.TestBranch != "" { 191 | oe.AddString("branch", rp.TestBranch) 192 | } 193 | if rp.TestTag != "" { 194 | oe.AddString("tag", rp.TestTag) 195 | } 196 | return nil 197 | } 198 | 199 | // RecordMessage records an informational message. 200 | func (re *RunEnv) RecordMessage(msg string, a ...interface{}) { 201 | if len(a) > 0 { 202 | msg = fmt.Sprintf(msg, a...) 203 | } 204 | e := &Event{MessageEvent: &MessageEvent{ 205 | Message: msg, 206 | }} 207 | re.logger.Info("", zap.Object("event", e)) 208 | } 209 | 210 | func (re *RunEnv) RecordStart() { 211 | e := &Event{StartEvent: &StartEvent{ 212 | Runenv: &re.RunParams, 213 | }} 214 | 215 | re.logger.Info("", zap.Object("event", e)) 216 | re.metrics.recordEvent(e) 217 | 218 | _ = re.signalEmitter.SignalEvent(context.Background(), e) 219 | } 220 | 221 | // RecordSuccess records that the calling instance succeeded. 222 | func (re *RunEnv) RecordSuccess() { 223 | e := &Event{SuccessEvent: &SuccessEvent{TestGroupID: re.RunParams.TestGroupID}} 224 | re.logger.Info("", zap.Object("event", e)) 225 | re.metrics.recordEvent(e) 226 | 227 | _ = re.signalEmitter.SignalEvent(context.Background(), e) 228 | } 229 | 230 | // RecordFailure records that the calling instance failed with the supplied 231 | // error. 232 | func (re *RunEnv) RecordFailure(err error) { 233 | e := &Event{FailureEvent: &FailureEvent{TestGroupID: re.RunParams.TestGroupID, Error: err.Error()}} 234 | re.logger.Error("", zap.Object("event", e)) 235 | re.metrics.recordEvent(e) 236 | 237 | _ = re.signalEmitter.SignalEvent(context.Background(), e) 238 | } 239 | 240 | // RecordCrash records that the calling instance crashed/panicked with the 241 | // supplied error. 242 | func (re *RunEnv) RecordCrash(err interface{}) { 243 | e := &Event{CrashEvent: &CrashEvent{ 244 | TestGroupID: re.RunParams.TestGroupID, 245 | Error: fmt.Sprintf("%s", err), 246 | Stacktrace: string(debug.Stack()), 247 | }} 248 | re.logger.Error("", zap.Object("event", e)) 249 | re.metrics.recordEvent(e) 250 | 251 | _ = re.signalEmitter.SignalEvent(context.Background(), e) 252 | } 253 | -------------------------------------------------------------------------------- /runtime/runenv_http.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "os" 8 | "path" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | // HTTPPeriodicSnapshots periodically fetches the snapshots from the given address 14 | // and outputs them to the out directory. Every file will be in the format timestamp.out. 15 | func (re *RunEnv) HTTPPeriodicSnapshots(ctx context.Context, addr string, dur time.Duration, outDir string) error { 16 | err := os.MkdirAll(path.Join(re.TestOutputsPath, outDir), 0777) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | nextFile := func() (*os.File, error) { 22 | timestamp := strconv.FormatInt(time.Now().Unix(), 10) 23 | return os.Create(path.Join(re.TestOutputsPath, outDir, timestamp+".out")) 24 | } 25 | 26 | go func() { 27 | ticker := time.NewTicker(dur) 28 | defer ticker.Stop() 29 | 30 | for { 31 | select { 32 | case <-ctx.Done(): 33 | return 34 | case <-ticker.C: 35 | func() { 36 | req, err := http.NewRequestWithContext(ctx, "GET", addr, nil) 37 | if err != nil { 38 | re.RecordMessage("error while creating http request: %v", err) 39 | return 40 | } 41 | 42 | resp, err := http.DefaultClient.Do(req) 43 | if err != nil { 44 | re.RecordMessage("error while scraping http endpoint: %v", err) 45 | return 46 | } 47 | defer resp.Body.Close() 48 | 49 | file, err := nextFile() 50 | if err != nil { 51 | re.RecordMessage("error while getting metrics output file: %v", err) 52 | return 53 | } 54 | defer file.Close() 55 | 56 | _, err = io.Copy(file, resp.Body) 57 | if err != nil { 58 | re.RecordMessage("error while copying data to file: %v", err) 59 | return 60 | } 61 | }() 62 | } 63 | } 64 | }() 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /runtime/runenv_logger.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | ) 10 | 11 | func (re *RunEnv) initLogger() { 12 | level := zap.NewAtomicLevel() 13 | 14 | if lvl := os.Getenv("LOG_LEVEL"); lvl != "" { 15 | if err := level.UnmarshalText([]byte(lvl)); err != nil { 16 | defer func() { 17 | // once the logger is defined... 18 | if re.logger != nil { 19 | re.logger.Sugar().Errorf("failed to decode log level '%q': %s", lvl, err) 20 | } 21 | }() 22 | } 23 | } else { 24 | level.SetLevel(zapcore.InfoLevel) 25 | } 26 | 27 | paths := []string{"stdout"} 28 | if re.TestOutputsPath != "" { 29 | paths = append(paths, filepath.Join(re.TestOutputsPath, "run.out")) 30 | } 31 | 32 | cfg := zap.Config{ 33 | Development: false, 34 | Level: level, 35 | DisableCaller: true, 36 | DisableStacktrace: true, 37 | OutputPaths: paths, 38 | Encoding: "json", 39 | InitialFields: map[string]interface{}{ 40 | "run_id": re.TestRun, 41 | "group_id": re.TestGroupID, 42 | }, 43 | } 44 | 45 | enc := zap.NewProductionEncoderConfig() 46 | enc.LevelKey, enc.NameKey = "", "" 47 | enc.EncodeTime = zapcore.EpochNanosTimeEncoder 48 | cfg.EncoderConfig = enc 49 | 50 | var err error 51 | re.logger, err = cfg.Build() 52 | if err != nil { 53 | panic(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /runtime/runenv_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestParseKeyValues(t *testing.T) { 17 | type args struct { 18 | in []string 19 | } 20 | tests := []struct { 21 | name string 22 | args args 23 | wantRes map[string]string 24 | wantErr bool 25 | }{ 26 | { 27 | name: "empty, int, string, bool", 28 | args: args{ 29 | []string{ 30 | "TEST_INSTANCE_ROLE=", 31 | "TEST_ARTIFACTS=/artifacts", 32 | "TEST_SIDECAR=true", 33 | }, 34 | }, 35 | wantErr: false, 36 | wantRes: map[string]string{ 37 | "TEST_INSTANCE_ROLE": "", 38 | "TEST_ARTIFACTS": "/artifacts", 39 | "TEST_SIDECAR": "true", 40 | }, 41 | }, 42 | { 43 | name: "empty, string, int, complex", 44 | args: args{ 45 | []string{ 46 | "TEST_BRANCH=", 47 | "TEST_RUN=e765696a-bdf2-408e-8b39-aeb0e90c0ff6", 48 | "TEST_GROUP_INSTANCE_COUNT=200", 49 | "TEST_GROUP_ID=single", 50 | "TEST_INSTANCE_PARAMS=bucket_size=2|n_find_peers=1|timeout_secs=300|auto_refresh=true|random_walk=false|n_bootstrap=1", 51 | "TEST_SUBNET=30.38.0.0/16", 52 | }, 53 | }, 54 | wantErr: false, 55 | wantRes: map[string]string{ 56 | "TEST_BRANCH": "", 57 | "TEST_RUN": "e765696a-bdf2-408e-8b39-aeb0e90c0ff6", 58 | "TEST_GROUP_INSTANCE_COUNT": "200", 59 | "TEST_GROUP_ID": "single", 60 | "TEST_INSTANCE_PARAMS": "bucket_size=2|n_find_peers=1|timeout_secs=300|auto_refresh=true|random_walk=false|n_bootstrap=1", 61 | "TEST_SUBNET": "30.38.0.0/16", 62 | }, 63 | }, 64 | } 65 | for _, tt := range tests { 66 | t.Run(tt.name, func(t *testing.T) { 67 | gotRes, err := ParseKeyValues(tt.args.in) 68 | if (err != nil) != tt.wantErr { 69 | t.Errorf("ParseKeyValues() error = %v, wantErr %v", err, tt.wantErr) 70 | return 71 | } 72 | if !reflect.DeepEqual(gotRes, tt.wantRes) { 73 | t.Errorf("ParseKeyValues() = %v, want %v", gotRes, tt.wantRes) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | func TestAllEvents(t *testing.T) { 80 | re, cleanup := RandomTestRunEnv(t) 81 | t.Cleanup(cleanup) 82 | 83 | re.RecordStart() 84 | re.RecordFailure(fmt.Errorf("bang")) 85 | re.RecordCrash(fmt.Errorf("terrible bang")) 86 | re.RecordMessage("i have something to %s", "say") 87 | re.RecordSuccess() 88 | 89 | if err := re.Close(); err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | file, err := os.OpenFile(re.TestOutputsPath+"/run.out", os.O_RDONLY, 0644) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | defer file.Close() 98 | 99 | require := require.New(t) 100 | 101 | var i int 102 | for dec := json.NewDecoder(file); dec.More(); { 103 | var m = struct { 104 | Event Event `json:"event"` 105 | }{} 106 | if err := dec.Decode(&m); err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | switch evt := m.Event; i { 111 | case 0: 112 | require.NotNil(evt.MessageEvent) 113 | require.Condition(func() bool { return strings.HasPrefix(evt.Message, "InfluxDB unavailable") }) 114 | case 1: 115 | require.NotNil(evt.StartEvent) 116 | require.Equal(evt.Runenv.TestPlan, re.TestPlan) 117 | require.Equal(evt.Runenv.TestCase, re.TestCase) 118 | require.Equal(evt.Runenv.TestRun, re.TestRun) 119 | require.Equal(evt.Runenv.TestGroupID, re.TestGroupID) 120 | case 2: 121 | require.NotNil(evt.FailureEvent) 122 | require.Equal("bang", evt.FailureEvent.Error) 123 | case 3: 124 | require.NotNil(evt.CrashEvent) 125 | require.Equal("terrible bang", evt.CrashEvent.Error) 126 | require.NotEmpty(evt.Stacktrace) 127 | case 4: 128 | require.NotNil(evt.MessageEvent) 129 | require.Equal(evt.Type(), "message_event") 130 | require.Equal("i have something to say", evt.MessageEvent.Message) 131 | case 5: 132 | require.NotNil(evt.SuccessEvent) 133 | } 134 | i++ 135 | } 136 | } 137 | 138 | func TestMetricsRecordedInFile(t *testing.T) { 139 | test := func(f func(*RunEnv) *MetricsApi, file string) func(t *testing.T) { 140 | return func(t *testing.T) { 141 | re, cleanup := RandomTestRunEnv(t) 142 | t.Cleanup(cleanup) 143 | 144 | api := f(re) 145 | 146 | names := []string{"point1", "point2", "counter1", "meter1", "timer1"} 147 | types := []string{"point", "counter", "meter", "timer"} 148 | api.SetFrequency(200 * time.Millisecond) 149 | api.RecordPoint("point1", 123) 150 | api.RecordPoint("point2", 123) 151 | api.Counter("counter1").Inc(50) 152 | api.Meter("meter1").Mark(50) 153 | api.Timer("timer1").Update(5 * time.Second) 154 | 155 | time.Sleep(1 * time.Second) 156 | 157 | _ = re.Close() 158 | 159 | file, err := os.OpenFile(filepath.Join(re.TestOutputsPath, file), os.O_RDONLY, 0644) 160 | if err != nil { 161 | t.Fatal(err) 162 | } 163 | defer file.Close() 164 | 165 | var metrics []*Metric 166 | for dec := json.NewDecoder(file); dec.More(); { 167 | var m *Metric 168 | if err := dec.Decode(&m); err != nil { 169 | t.Fatal(err) 170 | } 171 | metrics = append(metrics, m) 172 | } 173 | 174 | require := require.New(t) 175 | 176 | na := make(map[string]struct{}) 177 | ty := make(map[string]struct{}) 178 | for _, m := range metrics { 179 | require.Greater(m.Timestamp, int64(0)) 180 | na[m.Name] = struct{}{} 181 | ty[m.Type.String()] = struct{}{} 182 | require.NotZero(len(m.Measures)) 183 | } 184 | 185 | namesActual := make([]string, 0, len(na)) 186 | for k := range na { 187 | namesActual = append(namesActual, k) 188 | } 189 | 190 | typesActual := make([]string, 0, len(ty)) 191 | for k := range ty { 192 | typesActual = append(typesActual, k) 193 | } 194 | 195 | require.ElementsMatch(names, namesActual) 196 | require.ElementsMatch(types, typesActual) 197 | } 198 | } 199 | 200 | t.Run("diagnostics", test((*RunEnv).D, "diagnostics.out")) 201 | t.Run("results", test((*RunEnv).R, "results.out")) 202 | } 203 | 204 | func TestDiagnosticsDispatchedToInfluxDB(t *testing.T) { 205 | InfluxTestBatcher = true 206 | tc := &testClient{} 207 | TestInfluxDBClient = tc 208 | 209 | re, cleanup := RandomTestRunEnv(t) 210 | t.Cleanup(cleanup) 211 | 212 | re.D().RecordPoint("foo", 1234) 213 | re.D().RecordPoint("foo", 1234) 214 | re.D().RecordPoint("foo", 1234) 215 | re.D().RecordPoint("foo", 1234) 216 | 217 | require := require.New(t) 218 | 219 | tc.RLock() 220 | require.Len(tc.batchPoints, 4) 221 | tc.RUnlock() 222 | 223 | re.D().SetFrequency(500 * time.Millisecond) 224 | re.D().Counter("counter").Inc(100) 225 | re.D().Histogram("histogram1", re.D().NewUniformSample(100)).Update(123) 226 | 227 | time.Sleep(1500 * time.Millisecond) 228 | 229 | tc.RLock() 230 | if l := len(tc.batchPoints); l != 30 && l != 144 { 231 | t.Fatalf("expected length to be 30 or 144; was: %d", l) 232 | } 233 | tc.RUnlock() 234 | 235 | _ = re.Close() 236 | } 237 | 238 | func TestResultsDispatchedOnClose(t *testing.T) { 239 | InfluxTestBatcher = true 240 | tc := &testClient{} 241 | TestInfluxDBClient = tc 242 | 243 | re, cleanup := RandomTestRunEnv(t) 244 | t.Cleanup(cleanup) 245 | 246 | re.R().RecordPoint("foo", 1234) 247 | re.R().RecordPoint("foo", 1234) 248 | re.R().RecordPoint("foo", 1234) 249 | re.R().RecordPoint("foo", 1234) 250 | 251 | require := require.New(t) 252 | 253 | tc.RLock() 254 | require.Empty(tc.batchPoints) 255 | tc.RUnlock() 256 | 257 | re.R().SetFrequency(500 * time.Millisecond) 258 | re.R().Counter("counter").Inc(100) 259 | re.R().Histogram("histogram1", re.D().NewUniformSample(100)).Update(123) 260 | 261 | time.Sleep(1500 * time.Millisecond) 262 | 263 | tc.RLock() 264 | require.Empty(tc.batchPoints) 265 | tc.RUnlock() 266 | 267 | _ = re.Close() 268 | 269 | tc.RLock() 270 | require.NotEmpty(tc.batchPoints) 271 | tc.RUnlock() 272 | } 273 | 274 | func TestFrequencyChange(t *testing.T) { 275 | InfluxTestBatcher = true 276 | tc := &testClient{} 277 | TestInfluxDBClient = tc 278 | 279 | re, cleanup := RandomTestRunEnv(t) 280 | t.Cleanup(cleanup) 281 | 282 | // set an abnormally high frequency to verify that no points are produced. 283 | re.D().SetFrequency(24 * time.Hour) 284 | counter := re.D().Counter("foo") 285 | counter.Inc(100) 286 | 287 | require := require.New(t) 288 | 289 | time.Sleep(1500 * time.Millisecond) 290 | 291 | tc.RLock() 292 | require.Empty(tc.batchPoints) 293 | tc.RUnlock() 294 | 295 | re.D().SetFrequency(100 * time.Millisecond) 296 | time.Sleep(1000 * time.Millisecond) 297 | 298 | tc.RLock() 299 | require.Greater(len(tc.batchPoints), 5) 300 | tc.RUnlock() 301 | } 302 | 303 | func TestInvalidMetricName(t *testing.T) { 304 | InfluxTestBatcher = false 305 | tc := &testClient{} 306 | TestInfluxDBClient = tc 307 | 308 | re, cleanup := RandomTestRunEnv(t) 309 | t.Cleanup(cleanup) 310 | 311 | re.R().RecordPoint("foo,i_am_an_invalid_tag_because_i_have_no_value", 1234) 312 | re.R().RecordPoint("foo,i_am_an_invalid_tag_because_i_have_no_value,another,one_more", 1234) 313 | re.R().RecordPoint("foo,", 1234) 314 | 315 | require := require.New(t) 316 | 317 | tc.RLock() 318 | require.Empty(tc.batchPoints) 319 | tc.RUnlock() 320 | 321 | _ = re.Close() 322 | 323 | tc.RLock() 324 | require.NotEmpty(tc.batchPoints) 325 | require.Len(tc.batchPoints, 1) 326 | require.Len(tc.batchPoints[0].Points(), 3) 327 | tc.RUnlock() 328 | } 329 | -------------------------------------------------------------------------------- /runtime/runparams.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/dustin/go-humanize" 13 | "github.com/testground/sdk-go/ptypes" 14 | ) 15 | 16 | // RunParams encapsulates the runtime parameters for this test. 17 | type RunParams struct { 18 | TestPlan string `json:"plan"` 19 | TestCase string `json:"case"` 20 | TestRun string `json:"run"` 21 | 22 | TestRepo string `json:"repo,omitempty"` 23 | TestCommit string `json:"commit,omitempty"` 24 | TestBranch string `json:"branch,omitempty"` 25 | TestTag string `json:"tag,omitempty"` 26 | 27 | TestOutputsPath string `json:"outputs_path,omitempty"` 28 | TestTempPath string `json:"temp_path,omitempty"` 29 | 30 | TestInstanceCount int `json:"instances"` 31 | TestInstanceRole string `json:"role,omitempty"` 32 | TestInstanceParams map[string]string `json:"params,omitempty"` 33 | 34 | TestGroupID string `json:"group,omitempty"` 35 | TestGroupInstanceCount int `json:"group_instances,omitempty"` 36 | 37 | // true if the test has access to the sidecar. 38 | TestSidecar bool `json:"test_sidecar,omitempty"` 39 | 40 | // The subnet on which this test is running. 41 | // 42 | // The test instance can use this to pick an IP address and/or determine 43 | // the "data" network interface. 44 | // 45 | // This will be 127.1.0.0/16 when using the local exec runner. 46 | TestSubnet *ptypes.IPNet `json:"network,omitempty"` 47 | TestStartTime time.Time `json:"start_time,omitempty"` 48 | 49 | // TestCaptureProfiles lists the profile types to capture. These are 50 | // SDK-dependent. The Go SDK supports these profiles: 51 | // 52 | // * cpu => value ignored; CPU profile spans the entire life of the test. 53 | // * any supported profile type https://golang.org/pkg/runtime/pprof/#Profile => 54 | // value is a string representation of time.Duration, referring to 55 | // the frequency at which profiles will be captured. 56 | TestCaptureProfiles map[string]string `json:"capture_profiles,omitempty"` 57 | 58 | // TestDisableMetrics disables Influx batching. It is false by default. 59 | TestDisableMetrics bool `json:"disable_metrics,omitempty"` 60 | } 61 | 62 | // ParseRunParams parses a list of environment variables into a RunParams. 63 | func ParseRunParams(env []string) (*RunParams, error) { 64 | m, err := ParseKeyValues(env) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return &RunParams{ 70 | TestBranch: m[EnvTestBranch], 71 | TestCase: m[EnvTestCase], 72 | TestGroupID: m[EnvTestGroupID], 73 | TestGroupInstanceCount: toInt(m[EnvTestGroupInstanceCount]), 74 | TestInstanceCount: toInt(m[EnvTestInstanceCount]), 75 | TestInstanceParams: unpackParams(m[EnvTestInstanceParams]), 76 | TestInstanceRole: m[EnvTestInstanceRole], 77 | TestOutputsPath: m[EnvTestOutputsPath], 78 | TestTempPath: m[EnvTestTempPath], 79 | TestPlan: m[EnvTestPlan], 80 | TestRepo: m[EnvTestRepo], 81 | TestRun: m[EnvTestRun], 82 | TestSidecar: toBool(m[EnvTestSidecar]), 83 | TestStartTime: toTime(EnvTestStartTime), 84 | TestSubnet: toNet(m[EnvTestSubnet]), 85 | TestTag: m[EnvTestTag], 86 | TestCaptureProfiles: unpackParams(m[EnvTestCaptureProfiles]), 87 | TestDisableMetrics: toBool(m[EnvTestDisableMetrics]), 88 | }, nil 89 | } 90 | 91 | func (rp *RunParams) ToEnvVars() map[string]string { 92 | packParams := func(in map[string]string) string { 93 | if in == nil { 94 | return "" 95 | } 96 | arr := make([]string, 0, len(in)) 97 | for k, v := range in { 98 | arr = append(arr, k+"="+v) 99 | } 100 | return strings.Join(arr, "|") 101 | } 102 | 103 | out := map[string]string{ 104 | EnvTestBranch: rp.TestBranch, 105 | EnvTestCase: rp.TestCase, 106 | EnvTestGroupID: rp.TestGroupID, 107 | EnvTestGroupInstanceCount: strconv.Itoa(rp.TestGroupInstanceCount), 108 | EnvTestInstanceCount: strconv.Itoa(rp.TestInstanceCount), 109 | EnvTestInstanceParams: packParams(rp.TestInstanceParams), 110 | EnvTestInstanceRole: rp.TestInstanceRole, 111 | EnvTestOutputsPath: rp.TestOutputsPath, 112 | EnvTestTempPath: rp.TestTempPath, 113 | EnvTestPlan: rp.TestPlan, 114 | EnvTestRepo: rp.TestRepo, 115 | EnvTestRun: rp.TestRun, 116 | EnvTestSidecar: strconv.FormatBool(rp.TestSidecar), 117 | EnvTestStartTime: rp.TestStartTime.Format(time.RFC3339), 118 | EnvTestSubnet: rp.TestSubnet.String(), 119 | EnvTestTag: rp.TestTag, 120 | EnvTestCaptureProfiles: packParams(rp.TestCaptureProfiles), 121 | EnvTestDisableMetrics: strconv.FormatBool(rp.TestDisableMetrics), 122 | } 123 | 124 | return out 125 | } 126 | 127 | // IsParamSet checks if a certain parameter is set. 128 | func (rp *RunParams) IsParamSet(name string) bool { 129 | _, ok := rp.TestInstanceParams[name] 130 | return ok 131 | } 132 | 133 | // StringParam returns a string parameter, or "" if the parameter is not set. 134 | func (rp *RunParams) StringParam(name string) string { 135 | v, ok := rp.TestInstanceParams[name] 136 | if !ok { 137 | panic(fmt.Errorf("%s was not set", name)) 138 | } 139 | return v 140 | } 141 | 142 | func (rp *RunParams) SizeParam(name string) uint64 { 143 | v := rp.TestInstanceParams[name] 144 | m, err := humanize.ParseBytes(v) 145 | if err != nil { 146 | panic(err) 147 | } 148 | return m 149 | } 150 | 151 | // IntParam returns an int parameter, or -1 if the parameter is not set or 152 | // the conversion failed. It panics on error. 153 | func (rp *RunParams) IntParam(name string) int { 154 | v, ok := rp.TestInstanceParams[name] 155 | if !ok { 156 | panic(fmt.Errorf("%s was not set", name)) 157 | } 158 | 159 | i, err := strconv.Atoi(v) 160 | if err != nil { 161 | panic(err) 162 | } 163 | return i 164 | } 165 | 166 | // FloatParam returns a float64 parameter, or -1.0 if the parameter is not set or 167 | // the conversion failed. It panics on error. 168 | func (rp *RunParams) FloatParam(name string) float64 { 169 | v, ok := rp.TestInstanceParams[name] 170 | if !ok { 171 | return -1.0 172 | } 173 | 174 | f, err := strconv.ParseFloat(v, 32) 175 | if err != nil { 176 | panic(err) 177 | } 178 | return f 179 | } 180 | 181 | // BooleanParam returns the Boolean value of the parameter, or false if not passed 182 | func (rp *RunParams) BooleanParam(name string) bool { 183 | s, ok := rp.TestInstanceParams[name] 184 | return ok && strings.ToLower(s) == "true" 185 | } 186 | 187 | // StringArrayParam returns an array of string parameter, or an empty array 188 | // if it does not exist. It panics on error. 189 | func (rp *RunParams) StringArrayParam(name string) []string { 190 | var a []string 191 | rp.JSONParam(name, &a) 192 | return a 193 | } 194 | 195 | // SizeArrayParam returns an array of uint64 elements which represent sizes, 196 | // in bytes. If the response is nil, then there was an error parsing the input. 197 | // It panics on error. 198 | func (rp *RunParams) SizeArrayParam(name string) []uint64 { 199 | humanSizes := rp.StringArrayParam(name) 200 | var sizes []uint64 201 | 202 | for _, size := range humanSizes { 203 | n, err := humanize.ParseBytes(size) 204 | if err != nil { 205 | panic(err) 206 | } 207 | sizes = append(sizes, n) 208 | } 209 | 210 | return sizes 211 | } 212 | 213 | // PortNumber returns the port number assigned to the provided label, or falls 214 | // back to the default value if none is assigned. 215 | // 216 | // TODO: we're getting this directly from an environment variable. We may want 217 | // to unpack in RunParams first. 218 | func (rp *RunParams) PortNumber(label string, def string) string { 219 | v := strings.ToUpper(strings.TrimSpace(label)) + "_PORT" 220 | port, ok := os.LookupEnv(v) 221 | if !ok { 222 | return def 223 | } 224 | return port 225 | } 226 | 227 | // JSONParam unmarshals a JSON parameter in an arbitrary interface. 228 | // It panics on error. 229 | func (rp *RunParams) JSONParam(name string, v interface{}) { 230 | s, ok := rp.TestInstanceParams[name] 231 | if !ok { 232 | panic(fmt.Errorf("%s was not set", name)) 233 | } 234 | 235 | if err := json.Unmarshal([]byte(s), v); err != nil { 236 | panic(err) 237 | } 238 | } 239 | 240 | // Copied from github.com/ipfs/testground/pkg/conv, because we don't want the 241 | // SDK to depend on that package. 242 | func ParseKeyValues(in []string) (res map[string]string, err error) { 243 | res = make(map[string]string, len(in)) 244 | for _, d := range in { 245 | splt := strings.Split(d, "=") 246 | if len(splt) < 2 { 247 | return nil, fmt.Errorf("invalid key-value: %s", d) 248 | } 249 | res[splt[0]] = strings.Join(splt[1:], "=") 250 | } 251 | return res, nil 252 | } 253 | 254 | func unpackParams(packed string) map[string]string { 255 | spltparams := strings.Split(packed, "|") 256 | params := make(map[string]string, len(spltparams)) 257 | for _, s := range spltparams { 258 | v := strings.Split(s, "=") 259 | if len(v) != 2 { 260 | continue 261 | } 262 | params[v[0]] = v[1] 263 | } 264 | return params 265 | } 266 | 267 | func toInt(s string) int { 268 | v, err := strconv.Atoi(s) 269 | if err != nil { 270 | return -1 271 | } 272 | return v 273 | } 274 | 275 | func toBool(s string) bool { 276 | v, _ := strconv.ParseBool(s) 277 | return v 278 | } 279 | 280 | // toNet might parse any input, so it is possible to get an error and nil return value 281 | func toNet(s string) *ptypes.IPNet { 282 | _, ipnet, err := net.ParseCIDR(s) 283 | if err != nil { 284 | return nil 285 | } 286 | return &ptypes.IPNet{IPNet: *ipnet} 287 | } 288 | 289 | // Try to parse the time. 290 | // Failing to do so, return a zero value time 291 | func toTime(s string) time.Time { 292 | t, err := time.Parse(time.RFC3339, s) 293 | if err != nil { 294 | return time.Time{} 295 | } 296 | return t 297 | } 298 | -------------------------------------------------------------------------------- /runtime/test_utils.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math/rand" 7 | "net" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/testground/sdk-go/ptypes" 13 | ) 14 | 15 | // RandomTestRunEnv generates a random RunEnv for testing purposes. 16 | func RandomTestRunEnv(t *testing.T) (re *RunEnv, cleanup func()) { 17 | t.Helper() 18 | 19 | b := make([]byte, 32) 20 | _, _ = rand.Read(b) 21 | 22 | _, subnet, _ := net.ParseCIDR("127.1.0.1/16") 23 | 24 | odir, err := ioutil.TempDir("", "testground-tests-*") 25 | if err != nil { 26 | t.Fatalf("failed to create temp output dir: %s", err) 27 | } 28 | 29 | rp := RunParams{ 30 | TestPlan: fmt.Sprintf("testplan-%d", rand.Uint32()), 31 | TestSidecar: false, 32 | TestCase: fmt.Sprintf("testcase-%d", rand.Uint32()), 33 | TestRun: fmt.Sprintf("testrun-%d", rand.Uint32()), 34 | TestSubnet: &ptypes.IPNet{IPNet: *subnet}, 35 | TestInstanceCount: int(1 + (rand.Uint32() % 999)), 36 | TestInstanceRole: "", 37 | TestInstanceParams: make(map[string]string), 38 | TestGroupID: fmt.Sprintf("group-%d", rand.Uint32()), 39 | TestStartTime: time.Now(), 40 | TestGroupInstanceCount: int(1 + (rand.Uint32() % 999)), 41 | TestOutputsPath: odir, 42 | TestDisableMetrics: false, 43 | } 44 | 45 | return NewRunEnv(rp), func() { 46 | _ = os.RemoveAll(odir) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sync/client.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "sync" 8 | 9 | "github.com/testground/sdk-go/runtime" 10 | tgsync "github.com/testground/sync-service" 11 | "go.uber.org/zap" 12 | "nhooyr.io/websocket" 13 | ) 14 | 15 | const ( 16 | EnvServiceHost = "SYNC_SERVICE_HOST" 17 | EnvServicePort = "SYNC_SERVICE_PORT" 18 | ) 19 | 20 | // ErrNoRunParameters is returned by the generic client when an unbound context 21 | // is passed in. See WithRunParams to bind RunParams to the context. 22 | var ErrNoRunParameters = fmt.Errorf("no run parameters provided") 23 | 24 | type DefaultClient struct { 25 | *sugarOperations 26 | 27 | ctx context.Context 28 | cancel context.CancelFunc 29 | wg sync.WaitGroup 30 | log *zap.SugaredLogger 31 | extractor func(ctx context.Context) (rp *runtime.RunParams) 32 | 33 | nextMu sync.Mutex 34 | next int 35 | handlersMu sync.Mutex 36 | handlers map[string]chan *tgsync.Response 37 | socket *websocket.Conn 38 | } 39 | 40 | // NewBoundClient returns a new sync DefaultClient that is bound to the provided 41 | // RunEnv. All operations will be automatically scoped to the keyspace of that 42 | // run. 43 | // 44 | // The context passed in here will govern the lifecycle of the client. 45 | // Cancelling it will cancel all ongoing operations. However, for a clean 46 | // closure, the user should call Close(). 47 | // 48 | // For test plans, a suitable context to pass here is the background context. 49 | func NewBoundClient(ctx context.Context, runenv *runtime.RunEnv) (*DefaultClient, error) { 50 | log := runenv.SLogger() 51 | 52 | return newClient(ctx, log, func(ctx context.Context) *runtime.RunParams { 53 | return &runenv.RunParams 54 | }) 55 | } 56 | 57 | // MustBoundClient creates a new bound client by calling NewBoundClient, and 58 | // panicking if it errors. 59 | func MustBoundClient(ctx context.Context, runenv *runtime.RunEnv) *DefaultClient { 60 | c, err := NewBoundClient(ctx, runenv) 61 | if err != nil { 62 | panic(err) 63 | } 64 | return c 65 | } 66 | 67 | // NewGenericClient returns a new sync DefaultClient that is bound to no RunEnv. 68 | // It is intended to be used by testground services like the sidecar. 69 | // 70 | // All operations expect to find the RunParams of the run to scope its actions 71 | // inside the supplied context.Context. Call WithRunParams to bind the 72 | // appropriate RunParams. 73 | // 74 | // The context passed in here will govern the lifecycle of the client. 75 | // Cancelling it will cancel all ongoing operations. However, for a clean 76 | // closure, the user should call Close(). 77 | // 78 | // A suitable context to pass here is the background context of the main 79 | // process. 80 | func NewGenericClient(ctx context.Context, log *zap.SugaredLogger) (*DefaultClient, error) { 81 | return newClient(ctx, log, GetRunParams) 82 | } 83 | 84 | // MustGenericClient creates a new generic client by calling NewGenericClient, 85 | // and panicking if it errors. 86 | func MustGenericClient(ctx context.Context, log *zap.SugaredLogger) *DefaultClient { 87 | c, err := NewGenericClient(ctx, log) 88 | if err != nil { 89 | panic(err) 90 | } 91 | return c 92 | } 93 | 94 | // newClient creates a new sync client. 95 | func newClient(ctx context.Context, log *zap.SugaredLogger, extractor func(ctx context.Context) *runtime.RunParams) (*DefaultClient, error) { 96 | ctx, cancel := context.WithCancel(ctx) 97 | c := &DefaultClient{ 98 | ctx: ctx, 99 | cancel: cancel, 100 | log: log, 101 | extractor: extractor, 102 | handlers: map[string]chan *tgsync.Response{}, 103 | } 104 | 105 | c.sugarOperations = &sugarOperations{c} 106 | 107 | addr, err := socketAddress() 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | c.socket, _, err = websocket.Dial(ctx, addr, nil) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | c.wg.Add(1) 118 | go c.responsesWorker() 119 | 120 | return c, nil 121 | } 122 | 123 | // Close closes this client, cancels ongoing operations, and releases resources. 124 | func (c *DefaultClient) Close() error { 125 | err := c.socket.Close(websocket.StatusNormalClosure, "") 126 | if err != nil { 127 | return err 128 | } 129 | 130 | c.cancel() 131 | c.wg.Wait() 132 | return nil 133 | } 134 | 135 | func socketAddress() (string, error) { 136 | var ( 137 | port = os.Getenv(EnvServicePort) 138 | host = os.Getenv(EnvServiceHost) 139 | ) 140 | 141 | if port == "" { 142 | port = "5050" 143 | } 144 | 145 | if host == "" { 146 | host = "testground-sync-service" 147 | } 148 | 149 | return fmt.Sprintf("ws://%s:%s", host, port), nil 150 | } 151 | -------------------------------------------------------------------------------- /sync/client_conn.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strconv" 7 | "time" 8 | 9 | sync "github.com/testground/sync-service" 10 | "nhooyr.io/websocket" 11 | "nhooyr.io/websocket/wsjson" 12 | ) 13 | 14 | func (c *DefaultClient) nextID() (id string) { 15 | c.nextMu.Lock() 16 | id = strconv.Itoa(c.next) 17 | c.next++ 18 | c.nextMu.Unlock() 19 | return id 20 | } 21 | 22 | func (c *DefaultClient) responsesWorker() { 23 | for { 24 | res, err := c.readSocket() 25 | if err != nil { 26 | if errors.Is(err, context.Canceled) || 27 | errors.Is(c.ctx.Err(), context.Canceled) || 28 | websocket.CloseStatus(err) == websocket.StatusNormalClosure { 29 | break 30 | } 31 | 32 | c.log.Fatalw("error while reading socket", "error", err) 33 | } 34 | 35 | var ch chan *sync.Response 36 | c.handlersMu.Lock() 37 | ch = c.handlers[res.ID] 38 | c.handlersMu.Unlock() 39 | 40 | if ch == nil { 41 | c.log.Warnf("no handler available for response: %s", res.ID) 42 | } else { 43 | ch <- res 44 | } 45 | } 46 | 47 | c.wg.Done() 48 | } 49 | 50 | func (c *DefaultClient) makeRequest(ctx context.Context, req *sync.Request) (chan *sync.Response, error) { 51 | if c.ctx.Err() != nil { 52 | return nil, errors.New("tried to make request after context being cancelled") 53 | } 54 | 55 | if req.ID == "" { 56 | req.ID = c.nextID() 57 | } 58 | 59 | ch := make(chan *sync.Response) 60 | 61 | c.handlersMu.Lock() 62 | c.handlers[req.ID] = ch 63 | c.handlersMu.Unlock() 64 | 65 | err := c.writeSocket(req) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | c.wg.Add(1) 71 | 72 | go func() { 73 | // Wait for either of the contexts to fire. 74 | select { 75 | case <-c.ctx.Done(): 76 | case <-ctx.Done(): 77 | } 78 | 79 | c.handlersMu.Lock() 80 | close(c.handlers[req.ID]) 81 | delete(c.handlers, req.ID) 82 | c.handlersMu.Unlock() 83 | c.wg.Done() 84 | }() 85 | 86 | return ch, nil 87 | } 88 | 89 | func (c *DefaultClient) readSocket() (*sync.Response, error) { 90 | // After one hour without receiving information from the sync service, 91 | // the test will inevitably fail. Note(hacdias): consider changing 92 | // the timeout to a larger value in case slower tests fail. The same 93 | // value must be changed on the sync service side too. 94 | ctx, cancel := context.WithTimeout(c.ctx, time.Hour) 95 | defer cancel() 96 | 97 | var req *sync.Response 98 | err := wsjson.Read(ctx, c.socket, &req) 99 | if err != nil { 100 | return nil, err 101 | } 102 | if req == nil { 103 | return nil, errors.New("received nil from socket") 104 | } 105 | return req, err 106 | } 107 | 108 | func (c *DefaultClient) writeSocket(req *sync.Request) error { 109 | ctx, cancel := context.WithTimeout(c.ctx, time.Second) 110 | defer cancel() 111 | return wsjson.Write(ctx, c.socket, req) 112 | } 113 | -------------------------------------------------------------------------------- /sync/client_inmem.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "sync" 7 | 8 | "github.com/testground/sdk-go/runtime" 9 | ) 10 | 11 | type inmemClient struct { 12 | sync.Mutex 13 | *sugarOperations 14 | 15 | states map[State]int 16 | barriers map[State][]*Barrier 17 | subscriptions map[string][]reflect.Value 18 | published map[string][]interface{} 19 | } 20 | 21 | // NewInmemClient creates an in-memory sync client for testing. 22 | func NewInmemClient() *inmemClient { 23 | c := &inmemClient{ 24 | states: make(map[State]int), 25 | barriers: make(map[State][]*Barrier), 26 | subscriptions: make(map[string][]reflect.Value), 27 | published: make(map[string][]interface{}), 28 | } 29 | c.sugarOperations = &sugarOperations{c} 30 | return c 31 | } 32 | 33 | // Elemental operations 34 | // ==================== 35 | 36 | func (i *inmemClient) Publish(_ context.Context, topic *Topic, payload interface{}) (seq int64, err error) { 37 | i.Lock() 38 | defer i.Unlock() 39 | 40 | p, ok := i.published[topic.name] 41 | if !ok { 42 | p = make([]interface{}, 0, 10) 43 | } 44 | p = append(p, payload) 45 | i.published[topic.name] = p 46 | 47 | for _, ch := range i.subscriptions[topic.name] { 48 | ch.Send(reflect.ValueOf(payload)) 49 | } 50 | 51 | return int64(len(p)), nil 52 | } 53 | 54 | func (i *inmemClient) Subscribe(_ context.Context, topic *Topic, ch interface{}) (*Subscription, error) { 55 | i.Lock() 56 | defer i.Unlock() 57 | 58 | s, ok := i.subscriptions[topic.name] 59 | if !ok { 60 | s = make([]reflect.Value, 0, 10) 61 | } 62 | chV := reflect.ValueOf(ch) 63 | s = append(s, chV) 64 | i.subscriptions[topic.name] = s 65 | 66 | // replay any payloads for this topic. 67 | for _, p := range i.published[topic.name] { 68 | chV.Send(reflect.ValueOf(p)) 69 | } 70 | 71 | return &Subscription{}, nil 72 | } 73 | 74 | func (i *inmemClient) Barrier(_ context.Context, state State, target int) (*Barrier, error) { 75 | b := &Barrier{ 76 | C: make(chan error, 1), 77 | target: int64(target), 78 | } 79 | 80 | i.Lock() 81 | defer i.Unlock() 82 | 83 | if i.states[state] == target { 84 | b.C <- nil 85 | close(b.C) 86 | return b, nil 87 | } 88 | 89 | barriers, ok := i.barriers[state] 90 | if !ok { 91 | barriers = make([]*Barrier, 0, 10) 92 | } 93 | barriers = append(barriers, b) 94 | i.barriers[state] = barriers 95 | 96 | return b, nil 97 | } 98 | 99 | func (i *inmemClient) SignalEntry(_ context.Context, state State) (after int64, err error) { 100 | i.Lock() 101 | defer i.Unlock() 102 | 103 | i.states[state]++ 104 | 105 | v := int64(i.states[state]) 106 | 107 | var idx int 108 | for _, b := range i.barriers[state] { 109 | if v == b.target { 110 | b.C <- nil 111 | close(b.C) 112 | continue 113 | } 114 | i.barriers[state][idx] = b 115 | idx++ 116 | } 117 | 118 | i.barriers[state] = i.barriers[state][:idx] 119 | 120 | return v, nil 121 | } 122 | 123 | func (i *inmemClient) SignalEvent(_ context.Context, event *runtime.Event) error { 124 | return nil 125 | } 126 | 127 | func (i *inmemClient) Close() error { 128 | return nil 129 | } 130 | -------------------------------------------------------------------------------- /sync/client_pubsub.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | 8 | sync "github.com/testground/sync-service" 9 | ) 10 | 11 | func (c *DefaultClient) publish(ctx context.Context, topic string, payload interface{}) (int64, error) { 12 | ctx, cancel := context.WithCancel(ctx) 13 | defer cancel() 14 | 15 | ch, err := c.makeRequest(ctx, &sync.Request{ 16 | PublishRequest: &sync.PublishRequest{ 17 | Topic: topic, 18 | Payload: payload, 19 | }, 20 | }) 21 | if err != nil { 22 | return -1, err 23 | } 24 | 25 | res, ok := <-ch 26 | if !ok { 27 | return -1, errors.New("channel closed before getting response") 28 | } 29 | if res.Error != "" { 30 | return -1, errors.New(res.Error) 31 | } 32 | 33 | return int64(res.PublishResponse.Seq), nil 34 | } 35 | 36 | func (c *DefaultClient) subscribe(ctx context.Context, key string, deref bool, val *typeValidator, ch interface{}) (sub *Subscription, err error) { 37 | if err := ctx.Err(); err != nil { 38 | return nil, err 39 | } 40 | 41 | if err := c.ctx.Err(); err != nil { 42 | return nil, err 43 | } 44 | 45 | ctx, cancel := context.WithCancel(ctx) 46 | 47 | // sendFn is a closure that sends an element into the supplied ch, 48 | // performing necessary pointer to value conversions if necessary. 49 | // 50 | // sendFn will block if the receiver is not consuming from the channel. 51 | // If the context is closed, the send will be aborted, and the closure will 52 | // return a false value. 53 | sendFn := func(v reflect.Value) (sent bool) { 54 | if deref { 55 | v = v.Elem() 56 | } 57 | cases := []reflect.SelectCase{ 58 | {Dir: reflect.SelectSend, Chan: reflect.ValueOf(ch), Send: v}, 59 | {Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ctx.Done())}, 60 | } 61 | _, _, ctxFired := reflect.Select(cases) 62 | return !ctxFired 63 | } 64 | 65 | req := &sync.Request{ 66 | SubscribeRequest: &sync.SubscribeRequest{ 67 | Topic: key, 68 | }, 69 | } 70 | 71 | resCh, err := c.makeRequest(ctx, req) 72 | if err != nil { 73 | cancel() 74 | return nil, err 75 | } 76 | 77 | sub = &Subscription{make(chan error, 1)} 78 | 79 | go func() { 80 | defer cancel() 81 | 82 | for { 83 | select { 84 | case <-c.ctx.Done(): 85 | sub.doneCh <- nil 86 | close(sub.doneCh) 87 | return 88 | case <-ctx.Done(): 89 | sub.doneCh <- nil 90 | close(sub.doneCh) 91 | return 92 | case res, ok := <-resCh: 93 | if !ok { 94 | // Channel closed. 95 | sub.doneCh <- nil 96 | close(sub.doneCh) 97 | return 98 | } 99 | if res.Error != "" { 100 | sub.doneCh <- errors.New(res.Error) 101 | close(sub.doneCh) 102 | return 103 | } 104 | 105 | val, err := val.decodePayload(res.SubscribeResponse) 106 | if err != nil { 107 | c.log.Debugw("XREAD response: failed to decode message", "key", key, "error", err, "id", req.ID) 108 | continue 109 | } 110 | 111 | c.log.Debugw("dispatching message to subscriber", "key", key, "id", req.ID) 112 | if sent := sendFn(val); !sent { 113 | // we could not send value because context fired. 114 | // skip all further messages on this stream, and queue for 115 | // removal. 116 | c.log.Debugw("context was closed when dispatching message to subscriber; rm subscription", "key", key, "id", req.ID) 117 | sub.doneCh <- nil 118 | close(sub.doneCh) 119 | return 120 | } 121 | } 122 | } 123 | }() 124 | 125 | return sub, err 126 | } 127 | -------------------------------------------------------------------------------- /sync/client_state.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | 9 | "github.com/testground/sdk-go/runtime" 10 | sync "github.com/testground/sync-service" 11 | ) 12 | 13 | // Barrier sets a barrier on the supplied State that fires when it reaches its 14 | // target value (or higher). 15 | // 16 | // The caller should monitor the channel C returned inside the Barrier object. 17 | // It fires when the barrier reaches its target, is cancelled, or fails. 18 | // If the barrier is satisfied, the value sent will be nil. C must not be closed 19 | // by the caller. 20 | // 21 | // When the context fires, the context's error will be propagated instead. The 22 | // same will occur if the DefaultClient's context fires. 23 | // 24 | // It is safe to use a non-cancellable context here, like the background 25 | // context. No cancellation is needed unless you want to stop the process early. 26 | func (c *DefaultClient) Barrier(ctx context.Context, state State, target int) (*Barrier, error) { 27 | // a barrier with target zero is satisfied immediately; log a warning as 28 | // this is probably programmer error. 29 | if target == 0 { 30 | c.log.Warnw("requested a barrier with target zero; satisfying immediately", "state", state) 31 | b := &Barrier{C: make(chan error, 1)} 32 | b.C <- nil 33 | close(b.C) 34 | return b, nil 35 | } 36 | 37 | rp := c.extractor(ctx) 38 | if rp == nil { 39 | return nil, ErrNoRunParameters 40 | } 41 | 42 | key := state.Key(rp) 43 | 44 | ctx, cancel := context.WithCancel(ctx) 45 | 46 | ch, err := c.makeRequest(ctx, &sync.Request{ 47 | BarrierRequest: &sync.BarrierRequest{ 48 | State: key, 49 | Target: target, 50 | }, 51 | }) 52 | if err != nil { 53 | cancel() 54 | return nil, err 55 | } 56 | 57 | b := &Barrier{ 58 | C: make(chan error, 1), 59 | } 60 | 61 | go func() { 62 | res, ok := <-ch 63 | if !ok { 64 | b.C <- errors.New("channel closed before getting response") 65 | } else if res.Error == "" { 66 | b.C <- nil 67 | } else { 68 | b.C <- errors.New(res.Error) 69 | } 70 | 71 | cancel() 72 | }() 73 | 74 | return b, nil 75 | } 76 | 77 | // SignalEntry increments the state counter by one, returning the value of the 78 | // new value of the counter, or an error if the operation fails. 79 | func (c *DefaultClient) SignalEntry(ctx context.Context, state State) (int64, error) { 80 | rp := c.extractor(ctx) 81 | if rp == nil { 82 | return -1, ErrNoRunParameters 83 | } 84 | 85 | key := state.Key(rp) 86 | 87 | c.log.Debugw("signalling entry to state", "key", key) 88 | 89 | ctx, cancel := context.WithCancel(ctx) 90 | defer cancel() 91 | 92 | // Increment a counter on the state key. 93 | ch, err := c.makeRequest(ctx, &sync.Request{ 94 | SignalEntryRequest: &sync.SignalEntryRequest{ 95 | State: key, 96 | }, 97 | }) 98 | if err != nil { 99 | return -1, err 100 | } 101 | 102 | res, ok := <-ch 103 | if !ok { 104 | return -1, errors.New("channel closed before getting response") 105 | } 106 | if res.Error != "" { 107 | return -1, errors.New(res.Error) 108 | } 109 | 110 | c.log.Debugw("new value of state", "key", key, "value", res.SignalEntryResponse.Seq) 111 | return int64(res.SignalEntryResponse.Seq), nil 112 | } 113 | 114 | // SignalEvent emits an event attached to a certain test plan. 115 | func (c *DefaultClient) SignalEvent(ctx context.Context, event *runtime.Event) (err error) { 116 | rp := c.extractor(ctx) 117 | if rp == nil { 118 | return ErrNoRunParameters 119 | } 120 | 121 | key := fmt.Sprintf("run:%s:plan:%s:case:%s:run_events", rp.TestRun, rp.TestPlan, rp.TestCase) 122 | _, err = c.publish(ctx, key, event) 123 | return err 124 | } 125 | 126 | // SubscribeEvents monitors the events sent by a specific test plan. This function is used by Testground 127 | // to monitor all emitted events by the testplans, in particular the terminal events, such as SuccessEvent, 128 | // FailureEvent and CrashEvent. 129 | func (c *DefaultClient) SubscribeEvents(ctx context.Context, rp *runtime.RunParams) (chan *runtime.Event, error) { 130 | ch := make(chan *runtime.Event) 131 | key := fmt.Sprintf("run:%s:plan:%s:case:%s:run_events", rp.TestRun, rp.TestPlan, rp.TestCase) 132 | 133 | _, err := c.subscribe(ctx, key, false, &typeValidator{reflect.TypeOf(&runtime.Event{})}, ch) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | return ch, nil 139 | } 140 | -------------------------------------------------------------------------------- /sync/client_sugar.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | type sugarOperations struct { 9 | Client 10 | } 11 | 12 | // PublishAndWait composes Publish and a Barrier. It first publishes the 13 | // provided payload to the specified topic, then awaits for a barrier on the 14 | // supplied state to reach the indicated target. 15 | // 16 | // If any operation fails, PublishAndWait short-circuits and returns a non-nil 17 | // error and a negative sequence. If Publish succeeds, but the Barrier fails, 18 | // the seq number will be greater than zero. 19 | func (c *sugarOperations) PublishAndWait(ctx context.Context, topic *Topic, payload interface{}, state State, target int) (seq int64, err error) { 20 | seq, err = c.Publish(ctx, topic, payload) 21 | if err != nil { 22 | return -1, err 23 | } 24 | 25 | b, err := c.Barrier(ctx, state, target) 26 | if err != nil { 27 | return seq, err 28 | } 29 | 30 | <-b.C 31 | return seq, err 32 | } 33 | 34 | // MustPublishAndWait calls PublishAndWait, panicking if it errors. 35 | // 36 | // Suitable for shorthanding in test plans. 37 | func (c *sugarOperations) MustPublishAndWait(ctx context.Context, topic *Topic, payload interface{}, state State, target int) (seq int64) { 38 | seq, err := c.PublishAndWait(ctx, topic, payload, state, target) 39 | if err != nil { 40 | panic(err) 41 | } 42 | return seq 43 | } 44 | 45 | // PublishSubscribe publishes the payload on the supplied Topic, then subscribes 46 | // to it, sending paylods to the supplied channel. 47 | // 48 | // If any operation fails, PublishSubscribe short-circuits and returns a non-nil 49 | // error and a negative sequence. If Publish succeeds, but Subscribe fails, 50 | // the seq number will be greater than zero, but the returned Subscription will 51 | // be nil, and the error, non-nil. 52 | func (c *sugarOperations) PublishSubscribe(ctx context.Context, topic *Topic, payload interface{}, ch interface{}) (seq int64, sub *Subscription, err error) { 53 | seq, err = c.Publish(ctx, topic, payload) 54 | if err != nil { 55 | return -1, nil, err 56 | } 57 | sub, err = c.Subscribe(ctx, topic, ch) 58 | if err != nil { 59 | return seq, nil, err 60 | } 61 | return seq, sub, err 62 | } 63 | 64 | // MustPublishSubscribe calls PublishSubscribe, panicking if it errors. 65 | // 66 | // Suitable for shorthanding in test plans. 67 | func (c *sugarOperations) MustPublishSubscribe(ctx context.Context, topic *Topic, payload interface{}, ch interface{}) (seq int64, sub *Subscription) { 68 | seq, sub, err := c.PublishSubscribe(ctx, topic, payload, ch) 69 | if err != nil { 70 | panic(err) 71 | } 72 | return seq, sub 73 | } 74 | 75 | // SignalAndWait composes SignalEntry and Barrier, signalling entry on the 76 | // supplied state, and then awaiting until the required value has been reached. 77 | // 78 | // The returned error will be nil if the barrier was met successfully, 79 | // or non-nil if the context expired, or some other error ocurred. 80 | func (c *sugarOperations) SignalAndWait(ctx context.Context, state State, target int) (seq int64, err error) { 81 | // rp := c.extractor(ctx) 82 | // if rp == nil { 83 | // return -1, ErrNoRunParameters 84 | // } 85 | 86 | seq, err = c.SignalEntry(ctx, state) 87 | if err != nil { 88 | return -1, fmt.Errorf("failed while signalling entry to state %s: %w", state, err) 89 | } 90 | 91 | b, err := c.Barrier(ctx, state, target) 92 | if err != nil { 93 | return -1, fmt.Errorf("failed while setting barrier for state %s, with target %d: %w", state, target, err) 94 | } 95 | return seq, <-b.C 96 | } 97 | 98 | // MustSignalAndWait calls SignalAndWait, panicking if it errors. 99 | // 100 | // Suitable for shorthanding in test plans. 101 | func (c *sugarOperations) MustSignalAndWait(ctx context.Context, state State, target int) (seq int64) { 102 | seq, err := c.SignalAndWait(ctx, state, target) 103 | if err != nil { 104 | panic(err) 105 | } 106 | return seq 107 | } 108 | 109 | // MustSignalEntry calls SignalEntry, panicking if it errors. 110 | // 111 | // Suitable for shorthanding in test plans. 112 | func (c *sugarOperations) MustSignalEntry(ctx context.Context, state State) (current int64) { 113 | current, err := c.SignalEntry(ctx, state) 114 | if err != nil { 115 | panic(err) 116 | } 117 | return current 118 | } 119 | 120 | // MustBarrier calls Barrier, panicking if it errors. 121 | // 122 | // Suitable for shorthanding in test plans. 123 | func (c *sugarOperations) MustBarrier(ctx context.Context, state State, required int) *Barrier { 124 | b, err := c.Barrier(ctx, state, required) 125 | if err != nil { 126 | panic(err) 127 | } 128 | return b 129 | } 130 | 131 | // MustPublish calls Publish, panicking if it errors. 132 | // 133 | // Suitable for shorthanding in test plans. 134 | func (c *sugarOperations) MustPublish(ctx context.Context, topic *Topic, payload interface{}) (seq int64) { 135 | seq, err := c.Publish(ctx, topic, payload) 136 | if err != nil { 137 | panic(err) 138 | } 139 | return seq 140 | } 141 | 142 | // MustSubscribe calls Subscribe, panicking if it errors. 143 | // 144 | // Suitable for shorthanding in test plans. 145 | func (c *sugarOperations) MustSubscribe(ctx context.Context, topic *Topic, ch interface{}) (sub *Subscription) { 146 | sub, err := c.Subscribe(ctx, topic, ch) 147 | if err != nil { 148 | panic(err) 149 | } 150 | return sub 151 | } 152 | -------------------------------------------------------------------------------- /sync/client_topic.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | // Publish publishes an item on the supplied topic. The payload type must match 10 | // the payload type on the Topic; otherwise Publish will error. 11 | // 12 | // This method returns synchronously, once the item has been published 13 | // successfully, returning the sequence number of the new item in the ordered 14 | // topic, or an error if one occurred, starting with 1 (for the first item). 15 | // 16 | // If error is non-nil, the sequence number must be disregarded. 17 | func (c *DefaultClient) Publish(ctx context.Context, topic *Topic, payload interface{}) (int64, error) { 18 | rp := c.extractor(ctx) 19 | if rp == nil { 20 | return -1, ErrNoRunParameters 21 | } 22 | 23 | log := c.log.With("topic", topic.name) 24 | log.Debugw("publishing item on topic", "payload", payload) 25 | 26 | if !topic.validatePayload(payload) { 27 | err := fmt.Errorf("invalid payload type; expected: [*]%s, was: %T", topic.typ, payload) 28 | return -1, err 29 | } 30 | 31 | key := topic.Key(rp) 32 | log.Debugw("resolved key for publish", "key", key) 33 | 34 | seq, err := c.publish(ctx, key, payload) 35 | if err != nil { 36 | return -1, err 37 | } 38 | 39 | c.log.Debugw("successfully published item; sequence number obtained", "seq", seq) 40 | return seq, nil 41 | } 42 | 43 | // Subscribe subscribes to a topic, consuming ordered, typed elements from 44 | // index 0, and sending them to channel ch. 45 | // 46 | // The supplied channel must be buffered, and its type must be a value or 47 | // pointer type matching the topic type. If these conditions are unmet, this 48 | // method will error immediately. 49 | // 50 | // The caller must consume from this channel promptly; failure to do so will 51 | // backpressure the DefaultClient's subscription event loop. 52 | func (c *DefaultClient) Subscribe(ctx context.Context, topic *Topic, ch interface{}) (sub *Subscription, err error) { 53 | rp := c.extractor(ctx) 54 | if rp == nil { 55 | return nil, ErrNoRunParameters 56 | } 57 | 58 | chv := reflect.ValueOf(ch) 59 | if k := chv.Kind(); k != reflect.Chan { 60 | return nil, fmt.Errorf("value is not a channel: %T", ch) 61 | } 62 | 63 | // compare the naked types; this makes the subscription work with pointer 64 | // or value channels. 65 | var deref bool 66 | chtyp := chv.Type().Elem() 67 | if chtyp.Kind() == reflect.Ptr { 68 | chtyp = chtyp.Elem() 69 | } else { 70 | deref = true 71 | } 72 | 73 | return c.subscribe(ctx, topic.Key(rp), deref, topic.typeValidator, ch) 74 | } 75 | -------------------------------------------------------------------------------- /sync/context.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/testground/sdk-go/runtime" 7 | ) 8 | 9 | type runparamsCtxKey struct{} 10 | 11 | var runparams = runparamsCtxKey{} 12 | 13 | // WithRunParams returns a context that embeds the supplied RunParams, 14 | // such that it can be passed to a GenericClient. 15 | func WithRunParams(ctx context.Context, rp *runtime.RunParams) context.Context { 16 | return context.WithValue(ctx, runparams, rp) 17 | } 18 | 19 | // GetRunParams extracts the RunParams from a context, previously set by calling 20 | // WithRunParams. 21 | func GetRunParams(ctx context.Context) *runtime.RunParams { 22 | v := ctx.Value(runparams) 23 | if v == nil { 24 | return nil 25 | } 26 | return v.(*runtime.RunParams) 27 | } 28 | -------------------------------------------------------------------------------- /sync/doc.go: -------------------------------------------------------------------------------- 1 | // The sync package contains the distributed coordination and choreography 2 | // facility of Testground. 3 | // 4 | // The sync service is lightweight, and uses Redis recipes to implement 5 | // coordination primitives like barriers, signalling, and pubsub. Additional 6 | // primitives like locks, semaphores, etc. are in scope, and may be added in the 7 | // future. 8 | // 9 | // Constructing sync.Clients 10 | // 11 | // To use the sync service, test plan writers must create a sync.DefaultClient via the 12 | // sync.NewBoundClient constructor, passing a context that governs the lifetime 13 | // of the sync.DefaultClient, as well as a runtime.RunEnv to bind to. All sync 14 | // operations will be automatically scoped/namespaced to the runtime.RunEnv. 15 | // 16 | // Infrastructure services, such as sidecar instances, can create generic 17 | // sync.Clients via the sync.NewGenericClient constructor. Such clients are not 18 | // bound/constrained to a runtime.RunEnv, and instead are required to pass in 19 | // runtime.RunParams in the context.Context to all operations. See WithRunParams 20 | // for more info. 21 | // 22 | // Recommendations for test plan writers 23 | // 24 | // All constructors and methods on sync.DefaultClient have Must* versions, which panic 25 | // if an error occurs. Using these methods in combination with runtime.Invoke 26 | // is safe, as the runner captures panics and records them as test crashes. The 27 | // resulting code will be less pedantic. 28 | // 29 | // We have added sugar methods that compose basic primitives into frequently 30 | // used katas, such as client.PublishSubscribe, client.SignalAndWait, 31 | // client.PublishAndWait, etc. These katas also have Must* variations. We 32 | // encourage developers to adopt them in order to streamline their code. 33 | // 34 | // Garbage collection 35 | // 36 | // The sync service is decentralised: it has no centralised actor, dispatcher, 37 | // or coordinator that supervises the lifetime of a test. All participants in a 38 | // test hit Redis directly, using its operations to implement the sync 39 | // primitives. As a result, keys from past runs can accumulate. 40 | // 41 | // Sync clients can participate in collaborative garbage collection by enabling 42 | // background GC: 43 | // 44 | // client.EnableBackgroundGC(ch) // see method godoc for info on ch 45 | // 46 | // GC uses SCAN and OBJECT IDLETIME operations to find keys to purge, and its 47 | // configuration is controlled by the GC* variables. 48 | // 49 | // In the standard testground architecture, only sidecar processes are 50 | // participate in GC: 51 | package sync 52 | -------------------------------------------------------------------------------- /sync/interface.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/testground/sdk-go/runtime" 8 | ) 9 | 10 | type Client interface { 11 | io.Closer 12 | 13 | Publish(ctx context.Context, topic *Topic, payload interface{}) (seq int64, err error) 14 | Subscribe(ctx context.Context, topic *Topic, ch interface{}) (*Subscription, error) 15 | PublishAndWait(ctx context.Context, topic *Topic, payload interface{}, state State, target int) (seq int64, err error) 16 | PublishSubscribe(ctx context.Context, topic *Topic, payload interface{}, ch interface{}) (seq int64, sub *Subscription, err error) 17 | 18 | Barrier(ctx context.Context, state State, target int) (*Barrier, error) 19 | SignalEntry(ctx context.Context, state State) (after int64, err error) 20 | SignalAndWait(ctx context.Context, state State, target int) (seq int64, err error) 21 | 22 | MustBarrier(ctx context.Context, state State, target int) *Barrier 23 | MustSignalEntry(ctx context.Context, state State) int64 24 | MustSubscribe(ctx context.Context, topic *Topic, ch interface{}) *Subscription 25 | MustPublish(ctx context.Context, topic *Topic, payload interface{}) (seq int64) 26 | 27 | MustPublishAndWait(ctx context.Context, topic *Topic, payload interface{}, state State, target int) (seq int64) 28 | MustPublishSubscribe(ctx context.Context, topic *Topic, payload interface{}, ch interface{}) (seq int64, sub *Subscription) 29 | MustSignalAndWait(ctx context.Context, state State, target int) (seq int64) 30 | 31 | SignalEvent(context.Context, *runtime.Event) error 32 | } 33 | -------------------------------------------------------------------------------- /sync/types.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/testground/sdk-go/runtime" 9 | ) 10 | 11 | // State represents a state in a distributed state machine, identified by a 12 | // unique string within the test case. 13 | type State string 14 | 15 | // Key gets the Redis key for this State, contextualized to a set of RunParams. 16 | func (s State) Key(rp *runtime.RunParams) string { 17 | p := fmt.Sprintf("run:%s:plan:%s:case:%s:states:%s", rp.TestRun, rp.TestPlan, rp.TestCase, string(s)) 18 | return p 19 | } 20 | 21 | // Barrier represents a barrier over a State. A Barrier is a synchronisation 22 | // checkpoint that will fire once the `target` number of entries on that state 23 | // have been registered. 24 | type Barrier struct { 25 | C chan error 26 | target int64 // Only kept for client_inmem.go 27 | } 28 | 29 | // Topic represents a meeting place for test instances to exchange arbitrary 30 | // data. 31 | type Topic struct { 32 | name string 33 | *typeValidator 34 | } 35 | 36 | // NewTopic constructs a Topic with the provided name, and the type of the 37 | // supplied value, derived via reflect.TypeOf, unless the supplied value is 38 | // already a reflect.Type. This method does not retain actual value from which 39 | // the type is derived. 40 | func NewTopic(name string, typ interface{}) *Topic { 41 | t, ok := typ.(reflect.Type) 42 | if !ok { 43 | t = reflect.TypeOf(typ) 44 | } 45 | return &Topic{ 46 | name: name, 47 | typeValidator: &typeValidator{t}, 48 | } 49 | } 50 | 51 | // Key gets the key for this Topic, contextualized to a set of RunParams. 52 | func (t Topic) Key(rp *runtime.RunParams) string { 53 | p := fmt.Sprintf("run:%s:plan:%s:case:%s:topics:%s", rp.TestRun, rp.TestPlan, rp.TestCase, t.name) 54 | return p 55 | } 56 | 57 | type typeValidator struct { 58 | typ reflect.Type 59 | } 60 | 61 | func (t typeValidator) validatePayload(val interface{}) bool { 62 | ttyp, vtyp := t.typ, reflect.TypeOf(val) 63 | if ttyp.Kind() == reflect.Ptr { 64 | ttyp = ttyp.Elem() 65 | } 66 | if vtyp.Kind() == reflect.Ptr { 67 | vtyp = vtyp.Elem() 68 | } 69 | return ttyp == vtyp 70 | } 71 | 72 | // decodePayload extracts a value of the specified type from incoming json. 73 | func (t typeValidator) decodePayload(val interface{}) (reflect.Value, error) { 74 | // Deserialize the value. 75 | typ := t.typ 76 | if typ.Kind() == reflect.Ptr { 77 | typ = typ.Elem() 78 | } 79 | payload := reflect.New(typ) 80 | raw, ok := val.(string) 81 | if !ok { 82 | panic("payload not a string") 83 | } 84 | if err := json.Unmarshal([]byte(raw), payload.Interface()); err != nil { 85 | return reflect.Value{}, fmt.Errorf("failed to decode as type %s: %s", t.typ, raw) 86 | } 87 | return payload, nil 88 | } 89 | 90 | // Subscription represents a receive channel for data being published in a 91 | // Topic. 92 | type Subscription struct { 93 | doneCh chan error 94 | } 95 | 96 | func (s *Subscription) Done() <-chan error { 97 | return s.doneCh 98 | } 99 | -------------------------------------------------------------------------------- /verbose.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | // Verbose indicates whether the logs in verbose mode (default: false). 4 | var Verbose = false 5 | --------------------------------------------------------------------------------