diff --git a/README.md b/README.md index d7b8101c..c042959a 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,23 @@ Hatchery creates Kubernetes Pods for workspace services. Workspace services must expose HTTP servers. Ambassador is used to proxy user traffic through to their container workspace once it is launched by Hatchery. -## Documentation TOC - -See [this bLog](https://www.divio.com/blog/documentation/) for an introduction to the different types of documentation (explanation, how-to, tutorial, reference). +## Documentation ### Explanation -* [hatchery overview](doc/explanation/hatcheryOverview.md) -* [api](doc/explanation/hatcheryApi.md) -* [dockstore apps](doc/explanation/dockstore.md) +* [Hatchery overview](doc/explanation/hatcheryOverview.md) +* [API documentation](doc/explanation/hatcheryApi.md) +* [Configuring Dockstore apps](doc/explanation/dockstore.md) ### How-to -* [dev-test](doc/howto/devTest.md) -* [gen3-fuse](doc/howto/fuseSidecar.md) -* [Jupyter Notebook](doc/howto/jupyterNotebook.md) -* [R Studio](doc/howto/rStudio.md) -* [Galaxy](doc/howto/galaxy.md) +* [Hatchery configuration](doc/howto/configuration.md) +* [Quick start for local development](doc/howto/quick_start.md) +* [Run the tests locally](doc/howto/devTest.md) +* [Configuring gen3-fuse](doc/howto/fuseSidecar.md) +* Configuring workspaces: + * [Jupyter](doc/howto/jupyterNotebook.md) + * [RStudio](doc/howto/rStudio.md) + * [Galaxy](doc/howto/galaxy.md) + * [noNVC Firefox](doc/howto/noVNCFirefox.md) ### Tutorials diff --git a/doc/howto/configuration.md b/doc/howto/configuration.md index c601acd0..498f7f7a 100644 --- a/doc/howto/configuration.md +++ b/doc/howto/configuration.md @@ -35,6 +35,7 @@ An example manifest entry may look like "user-uid": 1000, "fs-gid": 100, "user-volume-location": "/home/jovyan/pd", + "gen3-volume-location": "/home/jovyan/.gen3" "friends": [] }] } @@ -66,7 +67,8 @@ An example manifest entry may look like * `ready-probe` the path to use for the Kubernetes readiness probe. * `user-uid` the UID for the user in this container. * `fs-gid` the GID for the filesystem mounts. - * `user-volume-location` the location where the user persistant storage should be mounted in this container. + * `user-volume-location` the location where the user persistent storage should be mounted in this container. + * `gen3-volume-location` the location where the user's API key file will be put into * `lifecycle-pre-stop` a string array as the container prestop command. * `lifecycle-post-start` a string array as the container poststart command. * `friends` is a list of kubernetes containers to deploy alongside the main container and the sidecar in the kubernetes pod diff --git a/doc/howto/jupyterNotebook.md b/doc/howto/jupyterNotebook.md index ad0514dc..6dcaf66c 100644 --- a/doc/howto/jupyterNotebook.md +++ b/doc/howto/jupyterNotebook.md @@ -40,6 +40,7 @@ Jupyter lab is the successor to jupyter notebook. "lifecycle-post-start": ["/bin/sh","-c","export IAM=`whoami`; rm -rf /home/$IAM/pd/dockerHome; ln -s $(pwd) /home/$IAM/pd/dockerHome; mkdir -p /home/$IAM/.jupyter/custom; echo \"define(['base/js/namespace'], function(Jupyter){Jupyter._target = '_self';})\" >/home/$IAM/.jupyter/custom/custom.js; ln -s /data /home/$IAM/pd/; true"], "user-uid": 1000, "fs-gid": 100, - "user-volume-location": "/home/jovyan/pd" + "user-volume-location": "/home/jovyan/pd", + "gen3-volume-location": "/home/jovyan/.gen3" }, ``` diff --git a/doc/howto/noVNCFirefox.md b/doc/howto/noVNCFirefox.md index 35ae3318..1664f191 100644 --- a/doc/howto/noVNCFirefox.md +++ b/doc/howto/noVNCFirefox.md @@ -63,6 +63,7 @@ In the main container's manifest, specify "export IAM=`whoami`; rm -rf /home/$IAM/pd/dockerHome; ln -s $(pwd) /home/$IAM/pd/dockerHome; mkdir -p /home/$IAM/.jupyter/custom; echo \"define(['base/js/namespace'], function(Jupyter){Jupyter._target = '_self';})\" >/home/$IAM/.jupyter/custom/custom.js; ln -s /data /home/$IAM/pd/; true" ], "user-volume-location": "/home/jovyan/pd", + "gen3-volume-location": "/home/jovyan/.gen3" "use-shared-memory": "true", "friends": [ { diff --git a/doc/howto/quick_start.md b/doc/howto/quick_start.md new file mode 100644 index 00000000..67ac9434 --- /dev/null +++ b/doc/howto/quick_start.md @@ -0,0 +1,19 @@ +# Quick start for local development + +- Create a configuration file at `./hatchery.json` with basic configuration: + +``` +{ + "user-namespace": "jupyter-pods", + "sub-dir": "/lw-workspace", + "user-volume-size": "10Gi" +} +``` + +- Install [nodemon](https://nodemon.io/) + +- Run Hatchery: + +`export GEN3_ENDPOINT=qa-heal.planx-pla.net; export GEN3_VPCID=qaplanetv1; nodemon --exec go run main.go -config ./hatchery.json --signal SIGTERM` + +The API is exposed at http://0.0.0.0:8000. diff --git a/doc/howto/rStudio.md b/doc/howto/rStudio.md index 7e952fd3..d0a7f5bb 100644 --- a/doc/howto/rStudio.md +++ b/doc/howto/rStudio.md @@ -27,6 +27,7 @@ to disable authentication as this is a single user container when run with Hatch "use-tls": "false", "ready-probe": "/", "user-volume-location": "/home/rstudio/pd", + "gen3-volume-location": "/home/jovyan/.gen3" "fs-gid": 100 } ``` diff --git a/go.mod b/go.mod index d3fb70b4..34bd1ab9 100644 --- a/go.mod +++ b/go.mod @@ -6,13 +6,18 @@ require ( github.com/DataDog/datadog-go v4.8.1+incompatible // indirect github.com/DataDog/sketches-go v1.1.0 // indirect github.com/Microsoft/go-winio v0.5.0 // indirect + github.com/apparentlymart/go-cidr v1.1.0 + github.com/aws/aws-sdk-go v1.40.15 github.com/go-logr/logr v1.0.0 // indirect + github.com/gofrs/flock v0.8.1 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.6 // indirect + github.com/google/pprof v0.0.0-20210804190019-f964ff605595 // indirect github.com/google/uuid v1.3.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/json-iterator/go v1.1.11 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect github.com/tinylib/msgp v1.1.6 // indirect golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 // indirect golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 // indirect @@ -29,6 +34,7 @@ require ( k8s.io/client-go v1.5.2 k8s.io/klog/v2 v2.10.0 // indirect k8s.io/utils v0.0.0-20210802155522-efc7438f0176 // indirect + sigs.k8s.io/aws-iam-authenticator v0.5.3 ) replace ( diff --git a/go.sum b/go.sum index 6ecd3af3..c8cea2bf 100644 --- a/go.sum +++ b/go.sum @@ -40,56 +40,104 @@ github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v4.4.0+incompatible h1:R7WqXWP4fIOAqWJtUKmSfuc7eDsBT58k9AY5WSHVosk= github.com/DataDog/datadog-go v4.4.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v4.8.1+incompatible h1:KgnZAqwHyxgl6Fdhu2GtdZT7xFizdUBhDI05Wly0zg0= github.com/DataDog/datadog-go v4.8.1+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/gostackparse v0.5.0 h1:jb72P6GFHPHz2W0onsN51cS3FkaMDcjb0QzgxxA4gDk= github.com/DataDog/gostackparse v0.5.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= -github.com/DataDog/sketches-go v1.0.0 h1:chm5KSXO7kO+ywGWJ0Zs6tdmWU8PBXSbywFVciL6BG4= github.com/DataDog/sketches-go v1.0.0/go.mod h1:O+XkJHWk9w4hDwY2ZUDU31ZC9sNYlYo8DiFsxjYeo1k= github.com/DataDog/sketches-go v1.1.0 h1:eeUOWMFL6Yygjq/8Lv116uanBlHXiF5kLihR7kYLnH4= github.com/DataDog/sketches-go v1.1.0/go.mod h1:O+XkJHWk9w4hDwY2ZUDU31ZC9sNYlYo8DiFsxjYeo1k= github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.37.1/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.40.15 h1:aqQCwW8meVzLCacWX8NEPg8bBkL0ZlcMSbhwrsg6eNE= +github.com/aws/aws-sdk-go v1.40.15/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.2.1 h1:fV3MLmabKIZ383XifUjFSwcoGee0v9qgPp8wy5svibE= github.com/go-logr/logr v0.2.1/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/flock v0.7.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -100,9 +148,9 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -111,13 +159,10 @@ github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -131,13 +176,11 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -151,8 +194,9 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20210125172800-10e9aeb4a998/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210804190019-f964ff605595 h1:uNrRgpnKjTfxu4qHaZAAs3eKTYV1EzGF3dAykpnxgDE= +github.com/google/pprof v0.0.0-20210804190019-f964ff605595/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -161,70 +205,134 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.5.2 h1:YVUUPsHTL1setd3iy+OvO3cToQBzLmYat6N9iIh++Gc= github.com/googleapis/gnostic v0.5.2/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -232,30 +340,43 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ= github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw= github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.hein.dev/go-version v0.1.0/go.mod h1:WOEm7DWMroRe5GdUgHMvx+Pji5WWIpMuXmK/3foylXs= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= @@ -284,21 +405,27 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -313,16 +440,16 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 h1:3B43BWw0xEBsLZ/NO1VALz6fppU3481pik+2Ksv45z8= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= @@ -335,17 +462,26 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190812172437-4e8604ab3aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -365,7 +501,6 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -373,30 +508,31 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -408,8 +544,10 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190812233024-afc3694995b6/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -444,6 +582,9 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -461,12 +602,10 @@ google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -501,6 +640,7 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -515,13 +655,11 @@ google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLY google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= @@ -529,9 +667,9 @@ google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+Rur google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/DataDog/dd-trace-go.v1 v1.31.1 h1:JvNfQKEqQT0jZyxTlSpHoJLV8A2cJa3ILazZgb5Eor8= gopkg.in/DataDog/dd-trace-go.v1 v1.31.1/go.mod h1:wRKMf/tRASHwH/UOfPQ3IQmVFhTz2/1a1/mpXoIjF54= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -539,15 +677,15 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -558,24 +696,47 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.16.8/go.mod h1:a8EOdYHO8en+YHhPBLiW5q+3RfHTr7wxTqqp7emJ7PM= k8s.io/api v0.21.3 h1:cblWILbLO8ar+Fj6xdDGr603HRsf8Wu9E9rngJeprZQ= k8s.io/api v0.21.3/go.mod h1:hUgeYHUbBp23Ue4qdX9tR8/ANi/g3ehylAqDn9NWVOg= +k8s.io/apimachinery v0.16.8/go.mod h1:Xk2vD2TRRpuWYLQNM6lT9R7DSFZUYG03SarNkbGrnKE= k8s.io/apimachinery v0.21.3 h1:3Ju4nvjCngxxMYby0BimUk+pQHPOQp3eCGChk5kfVII= k8s.io/apimachinery v0.21.3/go.mod h1:H/IM+5vH9kZRNJ4l3x/fXP/5bOPJaVP/guptnZPeCFI= k8s.io/client-go v0.21.3 h1:J9nxZTOmvkInRDCzcSNQmPJbDYN/PjlxXT9Mos3HcLg= k8s.io/client-go v0.21.3/go.mod h1:+VPhCgTsaFmGILxR/7E1N0S+ryO010QBeNCv5JwRGYU= +k8s.io/code-generator v0.16.8/go.mod h1:wFdrXdVi/UC+xIfLi+4l9elsTT/uEF61IfcN2wOLULQ= +k8s.io/component-base v0.16.8/go.mod h1:Q8UWOWShpP3MZZny4n/15gOncfaaVtc9SbCdkM5MhUE= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.3.0 h1:WmkrnW7fdrm0/DMClc+HIxtftvxVIPAhlVwMQo5yLco= k8s.io/klog/v2 v2.3.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= +k8s.io/sample-controller v0.16.8/go.mod h1:aXlORS1ekU77qhGybB5t3JORDurzDpWgvMYxmCsiuos= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210802155522-efc7438f0176 h1:Mx0aa+SUAcNRQbs5jUzV8lkDlGFU8laZsY9jrcVX5SY= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/aws-iam-authenticator v0.5.3 h1:EyqQ/uxzbe2mDETZZmuMnv0xHITnyLhZfPlGb6Mma20= +sigs.k8s.io/aws-iam-authenticator v0.5.3/go.mod h1:DIq7gy0lvnyaG88AgFyJzUVeix+ia5msHEp4RL0102I= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/hatchery/alb.go b/hatchery/alb.go new file mode 100644 index 00000000..45550f17 --- /dev/null +++ b/hatchery/alb.go @@ -0,0 +1,218 @@ +package hatchery + +import ( + "fmt" + "os" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/elbv2" +) + +func (creds *CREDS) createTargetGroup(userName string, vpcId string, svc *elbv2.ELBV2) (*elbv2.CreateTargetGroupOutput, error) { + tgName := truncateString(strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-")+userToResourceName(userName, "service")+"tg", 32) + input := &elbv2.CreateTargetGroupInput{ + Name: aws.String(tgName), + Port: aws.Int64(80), + Protocol: aws.String("HTTP"), + VpcId: aws.String(vpcId), + TargetType: aws.String("ip"), + HealthCheckPath: aws.String("/lw-workspace/proxy/"), + Matcher: &elbv2.Matcher{ + HttpCode: aws.String("200-499"), + }, + } + + result, err := svc.CreateTargetGroup(input) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + case elbv2.ErrCodeDuplicateTargetGroupNameException: + fmt.Println(elbv2.ErrCodeDuplicateTargetGroupNameException, aerr.Error()) + case elbv2.ErrCodeTooManyTargetGroupsException: + fmt.Println(elbv2.ErrCodeTooManyTargetGroupsException, aerr.Error()) + case elbv2.ErrCodeInvalidConfigurationRequestException: + fmt.Println(elbv2.ErrCodeInvalidConfigurationRequestException, aerr.Error()) + case elbv2.ErrCodeTooManyTagsException: + fmt.Println(elbv2.ErrCodeTooManyTagsException, aerr.Error()) + default: + fmt.Println(aerr.Error()) + return nil, err + } + } else { + // Print the error, cast err to awserr.Error to get the Code and + // Message from an error. + fmt.Println(err.Error()) + return nil, err + } + return nil, err + } + + return result, nil + +} + +func (creds *CREDS) setTargetGroupAttributes(svc *elbv2.ELBV2, targetGroupArn string) (*elbv2.ModifyTargetGroupAttributesOutput, error) { + modifyTargetGroupAttributesInput := &elbv2.ModifyTargetGroupAttributesInput{ + TargetGroupArn: aws.String(targetGroupArn), + Attributes: []*elbv2.TargetGroupAttribute{ + { + Key: aws.String("deregistration_delay.timeout_seconds"), + Value: aws.String("0"), + }, + }, + } + modifyTargetGroup, err := svc.ModifyTargetGroupAttributes(modifyTargetGroupAttributesInput) + if err != nil { + return nil, err + } + return modifyTargetGroup, nil +} + +func (creds *CREDS) createListener(svc *elbv2.ELBV2, loadBalancer string, targetGroup string) (*elbv2.CreateListenerOutput, error) { + input := &elbv2.CreateListenerInput{ + DefaultActions: []*elbv2.Action{ + { + TargetGroupArn: aws.String(targetGroup), + Type: aws.String("forward"), + }, + }, + LoadBalancerArn: aws.String(loadBalancer), + Port: aws.Int64(80), + Protocol: aws.String("HTTP"), + } + + result, err := svc.CreateListener(input) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + case elbv2.ErrCodeDuplicateListenerException: + fmt.Println(elbv2.ErrCodeDuplicateListenerException, aerr.Error()) + case elbv2.ErrCodeTooManyListenersException: + fmt.Println(elbv2.ErrCodeTooManyListenersException, aerr.Error()) + case elbv2.ErrCodeTooManyCertificatesException: + fmt.Println(elbv2.ErrCodeTooManyCertificatesException, aerr.Error()) + case elbv2.ErrCodeLoadBalancerNotFoundException: + fmt.Println(elbv2.ErrCodeLoadBalancerNotFoundException, aerr.Error()) + case elbv2.ErrCodeTargetGroupNotFoundException: + fmt.Println(elbv2.ErrCodeTargetGroupNotFoundException, aerr.Error()) + case elbv2.ErrCodeTargetGroupAssociationLimitException: + fmt.Println(elbv2.ErrCodeTargetGroupAssociationLimitException, aerr.Error()) + case elbv2.ErrCodeInvalidConfigurationRequestException: + fmt.Println(elbv2.ErrCodeInvalidConfigurationRequestException, aerr.Error()) + case elbv2.ErrCodeIncompatibleProtocolsException: + fmt.Println(elbv2.ErrCodeIncompatibleProtocolsException, aerr.Error()) + case elbv2.ErrCodeSSLPolicyNotFoundException: + fmt.Println(elbv2.ErrCodeSSLPolicyNotFoundException, aerr.Error()) + case elbv2.ErrCodeCertificateNotFoundException: + fmt.Println(elbv2.ErrCodeCertificateNotFoundException, aerr.Error()) + case elbv2.ErrCodeUnsupportedProtocolException: + fmt.Println(elbv2.ErrCodeUnsupportedProtocolException, aerr.Error()) + case elbv2.ErrCodeTooManyRegistrationsForTargetIdException: + fmt.Println(elbv2.ErrCodeTooManyRegistrationsForTargetIdException, aerr.Error()) + case elbv2.ErrCodeTooManyTargetsException: + fmt.Println(elbv2.ErrCodeTooManyTargetsException, aerr.Error()) + case elbv2.ErrCodeTooManyActionsException: + fmt.Println(elbv2.ErrCodeTooManyActionsException, aerr.Error()) + case elbv2.ErrCodeInvalidLoadBalancerActionException: + fmt.Println(elbv2.ErrCodeInvalidLoadBalancerActionException, aerr.Error()) + case elbv2.ErrCodeTooManyUniqueTargetGroupsPerLoadBalancerException: + fmt.Println(elbv2.ErrCodeTooManyUniqueTargetGroupsPerLoadBalancerException, aerr.Error()) + case elbv2.ErrCodeALPNPolicyNotSupportedException: + fmt.Println(elbv2.ErrCodeALPNPolicyNotSupportedException, aerr.Error()) + case elbv2.ErrCodeTooManyTagsException: + fmt.Println(elbv2.ErrCodeTooManyTagsException, aerr.Error()) + default: + fmt.Println(aerr.Error()) + } + } else { + // Print the error, cast err to awserr.Error to get the Code and + // Message from an error. + fmt.Println(err.Error()) + } + return result, nil + } + return result, nil +} + +func (creds *CREDS) CreateLoadBalancer(userName string) (*elbv2.CreateLoadBalancerOutput, *string, *elbv2.CreateListenerOutput, error) { + svc := elbv2.New(session.New(&aws.Config{ + Credentials: creds.creds, + Region: aws.String("us-east-1"), + })) + + networkInfo, err := creds.describeWorkspaceNetwork(userName) + if err != nil { + return nil, nil, nil, err + } + albName := truncateString(strings.ReplaceAll(userToResourceName(userName, "service")+os.Getenv("GEN3_ENDPOINT"), ".", "-")+"alb", 32) + input := &elbv2.CreateLoadBalancerInput{ + Name: aws.String(albName), + Scheme: aws.String("internal"), + SecurityGroups: []*string{ + networkInfo.securityGroups.SecurityGroups[0].GroupId, + }, + Subnets: []*string{ + networkInfo.subnets.Subnets[0].SubnetId, + networkInfo.subnets.Subnets[1].SubnetId, + }, + } + + loadBalancer, err := svc.CreateLoadBalancer(input) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + case elbv2.ErrCodeDuplicateLoadBalancerNameException: + fmt.Println(elbv2.ErrCodeDuplicateLoadBalancerNameException, aerr.Error()) + case elbv2.ErrCodeTooManyLoadBalancersException: + fmt.Println(elbv2.ErrCodeTooManyLoadBalancersException, aerr.Error()) + case elbv2.ErrCodeInvalidConfigurationRequestException: + fmt.Println(elbv2.ErrCodeInvalidConfigurationRequestException, aerr.Error()) + case elbv2.ErrCodeSubnetNotFoundException: + fmt.Println(elbv2.ErrCodeSubnetNotFoundException, aerr.Error()) + case elbv2.ErrCodeInvalidSubnetException: + fmt.Println(elbv2.ErrCodeInvalidSubnetException, aerr.Error()) + case elbv2.ErrCodeInvalidSecurityGroupException: + fmt.Println(elbv2.ErrCodeInvalidSecurityGroupException, aerr.Error()) + case elbv2.ErrCodeInvalidSchemeException: + fmt.Println(elbv2.ErrCodeInvalidSchemeException, aerr.Error()) + case elbv2.ErrCodeTooManyTagsException: + fmt.Println(elbv2.ErrCodeTooManyTagsException, aerr.Error()) + case elbv2.ErrCodeDuplicateTagKeysException: + fmt.Println(elbv2.ErrCodeDuplicateTagKeysException, aerr.Error()) + case elbv2.ErrCodeResourceInUseException: + fmt.Println(elbv2.ErrCodeResourceInUseException, aerr.Error()) + case elbv2.ErrCodeAllocationIdNotFoundException: + fmt.Println(elbv2.ErrCodeAllocationIdNotFoundException, aerr.Error()) + case elbv2.ErrCodeAvailabilityZoneNotSupportedException: + fmt.Println(elbv2.ErrCodeAvailabilityZoneNotSupportedException, aerr.Error()) + case elbv2.ErrCodeOperationNotPermittedException: + fmt.Println(elbv2.ErrCodeOperationNotPermittedException, aerr.Error()) + default: + fmt.Println(aerr.Error()) + } + } else { + // Print the error, cast err to awserr.Error to get the Code and + // Message from an error. + fmt.Println(err.Error()) + return nil, nil, nil, err + } + return nil, nil, nil, err + } + + targetGroup, err := creds.createTargetGroup(userName, *networkInfo.vpc.Vpcs[0].VpcId, svc) + if err != nil { + return nil, nil, nil, err + } + _, err = creds.setTargetGroupAttributes(svc, *targetGroup.TargetGroups[0].TargetGroupArn) + if err != nil { + return nil, nil, nil, err + } + listener, err := creds.createListener(svc, *loadBalancer.LoadBalancers[0].LoadBalancerArn, *targetGroup.TargetGroups[0].TargetGroupArn) + if err != nil { + return nil, nil, nil, err + } + return loadBalancer, targetGroup.TargetGroups[0].TargetGroupArn, listener, nil +} diff --git a/hatchery/cloudwatch.go b/hatchery/cloudwatch.go new file mode 100644 index 00000000..f1868896 --- /dev/null +++ b/hatchery/cloudwatch.go @@ -0,0 +1,38 @@ +package hatchery + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" +) + +//Create CloudWatch LogGroup for hatchery containers +func (sess *CREDS) CreateLogGroup(LogGroupName string, creds *credentials.Credentials) (string, error) { + c := cloudwatchlogs.New(session.New(&aws.Config{ + Credentials: creds, + Region: aws.String("us-east-1"), + })) + + describeLogGroupIn := &cloudwatchlogs.DescribeLogGroupsInput{ + LogGroupNamePrefix: aws.String(LogGroupName), + } + + logGroup, err := c.DescribeLogGroups(describeLogGroupIn) + if err != nil { + Config.Logger.Printf("Error in DescribeLogGroup: %s", err) + return "", err + } + if len(logGroup.LogGroups) == 0 { + createLogGroupIn := &cloudwatchlogs.CreateLogGroupInput{ + LogGroupName: aws.String(LogGroupName), + } + newLogGroup, err := c.CreateLogGroup(createLogGroupIn) + if err != nil { + Config.Logger.Printf("Error in CreateLogGroup: %s, %s", err, newLogGroup) + return "", err + } + return newLogGroup.String(), nil + } + return *logGroup.LogGroups[0].LogGroupName, nil +} diff --git a/hatchery/config.go b/hatchery/config.go index ad893236..223eb351 100644 --- a/hatchery/config.go +++ b/hatchery/config.go @@ -31,6 +31,7 @@ type Container struct { GroupUID int64 `json:"group-uid"` FSGID int64 `json:"fs-gid"` UserVolumeLocation string `json:"user-volume-location"` + Gen3VolumeLocation string `json:"gen3-volume-location"` UseSharedMemory string `json:"use-shared-memory"` Friends []k8sv1.Container `json:"friends"` } @@ -53,20 +54,34 @@ type AppConfigInfo struct { Name string } +// TODO remove PayModel from config once DynamoDB contains all necessary data +type PayModel struct { + Name string `json:"name"` + User string `json:"user_id"` + AWSAccountId string `json:"aws_account_id"` + Region string `json:"region"` + Ecs string `json:"ecs"` + VpcId string `json:vpcid` + Subnet int `json:subnet` +} + // HatcheryConfig is the root of all the configuration type HatcheryConfig struct { - UserNamespace string `json:"user-namespace"` - SubDir string `json:"sub-dir"` - Containers []Container `json:"containers"` - UserVolumeSize string `json:"user-volume-size"` - Sidecar SidecarContainer `json:"sidecar"` - MoreConfigs []AppConfigInfo `json:"more-configs"` + UserNamespace string `json:"user-namespace"` + PayModels []PayModel `json:"pay-models"` + PayModelsDynamodbTable string `json:"pay-models-dynamodb-table"` + SubDir string `json:"sub-dir"` + Containers []Container `json:"containers"` + UserVolumeSize string `json:"user-volume-size"` + Sidecar SidecarContainer `json:"sidecar"` + MoreConfigs []AppConfigInfo `json:"more-configs"` } // FullHatcheryConfig bucket result from loadConfig type FullHatcheryConfig struct { Config HatcheryConfig ContainersMap map[string]Container + PayModelMap map[string]PayModel Logger *log.Logger } @@ -89,6 +104,7 @@ func LoadConfig(configFilePath string, loggerIn *log.Logger) (config *FullHatche } data.Logger.Printf("loaded config: %v", string(plan)) data.ContainersMap = make(map[string]Container) + data.PayModelMap = make(map[string]PayModel) _ = json.Unmarshal(plan, &data.Config) if nil != data.Config.MoreConfigs && 0 < len(data.Config.MoreConfigs) { for _, info := range data.Config.MoreConfigs { @@ -120,5 +136,15 @@ func LoadConfig(configFilePath string, loggerIn *log.Logger) (config *FullHatche hash := fmt.Sprintf("%x", md5.Sum([]byte(jsonBytes))) data.ContainersMap[hash] = container } + + if data.Config.PayModelsDynamodbTable == "" { + data.Logger.Printf("Warning: no 'pay-models-dynamodb-table' in configuration: will be unable to query pay model data in DynamoDB") + } + + for _, payModel := range data.Config.PayModels { + user := payModel.User + data.PayModelMap[user] = payModel + } + return data, nil } diff --git a/hatchery/creds.go b/hatchery/creds.go new file mode 100644 index 00000000..dfea5f41 --- /dev/null +++ b/hatchery/creds.go @@ -0,0 +1,25 @@ +package hatchery + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ecs" +) + +type CREDS struct { + svc *ecs.ECS + creds *credentials.Credentials +} + +func NewSession(sess *session.Session, roleArn string) CREDS { + creds := stscreds.NewCredentials(sess, roleArn) + return CREDS{ + creds: creds, + svc: ecs.New(session.New(&aws.Config{ + Credentials: creds, + Region: aws.String("us-east-1"), + })), + } +} diff --git a/hatchery/dockstore.go b/hatchery/dockstore.go index 8dd72fe8..0b9c8d40 100644 --- a/hatchery/dockstore.go +++ b/hatchery/dockstore.go @@ -72,6 +72,7 @@ var dslog = log.New(os.Stdout, "hatchery/dockstore", log.LstdFlags) const userVolumePrefix = "${USER_VOLUME}" const dataVolumePrefix = "${DATA_VOLUME}" const sharedMemoryVolumePrefix = "${SHARED_MEMORY_VOLUME}" +const gen3VolumePrefix = "${GEN3_VOLUME}" const magicPort = "${SERVICE_PORT}" // make it easy to test locally // DockstoreComposeFromFile loads a hatchery application (container) @@ -116,8 +117,8 @@ func (model *ComposeFull) Sanitize() error { return fmt.Errorf("must specify an Image for service %v", key) } for _, mount := range service.Volumes { - if !strings.HasPrefix(mount, userVolumePrefix) && !strings.HasPrefix(mount, dataVolumePrefix) && !strings.HasPrefix(mount, sharedMemoryVolumePrefix) { - return fmt.Errorf("illegal volume mount - only support %s, %s and %s mounts: %v", userVolumePrefix, dataVolumePrefix, sharedMemoryVolumePrefix, mount) + if !strings.HasPrefix(mount, userVolumePrefix) && !strings.HasPrefix(mount, dataVolumePrefix) && !strings.HasPrefix(mount, gen3VolumePrefix) && !strings.HasPrefix(mount, sharedMemoryVolumePrefix) { + return fmt.Errorf("illegal volume mount - only support %s, %s, %s and %s mounts: %v", userVolumePrefix, dataVolumePrefix, gen3VolumePrefix, sharedMemoryVolumePrefix, mount) } mountSlice := strings.SplitN(mount, ":", 2) if len(mountSlice) != 2 && !strings.HasPrefix(mount, sharedMemoryVolumePrefix) { @@ -219,6 +220,14 @@ func (service *ComposeService) ToK8sContainer(friend *k8sv1.Container) (mountUse dest.ReadOnly = true dest.MountPropagation = &fuseDataPropagation volumeMountsIndex++ + } else if strings.HasPrefix(sourceDrive, gen3VolumePrefix) { + dest.MountPath = mountSplit[1] + if sourceDrive != gen3VolumePrefix { + // +1 to trim leading / + dest.SubPath = sourceDrive[len(gen3VolumePrefix)+1:] + } + dest.Name = "gen3" + volumeMountsIndex++ } else if strings.HasPrefix(sourceDrive, sharedMemoryVolumePrefix) { mountSharedMemory = true } else { @@ -349,5 +358,8 @@ func (model *ComposeFull) BuildHatchApp() (*Container, error) { if mountSharedMemory { hatchApp.UseSharedMemory = "true" } + if hatchApp.Gen3VolumeLocation == "" { + hatchApp.Gen3VolumeLocation = "/.gen3" + } return hatchApp, nil } diff --git a/hatchery/ec2.go b/hatchery/ec2.go new file mode 100644 index 00000000..f87f1137 --- /dev/null +++ b/hatchery/ec2.go @@ -0,0 +1,224 @@ +package hatchery + +import ( + "fmt" + "os" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ecs" +) + +type NetworkInfo struct { + vpc *ec2.DescribeVpcsOutput + subnets *ec2.DescribeSubnetsOutput + securityGroups *ec2.DescribeSecurityGroupsOutput + routeTable *ec2.DescribeRouteTablesOutput +} + +func (creds *CREDS) describeWorkspaceNetwork(userName string) (*NetworkInfo, error) { + svc := ec2.New(session.New(&aws.Config{ + Credentials: creds.creds, + Region: aws.String("us-east-1"), + })) + + vpcname := userToResourceName(userName, "service") + "-" + strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + "-vpc" + vpcInput := &ec2.DescribeVpcsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("tag:Name"), + Values: []*string{aws.String(vpcname)}, + }, + { + Name: aws.String("tag:Environment"), + Values: []*string{aws.String(os.Getenv("GEN3_ENDPOINT"))}, + }, + }, + } + + vpcs, err := svc.DescribeVpcs(vpcInput) + if err != nil { + return nil, err + } + // TODO: BETTER ERROR HANDLING HERE!! + if len(vpcs.Vpcs) == 0 { + return nil, fmt.Errorf("No existing vpcs found.") + } + + subnetInput := &ec2.DescribeSubnetsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{aws.String(*vpcs.Vpcs[0].VpcId)}, + }, + }, + } + + subnets, err := svc.DescribeSubnets(subnetInput) + if err != nil { + return nil, err + } + + securityGroupInput := ec2.DescribeSecurityGroupsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{aws.String(*vpcs.Vpcs[0].VpcId)}, + }, + { + Name: aws.String("group-name"), + Values: []*string{aws.String("ws-security-group")}, + }, + { + Name: aws.String("tag:Environment"), + Values: []*string{aws.String(os.Getenv("GEN3_ENDPOINT"))}, + }, + }, + } + securityGroup, err := svc.DescribeSecurityGroups(&securityGroupInput) + if err != nil { + return nil, err + } + // Create security group if it doesn't exist + if len(securityGroup.SecurityGroups) == 0 { + createSecurityGroupInput := ec2.CreateSecurityGroupInput{ + GroupName: aws.String("ws-security-group"), + TagSpecifications: []*ec2.TagSpecification{ + { + ResourceType: aws.String("security-group"), + Tags: []*ec2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String("ws-security-group"), + }, + { + Key: aws.String("Environment"), + Value: aws.String(os.Getenv("GEN3_ENDPOINT")), + }, + }, + }, + }, + VpcId: aws.String(*vpcs.Vpcs[0].VpcId), + Description: aws.String("Security group for workspaces running in ECS"), + } + + newSecurityGroup, err := svc.CreateSecurityGroup(&createSecurityGroupInput) + if err != nil { + return nil, err + } + Config.Logger.Printf("Create Security Group: %s", *newSecurityGroup.GroupId) + + // TODO: Make this secure. Right now it's wide open + ingressRules := ec2.AuthorizeSecurityGroupIngressInput{ + GroupId: newSecurityGroup.GroupId, + IpPermissions: []*ec2.IpPermission{ + { + UserIdGroupPairs: []*ec2.UserIdGroupPair{ + { + GroupId: newSecurityGroup.GroupId, + }, + }, + IpProtocol: aws.String("tcp"), + // Port-range + FromPort: aws.Int64(2049), + ToPort: aws.Int64(2049), + }, + { + IpProtocol: aws.String("tcp"), + IpRanges: []*ec2.IpRange{ + { + CidrIp: aws.String("0.0.0.0/0"), + Description: aws.String("All IPv4"), + }, + }, + Ipv6Ranges: []*ec2.Ipv6Range{ + { + CidrIpv6: aws.String("::/0"), + Description: aws.String("All IPv6"), + }, + }, + // Port-range + FromPort: aws.Int64(80), + ToPort: aws.Int64(80), + }, + { + IpProtocol: aws.String("tcp"), + // Port-range + FromPort: aws.Int64(0), + ToPort: aws.Int64(65535), + IpRanges: []*ec2.IpRange{ + { + CidrIp: vpcs.Vpcs[0].CidrBlock, + Description: aws.String("All IPv4"), + }, + }, + }, + }, + } + _, err = svc.AuthorizeSecurityGroupIngress(&ingressRules) + if err != nil { + return nil, err + } + + securityGroup, _ = svc.DescribeSecurityGroups(&securityGroupInput) + } + + routeTableInput := &ec2.DescribeRouteTablesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{vpcs.Vpcs[0].VpcId}, + }, + { + Name: aws.String("association.main"), + Values: []*string{aws.String("true")}, + }, + }, + } + routeTable, err := svc.DescribeRouteTables(routeTableInput) + if err != nil { + return nil, err + } + + networkInfo := NetworkInfo{ + vpc: vpcs, + subnets: subnets, + securityGroups: securityGroup, + routeTable: routeTable, + } + return &networkInfo, nil +} + +func (creds *CREDS) NetworkConfig(userName string) (ecs.NetworkConfiguration, error) { + + networkInfo, err := creds.describeWorkspaceNetwork(userName) + if err != nil { + return ecs.NetworkConfiguration{}, err + } + + networkConfig := ecs.NetworkConfiguration{ + AwsvpcConfiguration: &ecs.AwsVpcConfiguration{ + // Whether the task's elastic network interface receives a public IP address. + // The default value is DISABLED. + AssignPublicIp: aws.String("ENABLED"), + // The IDs of the security groups associated with the task or service. If you + // do not specify a security group, the default security group for the VPC is + // used. There is a limit of 5 security groups that can be specified per AwsVpcConfiguration. + // + // All specified security groups must be from the same VPC. + SecurityGroups: []*string{aws.String(*networkInfo.securityGroups.SecurityGroups[0].GroupId)}, + // + // The IDs of the subnets associated with the task or service. There is a limit + // of 16 subnets that can be specified per AwsVpcConfiguration. + // + // All specified subnets must be from the same VPC. + // + // Subnets is a required field + Subnets: []*string{aws.String(*networkInfo.subnets.Subnets[0].SubnetId)}, + // contains filtered or unexported fields + }, + } + return networkConfig, nil +} diff --git a/hatchery/ecs.go b/hatchery/ecs.go new file mode 100644 index 00000000..88b294a8 --- /dev/null +++ b/hatchery/ecs.go @@ -0,0 +1,648 @@ +package hatchery + +import ( + "context" + "errors" + "fmt" + "os" + "strconv" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ecs" +) + +type CreateTaskDefinitionInput struct { + Cpu string + EnvVars []EnvVar + ExecutionRoleArn string + Image string + Memory string + Name string + Port int64 + LogGroupName string + Volumes []*ecs.Volume + MountPoints []*ecs.MountPoint + LogRegion string + TaskRole string + Type string + EntryPoint []string + Args []string + SidecarContainer ecs.ContainerDefinition +} + +type EnvVar struct { + Key string + Value string +} + +func (input *CreateTaskDefinitionInput) Environment() []*ecs.KeyValuePair { + var environment []*ecs.KeyValuePair + + for _, envVar := range input.EnvVars { + environment = append(environment, + &ecs.KeyValuePair{ + Name: aws.String(envVar.Key), + Value: aws.String(envVar.Value), + }, + ) + } + + return environment +} + +// Create ECS cluster +// TODO: Evaluate if this is still this needed.. +func (sess *CREDS) launchEcsCluster(userName string) (*ecs.Cluster, error) { + svc := sess.svc + clusterName := strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + "-cluster" + + _, err := setupVPC(userName) + if err != nil { + return nil, err + } + + describeClusterInput := &ecs.DescribeClustersInput{ + Clusters: []*string{aws.String(clusterName)}, + } + + exCluster, err := svc.DescribeClusters(describeClusterInput) + if err != nil { + return nil, err + } + provision := false + if len(exCluster.Clusters) == 1 { + if *exCluster.Clusters[0].Status == "INACTIVE" { + // Force recreation of inactive/deleted clusters + provision = true + } + } + if len(exCluster.Clusters) == 0 || provision { + + input := &ecs.CreateClusterInput{ + ClusterName: aws.String(clusterName), + Tags: []*ecs.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(clusterName), + }, + { + Key: aws.String("Environment"), + Value: aws.String(os.Getenv("GEN3_ENDPOINT")), + }, + }, + } + + result, err := svc.CreateCluster(input) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + default: + return nil, aerr + } + } + return nil, err + } + return result.Cluster, nil + } + return exCluster.Clusters[0], nil +} + +func (sess *CREDS) findEcsCluster() (*ecs.Cluster, error) { + svc := sess.svc + clusterName := strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + "-cluster" + clusterInput := &ecs.DescribeClustersInput{ + Clusters: []*string{ + aws.String(clusterName), + }, + } + describeClusterResult, err := svc.DescribeClusters(clusterInput) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + default: + return nil, aerr + } + } else { + // Print the error, cast err to awserr.Error to get the Code and + // Message from an error. + Config.Logger.Println(err.Error()) + } + } + if len(describeClusterResult.Failures) > 0 { + for _, failure := range describeClusterResult.Failures { + if *failure.Reason == "MISSING" { + Config.Logger.Printf("ECS cluster named %s not found, trying to create this ECS cluster", clusterName) + input := &ecs.CreateClusterInput{ + ClusterName: aws.String(clusterName), + Tags: []*ecs.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(clusterName), + }, + { + Key: aws.String("Environment"), + Value: aws.String(os.Getenv("GEN3_ENDPOINT")), + }, + }, + } + + _, err := svc.CreateCluster(input) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + default: + return nil, errors.New(fmt.Sprintf("Cannot create ECS cluster named %s: %s", clusterName, aerr.Code())) + } + } + return nil, errors.New(fmt.Sprintf("Cannot create ECS cluster named %s: %s", clusterName, err.Error())) + } + describeClusterResult, err = svc.DescribeClusters(clusterInput) + if err != nil || len(describeClusterResult.Failures) > 0 { + return nil, errors.New(fmt.Sprintf("Still cannot find ECS cluster named %s: %s", clusterName, err.Error())) + } + return describeClusterResult.Clusters[0], nil + } + } + Config.Logger.Printf("ECS cluster named %s cannot be described", clusterName) + return nil, errors.New(fmt.Sprintf("ECS cluster named %s cannot be described", clusterName)) + } else { + return describeClusterResult.Clusters[0], nil + } +} + +// Status of workspace running in ECS +func (sess *CREDS) statusEcsWorkspace(ctx context.Context, userName string, accessToken string) (*WorkspaceStatus, error) { + status := WorkspaceStatus{} + statusMap := map[string]string{ + "ACTIVE": "Running", + "DRAINING": "Terminating", + "LAUNCHING": "Launching", + "STOPPED": "Not Found", + "INACTIVE": "Not Found", + } + statusMessage := "INACTIVE" + status.Status = statusMap[statusMessage] + status.IdleTimeLimit = -1 + status.LastActivityTime = -1 + svcName := strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + userToResourceName(userName, "pod") + "svc" + cluster, err := sess.findEcsCluster() + if err != nil { + return &status, err + } + service, err := sess.svc.DescribeServices(&ecs.DescribeServicesInput{ + Cluster: cluster.ClusterName, + Services: []*string{ + aws.String(svcName), + }, + }) + if err != nil { + return &status, err + } + + var taskDefName string + if len(service.Services) > 0 { + statusMessage = *service.Services[0].Status + if statusMessage == "ACTIVE" && (*service.Services[0].RunningCount == *service.Services[0].DesiredCount) { + taskDefName = *service.Services[0].TaskDefinition + if taskDefName == "" { + Config.Logger.Printf("No task definition found for user %s", userName) + } else { + desTaskDefOutput, err := sess.svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ + TaskDefinition: &taskDefName, + }) + if err == nil { + containerDefs := desTaskDefOutput.TaskDefinition.ContainerDefinitions + if len(containerDefs) > 0 { + args := containerDefs[0].Command + if len(args) > 0 { + for i, arg := range args { + if strings.Contains(*arg, "shutdown_no_activity_timeout=") { + Config.Logger.Printf("Found kernel idle shutdown time in args. Attempting to get last activity time\n") + argSplit := strings.Split(*arg, "=") + idleTimeLimit, err := strconv.Atoi(argSplit[len(argSplit)-1]) + if err == nil { + status.IdleTimeLimit = idleTimeLimit * 1000 + lastActivityTime, err := getKernelIdleTimeWithContext(ctx, accessToken) + status.LastActivityTime = lastActivityTime + if err != nil { + Config.Logger.Println(err.Error()) + } + } else { + Config.Logger.Println(err.Error()) + } + break + } + if i == len(args)-1 { + Config.Logger.Printf("Unable to find kernel idle shutdown time in args\n") + } + } + } else { + Config.Logger.Printf("No env vars found for task definition %s\n", taskDefName) + } + } else { + Config.Logger.Printf("No container definition found for task definition %s\n", taskDefName) + } + } + } + } + if (*service.Services[0].PendingCount > *service.Services[0].RunningCount) || *service.Services[0].PendingCount > 0 { + status.Status = statusMap["LAUNCHING"] + } else { + status.Status = statusMap[statusMessage] + } + } else { + status.Status = statusMap[statusMessage] + } + return &status, nil +} + +// Terminate workspace running in ECS +// TODO: Make this terminate ALB as well. +func terminateEcsWorkspace(ctx context.Context, userName string, accessToken string, awsAcctID string) (string, error) { + roleARN := "arn:aws:iam::" + awsAcctID + ":role/csoc_adminvm" + sess := session.Must(session.NewSession(&aws.Config{ + // TODO: Make this configurable + Region: aws.String("us-east-1"), + })) + svc := NewSession(sess, roleARN) + cluster, err := svc.findEcsCluster() + if err != nil { + return "", err + } + svcName := strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + userToResourceName(userName, "pod") + "svc" + desServiceOutput, err := svc.svc.DescribeServices(&ecs.DescribeServicesInput{ + Cluster: cluster.ClusterName, + Services: []*string{ + aws.String(svcName), + }, + }) + if err != nil { + return "", err + } + var taskDefName string + if len(desServiceOutput.Services) > 0 { + taskDefName = *desServiceOutput.Services[0].TaskDefinition + } else { + return "", errors.New("No service found for " + userName) + } + if taskDefName == "" { + Config.Logger.Printf("No task definition found for user %s, skipping API key deletion", userName) + } else { + desTaskDefOutput, err := svc.svc.DescribeTaskDefinition(&ecs.DescribeTaskDefinitionInput{ + TaskDefinition: &taskDefName, + }) + if err != nil { + return "", err + } + containerDefs := desTaskDefOutput.TaskDefinition.ContainerDefinitions + if len(containerDefs) > 0 { + envVars := containerDefs[0].Environment + if len(envVars) > 0 { + for i, ev := range envVars { + if *ev.Name == "API_KEY_ID" { + Config.Logger.Printf("Found mounted API key. Attempting to delete API Key with ID %s for user %s\n", *ev.Value, userName) + err := deleteAPIKeyWithContext(ctx, accessToken, *ev.Value) + if err != nil { + Config.Logger.Printf("Error occurred when deleting API Key with ID %s for user %s: %s\n", *ev.Value, userName, err.Error()) + } + break + } + if i == len(envVars)-1 { + Config.Logger.Printf("Unable to find API Key ID in env vars for user %s\n", userName) + } + } + } else { + Config.Logger.Printf("No env vars found for task definition %s, skipping API key deletion\n", taskDefName) + } + } else { + Config.Logger.Printf("No container definition found for task definition %s, skipping API key deletion\n", taskDefName) + } + } + + delServiceOutput, err := svc.svc.DeleteService(&ecs.DeleteServiceInput{ + Cluster: cluster.ClusterName, + Force: aws.Bool(true), + Service: aws.String(svcName), + }) + if err != nil { + return "", err + } + // TODO: Terminate ALB + target group here too + err = teardownTransitGateway(userName) + if err != nil { + return "", err + } + return fmt.Sprintf("Service '%s' is in status: %s", userToResourceName(userName, "pod"), *delServiceOutput.Service.Status), nil +} + +func launchEcsWorkspace(ctx context.Context, userName string, hash string, accessToken string, payModel PayModel) error { + // TODO: Setup EBS volume as pd + // Must create volume using SDK too.. :( + roleARN := "arn:aws:iam::" + payModel.AWSAccountId + ":role/csoc_adminvm" + sess := session.Must(session.NewSession(&aws.Config{ + // TODO: Make this configurable + Region: aws.String("us-east-1"), + })) + svc := NewSession(sess, roleARN) + Config.Logger.Printf("%s", userName) + + hatchApp := Config.ContainersMap[hash] + mem, err := mem(hatchApp.MemoryLimit) + if err != nil { + return err + } + cpu, err := cpu(hatchApp.CPULimit) + if err != nil { + return err + } + + _, err = svc.launchEcsCluster(userName) + if err != nil { + return err + } + + apiKey, err := getAPIKeyWithContext(ctx, accessToken) + if err != nil { + Config.Logger.Printf("Failed to get API key for user %v, Error: %v", userName, err) + return err + } + Config.Logger.Printf("Created API key for user %v, key ID: %v", userName, apiKey.KeyID) + + envVars := []EnvVar{} + for k, v := range hatchApp.Env { + envVars = append(envVars, EnvVar{ + Key: k, + Value: v, + }) + } + envVars = append(envVars, EnvVar{ + Key: "API_KEY", + Value: apiKey.APIKey, + }) + envVars = append(envVars, EnvVar{ + Key: "API_KEY_ID", + Value: apiKey.KeyID, + }) + // TODO: still mounting access token for now, remove this when fully switched to use API key + envVars = append(envVars, EnvVar{ + Key: "ACCESS_TOKEN", + Value: accessToken, + }) + envVars = append(envVars, EnvVar{ + Key: "GEN3_ENDPOINT", + Value: os.Getenv("GEN3_ENDPOINT"), + }) + volumes, err := svc.EFSFileSystem(userName) + if err != nil { + return err + } + + taskRole, err := svc.taskRole(userName) + if err != nil { + return err + } + + taskDef := CreateTaskDefinitionInput{ + Image: hatchApp.Image, + Cpu: cpu, + Memory: mem, + Name: userToResourceName(userName, "pod"), + Type: "ws", + TaskRole: *taskRole, + EntryPoint: hatchApp.Command, + Volumes: []*ecs.Volume{ + { + Name: aws.String("pd"), + EfsVolumeConfiguration: &ecs.EFSVolumeConfiguration{ + AuthorizationConfig: &ecs.EFSAuthorizationConfig{ + AccessPointId: &volumes.AccessPointId, + Iam: aws.String("ENABLED"), + }, + FileSystemId: &volumes.FileSystemId, + RootDirectory: aws.String("/"), + TransitEncryption: aws.String("ENABLED"), + }, + }, + { + Name: aws.String("data-volume"), + }, + { + Name: aws.String("gen3"), + }, + }, + MountPoints: []*ecs.MountPoint{ + // TODO: make these path respect the container def in hatchery config + { + ContainerPath: aws.String("/home/jovyan/data"), + SourceVolume: aws.String("data-volume"), + }, + { + ContainerPath: aws.String("/home/jovyan/pd"), + SourceVolume: aws.String("pd"), + }, + { + ContainerPath: aws.String("/home/jovyan/.gen3"), + SourceVolume: aws.String("gen3"), + }, + }, + Args: hatchApp.Args, + EnvVars: envVars, + Port: int64(hatchApp.TargetPort), + ExecutionRoleArn: fmt.Sprintf("arn:aws:iam::%s:role/ecsTaskExecutionRole", payModel.AWSAccountId), // TODO: Make this configurable? + SidecarContainer: ecs.ContainerDefinition{ + Image: &Config.Config.Sidecar.Image, + Name: aws.String("sidecar-container"), + // 2 seconds is the smallest value allowed. + StopTimeout: aws.Int64(2), + Essential: aws.Bool(false), + MountPoints: []*ecs.MountPoint{ + { + ContainerPath: aws.String("/data"), + SourceVolume: aws.String("data-volume"), + }, + { + ContainerPath: aws.String("/.gen3"), + SourceVolume: aws.String("gen3"), + }, + }, + }, + } + taskDefResult, err := svc.CreateTaskDefinition(&taskDef, userName, hash, payModel.AWSAccountId) + if err != nil { + deleteAPIKeyWithContext(ctx, accessToken, apiKey.KeyID) + return err + } + err = setupTransitGateway(userName) + if err != nil { + return err + } + + launchTask, err := svc.launchService(ctx, taskDefResult, userName, hash, payModel) + if err != nil { + deleteAPIKeyWithContext(ctx, accessToken, apiKey.KeyID) + return err + } + + fmt.Printf("Launched ECS workspace service at %s for user %s\n", launchTask, userName) + return nil +} + +// Launch ECS service for task definition + LB for routing +func (sess *CREDS) launchService(ctx context.Context, taskDefArn string, userName string, hash string, payModel PayModel) (string, error) { + svc := sess.svc + hatchApp := Config.ContainersMap[hash] + cluster, err := sess.findEcsCluster() + if err != nil { + return "", err + } + Config.Logger.Printf("Cluster: %s", *cluster.ClusterName) + + networkConfig, err := sess.NetworkConfig(userName) + if err != nil { + return "", err + } + + loadBalancer, targetGroupArn, _, err := sess.CreateLoadBalancer(userName) + if err != nil { + return "", err + } + svcName := strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + userToResourceName(userName, "pod") + "svc" + input := &ecs.CreateServiceInput{ + DesiredCount: aws.Int64(1), + Cluster: cluster.ClusterArn, + ServiceName: aws.String(svcName), + TaskDefinition: &taskDefArn, + NetworkConfiguration: &networkConfig, + DeploymentConfiguration: &ecs.DeploymentConfiguration{ + MinimumHealthyPercent: aws.Int64(0), + }, + EnableECSManagedTags: aws.Bool(true), + LaunchType: aws.String("FARGATE"), + LoadBalancers: []*ecs.LoadBalancer{ + { + ContainerName: aws.String(userToResourceName(userName, "pod")), + ContainerPort: aws.Int64(int64(hatchApp.TargetPort)), + TargetGroupArn: targetGroupArn, + }, + }, + } + + result, err := svc.CreateService(input) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + case ecs.ErrCodeServerException: + Config.Logger.Println(ecs.ErrCodeServerException, aerr.Error()) + case ecs.ErrCodeClientException: + Config.Logger.Println(ecs.ErrCodeClientException, aerr.Error()) + case ecs.ErrCodeInvalidParameterException: + Config.Logger.Println(ecs.ErrCodeInvalidParameterException, aerr.Error()) + case ecs.ErrCodeClusterNotFoundException: + Config.Logger.Println(ecs.ErrCodeClusterNotFoundException, aerr.Error()) + case ecs.ErrCodeUnsupportedFeatureException: + Config.Logger.Println(ecs.ErrCodeUnsupportedFeatureException, aerr.Error()) + case ecs.ErrCodePlatformUnknownException: + Config.Logger.Println(ecs.ErrCodePlatformUnknownException, aerr.Error()) + case ecs.ErrCodePlatformTaskDefinitionIncompatibilityException: + Config.Logger.Println(ecs.ErrCodePlatformTaskDefinitionIncompatibilityException, aerr.Error()) + case ecs.ErrCodeAccessDeniedException: + Config.Logger.Println(ecs.ErrCodeAccessDeniedException, aerr.Error()) + default: + Config.Logger.Println(aerr.Error()) + } + } else { + // Print the error, cast err to awserr.Error to get the Code and + // Message from an error. + Config.Logger.Println(err.Error()) + } + return "", err + } + Config.Logger.Printf("Service launched: %s", *result.Service.ClusterArn) + err = createLocalService(ctx, userName, hash, *loadBalancer.LoadBalancers[0].DNSName, payModel) + if err != nil { + return "", err + } + return *loadBalancer.LoadBalancers[0].DNSName, nil +} + +// Create/Update Task Definition in ECS +func (sess *CREDS) CreateTaskDefinition(input *CreateTaskDefinitionInput, userName string, hash string, awsAcctID string) (string, error) { + creds := sess.creds + LogGroup, err := sess.CreateLogGroup(fmt.Sprintf("/hatchery/%s/", awsAcctID), creds) + if err != nil { + Config.Logger.Printf("Failed to create/get LogGroup. Error: %s", err) + return "", err + } + svc := ecs.New(session.New(&aws.Config{ + Credentials: creds, + Region: aws.String("us-east-1"), + })) + + Config.Logger.Printf("Creating ECS task definition") + + logConfiguration := &ecs.LogConfiguration{ + LogDriver: aws.String(ecs.LogDriverAwslogs), + Options: map[string]*string{ + "awslogs-region": aws.String("us-east-1"), + "awslogs-group": aws.String(LogGroup), + "awslogs-stream-prefix": aws.String(userName), + }, + } + + containerDefinition := &ecs.ContainerDefinition{ + Environment: input.Environment(), + StopTimeout: aws.Int64(2), + Essential: aws.Bool(true), + MountPoints: input.MountPoints, + Image: aws.String(input.Image), + LogConfiguration: logConfiguration, + Name: aws.String(input.Name), + EntryPoint: aws.StringSlice(input.EntryPoint), + Command: aws.StringSlice(input.Args), + } + + sidecarContainerDefinition := input.SidecarContainer + sidecarContainerDefinition.LogConfiguration = logConfiguration + sidecarContainerDefinition.Environment = input.Environment() + + if input.Port != 0 { + containerDefinition.SetPortMappings( + []*ecs.PortMapping{ + { + ContainerPort: aws.Int64(int64(input.Port)), + }, + }, + ) + } + + resp, err := svc.RegisterTaskDefinition( + &ecs.RegisterTaskDefinitionInput{ + ContainerDefinitions: []*ecs.ContainerDefinition{ + containerDefinition, + &sidecarContainerDefinition, + }, + Cpu: aws.String(input.Cpu), + ExecutionRoleArn: aws.String(input.ExecutionRoleArn), + Family: aws.String(fmt.Sprintf("%s_%s", input.Type, input.Name)), + Memory: aws.String(input.Memory), + NetworkMode: aws.String(ecs.NetworkModeAwsvpc), + RequiresCompatibilities: aws.StringSlice([]string{ecs.CompatibilityFargate}), + TaskRoleArn: aws.String(input.TaskRole), + Volumes: input.Volumes, + }, + ) + + if err != nil { + Config.Logger.Print(err, " Couldn't register ECS task definition") + return "", err + } + + td := resp.TaskDefinition + + Config.Logger.Printf("Created ECS task definition [%s:%d]", aws.StringValue(td.Family), aws.Int64Value(td.Revision)) + + return aws.StringValue(td.TaskDefinitionArn), nil +} diff --git a/hatchery/efs.go b/hatchery/efs.go new file mode 100644 index 00000000..05b037de --- /dev/null +++ b/hatchery/efs.go @@ -0,0 +1,195 @@ +package hatchery + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/efs" +) + +type EFS struct { + EFSArn string + FileSystemId string + AccessPointId string +} + +func (creds *CREDS) getEFSFileSystem(userName string, svc *efs.EFS) (*efs.DescribeFileSystemsOutput, error) { + fsName := strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + userToResourceName(userName, "pod") + "fs" + input := &efs.DescribeFileSystemsInput{ + CreationToken: aws.String(fsName), + } + result, err := svc.DescribeFileSystems(input) + if err != nil { + return nil, fmt.Errorf("Failed to describe EFS FS: %s", err) + } + // return empty struct if no filesystems are found + if len(result.FileSystems) == 0 { + return nil, nil + } + return result, nil +} + +// TODO: Check for MountTarget state regardless if fs exists or not +func (creds *CREDS) createMountTarget(FileSystemId string, svc *efs.EFS, userName string) (*efs.MountTargetDescription, error) { + networkInfo, err := creds.describeWorkspaceNetwork(userName) + if err != nil { + return nil, err + } + input := &efs.CreateMountTargetInput{ + FileSystemId: aws.String(FileSystemId), + SubnetId: networkInfo.subnets.Subnets[0].SubnetId, + // TODO: Make this correct, currently it's all using the same SG + SecurityGroups: []*string{ + networkInfo.securityGroups.SecurityGroups[0].GroupId, + }, + } + + result, err := svc.CreateMountTarget(input) + if err != nil { + return nil, fmt.Errorf("Failed to create mount target: %s", err) + } + return result, nil +} + +func (creds *CREDS) createAccessPoint(FileSystemId string, userName string, svc *efs.EFS) (*string, error) { + exAccessPointInput := &efs.DescribeAccessPointsInput{ + FileSystemId: &FileSystemId, + } + exResult, err := svc.DescribeAccessPoints(exAccessPointInput) + if err != nil { + return nil, err + } + + if len(exResult.AccessPoints) == 0 { + input := &efs.CreateAccessPointInput{ + ClientToken: aws.String(fmt.Sprintf("ap-%s", userToResourceName(userName, "pod"))), + FileSystemId: aws.String(FileSystemId), + PosixUser: &efs.PosixUser{ + Gid: aws.Int64(100), + Uid: aws.Int64(1000), + }, + RootDirectory: &efs.RootDirectory{ + CreationInfo: &efs.CreationInfo{ + OwnerGid: aws.Int64(100), + OwnerUid: aws.Int64(1000), + Permissions: aws.String("0755"), + }, + Path: aws.String("/"), + }, + } + + result, err := svc.CreateAccessPoint(input) + if err != nil { + return nil, fmt.Errorf("Failed to create accessPoint: %s", err) + } + return result.AccessPointId, nil + + } else { + return exResult.AccessPoints[0].AccessPointId, nil + } + +} + +func (creds *CREDS) EFSFileSystem(userName string) (*EFS, error) { + svc := efs.New(session.New(&aws.Config{ + Credentials: creds.creds, + // TODO: Make this configurable + Region: aws.String("us-east-1"), + })) + fsName := strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + userToResourceName(userName, "pod") + "fs" + exisitingFS, err := creds.getEFSFileSystem(userName, svc) + if err != nil { + return nil, err + } + if exisitingFS == nil { + input := &efs.CreateFileSystemInput{ + Backup: aws.Bool(false), + CreationToken: aws.String(fsName), + Encrypted: aws.Bool(true), + PerformanceMode: aws.String("generalPurpose"), + Tags: []*efs.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(fsName), + }, + { + Key: aws.String("Environment"), + Value: aws.String(os.Getenv("GEN3_ENDPOINT")), + }, + }, + } + + result, err := svc.CreateFileSystem(input) + if err != nil { + return nil, fmt.Errorf("Error creating EFS filesystem: %s", err) + } + + exisitingFS, _ = creds.getEFSFileSystem(userName, svc) + for *exisitingFS.FileSystems[0].LifeCycleState != "available" { + Config.Logger.Printf("EFS filesystem is in state: %s ... Waiting for 2 seconds", *exisitingFS.FileSystems[0].LifeCycleState) + // sleep for 2 sec + time.Sleep(2 * time.Second) + exisitingFS, _ = creds.getEFSFileSystem(userName, svc) + } + + // Create mount target + mountTarget, err := creds.createMountTarget(*result.FileSystemId, svc, userName) + if err != nil { + return nil, fmt.Errorf("Failed to create EFS MountTarget: %s", err) + } + Config.Logger.Printf("MountTarget created: %s", *mountTarget.MountTargetId) + accessPoint, err := creds.createAccessPoint(*result.FileSystemId, userName, svc) + if err != nil { + return nil, fmt.Errorf("Failed to create EFS AccessPoint: %s", err) + } + Config.Logger.Printf("AccessPoint created: %s", *accessPoint) + + return &EFS{ + EFSArn: *result.FileSystemArn, + FileSystemId: *result.FileSystemId, + AccessPointId: *accessPoint, + }, nil + } else { + // create accesspoint if it doesn't exist + accessPoint, err := svc.DescribeAccessPoints(&efs.DescribeAccessPointsInput{ + FileSystemId: exisitingFS.FileSystems[0].FileSystemId, + }) + if err != nil { + return nil, fmt.Errorf("failed to describe accesspoint: %s", err) + } + var accessPointId string + if len(accessPoint.AccessPoints) == 0 { + accessPointResult, err := creds.createAccessPoint(*exisitingFS.FileSystems[0].FileSystemId, userName, svc) + if err != nil { + return nil, fmt.Errorf("failed to create EFS AccessPoint: %s", err) + } + accessPointId = *accessPointResult + } else { + accessPointId = *accessPoint.AccessPoints[0].AccessPointId + } + // create mountTarget if it doesn't exist + exMountTarget, err := svc.DescribeMountTargets(&efs.DescribeMountTargetsInput{ + FileSystemId: exisitingFS.FileSystems[0].FileSystemId, + }) + if err != nil { + return nil, err + } + if len(exMountTarget.MountTargets) == 0 { + mountTarget, err := creds.createMountTarget(*exisitingFS.FileSystems[0].FileSystemId, svc, userName) + if err != nil { + return nil, fmt.Errorf("Failed to create EFS MountTarget: %s", err) + } + Config.Logger.Printf("MountTarget created: %s", *mountTarget.MountTargetId) + } + + return &EFS{ + EFSArn: *exisitingFS.FileSystems[0].FileSystemArn, + FileSystemId: *exisitingFS.FileSystems[0].FileSystemId, + AccessPointId: accessPointId, + }, nil + } +} diff --git a/hatchery/hatchery.go b/hatchery/hatchery.go index 00ddc22a..e8323d8f 100644 --- a/hatchery/hatchery.go +++ b/hatchery/hatchery.go @@ -1,11 +1,15 @@ package hatchery import ( + "context" "encoding/json" "fmt" "net/http" + "strconv" "strings" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http" ) @@ -19,6 +23,10 @@ func RegisterHatchery(mux *httptrace.ServeMux) { mux.HandleFunc("/terminate", terminate) mux.HandleFunc("/status", status) mux.HandleFunc("/options", options) + mux.HandleFunc("/paymodels", paymodels) + + // ECS functions + mux.HandleFunc("/create-ecs-cluster", createECSCluster) } func home(w http.ResponseWriter, r *http.Request) { @@ -37,31 +45,68 @@ func home(w http.ResponseWriter, r *http.Request) { } +func getCurrentUserName(r *http.Request) (userName string) { + return r.Header.Get("REMOTE_USER") +} + +func paymodels(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + userName := getCurrentUserName(r) + payModel, err := getPayModelForUser(userName) + if payModel == nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + out, err := json.Marshal(payModel) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Fprintf(w, string(out)) +} + func status(w http.ResponseWriter, r *http.Request) { - userName := r.Header.Get("REMOTE_USER") + userName := getCurrentUserName(r) + accessToken := getBearerToken(r) - result, err := statusK8sPod(userName) + payModel, err := getPayModelForUser(userName) + if err != nil { + Config.Logger.Printf(err.Error()) + } + var result *WorkspaceStatus + if payModel != nil && payModel.Ecs == "true" { + result, err = statusEcs(r.Context(), userName, accessToken, payModel.AWSAccountId) + } else { + result, err = statusK8sPod(r.Context(), userName, accessToken, payModel) + } if err != nil { - http.Error(w, err.Error(), 500) + http.Error(w, err.Error(), http.StatusInternalServerError) return } out, err := json.Marshal(result) if err != nil { - http.Error(w, err.Error(), 500) + http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprintf(w, string(out)) - } func options(w http.ResponseWriter, r *http.Request) { type container struct { - Name string `json:"name"` - CPULimit string `json:"cpu-limit"` - MemoryLimit string `json:"memory-limit"` - ID string `json:"id"` + Name string `json:"name"` + CPULimit string `json:"cpu-limit"` + MemoryLimit string `json:"memory-limit"` + ID string `json:"id"` + IdleTimeLimit int `json:"idle-time-limit"` } var options []container for k, v := range Config.ContainersMap { @@ -71,57 +116,89 @@ func options(w http.ResponseWriter, r *http.Request) { MemoryLimit: v.MemoryLimit, ID: k, } + c.IdleTimeLimit = -1 + for _, arg := range v.Args { + if strings.Contains(arg, "shutdown_no_activity_timeout=") { + argSplit := strings.Split(arg, "=") + idleTimeLimit, err := strconv.Atoi(argSplit[len(argSplit)-1]) + if err == nil { + c.IdleTimeLimit = idleTimeLimit * 1000 + } + break + } + } options = append(options, c) } out, err := json.Marshal(options) if err != nil { - http.Error(w, err.Error(), 500) + http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprintf(w, string(out)) - } func launch(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - http.Error(w, "Not Found", 404) + http.Error(w, "Not Found", http.StatusNotFound) return } accessToken := getBearerToken(r) hash := r.URL.Query().Get("id") + if hash == "" { - http.Error(w, "Missing ID argument", 400) + http.Error(w, "Missing ID argument", http.StatusBadRequest) return } - userName := r.Header.Get("REMOTE_USER") - - err := createK8sPod(string(hash), accessToken, userName) + userName := getCurrentUserName(r) + payModel, err := getPayModelForUser(userName) if err != nil { - http.Error(w, err.Error(), 500) + Config.Logger.Printf(err.Error()) + } + if payModel == nil { + err = createLocalK8sPod(r.Context(), hash, userName, accessToken) + } else if payModel.Ecs == "true" { + err = launchEcsWorkspace(r.Context(), userName, hash, accessToken, *payModel) + } else { + err = createExternalK8sPod(r.Context(), hash, userName, accessToken, *payModel) + } + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) return } - fmt.Fprintf(w, "Success") } func terminate(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { - http.Error(w, "Not Found", 404) + http.Error(w, "Not Found", http.StatusNotFound) return } - userName := r.Header.Get("REMOTE_USER") - - err := deleteK8sPod(userName) + accessToken := getBearerToken(r) + userName := getCurrentUserName(r) + payModel, err := getPayModelForUser(userName) if err != nil { - http.Error(w, err.Error(), 500) - return + Config.Logger.Printf(err.Error()) + } + if payModel != nil && payModel.Ecs == "true" { + svc, err := terminateEcsWorkspace(r.Context(), userName, accessToken, payModel.AWSAccountId) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } else { + fmt.Fprintf(w, fmt.Sprintf("Terminated ECS workspace at %s", svc)) + } + } else { + err := deleteK8sPod(r.Context(), userName, accessToken, payModel) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Fprintf(w, "Terminated workspace") } - - fmt.Fprintf(w, "Terminated workspace") } func getBearerToken(r *http.Request) string { @@ -135,3 +212,50 @@ func getBearerToken(r *http.Request) string { } return "" } + +// ECS functions + +// Function to create ECS cluster. +// TODO: NEED TO CALL THIS FUNCTION IF IT DOESN'T EXIST!!! +func createECSCluster(w http.ResponseWriter, r *http.Request) { + userName := getCurrentUserName(r) + payModel, err := getPayModelForUser(userName) + if payModel == nil { + http.Error(w, "Paymodel has not been setup for user", http.StatusNotFound) + return + } + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + roleARN := "arn:aws:iam::" + payModel.AWSAccountId + ":role/csoc_adminvm" + sess := session.Must(session.NewSession(&aws.Config{ + // TODO: Make this configurable + Region: aws.String("us-east-1"), + })) + svc := NewSession(sess, roleARN) + + result, err := svc.launchEcsCluster(userName) + if err != nil { + fmt.Fprintf(w, fmt.Sprintf("%s", err)) + Config.Logger.Printf("Error: %s", err) + } else { + fmt.Fprintf(w, fmt.Sprintf("%s", result)) + } +} + +// Function to check status of ECS workspace. +func statusEcs(ctx context.Context, userName string, accessToken string, awsAcctID string) (*WorkspaceStatus, error) { + roleARN := "arn:aws:iam::" + awsAcctID + ":role/csoc_adminvm" + sess := session.Must(session.NewSession(&aws.Config{ + // TODO: Make this configurable + Region: aws.String("us-east-1"), + })) + svc := NewSession(sess, roleARN) + result, err := svc.statusEcsWorkspace(ctx, userName, accessToken) + if err != nil { + Config.Logger.Printf("Error: %s", err) + return nil, err + } + return result, nil +} diff --git a/hatchery/helpers.go b/hatchery/helpers.go new file mode 100644 index 00000000..fdf1ee34 --- /dev/null +++ b/hatchery/helpers.go @@ -0,0 +1,207 @@ +package hatchery + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "regexp" + "strconv" + "strings" + "time" +) + +type APIKeyStruct struct { + APIKey string `json:"api_key"` + KeyID string `json:"key_id"` +} + +type WorkspaceKernelStatusStruct struct { + LastActivityTime string `json:"last_activity"` +} + +func StrToInt(str string) (string, error) { + nonFractionalPart := strings.Split(str, ".") + return nonFractionalPart[0], nil +} + +func mem(str string) (string, error) { + res := regexp.MustCompile(`(\d*)([M|G])ib?`) + matches := res.FindStringSubmatch(str) + num, err := strconv.Atoi(matches[1]) + if err != nil { + return "", err + } + if matches[2] == "G" { + num = num * 1024 + } + return strconv.Itoa(num), nil +} + +func cpu(str string) (string, error) { + num, err := strconv.Atoi(str[:strings.IndexByte(str, '.')]) + if err != nil { + return "", err + } + num = num * 1024 + return strconv.Itoa(num), nil +} + +// Escapism escapes characters not allowed into hex with - +func escapism(input string) string { + safeBytes := "abcdefghijklmnopqrstuvwxyz0123456789" + var escaped string + for _, v := range input { + if !characterInString(v, safeBytes) { + hexCode := fmt.Sprintf("%2x", v) + escaped += "-" + hexCode + } else { + escaped += string(v) + } + } + return escaped +} + +func characterInString(a rune, list string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + +func truncateString(str string, num int) string { + bnoden := str + if len(str) > num { + bnoden = str[0:num] + } + if bnoden[len(bnoden)-1] == '-' { + bnoden = bnoden[0 : len(bnoden)-2] + } + return bnoden +} + +// API key related helper functions +// Make http request with header and body +func MakeARequestWithContext(ctx context.Context, method string, apiEndpoint string, accessToken string, contentType string, headers map[string]string, body *bytes.Buffer) (*http.Response, error) { + if headers == nil { + headers = make(map[string]string) + } + if accessToken != "" { + headers["Authorization"] = "Bearer " + accessToken + } + if contentType != "" { + headers["Content-Type"] = contentType + } + client := &http.Client{Timeout: 10 * time.Second} + var req *http.Request + var err error + if body == nil { + req, err = http.NewRequestWithContext(ctx, method, apiEndpoint, nil) + } else { + req, err = http.NewRequestWithContext(ctx, method, apiEndpoint, body) + } + + if err != nil { + return nil, errors.New("Error occurred during generating HTTP request: " + err.Error()) + } + for k, v := range headers { + req.Header.Add(k, v) + } + resp, err := client.Do(req) + if err != nil { + return nil, errors.New("Error occurred during making HTTP request: " + err.Error()) + } + return resp, nil +} + +func getFenceURL() string { + fenceURL := "http://fence-service/" + _, ok := os.LookupEnv("GEN3_ENDPOINT") + if ok { + fenceURL = "https://" + os.Getenv("GEN3_ENDPOINT") + "/user/" + } + return fenceURL +} + +func getAmbassadorURL() string { + ambassadorURL := "http://ambassador-service/" + _, ok := os.LookupEnv("GEN3_ENDPOINT") + if ok { + ambassadorURL = "https://" + os.Getenv("GEN3_ENDPOINT") + "/lw-workspace/proxy/" + } + return ambassadorURL +} + +func getAPIKeyWithContext(ctx context.Context, accessToken string) (apiKey *APIKeyStruct, err error) { + if accessToken == "" { + return nil, errors.New("No valid access token") + } + + fenceAPIKeyURL := getFenceURL() + "credentials/api/" + body := bytes.NewBufferString("{\"scope\": [\"data\", \"user\"]}") + + resp, err := MakeARequestWithContext(ctx, "POST", fenceAPIKeyURL, accessToken, "application/json", nil, body) + if err != nil { + return nil, err + } + + if resp != nil && resp.StatusCode != 200 { + return nil, errors.New("Error occurred when creating API key with error code " + strconv.Itoa(resp.StatusCode)) + } + defer resp.Body.Close() + + fenceApiKeyResponse := new(APIKeyStruct) + err = json.NewDecoder(resp.Body).Decode(fenceApiKeyResponse) + if err != nil { + return nil, errors.New("Unable to decode API key response: " + err.Error()) + } + return fenceApiKeyResponse, nil +} + +func deleteAPIKeyWithContext(ctx context.Context, accessToken string, apiKeyID string) error { + if accessToken == "" { + return errors.New("No valid access token") + } + + fenceDeleteAPIKeyURL := getFenceURL() + "credentials/api/" + apiKeyID + resp, err := MakeARequestWithContext(ctx, "DELETE", fenceDeleteAPIKeyURL, accessToken, "", nil, nil) + if err != nil { + return err + } + if resp != nil && resp.StatusCode != 204 { + return errors.New("Error occurred when deleting API key with error code " + strconv.Itoa(resp.StatusCode)) + } + return nil +} + +func getKernelIdleTimeWithContext(ctx context.Context, accessToken string) (lastActivityTime int64, err error) { + if accessToken == "" { + return -1, errors.New("No valid access token") + } + + workspaceKernelStatusURL := getAmbassadorURL() + "api/status" + resp, err := MakeARequestWithContext(ctx, "GET", workspaceKernelStatusURL, accessToken, "", nil, nil) + if err != nil { + return -1, err + } + if resp != nil && resp.StatusCode != 200 { + return -1, errors.New("Error occurred when getting workspace kernel status with error code " + strconv.Itoa(resp.StatusCode)) + } + defer resp.Body.Close() + + workspaceKernelStatusResponse := new(WorkspaceKernelStatusStruct) + err = json.NewDecoder(resp.Body).Decode(workspaceKernelStatusResponse) + if err != nil { + return -1, errors.New("Unable to decode workspace kernel status response: " + err.Error()) + } + lastAct, err := time.Parse(time.RFC3339, workspaceKernelStatusResponse.LastActivityTime) + if err != nil { + return -1, errors.New("Unable to parse last activity time: " + err.Error()) + } + return lastAct.Unix() * 1000, nil +} diff --git a/hatchery/iam.go b/hatchery/iam.go new file mode 100644 index 00000000..5f77894b --- /dev/null +++ b/hatchery/iam.go @@ -0,0 +1,74 @@ +package hatchery + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/iam" +) + +func (creds *CREDS) taskRole(userName string) (*string, error) { + svc := iam.New(session.New(&aws.Config{ + Credentials: creds.creds, + Region: aws.String("us-east-1"), + })) + + taskRoleInput := &iam.GetRoleInput{ + RoleName: aws.String(userToResourceName(userName, "pod")), + } + taskRole, _ := svc.GetRole(taskRoleInput) + if taskRole.Role != nil { + return taskRole.Role.Arn, nil + } else { + policy, err := svc.CreatePolicy(&iam.CreatePolicyInput{ + PolicyDocument: aws.String(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "HatcheryPolicy", + "Effect": "Allow", + "Action": "elasticfilesystem:*", + "Resource": [ + "arn:aws:elasticfilesystem:*:*:access-point/*", + "arn:aws:elasticfilesystem:*:*:file-system/*" + ] + } + ] + }`), + PolicyName: aws.String(fmt.Sprintf("ws-task-policy-%s", userName)), + }) + if err != nil { + return nil, fmt.Errorf("failed to create policy: %s", err) + } + createTaskRoleInput := &iam.CreateRoleInput{ + RoleName: aws.String(userToResourceName(userName, "pod")), + AssumeRolePolicyDocument: aws.String(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] + } + `), + } + + svc.AttachRolePolicy(&iam.AttachRolePolicyInput{ + PolicyArn: policy.Policy.Arn, + RoleName: aws.String(userToResourceName(userName, "pod")), + }) + createTaskRole, err := svc.CreateRole(createTaskRoleInput) + if err != nil { + return nil, fmt.Errorf("failed to create TaskRole: %s", err) + } + + return createTaskRole.Role.Arn, nil + } + +} diff --git a/hatchery/pods.go b/hatchery/pods.go index 5f72013c..f1036953 100644 --- a/hatchery/pods.go +++ b/hatchery/pods.go @@ -2,8 +2,13 @@ package hatchery import ( "context" + "encoding/base64" + "errors" "fmt" - "math/rand" + "log" + "os" + "strconv" + "strings" k8sv1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -12,6 +17,20 @@ import ( "k8s.io/client-go/kubernetes" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" + + // AWS modules + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go/service/dynamodb/expression" + "github.com/aws/aws-sdk-go/service/eks" + + "sigs.k8s.io/aws-iam-authenticator/pkg/token" + + awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws" + kubernetestrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/k8s.io/client-go/kubernetes" ) var ( @@ -46,14 +65,31 @@ type ContainerStates struct { } type WorkspaceStatus struct { - Status string `json:"status"` - Conditions []PodConditions `json:"conditions"` - ContainerStates []ContainerStates `json:"containerStates"` + Status string `json:"status"` + Conditions []PodConditions `json:"conditions"` + ContainerStates []ContainerStates `json:"containerStates"` + IdleTimeLimit int `json:"idleTimeLimit"` + LastActivityTime int64 `json:"lastActivityTime"` +} + +func getPodClient(ctx context.Context, userName string, payModelPtr *PayModel) (corev1.CoreV1Interface, bool, error) { + if payModelPtr != nil { + podClient, err := NewEKSClientset(ctx, userName, *payModelPtr) + if err != nil { + Config.Logger.Printf("Error fetching EKS kubeconfig: %v", err) + return nil, true, err + } else { + return podClient, true, nil + } + } else { + return getLocalPodClient(), false, nil + } } -func getPodClient() corev1.CoreV1Interface { +func getLocalPodClient() corev1.CoreV1Interface { // creates the in-cluster config config, err := rest.InClusterConfig() + config.WrapTransport = kubernetestrace.WrapRoundTripper if err != nil { panic(err.Error()) } @@ -65,6 +101,56 @@ func getPodClient() corev1.CoreV1Interface { return podClient } +// Generate EKS kubeconfig using AWS role +func NewEKSClientset(ctx context.Context, userName string, payModel PayModel) (corev1.CoreV1Interface, error) { + roleARN := "arn:aws:iam::" + payModel.AWSAccountId + ":role/csoc_adminvm" + sess := awstrace.WrapSession(session.Must(session.NewSession(&aws.Config{ + Region: aws.String(payModel.Region), + }))) + + creds := stscreds.NewCredentials(sess, roleARN) + eksSvc := eks.New(sess, &aws.Config{Credentials: creds}) + input := &eks.DescribeClusterInput{ + Name: aws.String(payModel.Name), + } + result, err := eksSvc.DescribeClusterWithContext(ctx, input) + if err != nil { + Config.Logger.Printf("Error calling DescribeCluster: %v", err) + return nil, err + } + cluster := result.Cluster + gen, err := token.NewGenerator(true, false) + + if err != nil { + return nil, err + } + opts := &token.GetTokenOptions{ + ClusterID: aws.StringValue(result.Cluster.Name), + AssumeRoleARN: roleARN, + } + tok, err := gen.GetWithOptions(opts) + if err != nil { + return nil, err + } + ca, err := base64.StdEncoding.DecodeString(aws.StringValue(cluster.CertificateAuthority.Data)) + if err != nil { + return nil, err + } + clientset, err := kubernetes.NewForConfig( + &rest.Config{ + Host: aws.StringValue(cluster.Endpoint), + BearerToken: tok.Token, + TLSClientConfig: rest.TLSClientConfig{ + CAData: ca, + }, + }, + ) + if err != nil { + return nil, err + } + return clientset.CoreV1(), nil +} + func checkPodReadiness(pod *k8sv1.Pod) bool { if pod.Status.Phase == "Pending" { return false @@ -77,19 +163,33 @@ func checkPodReadiness(pod *k8sv1.Pod) bool { return true } -func statusK8sPod(userName string) (*WorkspaceStatus, error) { - podClient := getPodClient() +func podStatus(ctx context.Context, userName string, accessToken string, payModelPtr *PayModel) (*WorkspaceStatus, error) { + status := WorkspaceStatus{} + podClient, isExternalClient, err := getPodClient(ctx, userName, payModelPtr) + if err != nil { + // Config.Logger.Panic("Error trying to fetch kubeConfig: %v", err) + status.Status = fmt.Sprintf("%v", err) + return &status, err + } - safeUserName := escapism(userName) + podName := userToResourceName(userName, "pod") - status := WorkspaceStatus{} + serviceName := userToResourceName(userName, "service") - podName := fmt.Sprintf("hatchery-%s", safeUserName) - pod, err := podClient.Pods(Config.Config.UserNamespace).Get(context.TODO(), podName, metav1.GetOptions{}) + pod, err := podClient.Pods(Config.Config.UserNamespace).Get(ctx, podName, metav1.GetOptions{}) + _, serviceErr := podClient.Services(Config.Config.UserNamespace).Get(ctx, serviceName, metav1.GetOptions{}) if err != nil { - // not found - status.Status = "Not Found" - return &status, nil + if isExternalClient && serviceErr == nil { + // only worry for service if podClient is external EKS + Config.Logger.Printf("Pod has been terminated, but service is still being terminated. Wait for service to be killed.") + // Pod has been terminated, but service is still being terminated. Wait for service to be killed + status.Status = "Terminating" + return &status, nil + } else { + // not found + status.Status = "Not Found" + return &status, nil + } } if pod.DeletionTimestamp != nil { @@ -110,6 +210,25 @@ func statusK8sPod(userName string) (*WorkspaceStatus, error) { allReady := checkPodReadiness(pod) if allReady == true { status.Status = "Running" + for _, container := range pod.Spec.Containers { + for _, arg := range container.Args { + if strings.Contains(arg, "shutdown_no_activity_timeout=") { + argSplit := strings.Split(arg, "=") + idleTimeLimit, err := strconv.Atoi(argSplit[len(argSplit)-1]) + if err == nil { + status.IdleTimeLimit = idleTimeLimit * 1000 + lastActivityTime, err := getKernelIdleTimeWithContext(ctx, accessToken) + status.LastActivityTime = lastActivityTime + if err != nil { + log.Println(err.Error()) + } + } else { + log.Println(err.Error()) + } + break + } + } + } } else { status.Status = "Launching" conditions := make([]PodConditions, len(pod.Status.Conditions)) @@ -129,11 +248,24 @@ func statusK8sPod(userName string) (*WorkspaceStatus, error) { default: fmt.Printf("Unknown pod status for %s: %s\n", podName, string(pod.Status.Phase)) } + return &status, nil } -func deleteK8sPod(userName string) error { - podClient := getPodClient() +func statusK8sPod(ctx context.Context, userName string, accessToken string, payModelPtr *PayModel) (*WorkspaceStatus, error) { + status, err := podStatus(ctx, userName, accessToken, payModelPtr) + if err != nil { + status.Status = fmt.Sprintf("%v", err) + Config.Logger.Printf("Error getting status: %v", err) + } + return status, nil +} + +func deleteK8sPod(ctx context.Context, userName string, accessToken string, payModelPtr *PayModel) error { + podClient, _, err := getPodClient(ctx, userName, payModelPtr) + if err != nil { + return err + } policy := metav1.DeletePropagationBackground var grace int64 = 20 @@ -142,23 +274,44 @@ func deleteK8sPod(userName string) error { GracePeriodSeconds: &grace, } - safeUserName := escapism(userName) - - podName := fmt.Sprintf("hatchery-%s", safeUserName) - _, err := podClient.Pods(Config.Config.UserNamespace).Get(context.TODO(), podName, metav1.GetOptions{}) + podName := userToResourceName(userName, "pod") + pod, err := podClient.Pods(Config.Config.UserNamespace).Get(ctx, podName, metav1.GetOptions{}) if err != nil { return fmt.Errorf("A workspace pod was not found: %s", err) } + containers := pod.Spec.Containers + var mountedAPIKeyID string + for i := range containers { + if containers[i].Name == "hatchery-container" { + for j := range containers[i].Env { + if containers[i].Env[j].Name == "API_KEY_ID" { + mountedAPIKeyID = containers[i].Env[j].Value + break + } + } + break + } + } + if mountedAPIKeyID != "" { + fmt.Printf("Found mounted API key. Attempting to delete API Key with ID %s for user %s\n", mountedAPIKeyID, userName) + err := deleteAPIKeyWithContext(ctx, accessToken, mountedAPIKeyID) + if err != nil { + fmt.Printf("Error occurred when deleting API Key with ID %s for user %s: %s\n", mountedAPIKeyID, userName, err.Error()) + } else { + fmt.Printf("API Key with ID %s for user %s has been deleted\n", mountedAPIKeyID, userName) + } + } + fmt.Printf("Attempting to delete pod %s for user %s\n", podName, userName) - podClient.Pods(Config.Config.UserNamespace).Delete(context.TODO(), podName, deleteOptions) + podClient.Pods(Config.Config.UserNamespace).Delete(ctx, podName, deleteOptions) - serviceName := fmt.Sprintf("h-%s-s", safeUserName) - _, err = podClient.Services(Config.Config.UserNamespace).Get(context.TODO(), serviceName, metav1.GetOptions{}) + serviceName := userToResourceName(userName, "service") + _, err = podClient.Services(Config.Config.UserNamespace).Get(ctx, serviceName, metav1.GetOptions{}) if err != nil { return fmt.Errorf("A workspace service was not found: %s", err) } fmt.Printf("Attempting to delete service %s for user %s\n", serviceName, userName) - podClient.Services(Config.Config.UserNamespace).Delete(context.TODO(), serviceName, deleteOptions) + podClient.Services(Config.Config.UserNamespace).Delete(ctx, serviceName, deleteOptions) return nil } @@ -184,7 +337,7 @@ func userToResourceName(userName string, resourceType string) string { // buildPod returns a pod ready to pass to the k8s API given // a hatchery Container instance, and the name of the user // launching the app -func buildPod(hatchConfig *FullHatcheryConfig, hatchApp *Container, userName string) (pod *k8sv1.Pod, err error) { +func buildPod(hatchConfig *FullHatcheryConfig, hatchApp *Container, userName string, extraVars []k8sv1.EnvVar) (pod *k8sv1.Pod, err error) { podName := userToResourceName(userName, "pod") labels := make(map[string]string) labels["app"] = podName @@ -197,7 +350,7 @@ func buildPod(hatchConfig *FullHatcheryConfig, hatchApp *Container, userName str var envVars []k8sv1.EnvVar // a null image indicates a dockstore app - always mount user volume mountUserVolume := hatchApp.UserVolumeLocation != "" - hatchConfig.Logger.Printf("building pod %v for %v", hatchApp.Name, userName) + hatchConfig.Logger.Printf("building pod '%v' for user '%v'", hatchApp.Name, userName) for key, value := range hatchApp.Env { envVar := k8sv1.EnvVar{ @@ -217,6 +370,19 @@ func buildPod(hatchConfig *FullHatcheryConfig, hatchApp *Container, userName str } sidecarEnvVars = append(sidecarEnvVars, envVar) } + for _, value := range extraVars { + sidecarEnvVars = append(sidecarEnvVars, value) + envVars = append(envVars, value) + } + + sidecarEnvVars = append(sidecarEnvVars, k8sv1.EnvVar{ + Name: "GEN3_ENDPOINT", + Value: os.Getenv("GEN3_ENDPOINT"), + }) + envVars = append(envVars, k8sv1.EnvVar{ + Name: "GEN3_ENDPOINT", + Value: os.Getenv("GEN3_ENDPOINT"), + }) //hatchConfig.Logger.Printf("sidecar configured") @@ -258,6 +424,11 @@ func buildPod(hatchConfig *FullHatcheryConfig, hatchApp *Container, userName str }, } + volumes = append(volumes, k8sv1.Volume{ + Name: "gen3", + VolumeSource: k8sv1.VolumeSource{}, + }) + if mountSharedMemory { volumes = append(volumes, k8sv1.Volume{ Name: "dshm", @@ -303,6 +474,11 @@ func buildPod(hatchConfig *FullHatcheryConfig, hatchApp *Container, userName str }, } + volumeMounts = append(volumeMounts, k8sv1.VolumeMount{ + MountPath: "/.gen3", + Name: "gen3", + }) + if mountSharedMemory { volumeMounts = append(volumeMounts, k8sv1.VolumeMount{ MountPath: "/dev/shm", @@ -373,6 +549,18 @@ func buildPod(hatchConfig *FullHatcheryConfig, hatchApp *Container, userName str }, } + if "" != hatchApp.Gen3VolumeLocation { + volumeMounts = append(volumeMounts, k8sv1.VolumeMount{ + MountPath: hatchApp.Gen3VolumeLocation, + Name: "gen3", + }) + } else { + volumeMounts = append(volumeMounts, k8sv1.VolumeMount{ + MountPath: "/.gen3", + Name: "gen3", + }) + } + if "" != hatchApp.UserVolumeLocation { volumeMounts = append(volumeMounts, k8sv1.VolumeMount{ MountPath: hatchApp.UserVolumeLocation, @@ -418,21 +606,114 @@ func buildPod(hatchConfig *FullHatcheryConfig, hatchApp *Container, userName str return pod, nil } -func createK8sPod(hash string, accessToken string, userName string) error { +func getPayModelForUser(userName string) (result *PayModel, err error) { + if Config.Config.PayModelsDynamodbTable == "" { + // fallback for backward compatibility + Config.Logger.Printf("Unable to query pay model data in DynamoDB: no 'pay-models-dynamodb-table' in config. Fallback on config.") + for _, configPaymodel := range Config.PayModelMap { + if configPaymodel.User == userName { + return &configPaymodel, nil + } + } + return nil, errors.New(fmt.Sprintf("No pay model data for username '%s'.", userName)) + } + // query pay model data for this user from DynamoDB + sess := session.Must(session.NewSessionWithOptions(session.Options{ + Config: aws.Config{ + Region: aws.String("us-east-1"), + }, + })) + dynamodbSvc := dynamodb.New(sess) + + filt := expression.Name("user_id").Equal(expression.Value(userName)) + expr, err := expression.NewBuilder().WithFilter(filt).Build() + if err != nil { + Config.Logger.Printf("Got error building expression: %s", err) + return nil, err + } + + params := &dynamodb.ScanInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + TableName: aws.String(Config.Config.PayModelsDynamodbTable), + } + res, err := dynamodbSvc.Scan(params) + if err != nil { + Config.Logger.Printf("Query API call failed: %s", err) + return nil, err + } + + if len(res.Items) == 0 { + // temporary fallback to the config to get data for users that are not + // in DynamoDB + // TODO: remove this block once we only rely on DynamoDB + Config.Logger.Printf("No pay model data for username '%s' in DynamoDB. Fallback on config.", userName) + for _, configPaymodel := range Config.PayModelMap { + if configPaymodel.User == userName { + return &configPaymodel, nil + } + } + + return nil, errors.New(fmt.Sprintf("No pay model data for username '%s'.", userName)) + } + + if len(res.Items) > 1 { + Config.Logger.Printf("There is more than one pay model item in DynamoDB for username '%s'. Defaulting to the first one.", userName) + } + + // parse pay model data + payModel := PayModel{} + err = dynamodbattribute.UnmarshalMap(res.Items[0], &payModel) + if err != nil { + Config.Logger.Printf("Got error unmarshalling: %s", err) + return nil, err + } + + // temporary fallback to the config to get data that is not in DynamoDB + // TODO: remove this block once DynamoDB contains all necessary data + for _, configPaymodel := range Config.PayModelMap { + if configPaymodel.User == userName { + if payModel.Name == "" { + payModel.Name = configPaymodel.Name + } + if payModel.AWSAccountId == "" { + payModel.AWSAccountId = configPaymodel.AWSAccountId + } + if payModel.Region == "" { + payModel.Region = configPaymodel.Region + } + if payModel.Ecs == "" { + payModel.Ecs = configPaymodel.Ecs + } + break + } + } + + return &payModel, nil +} + +func createLocalK8sPod(ctx context.Context, hash string, userName string, accessToken string) error { hatchApp := Config.ContainersMap[hash] - pod, err := buildPod(Config, &hatchApp, userName) + + var extraVars []k8sv1.EnvVar + pod, err := buildPod(Config, &hatchApp, userName, extraVars) if err != nil { Config.Logger.Printf("Failed to configure pod for launch for user %v, Error: %v", userName, err) return err } podName := userToResourceName(userName, "pod") - podClient := getPodClient() + podClient, _, err := getPodClient(ctx, userName, nil) + if err != nil { + Config.Logger.Panicf("Error in createLocalK8sPod: %v", err) + return err + } // a null image indicates a dockstore app - always mount user volume mountUserVolume := hatchApp.UserVolumeLocation != "" if mountUserVolume { claimName := userToResourceName(userName, "claim") - _, err := podClient.PersistentVolumeClaims(Config.Config.UserNamespace).Get(context.TODO(), claimName, metav1.GetOptions{}) + _, err := podClient.PersistentVolumeClaims(Config.Config.UserNamespace).Get(ctx, claimName, metav1.GetOptions{}) if err != nil { Config.Logger.Printf("Creating PersistentVolumeClaim %s.\n", claimName) pvc := &k8sv1.PersistentVolumeClaim{ @@ -450,7 +731,7 @@ func createK8sPod(hash string, accessToken string, userName string) error { }, }, } - _, err := podClient.PersistentVolumeClaims(Config.Config.UserNamespace).Create(context.TODO(), pvc, metav1.CreateOptions{}) + _, err := podClient.PersistentVolumeClaims(Config.Config.UserNamespace).Create(ctx, pvc, metav1.CreateOptions{}) if err != nil { Config.Logger.Printf("Failed to create PVC %s. Error: %s\n", claimName, err) return err @@ -458,7 +739,7 @@ func createK8sPod(hash string, accessToken string, userName string) error { } } - _, err = podClient.Pods(Config.Config.UserNamespace).Create(context.TODO(), pod, metav1.CreateOptions{}) + _, err = podClient.Pods(Config.Config.UserNamespace).Create(ctx, pod, metav1.CreateOptions{}) if err != nil { Config.Logger.Printf("Failed to launch pod %s for user %s. Image: %s, CPU %s, Memory %s. Error: %s\n", hatchApp.Name, userName, hatchApp.Image, hatchApp.CPULimit, hatchApp.MemoryLimit, err) return err @@ -472,7 +753,7 @@ func createK8sPod(hash string, accessToken string, userName string) error { annotationsService := make(map[string]string) annotationsService["getambassador.io/config"] = fmt.Sprintf(ambassadorYaml, userToResourceName(userName, "mapping"), userName, serviceName, Config.Config.UserNamespace, hatchApp.PathRewrite, hatchApp.UseTLS) - _, err = podClient.Services(Config.Config.UserNamespace).Get(context.TODO(), serviceName, metav1.GetOptions{}) + _, err = podClient.Services(Config.Config.UserNamespace).Get(ctx, serviceName, metav1.GetOptions{}) if err == nil { // This probably happened as the result of some error... there was no pod but was a service // Lets just clean it up and proceed @@ -480,7 +761,7 @@ func createK8sPod(hash string, accessToken string, userName string) error { deleteOptions := metav1.DeleteOptions{ PropagationPolicy: &policy, } - podClient.Services(Config.Config.UserNamespace).Delete(context.TODO(), serviceName, deleteOptions) + podClient.Services(Config.Config.UserNamespace).Delete(ctx, serviceName, deleteOptions) } service := &k8sv1.Service{ @@ -507,7 +788,7 @@ func createK8sPod(hash string, accessToken string, userName string) error { }, } - _, err = podClient.Services(Config.Config.UserNamespace).Create(context.TODO(), service, metav1.CreateOptions{}) + _, err = podClient.Services(Config.Config.UserNamespace).Create(ctx, service, metav1.CreateOptions{}) if err != nil { fmt.Printf("Failed to launch service %s for user %s forwarding port %d. Error: %s\n", serviceName, userName, hatchApp.TargetPort, err) return err @@ -518,36 +799,240 @@ func createK8sPod(hash string, accessToken string, userName string) error { return nil } -// GetRandString returns a random string of lenght N -func GetRandString(n int) string { - letterBytes := "abcdefghijklmnopqrstuvwxyz" - b := make([]byte, n) - for i := range b { - b[i] = letterBytes[rand.Intn(len(letterBytes))] +func createExternalK8sPod(ctx context.Context, hash string, userName string, accessToken string, payModel PayModel) error { + hatchApp := Config.ContainersMap[hash] + + podClient, err := NewEKSClientset(ctx, userName, payModel) + if err != nil { + Config.Logger.Printf("Failed to create pod client for user %v, Error: %v", userName, err) + return err } - return string(b) -} -// Escapism escapes characters not allowed into hex with - -func escapism(input string) string { - safeBytes := "abcdefghijklmnopqrstuvwxyz0123456789" - var escaped string - for _, v := range input { - if !characterInString(v, safeBytes) { - hexCode := fmt.Sprintf("%2x", v) - escaped += "-" + hexCode - } else { - escaped += string(v) + apiKey, err := getAPIKeyWithContext(ctx, accessToken) + if err != nil { + Config.Logger.Printf("Failed to get API key for user %v, Error: %v", userName, err) + return err + } + Config.Logger.Printf("Created API key for user %v, key ID: %v", userName, apiKey.KeyID) + + // Check if NS exists in external cluster, if not create it. + ns, err := podClient.Namespaces().Get(ctx, Config.Config.UserNamespace, metav1.GetOptions{}) + if err != nil { + nsName := &k8sv1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: Config.Config.UserNamespace, + }, + } + Config.Logger.Printf("Namespace created: %v", ns) + podClient.Namespaces().Create(ctx, nsName, metav1.CreateOptions{}) + } + + var extraVars []k8sv1.EnvVar + + extraVars = append(extraVars, k8sv1.EnvVar{ + Name: "WTS_OVERRIDE_URL", + Value: "https://" + os.Getenv("GEN3_ENDPOINT") + "/wts", + }) + extraVars = append(extraVars, k8sv1.EnvVar{ + Name: "API_KEY", + Value: apiKey.APIKey, + }) + extraVars = append(extraVars, k8sv1.EnvVar{ + Name: "API_KEY_ID", + Value: apiKey.KeyID, + }) + // TODO: still mounting access token for now, remove this when fully switched to use API key + extraVars = append(extraVars, k8sv1.EnvVar{ + Name: "ACCESS_TOKEN", + Value: accessToken, + }) + + pod, err := buildPod(Config, &hatchApp, userName, extraVars) + if err != nil { + Config.Logger.Printf("Failed to configure pod for launch for user %v, Error: %v", userName, err) + return err + } + podName := userToResourceName(userName, "pod") + // a null image indicates a dockstore app - always mount user volume + mountUserVolume := hatchApp.UserVolumeLocation != "" + if mountUserVolume { + claimName := userToResourceName(userName, "claim") + + _, err := podClient.PersistentVolumeClaims(Config.Config.UserNamespace).Get(ctx, claimName, metav1.GetOptions{}) + if err != nil { + Config.Logger.Printf("Creating PersistentVolumeClaim %s.\n", claimName) + pvc := &k8sv1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Annotations: pod.Annotations, + Labels: pod.Labels, + }, + Spec: k8sv1.PersistentVolumeClaimSpec{ + AccessModes: []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce}, + Resources: k8sv1.ResourceRequirements{ + Requests: k8sv1.ResourceList{ + k8sv1.ResourceStorage: resource.MustParse(Config.Config.UserVolumeSize), + }, + }, + }, + } + + _, err := podClient.PersistentVolumeClaims(Config.Config.UserNamespace).Create(ctx, pvc, metav1.CreateOptions{}) + if err != nil { + Config.Logger.Printf("Failed to create PVC %s. Error: %s\n", claimName, err) + return err + } + } + } + + _, err = podClient.Pods(Config.Config.UserNamespace).Create(ctx, pod, metav1.CreateOptions{}) + if err != nil { + Config.Logger.Printf("Failed to launch pod %s for user %s. Image: %s, CPU %s, Memory %s. Error: %s\n", hatchApp.Name, userName, hatchApp.Image, hatchApp.CPULimit, hatchApp.MemoryLimit, err) + return err + } + + Config.Logger.Printf("Launched pod %s for user %s. Image: %s, CPU %s, Memory %s\n", hatchApp.Name, userName, hatchApp.Image, hatchApp.CPULimit, hatchApp.MemoryLimit) + + serviceName := userToResourceName(userName, "service") + labelsService := make(map[string]string) + labelsService["app"] = podName + annotationsService := make(map[string]string) + annotationsService["getambassador.io/config"] = fmt.Sprintf(ambassadorYaml, userToResourceName(userName, "mapping"), userName, serviceName, Config.Config.UserNamespace, hatchApp.PathRewrite, hatchApp.UseTLS) + annotationsService["service.beta.kubernetes.io/aws-load-balancer-internal"] = "true" + _, err = podClient.Services(Config.Config.UserNamespace).Get(ctx, serviceName, metav1.GetOptions{}) + if err == nil { + // This probably happened as the result of some error... there was no pod but was a service + // Lets just clean it up and proceed + policy := metav1.DeletePropagationBackground + deleteOptions := metav1.DeleteOptions{ + PropagationPolicy: &policy, } + podClient.Services(Config.Config.UserNamespace).Delete(ctx, serviceName, deleteOptions) + + } + + service := &k8sv1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: Config.Config.UserNamespace, + Labels: labelsService, + Annotations: annotationsService, + }, + Spec: k8sv1.ServiceSpec{ + Type: k8sv1.ServiceTypeNodePort, + Selector: map[string]string{"app": podName}, + Ports: []k8sv1.ServicePort{ + { + Name: podName, + Protocol: k8sv1.ProtocolTCP, + Port: 80, + TargetPort: intstr.IntOrString{ + Type: intstr.Int, + IntVal: hatchApp.TargetPort, + }, + }, + }, + }, + } + + _, err = podClient.Services(Config.Config.UserNamespace).Create(ctx, service, metav1.CreateOptions{}) + if err != nil { + fmt.Printf("Failed to launch service %s for user %s forwarding port %d. Error: %s\n", serviceName, userName, hatchApp.TargetPort, err) + return err } - return escaped + + Config.Logger.Printf("Launched service %s for user %s forwarding port %d\n", serviceName, userName, hatchApp.TargetPort) + + nodes, _ := podClient.Nodes().List(context.TODO(), metav1.ListOptions{}) + NodeIP := nodes.Items[0].Status.Addresses[0].Address + + createLocalService(ctx, userName, hash, NodeIP, payModel) + + return nil } -func characterInString(a rune, list string) bool { - for _, b := range list { - if b == a { - return true +// Creates a local service that portal can reach +// and route traffic to pod in external cluster. +func createLocalService(ctx context.Context, userName string, hash string, serviceURL string, payModel PayModel) error { + const localAmbassadorYaml = `--- +apiVersion: ambassador/v1 +kind: Mapping +name: %s +prefix: / +headers: + remote_user: %s +service: %s:%d +bypass_auth: true +timeout_ms: 300000 +use_websocket: true +rewrite: %s +tls: %s +` + hatchApp := Config.ContainersMap[hash] + + serviceName := userToResourceName(userName, "service") + NodePort := int32(80) + if payModel.Ecs != "true" { + externalPodClient, err := NewEKSClientset(ctx, userName, payModel) + if err != nil { + return err + } + service, err := externalPodClient.Services(Config.Config.UserNamespace).Get(ctx, serviceName, metav1.GetOptions{}) + NodePort = service.Spec.Ports[0].NodePort + if err != nil { + return err } } - return false + podName := userToResourceName(userName, "pod") + + labelsService := make(map[string]string) + labelsService["app"] = podName + annotationsService := make(map[string]string) + annotationsService["getambassador.io/config"] = fmt.Sprintf(localAmbassadorYaml, userToResourceName(userName, "mapping"), userName, serviceURL, NodePort, hatchApp.PathRewrite, hatchApp.UseTLS) + + localPodClient := getLocalPodClient() + _, err := localPodClient.Services(Config.Config.UserNamespace).Get(ctx, serviceName, metav1.GetOptions{}) + if err == nil { + // This probably happened as the result of some error... there was no pod but was a service + // Lets just clean it up and proceed + policy := metav1.DeletePropagationBackground + deleteOptions := metav1.DeleteOptions{ + PropagationPolicy: &policy, + } + localPodClient.Services(Config.Config.UserNamespace).Delete(ctx, serviceName, deleteOptions) + + } + + localService := &k8sv1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: Config.Config.UserNamespace, + Labels: labelsService, + Annotations: annotationsService, + }, + Spec: k8sv1.ServiceSpec{ + Type: k8sv1.ServiceTypeClusterIP, + Selector: map[string]string{"app": podName}, + Ports: []k8sv1.ServicePort{ + { + Name: podName, + Protocol: k8sv1.ProtocolTCP, + Port: 80, + TargetPort: intstr.IntOrString{ + Type: intstr.Int, + IntVal: hatchApp.TargetPort, + }, + }, + }, + }, + } + + _, err = localPodClient.Services(Config.Config.UserNamespace).Create(ctx, localService, metav1.CreateOptions{}) + if err != nil { + fmt.Printf("Failed to launch local service %s for user %s forwarding port %d. Error: %s\n", serviceName, userName, hatchApp.TargetPort, err) + return err + } + + Config.Logger.Printf("Launched local service %s for user %s forwarding port %d\n", serviceName, userName, hatchApp.TargetPort) + return nil } diff --git a/hatchery/pods_test.go b/hatchery/pods_test.go index 3743669f..c73d8403 100644 --- a/hatchery/pods_test.go +++ b/hatchery/pods_test.go @@ -18,7 +18,7 @@ func TestBuildPodFromJSON(t *testing.T) { return } app := &config.Config.Containers[numApps-3] - pod, err := buildPod(config, app, "frickjack") + pod, err := buildPod(config, app, "frickjack", nil) if nil != err { t.Error(fmt.Sprintf("failed to build a pod - %v", err)) @@ -45,7 +45,7 @@ func TestBuildPodFromDockstore(t *testing.T) { return } app := &config.Config.Containers[numApps-2] - pod, err := buildPod(config, app, "frickjack") + pod, err := buildPod(config, app, "frickjack", nil) if nil != err { t.Error(fmt.Sprintf("failed to build a pod - %v", err)) diff --git a/hatchery/system.go b/hatchery/system.go index a46c824f..350e3f2c 100644 --- a/hatchery/system.go +++ b/hatchery/system.go @@ -26,7 +26,7 @@ func systemVersion(w http.ResponseWriter, r *http.Request) { ver := versionSummary{Commit: gitcommit, Version: gitversion} out, err := json.Marshal(ver) if err != nil { - http.Error(w, err.Error(), 500) + http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/hatchery/transitgateway.go b/hatchery/transitgateway.go new file mode 100644 index 00000000..e17c6084 --- /dev/null +++ b/hatchery/transitgateway.go @@ -0,0 +1,645 @@ +package hatchery + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ram" +) + +func setupTransitGateway(userName string) error { + _, err := createTransitGateway(userName) + if err != nil { + return fmt.Errorf("error creating transit gateway: %s", err.Error()) + } + Config.Logger.Printf("Setting up remote account ") + err = setupRemoteAccount(userName, false) + if err != nil { + return fmt.Errorf("failed to setup remote account: %s", err.Error()) + } + + return nil +} + +func teardownTransitGateway(userName string) error { + err := setupRemoteAccount(userName, true) + if err != nil { + return err + } + + return nil + +} + +// TODO: Change the name of this function to match HUB/SPOKE model +func describeMainNetwork(vpcid string, svc *ec2.EC2) (*NetworkInfo, error) { + networkInfo := NetworkInfo{} + vpcInput := &ec2.DescribeVpcsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("tag:Name"), + Values: []*string{aws.String(vpcid)}, + }, + }, + } + vpc, err := svc.DescribeVpcs(vpcInput) + if err != nil { + return nil, err + } + if len(vpc.Vpcs) == 0 { + return nil, fmt.Errorf("no VPC's found in hub account: %s", vpc) + } + subnetInput := &ec2.DescribeSubnetsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{aws.String(*vpc.Vpcs[0].VpcId)}, + }, + { + Name: aws.String("tag:Name"), + Values: []*string{aws.String("eks_private_0"), aws.String("eks_private_1"), aws.String("eks_private_2")}, + }, + }, + } + subnets, err := svc.DescribeSubnets(subnetInput) + if err != nil { + return nil, err + } + if len(subnets.Subnets) == 0 { + return nil, fmt.Errorf("no subnets found: %s", subnets) + } + + routeTableInput := &ec2.DescribeRouteTablesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{vpc.Vpcs[0].VpcId}, + }, + { + Name: aws.String("tag:Name"), + Values: []*string{aws.String("main")}, + }, + }, + } + routeTable, err := svc.DescribeRouteTables(routeTableInput) + if err != nil { + return nil, err + } + + networkInfo.vpc = vpc + networkInfo.subnets = subnets + networkInfo.routeTable = routeTable + return &networkInfo, nil +} + +func createTransitGateway(userName string) (*string, error) { + pm := Config.PayModelMap[userName] + sess := session.Must(session.NewSession(&aws.Config{ + // TODO: Make this configurable + Region: aws.String("us-east-1"), + })) + + // ec2 session to main AWS account. + ec2Local := ec2.New(sess) + + vpcid := os.Getenv("GEN3_VPCID") + Config.Logger.Printf("VPCID: %s", vpcid) + tgwName := strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + "-tgw" + // Check for existing transit gateway + exTg, err := ec2Local.DescribeTransitGateways(&ec2.DescribeTransitGatewaysInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("state"), + Values: []*string{aws.String("available"), aws.String("pending")}, + }, + { + Name: aws.String("tag:Name"), + Values: []*string{aws.String(tgwName)}, + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to DescribeTransitGateways: %s", err.Error()) + } + + // Create Transit Gateway if it doesn't exist + if len(exTg.TransitGateways) == 0 { + Config.Logger.Printf("No transit gateway found. Creating one...") + tgwName := strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + "-tgw" + tg, err := ec2Local.CreateTransitGateway(&ec2.CreateTransitGatewayInput{ + DryRun: aws.Bool(false), + Description: aws.String("Transit gateway to connect external VPC's"), + Options: &ec2.TransitGatewayRequestOptions{ + AutoAcceptSharedAttachments: aws.String("enable"), + DefaultRouteTablePropagation: aws.String("disable"), + }, + TagSpecifications: []*ec2.TagSpecification{ + { + ResourceType: aws.String("transit-gateway"), + Tags: []*ec2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(tgwName), + }, + { + Key: aws.String("Environment"), + Value: aws.String(os.Getenv("GEN3_ENDPOINT")), + }, + }, + }, + }, + }) + if err != nil { + return nil, err + } + Config.Logger.Printf("Transit gateway created: %s", *tg.TransitGateway.TransitGatewayId) + tgwAttachment, err := createTransitGatewayAttachments(ec2Local, vpcid, *tg.TransitGateway.TransitGatewayId, true, nil, userName) + if err != nil { + return nil, err + } + Config.Logger.Printf("Attachment created: %s", *tgwAttachment) + _, err = TGWRoutes(userName, tg.TransitGateway.Options.AssociationDefaultRouteTableId, tgwAttachment, ec2Local, true, false, nil) + if err != nil { + return nil, err + } + resourceshare, err := shareTransitGateway(sess, *tg.TransitGateway.TransitGatewayArn, pm.AWSAccountId) + if err != nil { + return nil, err + } + Config.Logger.Printf("Resources shared: %s", *resourceshare) + return tg.TransitGateway.TransitGatewayId, nil + } else { + tgwAttachment, err := createTransitGatewayAttachments(ec2Local, vpcid, *exTg.TransitGateways[len(exTg.TransitGateways)-1].TransitGatewayId, true, nil, userName) + if err != nil { + return nil, err + } + Config.Logger.Printf("Attachment created: %s", *tgwAttachment) + resourceshare, err := shareTransitGateway(sess, *exTg.TransitGateways[len(exTg.TransitGateways)-1].TransitGatewayArn, pm.AWSAccountId) + if err != nil { + return nil, err + } + Config.Logger.Printf("Resources shared: %s", *resourceshare) + return exTg.TransitGateways[len(exTg.TransitGateways)-1].TransitGatewayId, nil + } +} + +func createTransitGatewayAttachments(svc *ec2.EC2, vpcid string, tgwid string, local bool, sess *CREDS, userName string) (*string, error) { + // Check for existing transit gateway + tgInput := &ec2.DescribeTransitGatewaysInput{ + TransitGatewayIds: []*string{aws.String(tgwid)}, + Filters: []*ec2.Filter{ + { + Name: aws.String("state"), + Values: []*string{aws.String("available"), aws.String("pending")}, + }, + }, + } + exTg, err := svc.DescribeTransitGateways(tgInput) + if err != nil { + return nil, err + } + for *exTg.TransitGateways[0].State != "available" { + Config.Logger.Printf("TransitGateway is in state: %s ... Waiting for 5 seconds", *exTg.TransitGateways[0].State) + // sleep for 2 sec + time.Sleep(10 * time.Second) + exTg, _ = svc.DescribeTransitGateways(tgInput) + } + networkInfo := &NetworkInfo{} + if local { + networkInfo, err = describeMainNetwork(vpcid, svc) + } else { + networkInfo, err = sess.describeWorkspaceNetwork(userName) + } + if err != nil { + return nil, fmt.Errorf("Failed to get network info: %s", err) + } + exTgwAttachmentInput := &ec2.DescribeTransitGatewayAttachmentsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("tag:Environment"), + Values: []*string{aws.String(os.Getenv("GEN3_ENDPOINT"))}, + }, + { + Name: aws.String("resource-id"), + Values: []*string{networkInfo.vpc.Vpcs[0].VpcId}, + }, + { + Name: aws.String("state"), + Values: []*string{aws.String("available"), aws.String("pending")}, + }, + }, + } + exTgwAttachment, err := svc.DescribeTransitGatewayAttachments(exTgwAttachmentInput) + if err != nil { + return nil, err + } + if len(exTgwAttachment.TransitGatewayAttachments) == 0 { + tgwAttachmentName := userToResourceName(userName, "service") + "tgwa" + tgwAttachmentInput := &ec2.CreateTransitGatewayVpcAttachmentInput{ + TransitGatewayId: exTg.TransitGateways[0].TransitGatewayId, + VpcId: networkInfo.vpc.Vpcs[len(networkInfo.vpc.Vpcs)-1].VpcId, + TagSpecifications: []*ec2.TagSpecification{ + { + ResourceType: aws.String("transit-gateway-attachment"), + Tags: []*ec2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(tgwAttachmentName), + }, + { + Key: aws.String("Environment"), + Value: aws.String(os.Getenv("GEN3_ENDPOINT")), + }, + }, + }, + }, + } + for i := range networkInfo.subnets.Subnets { + tgwAttachmentInput.SubnetIds = append(tgwAttachmentInput.SubnetIds, networkInfo.subnets.Subnets[i].SubnetId) + } + tgwAttachment, err := svc.CreateTransitGatewayVpcAttachment(tgwAttachmentInput) + if err != nil { + return nil, fmt.Errorf("cannot create transitgatewayattachment: %s", err.Error()) + } + return tgwAttachment.TransitGatewayVpcAttachment.TransitGatewayAttachmentId, nil + } else { + return exTgwAttachment.TransitGatewayAttachments[0].TransitGatewayAttachmentId, nil + } +} + +func deleteTransitGatewayAttachment(svc *ec2.EC2, tgwid string) (*string, error) { + + exTgwAttachmentInput := &ec2.DescribeTransitGatewayAttachmentsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("transit-gateway-id"), + Values: []*string{aws.String(tgwid)}, + }, + { + Name: aws.String("state"), + Values: []*string{aws.String("available"), aws.String("pending")}, + }, + }, + } + exTgwAttachment, err := svc.DescribeTransitGatewayAttachments(exTgwAttachmentInput) + if err != nil { + return nil, err + } + if len(exTgwAttachment.TransitGatewayAttachments) == 0 { + return nil, fmt.Errorf("No transit gateway attachments found") + } + + delTGWAttachmentInput := &ec2.DeleteTransitGatewayVpcAttachmentInput{ + TransitGatewayAttachmentId: exTgwAttachment.TransitGatewayAttachments[0].TransitGatewayAttachmentId, + } + delTGWAttachment, err := svc.DeleteTransitGatewayVpcAttachment(delTGWAttachmentInput) + + return delTGWAttachment.TransitGatewayVpcAttachment.TransitGatewayAttachmentId, nil +} + +func shareTransitGateway(session *session.Session, tgwArn string, accountid string) (*string, error) { + svc := ram.New(session) + + ramName := strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + "-ram" + getResourceShareInput := &ram.GetResourceSharesInput{ + Name: aws.String(ramName), + ResourceOwner: aws.String("SELF"), + ResourceShareStatus: aws.String("ACTIVE"), + } + exRs, err := svc.GetResourceShares(getResourceShareInput) + if err != nil { + return nil, err + } + if len(exRs.ResourceShares) == 0 { + Config.Logger.Printf("Did not find existing resource share, creating a resource share") + resourceShareInput := &ram.CreateResourceShareInput{ + // Indicates whether principals outside your organization in Organizations can + // be associated with a resource share. + AllowExternalPrincipals: aws.Bool(true), + Name: aws.String(ramName), + Principals: []*string{aws.String(accountid)}, + ResourceArns: []*string{aws.String(tgwArn)}, + Tags: []*ram.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(ramName), + }, + { + Key: aws.String("Environment"), + Value: aws.String(os.Getenv("GEN3_ENDPOINT")), + }, + }, + } + resourceShare, err := svc.CreateResourceShare(resourceShareInput) + if err != nil { + return nil, err + } + return resourceShare.ResourceShare.ResourceShareArn, nil + } else { + listResourcesInput := &ram.ListResourcesInput{ + ResourceOwner: aws.String("SELF"), + ResourceArns: []*string{&tgwArn}, + } + listResources, err := svc.ListResources(listResourcesInput) + + listPrincipalsInput := &ram.ListPrincipalsInput{ + ResourceArn: aws.String(tgwArn), + Principals: []*string{aws.String(accountid)}, + ResourceOwner: aws.String("SELF"), + } + listPrincipals, err := svc.ListPrincipals(listPrincipalsInput) + if err != nil { + return nil, fmt.Errorf("failed to ListPrincipals: %s", err) + } + if len(listPrincipals.Principals) == 0 || len(listResources.Resources) == 0 { + associateResourceShareInput := &ram.AssociateResourceShareInput{ + Principals: []*string{aws.String(accountid)}, + ResourceArns: []*string{&tgwArn}, + ResourceShareArn: exRs.ResourceShares[len(exRs.ResourceShares)-1].ResourceShareArn, + } + _, err := svc.AssociateResourceShare(associateResourceShareInput) + if err != nil { + return nil, err + } + } else { + Config.Logger.Printf("TransitGateway is already shared with AWS account %s ", *listPrincipals.Principals[0].Id) + } + return exRs.ResourceShares[len(exRs.ResourceShares)-1].ResourceShareArn, nil + } +} + +func setupRemoteAccount(userName string, teardown bool) error { + pm := Config.PayModelMap[userName] + roleARN := "arn:aws:iam::" + pm.AWSAccountId + ":role/csoc_adminvm" + sess := session.Must(session.NewSession(&aws.Config{ + // TODO: Make this configurable + Region: aws.String("us-east-1"), + })) + svc := NewSession(sess, roleARN) + + ec2Local := ec2.New(sess) + ec2Remote := ec2.New(session.New(&aws.Config{ + Credentials: svc.creds, + Region: aws.String("us-east-1"), + })) + + vpcid := os.Getenv("GEN3_VPCID") + Config.Logger.Printf("VPCID: %s", vpcid) + err := svc.acceptTGWShare() + if err != nil { + return err + } + exTgInput := &ec2.DescribeTransitGatewaysInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("state"), + Values: []*string{aws.String("available"), aws.String("pending")}, + }, + { + Name: aws.String("tag:Environment"), + Values: []*string{aws.String(os.Getenv("GEN3_ENDPOINT"))}, + }, + }, + } + exTg, err := ec2Local.DescribeTransitGateways(exTgInput) + if err != nil { + return err + } + for len(exTg.TransitGateways) == 0 { + Config.Logger.Printf("Waiting to find ex_tgw") + err := svc.acceptTGWShare() + if err != nil { + return err + } + exTg, err = ec2Local.DescribeTransitGateways(exTgInput) + if err != nil { + return err + } + time.Sleep(5 * time.Second) + } + networkInfo, err := svc.describeWorkspaceNetwork(userName) + if err != nil { + return err + } + vpc := *networkInfo.vpc + + mainNetworkInfo, err := describeMainNetwork(vpcid, ec2Local) + if err != nil { + return err + } + var tgw_attachment *string + if teardown { + tgw_attachment, err = deleteTransitGatewayAttachment(ec2Remote, *exTg.TransitGateways[0].TransitGatewayId) + if err != nil { + return err + } + Config.Logger.Printf("tgw_attachment: %s", *tgw_attachment) + } else { + tgw_attachment, err = createTransitGatewayAttachments(ec2Remote, *vpc.Vpcs[0].VpcId, *exTg.TransitGateways[0].TransitGatewayId, false, &svc, userName) + if err != nil { + return fmt.Errorf("Cannot create TransitGatewayAttachment: ", err.Error()) + } + Config.Logger.Printf("tgw_attachment: %s", *tgw_attachment) + } + + // setup Transit Gateway Route Table + _, err = TGWRoutes(userName, exTg.TransitGateways[0].Options.AssociationDefaultRouteTableId, tgw_attachment, ec2Local, false, teardown, &svc) + if err != nil { + return fmt.Errorf("Cannot create TGW Route ", err.Error()) + } + // setup VPC Route Table + err = VPCRoutes(networkInfo, mainNetworkInfo, exTg.TransitGateways[0].TransitGatewayId, ec2Remote, ec2Local, teardown) + if err != nil { + return fmt.Errorf("failed to create vpc routes: %s", err.Error()) + } + + return nil +} + +func (creds *CREDS) acceptTGWShare() error { + session := session.New(&aws.Config{ + Credentials: creds.creds, + Region: aws.String("us-east-1"), + }) + svc := ram.New(session) + + resourceShareInvitation, err := svc.GetResourceShareInvitations(&ram.GetResourceShareInvitationsInput{}) + if err != nil { + return err + } + + if len(resourceShareInvitation.ResourceShareInvitations) == 0 { + return nil + } else { + if *resourceShareInvitation.ResourceShareInvitations[0].Status != "ACCEPTED" { + _, err := svc.AcceptResourceShareInvitation(&ram.AcceptResourceShareInvitationInput{ + ResourceShareInvitationArn: resourceShareInvitation.ResourceShareInvitations[0].ResourceShareInvitationArn, + }) + if err != nil { + return err + } + return nil + } + return nil + } +} + +func TGWRoutes(userName string, tgwRoutetableId *string, tgwAttachmentId *string, svc *ec2.EC2, local bool, teardown bool, sess *CREDS) (*string, error) { + networkInfo := &NetworkInfo{} + vpcid := os.Getenv("GEN3_VPCID") + Config.Logger.Printf("VPCID: %s", vpcid) + err := *new(error) + if local { + networkInfo, err = describeMainNetwork(vpcid, svc) + if err != nil { + return nil, err + } + } else { + networkInfo, err = sess.describeWorkspaceNetwork(userName) + if err != nil { + return nil, err + } + } + tgwAttachmentInput := &ec2.DescribeTransitGatewayAttachmentsInput{ + TransitGatewayAttachmentIds: []*string{tgwAttachmentId}, + } + tgwAttachment, err := svc.DescribeTransitGatewayAttachments(tgwAttachmentInput) + if err != nil { + return nil, fmt.Errorf("error DescribeTransitGatewayAttachments: %s", err.Error()) + } + if teardown { + delRouteInput := &ec2.DeleteTransitGatewayRouteInput{ + DestinationCidrBlock: networkInfo.vpc.Vpcs[0].CidrBlock, + TransitGatewayRouteTableId: tgwRoutetableId, + } + _, err := svc.DeleteTransitGatewayRoute(delRouteInput) + if err != nil { + return nil, err + } + return delRouteInput.TransitGatewayRouteTableId, nil + } else { + for *tgwAttachment.TransitGatewayAttachments[0].State != "available" { + Config.Logger.Printf("Transit Gateway Attachment is not ready. State is: %s", *tgwAttachment.TransitGatewayAttachments[0].State) + tgwAttachment, err = svc.DescribeTransitGatewayAttachments(tgwAttachmentInput) + if err != nil { + return nil, err + } + time.Sleep(5 * time.Second) + } + + exRoutesInput := &ec2.SearchTransitGatewayRoutesInput{ + TransitGatewayRouteTableId: tgwRoutetableId, + Filters: []*ec2.Filter{ + { + Name: aws.String("route-search.subnet-of-match"), + Values: []*string{networkInfo.vpc.Vpcs[0].CidrBlock}, + }, + }, + } + exRoutes, err := svc.SearchTransitGatewayRoutes(exRoutesInput) + if err != nil { + return nil, err + } + + if len(exRoutes.Routes) == 1 { + delRouteInput := &ec2.DeleteTransitGatewayRouteInput{ + DestinationCidrBlock: networkInfo.vpc.Vpcs[0].CidrBlock, + TransitGatewayRouteTableId: tgwRoutetableId, + } + _, err := svc.DeleteTransitGatewayRoute(delRouteInput) + if err != nil { + return nil, err + } + } + + tgRouteInput := &ec2.CreateTransitGatewayRouteInput{ + TransitGatewayRouteTableId: tgwRoutetableId, + DestinationCidrBlock: networkInfo.vpc.Vpcs[0].CidrBlock, + TransitGatewayAttachmentId: tgwAttachmentId, + } + tgRoute, err := svc.CreateTransitGatewayRoute(tgRouteInput) + if err != nil { + return nil, err + } + + return tgRoute.Route.PrefixListId, nil + } +} + +func VPCRoutes(remote_network_info *NetworkInfo, main_network_info *NetworkInfo, tgwId *string, ec2_remote *ec2.EC2, ec2_local *ec2.EC2, teardown bool) error { + if !teardown { + exRemoteRouteInput := &ec2.DescribeRouteTablesInput{ + RouteTableIds: []*string{remote_network_info.routeTable.RouteTables[0].RouteTableId}, + Filters: []*ec2.Filter{ + { + Name: aws.String("route.destination-cidr-block"), + Values: []*string{main_network_info.vpc.Vpcs[0].CidrBlock}, + }, + }, + } + exRemoteRoute, err := ec2_remote.DescribeRouteTables(exRemoteRouteInput) + if err != nil { + return err + } + + if len(exRemoteRoute.RouteTables) != 0 { + remoteDeleteRouteInput := &ec2.DeleteRouteInput{ + DestinationCidrBlock: main_network_info.vpc.Vpcs[0].CidrBlock, + RouteTableId: remote_network_info.routeTable.RouteTables[0].RouteTableId, + } + _, err := ec2_remote.DeleteRoute(remoteDeleteRouteInput) + if err != nil { + return err + } + } + + remoteCreateRouteInput := &ec2.CreateRouteInput{ + DestinationCidrBlock: main_network_info.vpc.Vpcs[0].CidrBlock, + RouteTableId: remote_network_info.routeTable.RouteTables[0].RouteTableId, + TransitGatewayId: tgwId, + } + + remoteRoute, err := ec2_remote.CreateRoute(remoteCreateRouteInput) + if err != nil { + return err + } + Config.Logger.Printf("Route added to remote VPC. %s", remoteRoute) + + localCreateRouteInput := &ec2.CreateRouteInput{ + DestinationCidrBlock: remote_network_info.vpc.Vpcs[0].CidrBlock, + RouteTableId: main_network_info.routeTable.RouteTables[0].RouteTableId, + TransitGatewayId: tgwId, + } + + localRoute, err := ec2_local.CreateRoute(localCreateRouteInput) + if err != nil { + return err + } + Config.Logger.Printf("Route added to local VPC. %s", localRoute) + return nil + } else { + remoteDeleteRouteInput := &ec2.DeleteRouteInput{ + DestinationCidrBlock: main_network_info.vpc.Vpcs[0].CidrBlock, + RouteTableId: remote_network_info.routeTable.RouteTables[0].RouteTableId, + } + _, err := ec2_remote.DeleteRoute(remoteDeleteRouteInput) + if err != nil { + return err + } + localDeleteRouteInput := &ec2.DeleteRouteInput{ + DestinationCidrBlock: remote_network_info.vpc.Vpcs[0].CidrBlock, + RouteTableId: main_network_info.routeTable.RouteTables[0].RouteTableId, + } + + _, err = ec2_local.DeleteRoute(localDeleteRouteInput) + if err != nil { + return err + } + return nil + } +} diff --git a/hatchery/vpc.go b/hatchery/vpc.go new file mode 100644 index 00000000..98973d77 --- /dev/null +++ b/hatchery/vpc.go @@ -0,0 +1,264 @@ +package hatchery + +import ( + "net" + "os" + "strings" + + "github.com/apparentlymart/go-cidr/cidr" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" +) + +func setupVPC(userName string) (*string, error) { + pm := Config.PayModelMap[userName] + + roleARN := "arn:aws:iam::" + pm.AWSAccountId + ":role/csoc_adminvm" + sess := session.Must(session.NewSession(&aws.Config{ + // TODO: Make this configurable + Region: aws.String("us-east-1"), + })) + + svc := NewSession(sess, roleARN) + + ec2Remote := ec2.New(session.New(&aws.Config{ + Credentials: svc.creds, + Region: aws.String("us-east-1"), + })) + + // Subnets + // TODO: make base CIDR configurable? + cidrstring := "192.165.0.0/12" + _, IPNet, _ := net.ParseCIDR(cidrstring) + subnet, err := cidr.Subnet(IPNet, 15, pm.Subnet) + if err != nil { + return nil, err + } + subnetString := subnet.String() + + // VPC stuff + vpcname := userToResourceName(userName, "service") + "-" + strings.ReplaceAll(os.Getenv("GEN3_ENDPOINT"), ".", "-") + "-vpc" + descVPCInput := &ec2.DescribeVpcsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("cidr"), + Values: []*string{aws.String(subnetString)}, + }, + { + Name: aws.String("tag:Name"), + Values: []*string{aws.String(vpcname)}, + }, + { + Name: aws.String("tag:Environment"), + Values: []*string{aws.String(os.Getenv("GEN3_ENDPOINT"))}, + }, + }, + } + vpc, err := ec2Remote.DescribeVpcs(descVPCInput) + if err != nil { + return nil, err + } + // TODO: Check that VPC is configured correctly too, and not just the length + if len(vpc.Vpcs) == 0 { + vpc, err := createVPC(subnetString, vpcname, ec2Remote) + if err != nil { + return nil, err + } + Config.Logger.Printf("VPC created in remote account") + _, err = createInternetGW(vpcname, *vpc.Vpc.VpcId, ec2Remote) + if err != nil { + return nil, err + } + } + exNetwork, err := svc.describeWorkspaceNetwork(userName) + if err != nil { + return nil, err + } + _, err = createInternetGW(vpcname, *exNetwork.vpc.Vpcs[0].VpcId, ec2Remote) + if err != nil { + return nil, err + } + + // TODO: Check that subnets are configured correctly too, and not just the length + if len(exNetwork.subnets.Subnets) == 0 { + err = createSubnet(subnetString, *exNetwork.vpc.Vpcs[0].VpcId, ec2Remote) + if err != nil { + return nil, err + } + } + + return nil, nil +} + +func createVPC(cidr string, vpcname string, svc *ec2.EC2) (*ec2.CreateVpcOutput, error) { + createVPCInput := &ec2.CreateVpcInput{ + CidrBlock: aws.String(cidr), + TagSpecifications: []*ec2.TagSpecification{ + { + ResourceType: aws.String("vpc"), + Tags: []*ec2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(vpcname), + }, + { + Key: aws.String("Environment"), + Value: aws.String(os.Getenv("GEN3_ENDPOINT")), + }, + }, + }, + }, + } + vpc, err := svc.CreateVpc(createVPCInput) + if err != nil { + return nil, err + } + + svc.ModifyVpcAttribute(&ec2.ModifyVpcAttributeInput{ + EnableDnsHostnames: &ec2.AttributeBooleanValue{ + Value: aws.Bool(true), + }, + VpcId: vpc.Vpc.VpcId, + }) + + return vpc, nil +} + +func createSubnet(vpccidr string, vpcid string, svc *ec2.EC2) error { + _, cidrs, err := net.ParseCIDR(vpccidr) + if err != nil { + return err + } + subnet1Cidr, err := (cidr.Subnet(cidrs, 1, 0)) + if err != nil { + panic(err) + } + subnet2Cidr, _ := cidr.Subnet(cidrs, 1, 1) + if err != nil { + panic(err) + } + + Config.Logger.Print(cidrs) + createSubnet1Input := &ec2.CreateSubnetInput{ + CidrBlock: aws.String(subnet1Cidr.String()), + //TODO: Make this configurable ? + AvailabilityZone: aws.String("us-east-1a"), + VpcId: &vpcid, + } + createSubnet2Input := &ec2.CreateSubnetInput{ + //TODO: Make this configurable ? + AvailabilityZone: aws.String("us-east-1b"), + CidrBlock: aws.String(subnet2Cidr.String()), + VpcId: &vpcid, + } + _, err = svc.CreateSubnet(createSubnet1Input) + if err != nil { + return err + } + _, err = svc.CreateSubnet(createSubnet2Input) + if err != nil { + return err + } + return nil + +} + +func createInternetGW(name string, vpcid string, svc *ec2.EC2) (*string, error) { + describeInternetGWInput := &ec2.DescribeInternetGatewaysInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("attachment.vpc-id"), + Values: []*string{aws.String(vpcid)}, + }, + }, + } + exIgw, err := svc.DescribeInternetGateways(describeInternetGWInput) + if err != nil { + return nil, err + } + if len(exIgw.InternetGateways) == 0 { + createInternetGWInput := &ec2.CreateInternetGatewayInput{ + TagSpecifications: []*ec2.TagSpecification{ + { + ResourceType: aws.String("internet-gateway"), + Tags: []*ec2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String(name), + }, + { + Key: aws.String("Environment"), + Value: aws.String(os.Getenv("GEN3_ENDPOINT")), + }, + }, + }, + }, + } + igw, err := svc.CreateInternetGateway(createInternetGWInput) + if err != nil { + return nil, err + } + _, err = svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{ + InternetGatewayId: igw.InternetGateway.InternetGatewayId, + VpcId: &vpcid, + }) + if err != nil { + return nil, err + } + routeTable, err := svc.DescribeRouteTables(&ec2.DescribeRouteTablesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{&vpcid}, + }, + }, + }) + route, err := svc.CreateRoute(&ec2.CreateRouteInput{ + DestinationCidrBlock: aws.String("0.0.0.0/0"), + GatewayId: igw.InternetGateway.InternetGatewayId, + RouteTableId: routeTable.RouteTables[0].RouteTableId, + }) + if err != nil { + return nil, err + } + Config.Logger.Printf("Route: %s", route) + return igw.InternetGateway.InternetGatewayId, nil + } else { + if len(exIgw.InternetGateways[0].Attachments) == 0 { + _, err = svc.AttachInternetGateway(&ec2.AttachInternetGatewayInput{ + InternetGatewayId: exIgw.InternetGateways[0].InternetGatewayId, + VpcId: &vpcid, + }) + if err != nil { + return nil, err + } + } + + routeTable, err := svc.DescribeRouteTables(&ec2.DescribeRouteTablesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{&vpcid}, + }, + }, + }) + if err != nil { + return nil, err + } + + Config.Logger.Printf("Routes: %s", routeTable.RouteTables[0].Routes) + + route, err := svc.CreateRoute(&ec2.CreateRouteInput{ + DestinationCidrBlock: aws.String("0.0.0.0/0"), + GatewayId: exIgw.InternetGateways[0].InternetGatewayId, + RouteTableId: routeTable.RouteTables[0].RouteTableId, + }) + if err != nil { + return nil, err + } + Config.Logger.Printf("Route: %s", route) + return exIgw.InternetGateways[0].InternetGatewayId, nil + } + +} diff --git a/main.go b/main.go index b071472f..ff2e2d31 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "github.com/uc-cdis/hatchery/hatchery" httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/profiler" ) func main() { @@ -35,6 +36,20 @@ func main() { config.Logger.Printf("Setting up datadog") tracer.Start() defer tracer.Stop() + if err := profiler.Start( + profiler.WithProfileTypes( + profiler.CPUProfile, + profiler.HeapProfile, + + // The profiles below are disabled by default to keep overhead low, but can be enabled as needed. + // profiler.BlockProfile, + // profiler.MutexProfile, + // profiler.GoroutineProfile, + ), + ); err != nil { + config.Logger.Printf("DD profiler setup failed with error: %s", err) + } + defer profiler.Stop() } else { config.Logger.Printf("Datadog not enabled in manifest, skipping...") } diff --git a/openapis/openapi.yaml b/openapis/openapi.yaml index c16ffc93..24078b2a 100644 --- a/openapis/openapi.yaml +++ b/openapis/openapi.yaml @@ -74,6 +74,13 @@ paths: $ref: '#/components/schemas/Container' 401: $ref: '#/components/responses/UnauthorizedError' + /paymodels: + get: + tags: + - workspace + summary: Get the current user's pay model data + operationId: paymodels + components: schemas: Status: