├── .dockerignore ├── .github └── workflows │ └── release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── cmd └── yurt │ └── main.go ├── go.mod ├── go.sum ├── img └── yurt_fe.png ├── internal ├── cmdlets │ ├── http.go │ ├── http_hello.go │ ├── http_taskinfo.go │ ├── info.go │ ├── info_discover.go │ ├── info_trivy.go │ ├── info_versions.go │ ├── local.go │ ├── local_dump.go │ ├── local_list.go │ └── root.go ├── docker │ ├── registry.go │ ├── types.go │ ├── util.go │ └── util_test.go ├── http │ ├── filters │ │ └── supplemental.go │ ├── hello │ │ └── hello.go │ ├── http.go │ ├── taskinfo │ │ └── taskinfo.go │ └── type.go ├── kv │ ├── consul.go │ ├── redis.go │ ├── store.go │ └── type.go ├── nomad │ ├── doc.go │ └── nomad.go └── versions │ ├── version_test.go │ └── versions.go ├── nomad ├── trivy-scan.nomad ├── trivy-server.nomad ├── up2date.nomad ├── yurt-discover.nomad ├── yurt-trivy.nomad ├── yurt-versions.nomad └── yurt.nomad └── theme ├── p2 ├── base.p2 ├── icons │ ├── lock.p2 │ └── watch.p2 ├── partials │ ├── trivy.p2 │ ├── trivy_results.p2 │ └── trivy_vuln.p2 ├── taskinfo.p2 └── taskinfo_details.p2 └── static ├── css ├── reset.css └── theme.css └── js └── taskInfo.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | **/node_modules 3 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | 4 | on: 5 | push: 6 | tags: 7 | - v* 8 | 9 | jobs: 10 | docker: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Set up QEMU 14 | uses: docker/setup-qemu-action@v1 15 | - name: Set up Docker Buildx 16 | uses: docker/setup-buildx-action@v1 17 | - name: Login to GCHR 18 | uses: docker/login-action@v1 19 | with: 20 | registry: ghcr.io 21 | username: ${{ github.actor }} 22 | password: ${{ secrets.GITHUB_TOKEN }} 23 | - name: Set version 24 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 25 | - name: Build and push 26 | id: docker_build 27 | uses: docker/build-push-action@v2 28 | with: 29 | push: true 30 | tags: "ghcr.io/${{ github.repository }}:${{ env.RELEASE_VERSION }}" 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17-alpine as build 2 | WORKDIR /yurt-tools 3 | COPY . . 4 | RUN go mod vendor && \ 5 | CGO_ENABLED=0 go build -o /yurt ./cmd/yurt/main.go 6 | 7 | FROM alpine:latest as cacerts 8 | RUN apk add --no-cache ca-certificates 9 | 10 | FROM scratch 11 | COPY --from=build /yurt /yurt 12 | COPY --from=cacerts /etc/ssl /etc/ssl 13 | COPY theme /theme 14 | ENTRYPOINT ["/yurt"] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Michael Aldridge 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yurt-tools 2 | 3 | Yurt Tools is a collection of tools for your Nomad cluster. This 4 | contains things that shouldn't necessarily be built in to Nomad, but 5 | are very useful to have. 6 | 7 | ![Screenshot of yurt-tools web interface](img/yurt_fe.png) 8 | 9 | The Yurt Tools web interface is an intuitive way to visualize data 10 | about your cluster. The data is in an easily digestable format and is 11 | suitable for exporting into reports. 12 | 13 | The yurt-tools system is made up of many tools, described in brief 14 | below and in detail on the linked pages. 15 | 16 | If you want to deploy the yurt-tools suite, you can find a sample set 17 | of JobSpecs in the [nomad](nomad/) directory. 18 | 19 | --- 20 | 21 | ## [task-discover](cmd/task-discover/) 22 | 23 | This tool scrapes the Nomad job list and populates a key structure in 24 | Consul to allow other tools to fetch more detailed data on a job. 25 | 26 | ## [version-checker](cmd/version-checker/) 27 | 28 | This tool searches for newer versions of a task's input image to see 29 | if the latest version is running. 30 | 31 | ## [trivy-dispatch](cmd/trivy-dispatch/) 32 | 33 | This tool runs parallel scans of your Docker images using the Trivy 34 | security scanning tool. These results are surfaced directly in the 35 | web interface. 36 | 37 | ## [yurt-fe](cmd/yurt-fe/) 38 | 39 | This is the main web frontend of the Yurt Tools suite. This task must 40 | be running if you want to get the human-readable outputs from the 41 | system, but you can also parse the JSON tree from Consul directly to 42 | pull the results into your own monitoring and reporting tools. 43 | 44 | ## [up2date](cmd/up2date/) 45 | 46 | This is a legacy tool that helps you determine when tasks are out of 47 | date. Only use this if you have a good reason to avoid the full 48 | yurt-tools suite, as you will get a better experience from the full 49 | suite. 50 | -------------------------------------------------------------------------------- /cmd/yurt/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/the-maldridge/yurt-tools/internal/cmdlets" 5 | // additional custom filters 6 | _ "github.com/the-maldridge/yurt-tools/internal/http/filters" 7 | ) 8 | 9 | func main() { 10 | cmdlets.Entrypoint() 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/the-maldridge/yurt-tools 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/docker/distribution v0.0.0-20171011171712-7484e51bf6af // indirect 7 | github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect 8 | github.com/flosch/pongo2/v4 v4.0.2 9 | github.com/go-chi/chi/v5 v5.0.7 10 | github.com/go-redis/redis/v8 v8.11.5 // indirect 11 | github.com/gorilla/mux v1.7.3 // indirect 12 | github.com/hashicorp/consul/api v1.4.0 13 | github.com/hashicorp/go-version v1.2.0 14 | github.com/hashicorp/nomad/api v0.0.0-20191209204022-bbb6b2af09e8 15 | github.com/labstack/echo v3.3.10+incompatible // indirect 16 | github.com/labstack/gommon v0.3.0 // indirect 17 | github.com/nokia/docker-registry-client v0.0.0-20190305095957-e91f10057c5b 18 | github.com/opencontainers/go-digest v1.0.0-rc1 // indirect 19 | github.com/sirupsen/logrus v1.4.2 // indirect 20 | github.com/spf13/cobra v1.2.1 21 | github.com/stretchr/testify v1.7.0 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 17 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 18 | cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= 19 | cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= 20 | cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= 21 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 22 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 23 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 24 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 25 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 26 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 27 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 28 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 29 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 30 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 31 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 32 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 33 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 34 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 35 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 36 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 37 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 38 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 39 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 40 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 41 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 42 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 43 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 44 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= 45 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 46 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 47 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 48 | github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= 49 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 50 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 51 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 52 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 53 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 54 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 55 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 56 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 57 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 58 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 59 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 60 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 61 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 62 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 63 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 64 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 65 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 66 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 67 | github.com/docker/distribution v0.0.0-20171011171712-7484e51bf6af h1:ujR+JcSHkOZMctuIgvi+a/VHpTn0nSy0W7eV5p34xjg= 68 | github.com/docker/distribution v0.0.0-20171011171712-7484e51bf6af/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 69 | github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk= 70 | github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 71 | github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= 72 | github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= 73 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 74 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 75 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 76 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 77 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 78 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 79 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 80 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 81 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 82 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 83 | github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= 84 | github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= 85 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 86 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 87 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 88 | github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= 89 | github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 90 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 91 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 92 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 93 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= 94 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= 95 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 96 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 97 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 98 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 99 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 100 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 101 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 102 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 103 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 104 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 105 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 106 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 107 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 108 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 109 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 110 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 111 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 112 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 113 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 114 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 115 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 116 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 117 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 118 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 119 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 120 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 121 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 122 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 123 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 124 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 125 | github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= 126 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 127 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 128 | github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= 129 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 130 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 131 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 132 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 133 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 134 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 135 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 136 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 137 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 138 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 139 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 140 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 141 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 142 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 143 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 144 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 145 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 146 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 147 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 148 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 149 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 150 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 151 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 152 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 153 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 154 | github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 155 | github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 156 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 157 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 158 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 159 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 160 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 161 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 162 | github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 h1:f0n1xnMSmBLzVfsMMvriDyA75NB/oBgILX2GcHXIQzY= 163 | github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA= 164 | github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= 165 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 166 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 167 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 168 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 169 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 170 | github.com/hashicorp/consul/api v1.4.0 h1:jfESivXnO5uLdH650JU/6AnjRoHrLhULq0FnC3Kp9EY= 171 | github.com/hashicorp/consul/api v1.4.0/go.mod h1:xc8u05kyMa3Wjr9eEAsIAo3dg8+LywT5E/Cl7cNS5nU= 172 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 173 | github.com/hashicorp/consul/sdk v0.4.0 h1:zBtCfKJZcJDBvSCkQJch4ulp59m1rATFLKwNo/LYY30= 174 | github.com/hashicorp/consul/sdk v0.4.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= 175 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 176 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 177 | github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= 178 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 179 | github.com/hashicorp/go-hclog v0.12.0 h1:d4QkX8FRTYaKaCZBoXYY8zJX2BXjWxurN/GA2tkrmZM= 180 | github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= 181 | github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= 182 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 183 | github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= 184 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 185 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= 186 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 187 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 188 | github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= 189 | github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 190 | github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= 191 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 192 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 193 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 194 | github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= 195 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 196 | github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= 197 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 198 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 199 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 200 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 201 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 202 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 203 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 204 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 205 | github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= 206 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 207 | github.com/hashicorp/nomad/api v0.0.0-20191209204022-bbb6b2af09e8 h1:yhA/TgpfhXZQnuAOyXPN7PZjMR2XwpTyzPT/zPAeShM= 208 | github.com/hashicorp/nomad/api v0.0.0-20191209204022-bbb6b2af09e8/go.mod h1:Kbx02dGxN6wnAHhSbTqeg/sdACnMMD20BFkVuAxJzds= 209 | github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= 210 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 211 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 212 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 213 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 214 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 215 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 216 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 217 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 218 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 219 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 220 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 221 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 222 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 223 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 224 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 225 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 226 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 227 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 228 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 229 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 230 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 231 | github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= 232 | github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= 233 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= 234 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 235 | github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 236 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 237 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 238 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 239 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 240 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 241 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 242 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 243 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 244 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 245 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 246 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 247 | github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= 248 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 249 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 250 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 251 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 252 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 253 | github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= 254 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 255 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 256 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 257 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 258 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 259 | github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 260 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 261 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 262 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 263 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 264 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 265 | github.com/nokia/docker-registry-client v0.0.0-20190305095957-e91f10057c5b h1:6d02Onq/KxC2qZlMzSwLx12KZU80xIS7hRQw05/nDJs= 266 | github.com/nokia/docker-registry-client v0.0.0-20190305095957-e91f10057c5b/go.mod h1:0DpUaZpSvIXrsvYc6Wb+fKwjhKz0Lu1NHwMziqTqqvA= 267 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 268 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 269 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 270 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 271 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 272 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 273 | github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 274 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 275 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 276 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 277 | github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= 278 | github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= 279 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 280 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= 281 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 282 | github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 283 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 284 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 285 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 286 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 287 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 288 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 289 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 290 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 291 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 292 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 293 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 294 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= 295 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 296 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 297 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 298 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 299 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 300 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 301 | github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= 302 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 303 | github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= 304 | github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= 305 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 306 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 307 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 308 | github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= 309 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 310 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 311 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 312 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 313 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 314 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 315 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 316 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 317 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 318 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 319 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 320 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 321 | github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= 322 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 323 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 324 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 325 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 326 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 327 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 328 | go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= 329 | go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= 330 | go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= 331 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 332 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 333 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 334 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 335 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 336 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 337 | go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 338 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 339 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 340 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= 341 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 342 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 343 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 344 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 345 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 346 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 347 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 348 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 349 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 350 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 351 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 352 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 353 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 354 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 355 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 356 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 357 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 358 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 359 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 360 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 361 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 362 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 363 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 364 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 365 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 366 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 367 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 368 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 369 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 370 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 371 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 372 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 373 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 374 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 375 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 376 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 377 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 378 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 379 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 380 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 381 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 382 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 383 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 384 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 385 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 386 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 387 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 388 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 389 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 390 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 391 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 392 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 393 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 394 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 395 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 396 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 397 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 398 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 399 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 400 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 401 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 402 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 403 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 404 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 405 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 406 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 407 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 408 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 409 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 410 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 411 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 412 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 413 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 414 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 415 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 416 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 417 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 418 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 419 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 420 | golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= 421 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= 422 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 423 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 424 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 425 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 426 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 427 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 428 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 429 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 430 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 431 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 432 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 433 | golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 434 | golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 435 | golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 436 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 437 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 438 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 439 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 440 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 441 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 442 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 443 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 444 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 445 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 446 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 447 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 448 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 449 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 450 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 451 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 452 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 453 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 454 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 455 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 456 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 457 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 458 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 459 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 460 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 461 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 462 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 463 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 464 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 465 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 466 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 467 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 468 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 469 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 470 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 471 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 472 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 473 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 474 | golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 475 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 476 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 477 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 478 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 479 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 480 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 481 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 482 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 483 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 484 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 485 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 486 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 487 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 488 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 489 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 490 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 491 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 492 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 493 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 494 | golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 495 | golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 496 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 497 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 498 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 499 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 500 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= 501 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 502 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= 503 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 504 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 505 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 506 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 507 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 508 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 509 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 510 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 511 | golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= 512 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 513 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 514 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 515 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 516 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 517 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 518 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 519 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 520 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 521 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 522 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 523 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 524 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 525 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 526 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 527 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 528 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 529 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 530 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 531 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 532 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 533 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 534 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 535 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 536 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 537 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 538 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 539 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 540 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 541 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 542 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 543 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 544 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 545 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 546 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 547 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 548 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 549 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 550 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 551 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 552 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 553 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 554 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 555 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 556 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 557 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 558 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 559 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 560 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 561 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 562 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 563 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 564 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 565 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 566 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 567 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 568 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 569 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 570 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 571 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 572 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 573 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 574 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 575 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 576 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 577 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 578 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 579 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 580 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 581 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 582 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 583 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 584 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 585 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 586 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 587 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 588 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 589 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 590 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 591 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 592 | google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= 593 | google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= 594 | google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= 595 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 596 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 597 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 598 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 599 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 600 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 601 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 602 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 603 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 604 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 605 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 606 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 607 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 608 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 609 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 610 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 611 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 612 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 613 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 614 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 615 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 616 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 617 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 618 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 619 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 620 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 621 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 622 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 623 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 624 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 625 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 626 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 627 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 628 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 629 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 630 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 631 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 632 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 633 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 634 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 635 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 636 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 637 | google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 638 | google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 639 | google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 640 | google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 641 | google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= 642 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= 643 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 644 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 645 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 646 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 647 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 648 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 649 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 650 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 651 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 652 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 653 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 654 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 655 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 656 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 657 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 658 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 659 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 660 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 661 | google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 662 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 663 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 664 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 665 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 666 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 667 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 668 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 669 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 670 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 671 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 672 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 673 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 674 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 675 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 676 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 677 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 678 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 679 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 680 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 681 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 682 | gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 683 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 684 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 685 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 686 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 687 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 688 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 689 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 690 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 691 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 692 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 693 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 694 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 695 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 696 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 697 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 698 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 699 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 700 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 701 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 702 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 703 | -------------------------------------------------------------------------------- /img/yurt_fe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-maldridge/yurt-tools/7abd26cbc9ac8954b404c245bdc6680cabbc2743/img/yurt_fe.png -------------------------------------------------------------------------------- /internal/cmdlets/http.go: -------------------------------------------------------------------------------- 1 | package cmdlets 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var ( 8 | httpCmd = &cobra.Command{ 9 | Use: "http", 10 | Short: "http provides http servelets for various features and functions", 11 | Long: httpCmdLongDocs, 12 | } 13 | httpCmdLongDocs = `http cmdlets provide various servers for different parts of the 14 | yurt-tools stack.` 15 | ) 16 | 17 | func init() { 18 | rootCmd.AddCommand(httpCmd) 19 | } 20 | -------------------------------------------------------------------------------- /internal/cmdlets/http_hello.go: -------------------------------------------------------------------------------- 1 | package cmdlets 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "log" 6 | 7 | "github.com/the-maldridge/yurt-tools/internal/http" 8 | "github.com/the-maldridge/yurt-tools/internal/http/hello" 9 | ) 10 | 11 | var ( 12 | httpHelloCmd = &cobra.Command{ 13 | Use: "hello", 14 | Short: "hello provides a static webserver that generates a hello world and 204 response", 15 | Long: httpHelloCmdLongDocs, 16 | Run: httpHelloCmdRun, 17 | } 18 | httpHelloCmdLongDocs = `It is often useful to generate a static HTTP response to test load 19 | balancers, associated routing infrastructure, and other components of 20 | your network. The hello cmdlet launches a webserver with two 21 | endpoints available: 22 | 23 | * /hello/ - Returns the static text "Hello World!" 24 | * /hello/generate_204 - Returns an empty response with status 204 25 | (No Content) 26 | 27 | Additional endpoints can be added on request.` 28 | ) 29 | 30 | func init() { 31 | httpCmd.AddCommand(httpHelloCmd) 32 | } 33 | 34 | func httpHelloCmdRun(c *cobra.Command, args []string) { 35 | srv, err := http.New() 36 | if err != nil { 37 | log.Printf("Could not initialize webserver: %s", err) 38 | return 39 | } 40 | 41 | h := hello.New() 42 | 43 | srv.Mount("/hello", h.HTTPEntry()) 44 | 45 | srv.Serve(":8080") 46 | } 47 | -------------------------------------------------------------------------------- /internal/cmdlets/http_taskinfo.go: -------------------------------------------------------------------------------- 1 | package cmdlets 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/the-maldridge/yurt-tools/internal/http" 9 | "github.com/the-maldridge/yurt-tools/internal/http/taskinfo" 10 | ) 11 | 12 | var ( 13 | httpTaskInfoCmd = &cobra.Command{ 14 | Use: "taskinfo", 15 | Short: "taskinfo provides a web server with aggregated information", 16 | Long: httpTaskInfoCmdLongDocs, 17 | Run: httpTaskInfoCmdRun, 18 | } 19 | httpTaskInfoCmdLongDocs = `taskinfo provides 2 endpoints that may be queried: 20 | 21 | /data/all - Provides all tasks in one large array 22 | /data/detail/{namespace}/{job}/{group}/{task} - Detail for one task 23 | 24 | /view/ - HTML overview of status 25 | /view/{namespace}/ - Same as above, per namespace 26 | 27 | Endpoints under /data should be expected to return JSON. Endpoints 28 | under /view will return HTML.` 29 | ) 30 | 31 | func init() { 32 | httpCmd.AddCommand(httpTaskInfoCmd) 33 | } 34 | 35 | func httpTaskInfoCmdRun(c *cobra.Command, args []string) { 36 | srv, err := http.New() 37 | if err != nil { 38 | log.Printf("Could not initialize webserver: %s", err) 39 | return 40 | } 41 | 42 | h, err := taskinfo.New() 43 | if err != nil { 44 | log.Printf("Could not intialize component: %s", err) 45 | return 46 | } 47 | 48 | srv.Mount("/taskinfo", h.HTTPEntry()) 49 | 50 | srv.Serve(":8080") 51 | } 52 | -------------------------------------------------------------------------------- /internal/cmdlets/info.go: -------------------------------------------------------------------------------- 1 | package cmdlets 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var ( 8 | infoCmd = &cobra.Command{ 9 | Use: "info", 10 | Short: "info cmdlets scrape information from tasks", 11 | Long: infoCmdLongDocs, 12 | } 13 | infoCmdLongDocs = `info cmdlets provide various information services which 14 | either obtain information from tasks directly, or provide 15 | information based on external data sources.` 16 | ) 17 | 18 | func init() { 19 | rootCmd.AddCommand(infoCmd) 20 | } 21 | -------------------------------------------------------------------------------- /internal/cmdlets/info_discover.go: -------------------------------------------------------------------------------- 1 | package cmdlets 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/the-maldridge/yurt-tools/internal/kv" 9 | "github.com/the-maldridge/yurt-tools/internal/nomad" 10 | ) 11 | 12 | var ( 13 | infoDiscoverCmd = &cobra.Command{ 14 | Use: "discover", 15 | Short: "discover tasks running in Nomad", 16 | Long: discoverCmdLongDocs, 17 | Run: discoverCmdRun, 18 | } 19 | discoverCmdLongDocs = `scrape a listing of all tasks running in Nomad and store them for 20 | other tasks to key off of.` 21 | ) 22 | 23 | func init() { 24 | infoCmd.AddCommand(infoDiscoverCmd) 25 | } 26 | 27 | func discoverCmdRun(c *cobra.Command, args []string) { 28 | nc, err := nomad.New() 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | cs, err := kv.NewKVBackend() 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | namespaces, err := nc.ListNamespaces() 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | tasks := make(map[string]nomad.Task) 44 | for _, n := range namespaces { 45 | tl, err := nc.ListTasks(nomad.QueryOpts{Namespace: n}) 46 | if err != nil { 47 | log.Printf("Error querying namespace %s: %v", n, err) 48 | continue 49 | } 50 | for _, t := range tl { 51 | tasks[t.Path()] = t 52 | } 53 | } 54 | 55 | kt, err := cs.KnownTasks() 56 | if err != nil { 57 | log.Printf("Error reading back task list: %v", err) 58 | return 59 | } 60 | known := make(map[string]nomad.Task, len(kt)) 61 | for _, k := range kt { 62 | known[k.Path()] = k 63 | } 64 | 65 | for path, task := range tasks { 66 | if err := cs.UpdateTaskData(task, "metadata", task); err != nil { 67 | log.Printf("Could not update task metadata: %v", err) 68 | } 69 | delete(known, path) 70 | } 71 | 72 | for k, t := range known { 73 | log.Printf("Obsolete Task: %s", k) 74 | if err := cs.DeleteTask(t); err != nil { 75 | log.Printf("Could not remove obsolete task: %v", err) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/cmdlets/info_trivy.go: -------------------------------------------------------------------------------- 1 | package cmdlets 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/the-maldridge/yurt-tools/internal/kv" 11 | "github.com/the-maldridge/yurt-tools/internal/nomad" 12 | ) 13 | 14 | var ( 15 | infoTrivyCmd = &cobra.Command{ 16 | Use: "trivy-dispatch", 17 | Short: "Launch trivy scans for all compatible discovered containers", 18 | Long: trivyCmdLongDocs, 19 | Run: trivyCmdRun, 20 | } 21 | trivyCmdLongDocs = `Security scanning of compatible container images is performed by 22 | Aquasec Trivy (https://aquasecurity.github.io/trivy/. This info 23 | provider can be used to launch highly parallel scans of your 24 | discovered containers to a trivy server.).` 25 | ) 26 | 27 | func init() { 28 | infoCmd.AddCommand(infoTrivyCmd) 29 | } 30 | 31 | func trivyCmdRun(c *cobra.Command, args []string) { 32 | prefix := os.Getenv("STORE_PREFIX") 33 | 34 | job := os.Getenv("YURT_TRIVY_DISPATCHABLE") 35 | if job == "" { 36 | job = "yurt-task-trivy-scan" 37 | } 38 | 39 | cs, err := kv.NewKVBackend() 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | nc, err := nomad.New() 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | tasks, err := cs.KnownTasks() 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | for _, task := range tasks { 55 | switch task.Driver { 56 | case "docker": 57 | meta := map[string]string{ 58 | "TRIVY_CONTAINER": task.Docker.RepoStr(), 59 | "TRIVY_REGISTRY": task.Docker.Registry().Name, 60 | "TRIVY_OUTPUT": path.Clean(path.Join( 61 | prefix, 62 | "taskinfo", 63 | task.Namespace, 64 | task.Job, 65 | task.Group, 66 | task.Name, 67 | "trivy", 68 | )), 69 | } 70 | if err := nc.Dispatch(job, meta); err != nil { 71 | log.Printf("Error dispatching job: %v", err) 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/cmdlets/info_versions.go: -------------------------------------------------------------------------------- 1 | package cmdlets 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/the-maldridge/yurt-tools/internal/docker" 9 | "github.com/the-maldridge/yurt-tools/internal/kv" 10 | "github.com/the-maldridge/yurt-tools/internal/versions" 11 | ) 12 | 13 | var ( 14 | infoVersionsCmd = &cobra.Command{ 15 | Use: "version-check", 16 | Short: "Seek newer versions of task artifacts", 17 | Long: versionsCmdLongDocs, 18 | Run: versionsCmdRun, 19 | } 20 | versionsCmdLongDocs = `The version checker reaches out to remote repositories to determine 21 | newer versions of available containers. Versions will be compared 22 | using go-version to determine up to 5 versions newer than what is 23 | currently deployed.` 24 | ) 25 | 26 | func init() { 27 | infoCmd.AddCommand(infoVersionsCmd) 28 | } 29 | 30 | func versionsCmdRun(c *cobra.Command, args []string) { 31 | cs, err := kv.NewKVBackend() 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | ds, err := docker.New() 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | tasks, err := cs.KnownTasks() 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | for _, task := range tasks { 47 | switch task.Driver { 48 | case "docker": 49 | tags, err := ds.GetTags(task.Docker) 50 | if err != nil { 51 | log.Printf("Could not load tasks for %s: %v", task.Name, err) 52 | continue 53 | } 54 | 55 | info := versions.Compare(task.Docker.Tag, tags) 56 | if err := cs.UpdateTaskData(task, "versions", info); err != nil { 57 | log.Printf("Could not update task data for %s: %v", task.Name, err) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/cmdlets/local.go: -------------------------------------------------------------------------------- 1 | package cmdlets 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var ( 8 | localCmd = &cobra.Command{ 9 | Use: "local", 10 | Short: "local cmdlets provide debugging assistance locally.", 11 | Long: localCmdLongDocs, 12 | } 13 | localCmdLongDocs = `Local commands are intended to run from an administrator's machine 14 | and provide some help in debugging and getting various things 15 | working.` 16 | ) 17 | 18 | func init() { 19 | rootCmd.AddCommand(localCmd) 20 | } 21 | -------------------------------------------------------------------------------- /internal/cmdlets/local_dump.go: -------------------------------------------------------------------------------- 1 | package cmdlets 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/the-maldridge/yurt-tools/internal/kv" 11 | ) 12 | 13 | var ( 14 | localDumpCmd = &cobra.Command{ 15 | Use: "dump", 16 | Short: "dump resources available in kv taskinfo heirarchy", 17 | Long: localDumpCmdLongDocs, 18 | Run: localDumpCmdRun, 19 | } 20 | localDumpCmdLongDocs = `` 21 | ) 22 | 23 | func init() { 24 | localCmd.AddCommand(localDumpCmd) 25 | } 26 | 27 | func localDumpCmdRun(c *cobra.Command, args []string) { 28 | cc, err := kv.NewKVBackend() 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | tasks, err := cc.KnownTasks() 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | enc := json.NewEncoder(os.Stdout) 39 | enc.Encode(tasks) 40 | } 41 | -------------------------------------------------------------------------------- /internal/cmdlets/local_list.go: -------------------------------------------------------------------------------- 1 | package cmdlets 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/the-maldridge/yurt-tools/internal/nomad" 9 | ) 10 | 11 | var ( 12 | localListCmd = &cobra.Command{ 13 | Use: "list", 14 | Short: "list resources available in Nomad", 15 | Long: localListCmdLongDocs, 16 | Run: localListCmdRun, 17 | } 18 | localListCmdLongDocs = `list can be used to see what resources are visible to the yurt-tools. 19 | Its intended to be run from a local workstation to be able to debug 20 | token access issues, and it does not consult with the consul key/value 21 | store.` 22 | ) 23 | 24 | func init() { 25 | localCmd.AddCommand(localListCmd) 26 | } 27 | 28 | func localListCmdRun(c *cobra.Command, args []string) { 29 | nc, err := nomad.New() 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | namespaces, err := nc.ListNamespaces() 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | log.Printf("The following namespaces will be queried: %s", namespaces) 39 | 40 | for _, n := range namespaces { 41 | tasks, err := nc.ListTasks(nomad.QueryOpts{Namespace: n}) 42 | if err != nil { 43 | log.Printf("Error querying namespace %s: %v", n, err) 44 | continue 45 | } 46 | log.Printf("There are %d tasks in namespace %s", len(tasks), n) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/cmdlets/root.go: -------------------------------------------------------------------------------- 1 | package cmdlets 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | rootCmd = &cobra.Command{ 12 | Use: "yurt", 13 | Short: "yurt is a collection of tools for the prepared Nomad admin", 14 | Long: rootCmdLongDocs, 15 | } 16 | rootCmdLongDocs = `yurt is a complete suite of tools that you can use to make your life 17 | easier maintaining a Nomad cluster. yurt is a multi-call binary that 18 | you can use to invoke many yurt tools.` 19 | ) 20 | 21 | // Entrypoint is the entrypoint into all cmdlets, it will dispatch to 22 | // the right one. 23 | func Entrypoint() { 24 | if err := rootCmd.Execute(); err != nil { 25 | fmt.Fprint(os.Stderr, err.Error()) 26 | os.Exit(1) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/docker/registry.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/nokia/docker-registry-client/registry" 8 | ) 9 | 10 | type Docker struct { 11 | clients map[string]*registry.Registry 12 | } 13 | 14 | func New() (*Docker, error) { 15 | return &Docker{clients: make(map[string]*registry.Registry)}, nil 16 | } 17 | 18 | func (d *Docker) GetTags(i Image) ([]string, error) { 19 | ri := i.Registry() 20 | rh, ok := d.clients[ri.Name] 21 | if !ok { 22 | // No client for this registry exists yet. 23 | prefix := strings.ReplaceAll(strings.ToUpper(ri.Name), "-", "_") 24 | username := os.Getenv(prefix + "_USERNAME") 25 | password := os.Getenv(prefix + "_PASSWORD") 26 | 27 | c, err := registry.New(ri.URL, username, password) 28 | if err != nil { 29 | return nil, err 30 | } 31 | rh = c 32 | d.clients[ri.Name] = c 33 | } 34 | 35 | repoStr := i.Owner + "/" + i.Image 36 | if i.Owner == "" && ri.Name == "docker-hub" { 37 | repoStr = "library/" + i.Image 38 | } 39 | 40 | tags, err := rh.Tags(repoStr) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return tags, nil 45 | } 46 | -------------------------------------------------------------------------------- /internal/docker/types.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | // An Image is the complete specification that can be fed to the 4 | // docker command line to define a single image to pull and work with. 5 | type Image struct { 6 | Owner string 7 | Image string 8 | Tag string 9 | } 10 | 11 | type RegistryInfo struct { 12 | Name string 13 | URL string 14 | } 15 | -------------------------------------------------------------------------------- /internal/docker/util.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "path" 5 | "strings" 6 | ) 7 | 8 | // ParseIdentifier figures out what the different parts of the id mean 9 | // and splits them apart. This allows other parts of the system to 10 | // cleanly assert that parts of the Image struct are set. 11 | func ParseIdentifier(in string) Image { 12 | di := Image{} 13 | 14 | // First set the tag for the image. 15 | if !strings.Contains(in, ":") { 16 | di.Tag = "latest" 17 | } else { 18 | // Tag must be after a colon 19 | di.Tag = strings.FieldsFunc(in, func(c rune) bool { return c == ':' })[1] 20 | } 21 | 22 | // Split the image and owner. 23 | parts := strings.Split(in, "/") 24 | imgAndTag := parts[len(parts)-1] 25 | 26 | i := strings.SplitN(imgAndTag, ":", 2) 27 | di.Image = i[0] 28 | 29 | if len(parts) > 1 { 30 | // If there is more than one part, it must be from the 31 | // owner 32 | di.Owner = strings.Join(parts[:len(parts)-1], "/") 33 | } 34 | 35 | return di 36 | } 37 | 38 | func (i Image) URL() string { 39 | // If the owner is unset, it must be from the docker hub 40 | // library as that is where docker will pull unqualified image 41 | // names from. 42 | if i.Owner == "" { 43 | return "https://hub.docker.com/_/" + i.Image 44 | } 45 | 46 | // If there are no slashes in the Owner, it is likely to be from 47 | // hub.docker.com. To be sure we could check for any dots in 48 | // the name, but this should be sufficient for most cases. If 49 | // you're here because it wasn't, send a PR extending this 50 | // check. 51 | switch strings.Count(i.Owner, "/") { 52 | case 0: 53 | return "https://hub.docker.com/r/" + i.Owner + "/" + i.Image 54 | default: 55 | return "https://" + i.Owner + "/" + i.Image 56 | } 57 | } 58 | 59 | // Registry returns the registry that owns the requested image. 60 | // This allows pulling registries from a map rather than trying to 61 | // initialize a new registry for every context. 62 | func (i Image) Registry() RegistryInfo { 63 | imgURL := i.URL() 64 | switch { 65 | case strings.Contains(imgURL, "quay.io"): 66 | return RegistryInfo{"quay-io", "https://quay.io/"} 67 | case strings.Contains(imgURL, "ghcr.io"): 68 | return RegistryInfo{"ghcr", "https://ghcr.io/"} 69 | default: 70 | return RegistryInfo{"docker-hub", "https://registry-1.docker.io/"} 71 | } 72 | } 73 | 74 | // RepoStr returns the string that can be fed directly to docker to 75 | // pull the image. 76 | func (i Image) RepoStr() string { 77 | if i.Owner == "" { 78 | return i.Image + ":" + i.Tag 79 | } 80 | return path.Join(i.Owner, i.Image) + ":" + i.Tag 81 | } 82 | -------------------------------------------------------------------------------- /internal/docker/util_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestParseIdentifierNormal(t *testing.T) { 9 | id := ParseIdentifier("testowner/testimage:testtag") 10 | expectedId := Image{ 11 | Tag: "testtag", 12 | Owner: "testowner", 13 | Image: "testimage", 14 | } 15 | assert.Equal(t, id, expectedId) 16 | } 17 | 18 | func TestParseIdentifierNoTag(t *testing.T) { 19 | id := ParseIdentifier("testowner/testimage") 20 | expectedId := Image{ 21 | Tag: "latest", 22 | Owner: "testowner", 23 | Image: "testimage", 24 | } 25 | assert.Equal(t, id, expectedId) 26 | } 27 | 28 | func TestParseIdentifierNoOwner(t *testing.T) { 29 | id := ParseIdentifier("testimage:testtag") 30 | expectedId := Image{ 31 | Tag: "testtag", 32 | Image: "testimage", 33 | } 34 | assert.Equal(t, id, expectedId) 35 | } 36 | 37 | func TestParseIdentifierNoOwnerNoTag(t *testing.T) { 38 | id := ParseIdentifier("testimage") 39 | expectedId := Image{ 40 | Tag: "latest", 41 | Image: "testimage", 42 | } 43 | assert.Equal(t, id, expectedId) 44 | } 45 | 46 | func TestParseIdentifierURL(t *testing.T) { 47 | id := ParseIdentifier("quay.io/testowner/testimage") 48 | expectedId := Image{ 49 | Tag: "latest", 50 | Owner: "quay.io/testowner", 51 | Image: "testimage", 52 | } 53 | assert.Equal(t, id, expectedId) 54 | } 55 | 56 | func TestURLNormal(t *testing.T) { 57 | image := Image{ 58 | Tag: "testtag", 59 | Owner: "testowner", 60 | Image: "testimage", 61 | } 62 | url := image.URL() 63 | assert.Equal(t, url, "https://hub.docker.com/r/testowner/testimage") 64 | } 65 | 66 | func TestURLNoOwner(t *testing.T) { 67 | image := Image{ 68 | Tag: "testtag", 69 | Image: "testimage", 70 | } 71 | url := image.URL() 72 | assert.Equal(t, url, "https://hub.docker.com/_/testimage") 73 | } 74 | 75 | func TestURLSlashOwner(t *testing.T) { 76 | image := Image{ 77 | Tag: "testtag", 78 | Owner: "quay.io/testowner", 79 | Image: "testimage", 80 | } 81 | url := image.URL() 82 | assert.Equal(t, url, "https://quay.io/testowner/testimage") 83 | } 84 | 85 | func TestRegistryInfoDocker(t *testing.T) { 86 | image := Image{ 87 | Tag: "testtag", 88 | Owner: "testowner", 89 | Image: "testimage", 90 | } 91 | registry := image.Registry() 92 | expectedRegistry := RegistryInfo{"docker-hub", "https://registry-1.docker.io/"} 93 | assert.Equal(t, registry, expectedRegistry) 94 | } 95 | 96 | func TestRegistryInfoQuay(t *testing.T) { 97 | image := Image{ 98 | Tag: "testtag", 99 | Owner: "quay.io/testowner", 100 | Image: "testimage", 101 | } 102 | registry := image.Registry() 103 | expectedRegistry := RegistryInfo{"quay-io", "https://quay.io/"} 104 | assert.Equal(t, registry, expectedRegistry) 105 | } 106 | 107 | func TestRepoStrNormal(t *testing.T) { 108 | image := Image{ 109 | Tag: "testtag", 110 | Owner: "testowner", 111 | Image: "testimage", 112 | } 113 | repo := image.RepoStr() 114 | assert.Equal(t, repo, "testowner/testimage:testtag") 115 | } 116 | 117 | func TestRepoStrNoOwner(t *testing.T) { 118 | image := Image{ 119 | Tag: "testtag", 120 | Image: "testimage", 121 | } 122 | repo := image.RepoStr() 123 | assert.Equal(t, repo, "testimage:testtag") 124 | } 125 | -------------------------------------------------------------------------------- /internal/http/filters/supplemental.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/flosch/pongo2/v4" 7 | ) 8 | 9 | func init() { 10 | pongo2.RegisterFilter("countVulnerabilties", filterCountVulnerabilities) 11 | } 12 | 13 | // This expects to start from trivy.Results 14 | func filterCountVulnerabilities(resultsArray *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) { 15 | vulnerabilities := 0 16 | // this takes us into our list of maps with the key Vulnerabilities 17 | resultsArray.Iterate( 18 | func(idx, count int, resultObj, value *pongo2.Value) bool { 19 | // All hope abandon ye who enter here 20 | results := reflect.ValueOf(resultObj.Interface()) 21 | for _, t := range results.MapKeys() { 22 | // Once we get to the Vulnerabilties key, add it to the total 23 | if t.String() == "Vulnerabilities" { 24 | vulnerabilities_list := reflect.ValueOf(results.MapIndex(t).Interface()) 25 | vulnerabilities += vulnerabilities_list.Len() 26 | } 27 | } 28 | return true 29 | }, 30 | func() { 31 | }, 32 | ) 33 | return pongo2.AsValue(vulnerabilities), nil 34 | } 35 | -------------------------------------------------------------------------------- /internal/http/hello/hello.go: -------------------------------------------------------------------------------- 1 | package hello 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-chi/chi/v5" 8 | ) 9 | 10 | type Hello struct{} 11 | 12 | func New() *Hello { 13 | return &Hello{} 14 | } 15 | 16 | func (h *Hello) HTTPEntry() chi.Router { 17 | r := chi.NewRouter() 18 | 19 | r.Get("/", h.index) 20 | r.Get("/generate_204", h.generate204) 21 | 22 | return r 23 | } 24 | 25 | func (h *Hello) index(w http.ResponseWriter, r *http.Request) { 26 | fmt.Fprintf(w, "Hello World!") 27 | } 28 | 29 | func (h *Hello) generate204(w http.ResponseWriter, r *http.Request) { 30 | w.WriteHeader(http.StatusNoContent) 31 | } 32 | -------------------------------------------------------------------------------- /internal/http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/go-chi/chi/v5" 10 | "github.com/go-chi/chi/v5/middleware" 11 | ) 12 | 13 | // New initializes the server with its default routers. 14 | func New() (*Server, error) { 15 | s := Server{ 16 | r: chi.NewRouter(), 17 | n: &http.Server{}, 18 | } 19 | 20 | s.r.Use(middleware.Logger) 21 | s.r.Use(middleware.Heartbeat("/healthz")) 22 | 23 | s.r.Get("/-/alive", s.alive) 24 | 25 | return &s, nil 26 | } 27 | 28 | // Serve binds, initializes the mux, and serves forever. 29 | func (s *Server) Serve(bind string) error { 30 | log.Println("HTTP is starting") 31 | s.n.Addr = bind 32 | s.n.Handler = s.r 33 | s.fileServer(s.r, "/static", http.Dir("theme/static")) 34 | 35 | return s.n.ListenAndServe() 36 | } 37 | 38 | func (s *Server) alive(w http.ResponseWriter, r *http.Request) { 39 | fmt.Fprintf(w, "OK") 40 | } 41 | 42 | // Mount attaches a set of routes to the subpath specified by the path 43 | // argument. 44 | func (s *Server) Mount(path string, router chi.Router) { 45 | s.r.Mount(path, router) 46 | } 47 | 48 | func (s *Server) fileServer(r chi.Router, path string, root http.FileSystem) { 49 | if path != "/" && path[len(path)-1] != '/' { 50 | r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP) 51 | path += "/" 52 | } 53 | path += "*" 54 | 55 | r.Get(path, func(w http.ResponseWriter, r *http.Request) { 56 | rctx := chi.RouteContext(r.Context()) 57 | pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*") 58 | fs := http.StripPrefix(pathPrefix, http.FileServer(root)) 59 | fs.ServeHTTP(w, r) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /internal/http/taskinfo/taskinfo.go: -------------------------------------------------------------------------------- 1 | package taskinfo 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "sync" 8 | 9 | "github.com/flosch/pongo2/v4" 10 | "github.com/go-chi/chi/v5" 11 | "github.com/the-maldridge/yurt-tools/internal/kv" 12 | "github.com/the-maldridge/yurt-tools/internal/nomad" 13 | ) 14 | 15 | type TaskInfo struct { 16 | cc *kv.KV 17 | 18 | // We cache the task data in memory so we don't have to hit 19 | // consul every time we load up information on a given task. 20 | data map[string]map[string]map[string]map[string]kv.TaskData 21 | 22 | dMutex sync.RWMutex 23 | 24 | tmpls *pongo2.TemplateSet 25 | } 26 | 27 | func New() (*TaskInfo, error) { 28 | c, err := kv.NewKVBackend() 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | sbl, err := pongo2.NewSandboxedFilesystemLoader("theme/p2") 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | x := &TaskInfo{ 39 | cc: c, 40 | data: make(map[string]map[string]map[string]map[string]kv.TaskData), 41 | tmpls: pongo2.NewSet("html", sbl), 42 | } 43 | x.tmpls.Debug = true 44 | x.update() 45 | 46 | return x, nil 47 | } 48 | 49 | func (ti *TaskInfo) HTTPEntry() chi.Router { 50 | r := chi.NewRouter() 51 | 52 | r.Get("/data/all", ti.dumpAll) 53 | r.Get("/data/detail/{namespace}/{job}/{group}/{task}", ti.dumpTask) 54 | 55 | r.Get("/view", ti.viewAll) 56 | r.Get("/view/{namespace}/{job}/{group}/{task}", ti.viewAll) 57 | r.Get("/view/{namespace}/{job}/{group}/{task}/details", ti.viewDetails) 58 | 59 | return r 60 | } 61 | 62 | func (ti *TaskInfo) update() { 63 | known, err := ti.cc.KnownTasks() 64 | if err != nil { 65 | log.Printf("Error while updating cache: %s", err) 66 | return 67 | } 68 | 69 | for _, t := range known { 70 | d, err := ti.cc.LoadAllForTask(t) 71 | if err != nil { 72 | log.Printf("Error refreshing information for task: %s", err) 73 | continue 74 | } 75 | ti.fillPath(t, d) 76 | } 77 | } 78 | 79 | func (ti *TaskInfo) fillPath(task nomad.Task, data kv.TaskData) { 80 | ti.dMutex.Lock() 81 | defer ti.dMutex.Unlock() 82 | 83 | if ti.data[task.Namespace] == nil { 84 | ti.data[task.Namespace] = make(map[string]map[string]map[string]kv.TaskData) 85 | } 86 | if ti.data[task.Namespace][task.Job] == nil { 87 | ti.data[task.Namespace][task.Job] = make(map[string]map[string]kv.TaskData) 88 | } 89 | if ti.data[task.Namespace][task.Job][task.Group] == nil { 90 | ti.data[task.Namespace][task.Job][task.Group] = make(map[string]kv.TaskData) 91 | } 92 | 93 | ti.data[task.Namespace][task.Job][task.Group][task.Name] = data 94 | } 95 | 96 | func (ti *TaskInfo) dumpAll(w http.ResponseWriter, r *http.Request) { 97 | enc := json.NewEncoder(w) 98 | ti.dMutex.RLock() 99 | defer ti.dMutex.RUnlock() 100 | enc.Encode(ti.data) 101 | } 102 | 103 | func (ti *TaskInfo) dumpTask(w http.ResponseWriter, r *http.Request) { 104 | ti.dMutex.RLock() 105 | defer ti.dMutex.RUnlock() 106 | 107 | enc := json.NewEncoder(w) 108 | d, ok := ti.data[chi.URLParam(r, "namespace")][chi.URLParam(r, "job")][chi.URLParam(r, "group")][chi.URLParam(r, "task")] 109 | if !ok { 110 | w.WriteHeader(http.StatusNotFound) 111 | enc.Encode(map[string]string{"error": "task not found"}) 112 | return 113 | } 114 | 115 | enc.Encode(d) 116 | } 117 | 118 | func getTaskData(ti *TaskInfo, r *http.Request) (kv.TaskData, map[string]string, bool) { 119 | namespace := chi.URLParam(r, "namespace") 120 | job := chi.URLParam(r, "job") 121 | group := chi.URLParam(r, "group") 122 | task := chi.URLParam(r, "task") 123 | activeTask := make(map[string]string) 124 | d, ok := ti.data[namespace][job][group][task] 125 | if ok { 126 | activeTask["namespace"] = namespace 127 | activeTask["job"] = job 128 | activeTask["group"] = group 129 | activeTask["task"] = task 130 | } 131 | return d, activeTask, ok 132 | } 133 | 134 | func (ti *TaskInfo) viewAll(w http.ResponseWriter, r *http.Request) { 135 | t, err := ti.tmpls.FromCache("taskinfo.p2") 136 | if err != nil { 137 | ti.templateErrorHandler(w, err) 138 | return 139 | } 140 | ti.dMutex.RLock() 141 | defer ti.dMutex.RUnlock() 142 | ctx := make(map[string]interface{}) 143 | ctx["data"] = ti.data 144 | d, activeTask, hasActiveTask := getTaskData(ti, r) 145 | ctx["hasActiveTask"] = hasActiveTask 146 | ctx["activeTask"] = activeTask 147 | ctx["activeTaskData"] = d 148 | if err := t.ExecuteWriter(ctx, w); err != nil { 149 | ti.templateErrorHandler(w, err) 150 | } 151 | } 152 | 153 | func (ti *TaskInfo) viewDetails(w http.ResponseWriter, r *http.Request) { 154 | t, err := ti.tmpls.FromCache("taskinfo_details.p2") 155 | if err != nil { 156 | ti.templateErrorHandler(w, err) 157 | return 158 | } 159 | ti.dMutex.RLock() 160 | defer ti.dMutex.RUnlock() 161 | d, activeTask, _ := getTaskData(ti, r) 162 | jsonD, _ := json.MarshalIndent(d, "", " ") 163 | log.Print(string(jsonD)) 164 | ctx := make(map[string]interface{}) 165 | ctx["activeTask"] = activeTask 166 | ctx["activeTaskData"] = d 167 | if err := t.ExecuteWriter(ctx, w); err != nil { 168 | ti.templateErrorHandler(w, err) 169 | } 170 | } 171 | 172 | func (ti *TaskInfo) templateErrorHandler(w http.ResponseWriter, err error) { 173 | enc := json.NewEncoder(w) 174 | w.WriteHeader(http.StatusInternalServerError) 175 | enc.Encode(map[string]string{"error": err.Error()}) 176 | } 177 | -------------------------------------------------------------------------------- /internal/http/type.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/chi/v5" 7 | ) 8 | 9 | // Server wraps up all the request routers and associated components 10 | // that serve various parts of the nbuild stack. 11 | type Server struct { 12 | r chi.Router 13 | 14 | n *http.Server 15 | } 16 | -------------------------------------------------------------------------------- /internal/kv/consul.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/hashicorp/consul/api" 7 | ) 8 | 9 | // Consul provides durable key-value storage on top of a key/value 10 | // store, likely the one implementing service discovery for the nomad 11 | // cluster being scanned. 12 | type Consul struct { 13 | *api.Client 14 | 15 | prefix string 16 | } 17 | 18 | // NewConsul sets up a new consul client and returns a Store 19 | func NewConsul() (Store, error) { 20 | c, err := api.NewClient(api.DefaultConfig()) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | x := &Consul{Client: c} 26 | 27 | x.prefix = "yurt-tools" 28 | if prefix := os.Getenv("CONSUL_PREFIX"); prefix != "" { 29 | x.prefix = prefix 30 | } 31 | 32 | return x, nil 33 | } 34 | 35 | // Get returns the value at the given key, or an error if no such 36 | // value exists 37 | func (c *Consul) Get(key string) ([]byte, error) { 38 | kvp, _, err := c.KV().Get(key, nil) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return kvp.Value, nil 43 | } 44 | 45 | // Put stores a value to the given key 46 | func (c *Consul) Put(key string, val []byte) error { 47 | pair := api.KVPair{ 48 | Key: key, 49 | Value: val, 50 | } 51 | _, err := c.KV().Put(&pair, nil) 52 | return err 53 | } 54 | 55 | // ListPrefix takes a prefix and lists all keys at or below that 56 | // prefix. 57 | func (c *Consul) ListPrefix(prefix string) ([]string, error) { 58 | keys, _, err := c.KV().Keys(prefix, "", nil) 59 | return keys, err 60 | } 61 | 62 | // DelPrefix clears an entire subtree rooted at the given prefix. 63 | func (c *Consul) DelPrefix(prefix string) error { 64 | _, err := c.KV().DeleteTree(prefix, nil) 65 | return err 66 | } 67 | -------------------------------------------------------------------------------- /internal/kv/redis.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path" 7 | 8 | "github.com/go-redis/redis/v8" 9 | ) 10 | 11 | // Redis implements storage on top of a redis server. This should be 12 | // durable, but this is entirely dependent on the configuration of the 13 | // redis server. 14 | type Redis struct { 15 | rdb *redis.Client 16 | } 17 | 18 | // NewRedis returns a Store on top of a remote redis server. 19 | func NewRedis() (Store, error) { 20 | addr := "localhost:6379" 21 | if a := os.Getenv("REDIS_ADDR"); a != "" { 22 | addr = a 23 | } 24 | 25 | rdb := redis.NewClient(&redis.Options{ 26 | Addr: addr, 27 | }) 28 | return &Redis{rdb}, nil 29 | } 30 | 31 | // Get returns the available data or a suitable error. 32 | func (r *Redis) Get(key string) ([]byte, error) { 33 | res := r.rdb.Get(context.Background(), key) 34 | if res.Err() != nil { 35 | return nil, res.Err() 36 | } 37 | return res.Bytes() 38 | } 39 | 40 | // Put stores data to the named key. 41 | func (r *Redis) Put(key string, value []byte) error { 42 | res := r.rdb.Set(context.Background(), key, value, 0) 43 | return res.Err() 44 | } 45 | 46 | // ListPrefix returns the list of keys below a given prefix 47 | func (r *Redis) ListPrefix(prefix string) ([]string, error) { 48 | res := r.rdb.Keys(context.Background(), path.Join(prefix, "*")) 49 | return res.Result() 50 | } 51 | 52 | // DelPrefix removes all data below a given prefix 53 | func (r *Redis) DelPrefix(prefix string) error { 54 | keys, err := r.ListPrefix(prefix) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | res := r.rdb.Del(context.Background(), keys...) 60 | return res.Err() 61 | } 62 | -------------------------------------------------------------------------------- /internal/kv/store.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | "path" 8 | 9 | "github.com/the-maldridge/yurt-tools/internal/nomad" 10 | ) 11 | 12 | // NewKVBackend provides the KV interface on top of the generic Store 13 | // interface. 14 | func NewKVBackend() (*KV, error) { 15 | backend := "consul" 16 | if b := os.Getenv("YURT_BACKEND"); b != "" { 17 | backend = b 18 | } 19 | 20 | var store Store 21 | var err error 22 | switch backend { 23 | case "consul": 24 | store, err = NewConsul() 25 | case "redis": 26 | store, err = NewRedis() 27 | } 28 | 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return &KV{store}, nil 34 | } 35 | 36 | // UpdateTaskData creates or updates the data for the specified task. 37 | func (k *KV) UpdateTaskData(t nomad.Task, key string, data interface{}) error { 38 | bytes, err := json.Marshal(data) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return k.s.Put(path.Join("taskinfo", t.Namespace, t.Job, t.Group, t.Name, key), bytes) 44 | } 45 | 46 | // KnownTasks returns a list of all tasks that are known to 47 | // yurt-tools. This is not necessarily every task known to nomad, but 48 | // all the ones that yurt-tools was allowed to read. 49 | func (k *KV) KnownTasks() ([]nomad.Task, error) { 50 | keys, err := k.s.ListPrefix("taskinfo") 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | out := []nomad.Task{} 56 | for _, key := range keys { 57 | bytes, err := k.s.Get(key) 58 | if err != nil { 59 | log.Printf("Error with fetch for key: %s %v", key, err) 60 | continue 61 | } 62 | 63 | t := nomad.Task{} 64 | if err := json.Unmarshal(bytes, &t); err != nil { 65 | log.Printf("Bad json at key: %s %v", key, err) 66 | continue 67 | } 68 | if t.Name == "" { 69 | // Empty task 70 | continue 71 | } 72 | out = append(out, t) 73 | } 74 | return out, nil 75 | } 76 | 77 | // LoadAllForTask loads up all the information for a task including 78 | // the default taskinfo blob, and anything written by other discovery 79 | // plugins. 80 | func (k *KV) LoadAllForTask(t nomad.Task) (TaskData, error) { 81 | m := make(TaskData) 82 | 83 | keys, err := k.s.ListPrefix(path.Clean(path.Join("taskinfo", t.Path()))) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | for _, key := range keys { 89 | bytes, err := k.s.Get(key) 90 | if err != nil { 91 | log.Printf("Error pulling value at key: %s - %v", key, err) 92 | continue 93 | } 94 | 95 | var tmp interface{} 96 | if err := json.Unmarshal(bytes, &tmp); err != nil { 97 | log.Printf("Couldn't unmarshal data for %s:%s - %v", t.Name, key, err) 98 | continue 99 | } 100 | m[path.Base(key)] = tmp 101 | } 102 | return m, nil 103 | } 104 | 105 | // DeleteTask removes the taskinfo blob and all other information for 106 | // a given task and is mainly intended to be a way for removing 107 | // records of tasks that are no longer running. 108 | func (k *KV) DeleteTask(t nomad.Task) error { 109 | return k.s.DelPrefix(path.Clean(path.Join("taskinfo", t.Path()))) 110 | } 111 | -------------------------------------------------------------------------------- /internal/kv/type.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | // Store defines the mechanisms that a storage interface must provide. 4 | // These do not strictly need to be long term durable, but that is 5 | // preferable. 6 | type Store interface { 7 | Get(string) ([]byte, error) 8 | Put(string, []byte) error 9 | DelPrefix(string) error 10 | ListPrefix(string) ([]string, error) 11 | } 12 | 13 | // KV is a mechanism for storing and returning information within the 14 | // system about tasks. 15 | type KV struct { 16 | s Store 17 | } 18 | 19 | // TaskData is the base type for information concerning a task running 20 | // in the cluster. 21 | type TaskData map[string]interface{} 22 | -------------------------------------------------------------------------------- /internal/nomad/doc.go: -------------------------------------------------------------------------------- 1 | // Package nomad is an interface to nomad for retrieving tasks and 2 | // launching parameterized jobs. 3 | package nomad 4 | -------------------------------------------------------------------------------- /internal/nomad/nomad.go: -------------------------------------------------------------------------------- 1 | package nomad 2 | 3 | import ( 4 | "path" 5 | "strings" 6 | 7 | "github.com/hashicorp/nomad/api" 8 | 9 | "github.com/the-maldridge/yurt-tools/internal/docker" 10 | ) 11 | 12 | // A Client is a wrapper around a nomad API client which allows much 13 | // more convenient methods to be built on top of it to provide 14 | // information to the caller and simplify task dispatch. 15 | type Client struct { 16 | *api.Client 17 | } 18 | 19 | // QueryOpts shadows the nomad type of the same name. 20 | type QueryOpts struct { 21 | Region string 22 | Namespace string 23 | Prefix string 24 | } 25 | 26 | // Task represents a simplified view of a nomad task. 27 | type Task struct { 28 | Namespace string 29 | Job string 30 | Group string 31 | Name string 32 | Driver string 33 | URL string 34 | Meta map[string]string 35 | Docker docker.Image 36 | } 37 | 38 | // Path returns a path like string representation of where the task 39 | // lives in the hierarchy. 40 | func (t Task) Path() string { 41 | return "/" + path.Join(t.Namespace, t.Job, t.Group, t.Name) 42 | } 43 | 44 | // New returns a new nomad client initialized with parameters from the 45 | // environment. 46 | func New() (*Client, error) { 47 | c, err := api.NewClient(api.DefaultConfig()) 48 | if err != nil { 49 | return nil, err 50 | } 51 | x := new(Client) 52 | x.Client = c 53 | return x, nil 54 | } 55 | 56 | // ListNamespaces returns a list of namespaces in the given cluster. 57 | func (c *Client) ListNamespaces() ([]string, error) { 58 | list, _, err := c.Namespaces().List(&api.QueryOptions{}) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | out := make([]string, len(list)) 64 | for i := range list { 65 | out[i] = list[i].Name 66 | } 67 | 68 | return out, nil 69 | } 70 | 71 | // ListTasks crawls nomad for running tasks. This can include batch 72 | // tasks, service tasks, system tasks, and any other kind of task as 73 | // long as it is running. 74 | func (c *Client) ListTasks(cfg QueryOpts) ([]Task, error) { 75 | qopts := api.QueryOptions{ 76 | Region: cfg.Region, 77 | Namespace: cfg.Namespace, 78 | Prefix: cfg.Prefix, 79 | } 80 | 81 | list, _, err := c.Jobs().List(&qopts) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | tl := []Task{} 87 | for _, i := range list { 88 | if i.Stop || i.Type == api.JobTypeBatch { 89 | continue 90 | } 91 | 92 | job, _, err := c.Jobs().Info(i.ID, &qopts) 93 | if err != nil { 94 | return nil, err 95 | } 96 | for _, taskGroup := range job.TaskGroups { 97 | for _, task := range taskGroup.Tasks { 98 | if strings.HasPrefix(task.Name, "connect-proxy-") { 99 | // These produce meaningless 100 | // results due to being 101 | // parameterized versions on a 102 | // per-node basis. 103 | continue 104 | } 105 | 106 | meta := make(map[string]string, len(job.Meta)+len(taskGroup.Meta)+len(task.Meta)) 107 | for k, v := range job.Meta { 108 | meta[k] = v 109 | } 110 | for k, v := range taskGroup.Meta { 111 | meta[k] = v 112 | } 113 | for k, v := range task.Meta { 114 | meta[k] = v 115 | } 116 | 117 | if _, ok := meta["yurt:taskinfo:skip"]; ok { 118 | // Skip if the 119 | // yurt:taskinfo:skip key has 120 | // been specified, which 121 | // causes this task to be 122 | // invisible to the taskinfo 123 | // features of the yurt-tools. 124 | continue 125 | } 126 | 127 | t := Task{ 128 | Namespace: cfg.Namespace, 129 | Job: *job.Name, 130 | Group: *taskGroup.Name, 131 | Name: task.Name, 132 | Driver: task.Driver, 133 | Meta: meta, 134 | } 135 | 136 | switch t.Driver { 137 | case "docker": 138 | t.Docker = docker.ParseIdentifier(task.Config["image"].(string)) 139 | t.URL = t.Docker.URL() 140 | } 141 | 142 | tl = append(tl, t) 143 | } 144 | } 145 | } 146 | return tl, nil 147 | } 148 | 149 | // Dispatch can be used to dispatch a parameterized task. 150 | func (c *Client) Dispatch(name string, kv map[string]string) error { 151 | if _, _, err := c.Jobs().Dispatch(name, kv, nil, nil); err != nil { 152 | return err 153 | } 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /internal/versions/version_test.go: -------------------------------------------------------------------------------- 1 | package versions 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSemverUpToDate(t *testing.T) { 10 | info := Compare("2.3.4", []string{"0.1", "1.0.2", "2.3.4"}) 11 | expectedInfo := VersionInfo{ 12 | Current: "2.3.4", 13 | Available: nil, 14 | UpToDate: true, 15 | NonComparable: false, 16 | } 17 | assert.Equal(t, info, expectedInfo) 18 | } 19 | 20 | func TestSemverOutOfDate(t *testing.T) { 21 | info := Compare("2.3.4", []string{"0.1", "1.0.2", "2.3.4", "2.3.5", "3.0"}) 22 | expectedInfo := VersionInfo{ 23 | Current: "2.3.4", 24 | Available: []string{"2.3.5", "3.0"}, 25 | UpToDate: false, 26 | NonComparable: false, 27 | } 28 | assert.Equal(t, info.Current, expectedInfo.Current) 29 | assert.ElementsMatch(t, info.Available, expectedInfo.Available) 30 | assert.Equal(t, info.UpToDate, expectedInfo.UpToDate) 31 | assert.Equal(t, info.NonComparable, expectedInfo.NonComparable) 32 | } 33 | 34 | func TestNonComparable(t *testing.T) { 35 | info := Compare("bbb", []string{"aaa", "bbb"}) 36 | expectedInfo := VersionInfo{ 37 | Current: "bbb", 38 | Available: nil, 39 | UpToDate: true, 40 | NonComparable: true, 41 | } 42 | assert.Equal(t, info, expectedInfo) 43 | } 44 | 45 | func TestRCPrereleaseOutOfDate(t *testing.T) { 46 | info := Compare("20200420RC01", []string{"20200419RC02", "20200420RC01", "20200420RC02"}) 47 | expectedInfo := VersionInfo{ 48 | Current: "20200420RC01", 49 | Available: []string{"20200420RC02"}, 50 | UpToDate: false, 51 | NonComparable: false, 52 | } 53 | assert.Equal(t, info, expectedInfo) 54 | } 55 | 56 | func TestRCHashUpToDate(t *testing.T) { 57 | info := Compare("20200420RC91e61f094", []string{"20200419RCfc9ea1ba4", "20200420RC2748f6a34", "20200420RCeeba0ab3c"}) 58 | expectedInfo := VersionInfo{ 59 | Current: "20200420RC91e61f094", 60 | Available: nil, 61 | UpToDate: true, 62 | NonComparable: false, 63 | } 64 | assert.Equal(t, info, expectedInfo) 65 | } 66 | 67 | func TestRCHashOutOfDate(t *testing.T) { 68 | info := Compare("20200420RC91e61f094", []string{"20200419RCfc9ea1ba4", "20200420RC2748f6a34", "20200420RCeeba0ab3c", "20200421RCaf230b946"}) 69 | expectedInfo := VersionInfo{ 70 | Current: "20200420RC91e61f094", 71 | Available: []string{"20200421RCaf230b946"}, 72 | UpToDate: false, 73 | NonComparable: false, 74 | } 75 | assert.Equal(t, info, expectedInfo) 76 | } 77 | 78 | func TestRCDateOutOfDate(t *testing.T) { 79 | info := Compare("1.0RC20200420", []string{"0.9RC20200310", "1.0RC20200419", "1.0RC20200420", "1.0RC20200421", "1.1RC20200422"}) 80 | expectedInfo := VersionInfo{ 81 | Current: "1.0RC20200420", 82 | Available: []string{"1.0RC20200421", "1.1RC20200422"}, 83 | UpToDate: false, 84 | NonComparable: false, 85 | } 86 | assert.Equal(t, info.Current, expectedInfo.Current) 87 | assert.ElementsMatch(t, info.Available, expectedInfo.Available) 88 | assert.Equal(t, info.UpToDate, expectedInfo.UpToDate) 89 | assert.Equal(t, info.NonComparable, expectedInfo.NonComparable) 90 | } 91 | -------------------------------------------------------------------------------- /internal/versions/versions.go: -------------------------------------------------------------------------------- 1 | package versions 2 | 3 | import ( 4 | "regexp" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/hashicorp/go-version" 9 | ) 10 | 11 | type VersionInfo struct { 12 | Current string 13 | Available []string 14 | UpToDate bool 15 | NonComparable bool 16 | } 17 | 18 | func Compare(c string, a []string) VersionInfo { 19 | vi := VersionInfo{Current: c, UpToDate: true} 20 | 21 | have, err := version.NewVersion(c) 22 | if err != nil { 23 | vi.NonComparable = true 24 | return vi 25 | } 26 | 27 | // The practice of having git hashes as your release candidate makes version 28 | // comparison difficult. Instead we will ignore these hashes. 29 | // This could break comparison over very long release candidates, e.g. rc100000 30 | // But this kind of value is unlikely to exist in the Prerelease 31 | // We check 8-digit endings starting with `20` to catch most dates 32 | gitHash := regexp.MustCompile("[a-f0-9]{7,}$") 33 | date := regexp.MustCompile("20[0-9]{6}$") 34 | if gitHash.MatchString(have.Prerelease()) && !date.MatchString(have.Prerelease()) { 35 | noPreVersion := strings.Replace(c, have.Prerelease(), "", -1) 36 | newHave, err := version.NewVersion(noPreVersion) 37 | if err == nil { 38 | have = newHave 39 | } 40 | } 41 | 42 | versions := []*version.Version{} 43 | for i := range a { 44 | v, err := version.NewVersion(a[i]) 45 | if err != nil { 46 | continue 47 | } 48 | versions = append(versions, v) 49 | } 50 | sort.Sort(sort.Reverse(version.Collection(versions))) 51 | 52 | for _, v := range versions { 53 | if have.LessThan(v) { 54 | vi.Available = append(vi.Available, v.Original()) 55 | vi.UpToDate = false 56 | } 57 | if len(vi.Available) > 5 { 58 | break 59 | } 60 | } 61 | 62 | return vi 63 | } 64 | -------------------------------------------------------------------------------- /nomad/trivy-scan.nomad: -------------------------------------------------------------------------------- 1 | job "trivy-scan" { 2 | type = "batch" 3 | datacenters = ["DC1"] 4 | parameterized { 5 | payload = "forbidden" 6 | meta_required = [ 7 | "TRIVY_CONTAINER", 8 | "TRIVY_REGISTRY", 9 | "TRIVY_OUTPUT", 10 | ] 11 | } 12 | group "app-scan" { 13 | task "trivy" { 14 | driver = "docker" 15 | vault { 16 | policies = [ 17 | "trivy", 18 | "yurt-tools", 19 | ] 20 | change_mode = "noop" 21 | } 22 | config { 23 | image = "aquasec/trivy:0.4.4" 24 | entrypoint = ["/local/do-scan"] 25 | } 26 | resources { 27 | cpu = 1000 28 | memory = 512 29 | } 30 | template { 31 | data = < 2 | 3 | 4 | {% block title %}Yurt Tools{% endblock %} 5 | 6 | 7 | 8 | 9 | 12 |
13 | {% block content %} 14 | {% endblock %} 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /theme/p2/icons/lock.p2: -------------------------------------------------------------------------------- 1 | {% macro lock_icon(class) export %} 2 | 3 | 4 | 5 | {% endmacro %} 6 | 7 | {% macro lock_from_info(info) export %} 8 | {% if info.trivy && info.trivy.Results %} 9 | {% if info.trivy.Results|countVulnerabilties == 0 %} 10 | {{lock_icon("success")}} 11 | {% else %} 12 | {{lock_icon("error")}} 13 | {% endif %} 14 | {% else %} 15 | {{lock_icon("disabled")}} 16 | {% endif %} 17 | {% endmacro %} 18 | -------------------------------------------------------------------------------- /theme/p2/icons/watch.p2: -------------------------------------------------------------------------------- 1 | {% macro watch_icon(class) export %} 2 | 3 | 4 | 5 | {% endmacro %} 6 | 7 | {% macro watch_from_info(info) export %} 8 | {% if info.versions && info.versions.UpToDate %} 9 | {{watch_icon("success")}} 10 | {% else %} 11 | {% if info.versions %} 12 | {{watch_icon("error")}} 13 | {% else %} 14 | {{watch_icon("disabled")}} 15 | {% endif %} 16 | {% endif %} 17 | {% endmacro %} 18 | -------------------------------------------------------------------------------- /theme/p2/partials/trivy.p2: -------------------------------------------------------------------------------- 1 | Artifact: {{trivy.ArtifactName}}
2 | Type: {{trivy.ArtifactType}}
3 | 4 | {% for section in trivy.Results %} 5 | {% with section=section %} 6 | {% include "partials/trivy_results.p2" %} 7 | {% endwith %} 8 |
9 | {% endfor %} 10 | -------------------------------------------------------------------------------- /theme/p2/partials/trivy_results.p2: -------------------------------------------------------------------------------- 1 | Class: {{section.Class}}
2 | Type: {{section.Type}}
3 | 4 | {% for vuln in section.Vulnerabilities %} 5 | {% with vuln=vuln %} 6 | {% include "partials/trivy_vuln.p2" %} 7 | {% endwith %} 8 |
9 | {% endfor %} 10 | -------------------------------------------------------------------------------- /theme/p2/partials/trivy_vuln.p2: -------------------------------------------------------------------------------- 1 | PkgName: {{vuln.PkgName}}
2 | Vulnerability: {{vuln.VulnerabilityID}}
3 | Severity: {{vuln.Severity}}
4 | Installed: {{vuln.InstalledVersion}}
5 | Fixed: {{vuln.FixedVersion}}
6 | Cf.: More Info
7 | Description: {{vuln.Description|truncatewords:20}}
8 | -------------------------------------------------------------------------------- /theme/p2/taskinfo.p2: -------------------------------------------------------------------------------- 1 | {% extends "base.p2" %} 2 | 3 | {% block content %} 4 | 5 | {% import "./taskinfo_details.p2" taskinfo_details %} 6 | {% import "./icons/lock.p2" lock_icon, lock_from_info %} 7 | {% import "./icons/watch.p2" watch_icon, watch_from_info %} 8 | 9 |
10 |
11 | {% for namespace, jobs in data %} 12 |
13 |
14 |

{{namespace}}

15 |
16 |
17 | {% for job, groups in jobs %} 18 |
19 |
20 |

{{job}}

21 |
22 | {% for group, tasks in groups %} 23 | {% for task, info in tasks %} 24 |
31 |
32 |
33 |

34 | {{task}} 35 |

36 |

37 | {{group}} 38 |

39 |
40 |
41 |
42 | {{watch_from_info(info)}} 43 | {{lock_from_info(info)}} 44 |
45 |
46 | {% endfor %} 47 | {% endfor %} 48 |
49 | {% endfor %} 50 |
51 |
52 | {% endfor %} 53 |
54 |
55 | {% if hasActiveTask %} 56 |
57 | {{taskinfo_details(activeTaskData)}} 58 |
59 | {% else %} 60 |
61 | {% endif %} 62 | 63 | 66 | {% endblock %} 67 | -------------------------------------------------------------------------------- /theme/p2/taskinfo_details.p2: -------------------------------------------------------------------------------- 1 | {% import "./icons/lock.p2" lock_icon, lock_from_info %} 2 | {% import "./icons/watch.p2" watch_icon, watch_from_info %} 3 | 4 | {% macro taskinfo_details(task) export %} 5 |
6 |
7 | 8 | {{activeTask.namespace}} 9 | 10 | >> 11 | 12 | {{activeTask.job}} 13 | 14 | >> 15 | 16 | {{activeTask.group}} 17 | 18 |
19 |
20 |
21 |
22 |

23 | docker 24 |

25 |

26 | {{activeTask.group}} 27 |

28 |

29 | {{activeTaskData.versions.Current}} 30 |

31 |

32 | {% if activeTaskData.metadata.Docker.Owner %} 33 | {{activeTaskData.metadata.Docker.Owner}} 34 | {% else %} 35 | {{activeTaskData.metadata.Docker.Image}} 36 | {% endif %} 37 |

38 |

39 | 40 | REGISTRY 41 | 42 |

43 |
44 |
45 |
46 |
47 |

48 | security 49 |

50 |
51 | {% if activeTaskData.trivy.Results|length == 0 %} 52 | 53 | No Data available 54 | 55 | {% else %} 56 | {% if activeTaskData.trivy.Results|countVulnerabilties == 0 %} 57 | 58 | No Vulnerabilities Found 59 | 60 | {{lock_icon("success")}} 61 | {% else %} 62 | 63 | Vulnerabilities Found 64 | 65 | {{lock_icon("error")}} 66 | {% endif %} 67 | {% endif %} 68 |
69 | {% for Result in activeTaskData.trivy.Results %} 70 |
71 | 72 | {{Result.Class}}/{{Result.Type}}:  73 | 74 | 75 | {{Result.Vulnerabilities|length}} vulnerabilit{{Result|length|pluralize:"y,ies"}} discovered 76 | 77 |
78 | {% endfor %} 79 |
80 |
81 |
82 |
83 |

84 | version 85 |

86 | {% if activeTaskData.versions.UpToDate %} 87 |
88 | 89 | Up to date 90 | 91 | {{watch_icon("success")}} 92 |
93 | {% else %} 94 |
95 | 96 | Not up to date 97 | 98 | {{watch_icon("error")}} 99 |
100 | {% endif %} 101 |

102 | {{activeTaskData.versions.Current}} 103 |

104 |

105 | {% if activeTaskData.versions.UpToDate %} 106 | Latest version 107 | {% else %} 108 | Not on latest version 109 | {% endif %} 110 |

111 |

112 | 113 | TAGS 114 | 115 |

116 |
117 |
118 |
119 | {% if activeTaskData.trivy.Results|length != 0 %} 120 | {% for Result in activeTaskData.trivy.Results %} 121 |
122 | {{Result.Target}} 123 |
124 | 125 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 162 | {% for Vuln in Result.Vulnerabilities %} 163 | 164 | 169 | 174 | 179 | 184 | 189 | 192 | 197 | 198 | {% endfor %} 199 |
127 | 128 | Package Name 129 | 130 | 132 | 133 | Vulnerability 134 | 135 | 137 | 138 | Severity 139 | 140 | 142 | 143 | Installed Version 144 | 145 | 147 | 148 | Fixed Version 149 | 150 | 152 | 153 | More Info 154 | 155 | 157 | 158 | Description 159 | 160 |
165 | 166 | {{Vuln.PkgName}} 167 | 168 | 170 | 171 | {{Vuln.VulnerabilityID}} 172 | 173 | 175 | 176 | {{Vuln.Severity}} 177 | 178 | 180 | 181 | {{Vuln.InstalledVersion}} 182 | 183 | 185 | 186 | {{Vuln.FixedVersion}} 187 | 188 | 190 | Link 191 | 193 | 194 | {{Vuln.Description|truncatewords:20}} 195 | 196 |
200 | {% endfor %} 201 | {% endif %} 202 |
203 | {% endmacro %} 204 | 205 | {{taskinfo_details(task)}} 206 | -------------------------------------------------------------------------------- /theme/static/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /theme/static/css/theme.css: -------------------------------------------------------------------------------- 1 | /* Dark media queries / default */ 2 | :root { 3 | --adjust-sm-color-reverse: rgba(0, 0, 0, 0.025); 4 | --adjust-xs-color: rgba(255, 255, 255, 0.0125); 5 | --adjust-sm-color: rgba(255, 255, 255, 0.025); 6 | --adjust-color: rgba(255, 255, 255, 0.05); 7 | --adjust-lg-color: rgba(255, 255, 255, 0.1); 8 | --adjust-xl-color: rgba(255, 255, 255, 0.3); 9 | --border-radius: 8px; 10 | --error-color: #e67074; 11 | --info-color: #bbe; 12 | --info-color-2: #e390fc; 13 | --success-color: #22e6a8; 14 | --warning-color: #ebeb73; 15 | --warning-color-2: #e8b674; 16 | --bg-color: #121212; 17 | --text-color-light: rgba(255, 255, 255, 0.7); 18 | --text-color: rgba(255, 255, 255, 0.95); 19 | --darken-color: rgba(0, 0, 0, 0.05); 20 | } 21 | 22 | /* Light media queries */ 23 | @media (prefers-color-scheme: light) { 24 | :root { 25 | --adjust-sm-color-reverse: rgba(255, 255, 255, 0.025); 26 | --adjust-xs-color: rgba(0, 0, 0, 0.0125); 27 | --adjust-sm-color: rgba(0, 0, 0, 0.025); 28 | --adjust-color: rgba(0, 0, 0, 0.05); 29 | --adjust-lg-color: rgba(0, 0, 0, 0.1); 30 | --adjust-xl-color: rgba(0, 0, 0, 0.3); 31 | --bg-color: #fff; 32 | --error-color: #d41041; 33 | --info-color: #5656ac; 34 | --info-color-2: #b102e6; 35 | --success-color: #02ab85; 36 | --warning-color: #a9ac00; 37 | --warning-color-2: #d47800; 38 | --text-color-light: rgba(0, 0, 0, 0.7); 39 | --text-color: rgba(0, 0, 0, 0.95); 40 | --darken-color: rgba(255, 255, 255, 0.05); 41 | } 42 | } 43 | 44 | /* Baseline stuff */ 45 | html, body { 46 | width: 100%; 47 | height: 100%; 48 | } 49 | 50 | /* Table rules */ 51 | table { 52 | background-color: var(--adjust-xs-color); 53 | border-spacing: 0; 54 | border-radius: 0.5em; 55 | box-shadow: 0 0 0 1px var(--adjust-xl-color); 56 | display: table; 57 | border-collapse: separate; 58 | } 59 | tr:first-child { 60 | position: sticky; 61 | top: 0; 62 | background-color: var(--bg-color) !important; 63 | } 64 | tr:first-child, th:first-child { 65 | border-top-left-radius: 0.5em; 66 | } 67 | tr:first-child, th:last-child { 68 | border-top-right-radius: 0.5em; 69 | } 70 | tr:nth-child(odd) { 71 | background-color: var(--adjust-xs-color); 72 | } 73 | th, td { 74 | text-align: left; 75 | padding: 0.5em; 76 | border-bottom: 1px solid var(--adjust-lg-color); 77 | } 78 | body { 79 | overscroll-behavior: none; 80 | font-family: Arial, Helvetica, sans-serif; 81 | background-color: var(--bg-color); 82 | color: var(--text-color-light); 83 | display: flex; 84 | flex-direction: column; 85 | } 86 | a { 87 | text-decoration: none; 88 | color: var(--text-color-light); 89 | } 90 | a:hover { 91 | color: var(--text-color); 92 | } 93 | 94 | .nav { 95 | display: flex; 96 | flex: 0 0 2.5em; 97 | } 98 | .content { 99 | display: flex; 100 | flex: 1 1 auto; 101 | overflow: hidden; 102 | } 103 | #content, #root-detail { 104 | display: flex; 105 | flex: 1 1 auto; 106 | } 107 | #root-grid, #root-detail { 108 | height: fit-content; 109 | } 110 | #root-detail { 111 | width: min-content; 112 | } 113 | 114 | /* Display classees */ 115 | 116 | .d-inline-block { 117 | display: inline-block; 118 | } 119 | 120 | /* Border classes */ 121 | .srs-border { 122 | border: 1px solid var(--adjust-xl-color) 123 | } 124 | 125 | 126 | /* Text formats as needed */ 127 | h1 { 128 | font-size: 2em; 129 | font-weight: 700; 130 | color: var(--text-color); 131 | margin: 0.125em 0.25em; 132 | } 133 | h2 { 134 | font-size: 1.125em; 135 | font-weight: 700; 136 | margin: 0.25em; 137 | text-transform: uppercase; 138 | } 139 | h3 { 140 | font-size: 1em; 141 | margin: 0.125em; 142 | } 143 | h4 { 144 | font-size: 1.125em; 145 | } 146 | 147 | .text-sm { 148 | font-size: 0.875em; 149 | } 150 | .text-lg { 151 | font-size: 1.25em; 152 | color: var(--text-color); 153 | margin: 0.125em 0.25em; 154 | } 155 | .text-light { 156 | color: var(--text-color); 157 | } 158 | .text-wide { 159 | letter-spacing: 1px; 160 | } 161 | 162 | /* Margin classes */ 163 | .m-1 { 164 | margin: 0.125em; 165 | } 166 | .m-2 { 167 | margin: 0.25em; 168 | } 169 | .m-3 { 170 | margin: 0.5em; 171 | } 172 | .m-4 { 173 | margin: 1em; 174 | } 175 | .m-5 { 176 | margin: 2em; 177 | } 178 | .mb-3 { 179 | margin-bottom: 0.5em; 180 | } 181 | .mb-4 { 182 | margin-bottom: 1em; 183 | } 184 | .ml-0 { 185 | margin-left: 0; 186 | } 187 | .ml-1 { 188 | margin-left: 0.125em; 189 | } 190 | .ml-2 { 191 | margin-left: 0.25em; 192 | } 193 | .ml-3 { 194 | margin-left: 0.5em; 195 | } 196 | .mt-1 { 197 | margin-top: 0.125em; 198 | } 199 | .mt-2 { 200 | margin-top: 0.25em; 201 | } 202 | .mt-3 { 203 | margin-top: 0.5em; 204 | } 205 | .mt-4 { 206 | margin-top: 1em; 207 | } 208 | .mt-5 { 209 | margin-top: 2em; 210 | } 211 | /* Padding classes */ 212 | .px-0 { 213 | padding-left: 0 !important; 214 | padding-right: 0 !important; 215 | } 216 | .px-4 { 217 | padding-left: 1em; 218 | padding-right: 1em; 219 | } 220 | .p-0 { 221 | padding: 0 !important; 222 | } 223 | .p-4 { 224 | padding: 1em; 225 | } 226 | 227 | /* Text Align classes */ 228 | .center { 229 | text-align: center; 230 | } 231 | 232 | /* Standard color-ish variants */ 233 | .error { 234 | color: var(--error-color); 235 | } 236 | .info { 237 | color: var(--info-color); 238 | } 239 | .success { 240 | color: var(--success-color); 241 | } 242 | .light { 243 | color: var(--text-color-light); 244 | } 245 | .disabled { 246 | opacity: 0.5; 247 | } 248 | 249 | /* Menubar classes */ 250 | .card-nav, 251 | .nav { 252 | padding: 0.25em; 253 | box-shadow: 0px 3px 3px var(--darken-color); 254 | } 255 | .nav { 256 | background: var(--adjust-lg-color); 257 | border-bottom: 1px solid var(--adjust-xl-color); 258 | } 259 | .card-nav { 260 | background: var(--adjust-color); 261 | border-bottom: 1px solid var(--adjust-lg-color); 262 | } 263 | 264 | /* Card or panel classes */ 265 | .card, 266 | .card-light { 267 | align-content: baseline; 268 | border-radius: 3px; 269 | background: var(--adjust-color); 270 | outline: var(--adjust-lg-color) solid 1px; 271 | max-width: 100%; 272 | } 273 | .card { 274 | box-shadow: 3px var(--darken-color); 275 | margin: 1em; 276 | } 277 | .card-light { 278 | box-shadow: 2px var(--darken-color); 279 | margin: 0.5em; 280 | } 281 | 282 | /* Horizontal icon tray classes */ 283 | .icon-tray { 284 | padding: 1em; 285 | } 286 | .icon-tray * { 287 | padding-right: 1em; 288 | height: 1em; 289 | } 290 | .icon-tray :last-child { 291 | padding-right: 0.5em; 292 | } 293 | 294 | /* Grid utility classes */ 295 | .grid { 296 | display: grid; 297 | } 298 | .grid-responsive, 299 | .nested-grid-responsive { 300 | display: grid; 301 | grid-auto-columns: min-content; 302 | grid-auto-flow: row; 303 | padding: 0.5em; 304 | max-width: 100%; 305 | transition: all 0.3s ease-out; 306 | } 307 | .grid-responsive > *, 308 | .nested-grid-responsive > * { 309 | margin: 0 !important; 310 | max-width: 100%; 311 | } 312 | .grid-responsive { 313 | grid-gap: 0.5em; 314 | grid-template-columns: repeat(auto-fill, 15em); 315 | } 316 | .grid-responsive .card, .big-card { 317 | border-radius: 0.5rem; 318 | outline: var(--adjust-xl-color) solid 1px; 319 | } 320 | .grid-responsive .card-nav { 321 | border-radius: 0.5rem 0.5rem 0 0; 322 | } 323 | 324 | .nested-grid-responsive { 325 | grid-gap: 1.5em; 326 | grid-template-columns: repeat(auto-fit, 14em); 327 | } 328 | .nested-grid-responsive .card-nav { 329 | border-radius: 0.25rem 0.25rem 0 0; 330 | } 331 | .nested-grid-responsive .card, 332 | .nested-grid-responsive .card-light { 333 | border-radius: 0.25rem; 334 | outline: var(--adjust-lg-color) solid 1px; 335 | } 336 | .nested-grid-responsive .list-item { 337 | border-radius: 0 0 0.25rem 0.25rem; 338 | } 339 | .minimized { 340 | display: flex; 341 | flex: 0 0 auto; 342 | } 343 | .minimized > .card { 344 | padding-bottom: 1px; 345 | } 346 | /* Minimal variant converts to single column for sidebar UX */ 347 | .grid-responsive.minimized, 348 | .grid-responsive.minimized .nested-grid-responsive { 349 | grid-gap: 0.5em; 350 | grid-template-columns: 1fr; 351 | max-width: 20em; 352 | min-width: 16em; 353 | } 354 | .grid-responsive.minimized .col-1, 355 | .grid-responsive.minimized .col-2, 356 | .grid-responsive.minimized .col-3, 357 | .grid-responsive.minimized .col-4, 358 | .grid-responsive.minimized .col-5, 359 | .grid-responsive.minimized .col-6, 360 | .grid-responsive.minimized .col-7, 361 | .grid-responsive.minimized .col-8, 362 | .grid-responsive.minimized .col-9, 363 | .grid-responsive.minimized .col-10, 364 | .grid-responsive.minimized .col-11, 365 | .grid-responsive.minimized .col-12 { 366 | grid-column: span 1; 367 | } 368 | 369 | .collapsed { 370 | width: 0; 371 | } 372 | /* Making sure that the ux scales down well */ 373 | .col-1 { 374 | grid-column: span 1; 375 | } 376 | .col-2 { 377 | grid-column: span 2; 378 | } 379 | .col-3 { 380 | grid-column: span 3; 381 | } 382 | .col-4 { 383 | grid-column: span 4; 384 | } 385 | .col-5 { 386 | grid-column: span 5; 387 | } 388 | .col-6 { 389 | grid-column: span 6; 390 | } 391 | .col-7 { 392 | grid-column: span 7; 393 | } 394 | .col-8 { 395 | grid-column: span 8; 396 | } 397 | .col-9 { 398 | grid-column: span 9; 399 | } 400 | .col-10 { 401 | grid-column: span 10; 402 | } 403 | .col-11 { 404 | grid-column: span 11; 405 | } 406 | .col-12 { 407 | grid-column: span 12; 408 | } 409 | @media only screen and (min-width: 500px) { 410 | .col-2, 411 | .col-3, 412 | .col-4, 413 | .col-5, 414 | .col-6, 415 | .col-7, 416 | .col-8, 417 | .col-9, 418 | .col-10, 419 | .col-11, 420 | .col-12 { 421 | grid-column: span 2; 422 | } 423 | .grid-responsive { 424 | grid-template-columns: repeat(2, 15em); 425 | } 426 | } 427 | @media only screen and (min-width: 750px) { 428 | .col-3, 429 | .col-4, 430 | .col-5, 431 | .col-6, 432 | .col-7, 433 | .col-8, 434 | .col-9, 435 | .col-10, 436 | .col-11, 437 | .col-12 { 438 | grid-column: span 3; 439 | } 440 | .grid-responsive { 441 | grid-template-columns: repeat(3, 15em); 442 | } 443 | } 444 | @media only screen and (min-width: 1000px) { 445 | .col-4, 446 | .col-5, 447 | .col-6, 448 | .col-7, 449 | .col-8, 450 | .col-9, 451 | .col-10, 452 | .col-11, 453 | .col-12 { 454 | grid-column: span 4; 455 | } 456 | .grid-responsive { 457 | grid-template-columns: repeat(4, 15em); 458 | } 459 | } 460 | @media only screen and (min-width: 1250px) { 461 | .col-5, 462 | .col-6, 463 | .col-7, 464 | .col-8, 465 | .col-9, 466 | .col-10, 467 | .col-11, 468 | .col-12 { 469 | grid-column: span 5; 470 | } 471 | .grid-responsive { 472 | grid-template-columns: repeat(5, 15em); 473 | } 474 | } 475 | @media only screen and (min-width: 1500px) { 476 | .col-6, 477 | .col-7, 478 | .col-8, 479 | .col-9, 480 | .col-10, 481 | .col-11, 482 | .col-12 { 483 | grid-column: span 6; 484 | } 485 | .grid-responsive { 486 | grid-template-columns: repeat(6, 15em); 487 | } 488 | } 489 | @media only screen and (min-width: 1750px) { 490 | .col-7, 491 | .col-8, 492 | .col-9, 493 | .col-10, 494 | .col-11, 495 | .col-12 { 496 | grid-column: span 7; 497 | } 498 | .grid-responsive { 499 | grid-template-columns: repeat(7, 15em); 500 | } 501 | } 502 | @media only screen and (min-width: 2000px) { 503 | .col-8, 504 | .col-9, 505 | .col-10, 506 | .col-11, 507 | .col-12 { 508 | grid-column: span 8; 509 | } 510 | .grid-responsive { 511 | grid-template-columns: repeat(8, 15em); 512 | } 513 | } 514 | @media only screen and (min-width: 2250px) { 515 | .col-9, 516 | .col-10, 517 | .col-11, 518 | .col-12 { 519 | grid-column: span 9; 520 | } 521 | .grid-responsive { 522 | grid-template-columns: repeat(9, 15em); 523 | } 524 | } 525 | @media only screen and (min-width: 2500px) { 526 | .col-10, 527 | .col-11, 528 | .col-12 { 529 | grid-column: span 10; 530 | } 531 | .grid-responsive { 532 | grid-template-columns: repeat(10, 15em); 533 | } 534 | } 535 | @media only screen and (min-width: 2750px) { 536 | .col-11, 537 | .col-12 { 538 | grid-column: span 11; 539 | } 540 | .grid-responsive { 541 | grid-template-columns: repeat(11, 15em); 542 | } 543 | } 544 | @media only screen and (min-width: 3000px) { 545 | .col-12 { 546 | grid-column: span 12; 547 | } 548 | .grid-responsive { 549 | grid-template-columns: repeat(12, 15em); 550 | } 551 | } 552 | /* Scroll utility classes */ 553 | .scroll-region { 554 | height: inherit !important; 555 | overflow-y: auto; 556 | padding-right: 10px; 557 | } 558 | 559 | /* Dimension utility classes */ 560 | .h-fit-content { 561 | height: fit-content; 562 | } 563 | .w-100 { 564 | width: 100%; 565 | } 566 | 567 | /* Flex utility classes */ 568 | .flex { 569 | display: flex; 570 | align-items: center; 571 | } 572 | .flex-fixed { 573 | flex: 0 0 auto; 574 | } 575 | .flex-variable { 576 | flex: 1 1 auto; 577 | } 578 | .flex-grow { 579 | flex: 1 0 auto; 580 | width: 0; 581 | } 582 | .flex-stretch { 583 | align-items: stretch; 584 | } 585 | .flex-row { 586 | flex-direction: row; 587 | } 588 | 589 | /* List item controls */ 590 | .list-item { 591 | text-overflow: ellipsis; 592 | padding: 0.25em 0; 593 | } 594 | .list-item:nth-child(odd) { 595 | background: var(--adjust-sm-color-reverse); 596 | } 597 | .list-item:nth-child(even) { 598 | background: var(--adjust-sm-color); 599 | } 600 | .list-item:hover { 601 | background: var(--adjust-color); 602 | } 603 | .list-item.active { 604 | background: var(--adjust-lg-color); 605 | } 606 | .list-item:active { 607 | background: var(--adjust-xl-color); 608 | } 609 | 610 | .severity-critical { 611 | color: var(--error-color); 612 | } 613 | .severity-high { 614 | color: var(--warning-color-2); 615 | } 616 | .severity-medium { 617 | color: var(--warning-color); 618 | } 619 | .severity-low { 620 | color: var(--info-color); 621 | } 622 | .severity-unknown { 623 | color: var(--info-color-2); 624 | } 625 | 626 | /* Align helpers */ 627 | .align-i-inherit { 628 | align-items: inherit; 629 | } 630 | 631 | .align-i-stretch { 632 | align-items: stretch; 633 | } 634 | 635 | /* App specific table trickery */ 636 | 637 | .cve-col { 638 | min-width: 130px; 639 | } 640 | -------------------------------------------------------------------------------- /theme/static/js/taskInfo.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | let activeTaskElement = document.querySelector(".active"); 3 | const rootDetailElement = document.getElementById("root-detail"); 4 | const rootGridElement = document.getElementById("root-grid"); 5 | const individualTaskElements = document.querySelectorAll(".list-item"); 6 | 7 | const addClass = (el, className) => { 8 | if (!el.classList.contains(className)) { 9 | el.classList.add(className); 10 | } 11 | }; 12 | 13 | const resetActiveClasses = () => 14 | individualTaskElements.forEach((el) => el.classList.remove("active")); 15 | 16 | /* 17 | * This handler does a few things: 18 | * 1. Makes the root grid work as a one column grid, ie scrollable list 19 | * 2. Add an active class to only the clicked element 20 | * 3. Scrolls the element into view 21 | */ 22 | const selectTask = (el) => { 23 | activeTaskElement = el; 24 | addClass(rootGridElement, "minimized"); 25 | addClass(rootDetailElement, "maximized"); 26 | addClass(el, "active"); 27 | el.scrollIntoView({ 28 | behavior: "smooth", 29 | block: "start", 30 | inline: "nearest", 31 | }); 32 | const { namespace, job, group, task } = el.dataset; 33 | history.pushState( 34 | null, 35 | document.title, 36 | `/taskinfo/view/${namespace}/${job}/${group}/${task}` 37 | ); 38 | fetch(`/taskinfo/view/${namespace}/${job}/${group}/${task}/details`) 39 | .then((response) => response.text()) 40 | .then((responseHTML) => rootDetailElement.innerHTML = responseHTML) 41 | }; 42 | 43 | const unselectTask = () => { 44 | activeTaskElement = null; 45 | rootGridElement.classList.remove("minimized"); 46 | rootDetailElement.classList.remove("maximized"); 47 | rootDetailElement.innerHTML = ""; 48 | history.pushState(null, document.title, "/taskinfo/view"); 49 | }; 50 | 51 | const addClickHandler = (el) => { 52 | el.addEventListener("click", () => { 53 | resetActiveClasses(); 54 | if (activeTaskElement === el) { 55 | return unselectTask(); 56 | } 57 | selectTask(el); 58 | }); 59 | }; 60 | 61 | individualTaskElements.forEach(addClickHandler); 62 | })(); 63 | --------------------------------------------------------------------------------