├── .github └── workflows │ ├── build_and_test_action.yml │ ├── lint_action.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── docs └── releasing.md ├── example.json ├── go.mod ├── go.sum ├── main.go ├── pkg ├── client │ ├── client.go │ └── fakeclient │ │ └── fake_flintlock_client.go ├── command │ ├── app.go │ ├── app_test.go │ ├── create.go │ ├── create_test.go │ ├── delete.go │ ├── delete_test.go │ ├── get.go │ ├── get_test.go │ ├── helpers_test.go │ ├── list.go │ ├── list_test.go │ └── version.go ├── config │ └── config.go ├── defaults │ └── defaults.go ├── dialler │ ├── auth.go │ └── dialler.go ├── flags │ └── flags.go ├── microvm │ ├── data.go │ └── data_test.go ├── utils │ ├── file.go │ ├── file_test.go │ ├── print.go │ └── string.go └── version │ └── version.go └── test └── integration ├── README.md ├── integration_suite_test.go └── integration_test.go /.github/workflows/build_and_test_action.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: [pull_request, workflow_dispatch] 3 | jobs: 4 | run_tests: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: actions/setup-go@v2 9 | with: 10 | go-version: '1.18' 11 | - name: Build 12 | run: make build 13 | - name: Test 14 | run: | 15 | go install github.com/onsi/ginkgo/v2/ginkgo@latest 16 | make test 17 | -------------------------------------------------------------------------------- /.github/workflows/lint_action.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [pull_request, workflow_dispatch] 3 | jobs: 4 | run_linter: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: actions/setup-go@v2 9 | with: 10 | go-version: '1.18' 11 | - name: Lint 12 | run: | 13 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.46.2 14 | make lint 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-go@v2 14 | with: 15 | go-version: '1.18' 16 | - name: Build 17 | run: make build 18 | - name: Test 19 | run: | 20 | go install github.com/onsi/ginkgo/v2/ginkgo@latest 21 | make test 22 | build: 23 | runs-on: ubuntu-latest 24 | needs: [test] 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v2 28 | with: 29 | fetch-depth: 0 30 | - name: Set up Go 31 | uses: actions/setup-go@v2 32 | with: 33 | go-version: 1.18 34 | - name: Build binaries 35 | run: make release 36 | - name: Store binaries 37 | uses: actions/upload-artifact@v2 38 | with: 39 | name: hammertime-binaries 40 | path: bin/* 41 | retention-days: 1 42 | release: 43 | runs-on: ubuntu-latest 44 | needs: [test, build] 45 | permissions: 46 | contents: write 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v2 50 | with: 51 | fetch-depth: 0 52 | - name: Download binaries 53 | uses: actions/download-artifact@v2 54 | with: 55 | name: hammertime-binaries 56 | path: bin 57 | - name: Release 58 | uses: softprops/action-gh-release@v1 59 | with: 60 | prerelease: false 61 | draft: true 62 | fail_on_unmatched_files: true 63 | generate_release_notes: true 64 | files: | 65 | bin/hammertime-darwin-amd64 66 | bin/hammertime-darwin-arm64 67 | bin/hammertime-linux-amd64 68 | bin/hammertime-linux-arm64 69 | bin/hammertime-windows-amd64 70 | bin/hammertime-windows-arm64 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | hammertime 2 | bin/ 3 | test/fakeserver/fakeserver 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 10m 3 | tests: false 4 | allow-parallel-runners: true 5 | skip-dirs: 6 | - "./*/fake*" 7 | - "./pkg/version" 8 | 9 | linters-settings: 10 | funlen: 11 | lines: 110 12 | statements: 60 13 | staticcheck: 14 | go: "1.17" 15 | stylecheck: 16 | go: "1.17" 17 | cyclop: 18 | max-complexity: 15 19 | skip-tests: true 20 | gosec: 21 | exclude-generated: true 22 | lll: 23 | line-length: 120 24 | misspell: 25 | locale: GB 26 | goimports: 27 | local-prefixes: github.com/warehouse-13/hammertime 28 | gci: 29 | local-prefixes: github.com/warehouse-13/hammertime 30 | govet: 31 | check-shadowing: true 32 | nolintlint: 33 | allow-leading-space: false 34 | allow-unused: false 35 | require-explanation: true 36 | require-specific: false 37 | varnamelen: 38 | ignore-names: 39 | - err 40 | - wg 41 | - fs 42 | - id 43 | - vm 44 | - ns 45 | - ip 46 | - w 47 | 48 | issues: 49 | max-same-issues: 0 50 | max-issues-per-linter: 0 51 | exclude-rules: 52 | - text: "do not define dynamic errors" 53 | linters: 54 | - goerr113 55 | - text: "shadow: declaration of \"err\" shadows declaration" 56 | linters: 57 | - govet 58 | - text: "should not use dot imports|don't use an underscore in package name" 59 | linters: 60 | - golint 61 | - text: "local replacement are not allowed: github.com/warehouse-13/hammertime/" 62 | linters: 63 | - gomoddirectives 64 | - text: "sig: func github.com/warehouse-13/hammertime/" 65 | linters: 66 | - wrapcheck 67 | - source: "https://" 68 | linters: 69 | - lll 70 | - path: pkg/version/ 71 | linters: 72 | - gochecknoglobals 73 | - path: pkg/defaults/ 74 | linters: 75 | - lll 76 | - path: _test\.go 77 | linters: 78 | - goerr113 79 | - gocyclo 80 | - errcheck 81 | - gosec 82 | - dupl 83 | - funlen 84 | - scopelint 85 | - testpackage 86 | - path: test/ 87 | linters: 88 | - wrapcheck 89 | - path: main.go 90 | linters: 91 | - wrapcheck 92 | - goerr113 93 | - path: pkg/ 94 | linters: 95 | - wrapcheck 96 | - source: "// .* #\\d+" 97 | linters: 98 | - godox 99 | 100 | linters: 101 | enable-all: true 102 | disable: 103 | - exhaustivestruct 104 | - golint 105 | - interfacer 106 | - ireturn 107 | - maligned 108 | - nilnil 109 | - scopelint 110 | - tagliatelle 111 | - forbidigo 112 | - exhaustruct 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN_DIR := bin 2 | HT_CMD := . 3 | 4 | BUILD_DATE := $(shell date +%Y-%m-%dT%H:%M:%SZ) 5 | GIT_COMMIT := $(shell git rev-parse --short HEAD) 6 | VERSION := $(shell git describe --tags --abbrev=0) 7 | VERSION_PKG := github.com/warehouse-13/hammertime/pkg/version 8 | 9 | ##@ Build 10 | 11 | .PHONY: build 12 | build: ## Build hammertime 13 | go build -o hammertime main.go 14 | 15 | .PHONY: lint 16 | lint: ## Lint code 17 | golangci-lint run -v --fast=false 18 | 19 | ##@ Test 20 | 21 | .PHONY: test 22 | test: int unit 23 | 24 | .PHONY: int 25 | int: ## Run integration tests (ginkgo) 26 | ginkgo -r test/ 27 | 28 | .PHONY: unit 29 | unit: ## Run unit tests 30 | go test ./pkg/... 31 | 32 | .PHONY: mock 33 | mock: ## Generate mocks 34 | go generate ./... 35 | 36 | ##@ Release 37 | 38 | .PHONY: release 39 | release: ## Cross compile bins for linux, windows, mac 40 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $(BIN_DIR)/hammertime-linux-amd64 -ldflags "-X $(VERSION_PKG).Version=$(VERSION) -X $(VERSION_PKG).BuildDate=$(BUILD_DATE) -X $(VERSION_PKG).CommitHash=$(GIT_COMMIT)" $(HT_CMD) 41 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o $(BIN_DIR)/hammertime-linux-arm64 -ldflags "-X $(VERSION_PKG).Version=$(VERSION) -X $(VERSION_PKG).BuildDate=$(BUILD_DATE) -X $(VERSION_PKG).CommitHash=$(GIT_COMMIT)" $(HT_CMD) 42 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o $(BIN_DIR)/hammertime-windows-amd64 -ldflags "-X $(VERSION_PKG).Version=$(VERSION) -X $(VERSION_PKG).BuildDate=$(BUILD_DATE) -X $(VERSION_PKG).CommitHash=$(GIT_COMMIT)" $(HT_CMD) 43 | CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -o $(BIN_DIR)/hammertime-windows-arm64 -ldflags "-X $(VERSION_PKG).Version=$(VERSION) -X $(VERSION_PKG).BuildDate=$(BUILD_DATE) -X $(VERSION_PKG).CommitHash=$(GIT_COMMIT)" $(HT_CMD) 44 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o $(BIN_DIR)/hammertime-darwin-amd64 -ldflags "-X $(VERSION_PKG).Version=$(VERSION) -X $(VERSION_PKG).BuildDate=$(BUILD_DATE) -X $(VERSION_PKG).CommitHash=$(GIT_COMMIT)" $(HT_CMD) 45 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o $(BIN_DIR)/hammertime-darwin-arm64 -ldflags "-X $(VERSION_PKG).Version=$(VERSION) -X $(VERSION_PKG).BuildDate=$(BUILD_DATE) -X $(VERSION_PKG).CommitHash=$(GIT_COMMIT)" $(HT_CMD) 46 | 47 | .PHONY: help 48 | help: ## Display this help. Thanks to https://www.thapaliya.com/en/writings/well-documented-makefiles/ 49 | ifeq ($(OS),Windows_NT) 50 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \n"} /^[0-9a-zA-Z_-]+:.*?##/ { printf " %-40s %s\n", $$1, $$2 } /^##@/ { printf "\n%s\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 51 | else 52 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[0-9a-zA-Z_-]+:.*?##/ { printf " \033[36m%-40s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 53 | endif 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Hammertime 2 | 3 | A very basic CLI tool for interacting with [flintlock](https://github.com/weaveworks/flintlock) servers. 4 | 5 | (Those of you who know your archaic gun mechanisms will understand the name and no doubt 6 | find it hilarious.) (You are welcome.) 7 | 8 | This started off as a toy I made quickly to test live flintlock servers. 9 | I have kept it around because it makes working with flintlock very straightforward. 10 | 11 | It is currently being heavily refactored. 12 | 13 | 14 | 18 | 19 | 20 | - [Flintlock?](#flintlock) 21 | - [Versioning](#versioning) 22 | - [Installation](#installation) 23 | - [Usage](#usage) 24 | - [Development](#development) 25 | - [Testing](#testing) 26 | 27 | 28 | 29 | ### Flintlock? 30 | 31 | [Flintlock](https://github.com/weaveworks/flintlock) is a service to manage MicroVMs 32 | on bare-metal. 33 | 34 | MicroVMs are, as they sound, smaller VMs. Unlike regular VMs, which generally must 35 | be prepared to run any kernel, OS, environment with any number of features that a user 36 | may end up needing, MicroVMs are stripped down for a purpose. They provide a smaller 37 | subset of virtualisation tailored for a specific task (in the case of Flintlock, this is to 38 | run [Kubernetes](https://kubernetes.io/) nodes). This means they are smaller and "lighter" 39 | to run. In a best of both worlds thing: they provide the speed and lower resource allocation 40 | of container, and the security of full VMs. 41 | 42 | ### Versioning 43 | 44 | Check the release notes for each release to find Flintlock compatibility. 45 | Both Flintlock and this tool are in alpha development, thus the API is likely 46 | to change often until v1. 47 | 48 | ### Installation 49 | 50 | 1. Build from source: 51 | ```bash 52 | git clone 53 | cd hammertime 54 | make build 55 | ``` 56 | 57 | 2. Get a [released binary](https://github.com/warehouse-13/hammertime/releases) 58 | 59 | 3. Install with go: `go install github.com/warehouse-13/hammertime@latest` 60 | 61 | Alias to `ht` if you like. 62 | 63 | ### Usage 64 | 65 | 4 commands, very few configuration options. Each command simply spits out the response 66 | as JSON so you can pipe to `jq` or whatever as you like. 67 | 68 | ```bash 69 | # see all options 70 | hammertime --help 71 | 72 | # create 'mvm0' in 'ns0' (take note of the UID after creation) 73 | hammertime create 74 | 75 | # get 'mvm0' in 'ns0' 76 | hammertime get 77 | 78 | # get just the state of 'mvm0' in 'ns0' see below 79 | hammertime get -s 80 | 81 | # get 82 | hammertime get -i 83 | 84 | # get all mvms in `ns0` 85 | hammertime list --namespace ns0 86 | 87 | # delete 'bar' from 'foo' 88 | hammertime delete --namespace foo --name bar 89 | 90 | # delete 91 | hammertime delete -i 92 | ``` 93 | 94 | The name and namespace are configurable, as is the GRPC address. 95 | There is the option to create with an SSH key. 96 | You can also pass a full json configfile to `create`, `get` and `delete` if you want to override 97 | everything (see [example.json](example.json)). 98 | 99 | Run `hammertime --help` for all options. 100 | 101 | ### Development 102 | 103 | For a list of all make commands, run `make help`. 104 | 105 | You will need Go version >= `1.18`. 106 | 107 | #### Testing 108 | 109 | Our tests use [`ginkgo` v2](https://onsi.github.io/ginkgo/). To install v2 run: 110 | 111 | ```bash 112 | go install github.com/onsi/ginkgo/v2/ginkgo 113 | ginkgo version //should print out "Ginkgo Version 2.0.0" 114 | ``` 115 | 116 | Tests can then be run with `make test`. 117 | 118 | All new code must be submitted with at least unit tests. All new or changed core features 119 | must come with a matching or updated integration test. 120 | -------------------------------------------------------------------------------- /docs/releasing.md: -------------------------------------------------------------------------------- 1 | # Releasing hammertime 2 | 3 | ## Determine release version 4 | 5 | The projects follows [semantic versioning](https://semver.org/#semantic-versioning-200) 6 | (sort of). 7 | The next appropriate version number will depend on what is going into the release. 8 | 9 | First pull all tags: 10 | 11 | ```bash 12 | git pull --tags 13 | 14 | git describe --tags --abbrev=0 15 | ``` 16 | 17 | This will give you the latest release, the next release will increment from here. 18 | 19 | ## Create tag 20 | 21 | * Checkout upstream main 22 | * Create a tag with the version number: 23 | 24 | ```bash 25 | # assuming the answer to git describe was v0.0.9 26 | RELEASE_VERSION=v0.0.10 27 | git tag -s "${RELEASE_VERSION}" -m "${RELEASE_VERSION}" 28 | ``` 29 | 30 | * Push the tag (to upstream if working from a fork) 31 | 32 | ``` bash 33 | git push origin "${RELEASE_VERSION}" 34 | ``` 35 | 36 | * Check the [release](https://github.com/warehouse-13/hammertime/actions/workflows/release.yml) 37 | GitHub Actions workflow completes successfully. 38 | 39 | ## Edit & Publish GitHub Release 40 | 41 | * Go to the draft release in GitHub. 42 | * Make any edits to generated release notes 43 | * If there are any breaking changes then manually add a note at the beginning 44 | of the release notes informing the user what they need to be aware of/do. 45 | * Sometimes you may want to combine changes into 1 line 46 | -------------------------------------------------------------------------------- /example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "mvm1", 3 | "namespace": "ns1", 4 | "labels": { 5 | "env": "lab" 6 | }, 7 | "vcpu": 2, 8 | "memory_in_mb": 2048, 9 | "kernel": { 10 | "image": "ghcr.io/weaveworks-liquidmetal/kernel-bin:5.10.77", 11 | "filename": "boot/vmlinux", 12 | "add_network_config": true 13 | }, 14 | "root_volume": { 15 | "id": "root", 16 | "is_read_only": false, 17 | "source": { 18 | "container_source": "ghcr.io/weaveworks-liquidmetal/capmvm-k8s-os:1.23.5" 19 | } 20 | }, 21 | "additional_volumes": [ 22 | { 23 | "id": "modules", 24 | "is_read_only": false, 25 | "source": { 26 | "container_source": "ghcr.io/weaveworks-liquidmetal/kernel-modules:5.10.77" 27 | }, 28 | "mount_point": "/lib/modules/5.10.77" 29 | } 30 | ], 31 | "interfaces": [ 32 | { 33 | "device_id": "eth1", 34 | "type": 0 35 | } 36 | ], 37 | "metadata": { 38 | "meta-data": "aW5zdGFuY2VfaWQ6IG5zMS9tdm0wCmxvY2FsX2hvc3RuYW1lOiBtdm0wCnBsYXRmb3JtOiBsaXF1aWRfbWV0YWwK", 39 | "user-data": "I2Nsb3VkLWNvbmZpZwpob3N0bmFtZTogbXZtMApmcWRuOiBtdm0wLmZydWl0Y2FzZQp1c2VyczoKICAgIC0gbmFtZTogcm9vdAogICAgICBzc2hfYXV0aG9yaXplZF9rZXlzOgogICAgICAgIC0gfAogICAgICAgICAgc3NoLWVkMjU1MTkgQUFBQUMzTnphQzFsWkRJMU5URTVBQUFBSUdzbStWSSsyVk5WWFBDRmVmbFhrQTVKY21zMzByajFGUFFjcFNTdDFrdVYgcmljaGFyZEB3ZWF2ZS53b3JrcwpkaXNhYmxlX3Jvb3Q6IGZhbHNlCnBhY2thZ2VfdXBkYXRlOiBmYWxzZQpmaW5hbF9tZXNzYWdlOiBUaGUgcmVpZ25pdGVkIGJvb3RlZCBzeXN0ZW0gaXMgZ29vZCB0byBnbyBhZnRlciAkVVBUSU1FIHNlY29uZHMKcnVuY21kOgogICAgLSBkaGNsaWVudCAtcgogICAgLSBkaGNsaWVudAo=" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/warehouse-13/hammertime 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/onsi/ginkgo/v2 v2.4.0 7 | github.com/urfave/cli/v2 v2.10.2 8 | github.com/warehouse-13/safety v0.0.0-20230120170710-60c7451457c5 9 | github.com/weaveworks-liquidmetal/flintlock/api v0.0.0-20230113160655-b1354ef6d578 10 | github.com/weaveworks-liquidmetal/flintlock/client v0.0.0-20230113160655-b1354ef6d578 11 | google.golang.org/grpc v1.51.0 12 | google.golang.org/protobuf v1.28.1 13 | gopkg.in/yaml.v2 v2.4.0 14 | k8s.io/utils v0.0.0-20230115233650-391b47cb4029 15 | ) 16 | 17 | require ( 18 | cloud.google.com/go v0.75.0 // indirect 19 | github.com/beorn7/perks v1.0.1 // indirect 20 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 21 | github.com/go-logr/logr v1.2.3 // indirect 22 | github.com/google/go-cmp v0.5.8 // indirect 23 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect 24 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 25 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 26 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect 27 | github.com/prometheus/client_golang v1.13.1 // indirect 28 | github.com/prometheus/client_model v0.2.0 // indirect 29 | github.com/prometheus/common v0.37.0 // indirect 30 | github.com/prometheus/procfs v0.8.0 // indirect 31 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 32 | golang.org/x/net v0.1.0 // indirect 33 | golang.org/x/sys v0.1.0 // indirect 34 | golang.org/x/text v0.4.0 // indirect 35 | google.golang.org/appengine v1.6.7 // indirect 36 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | 40 | require ( 41 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 42 | github.com/golang/protobuf v1.5.2 // indirect 43 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.3 // indirect 44 | github.com/onsi/gomega v1.22.1 45 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 46 | google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /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.75.0 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY= 18 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 19 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 20 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 21 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 22 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 23 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 24 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 25 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 26 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 27 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 28 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 29 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 30 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 31 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 32 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 33 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 34 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 35 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 36 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 37 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 38 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 39 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 40 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 41 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 42 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 43 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 44 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 45 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 46 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 47 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 48 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 49 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 50 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 51 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 52 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 53 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 54 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 55 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 56 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 57 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 58 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 59 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 60 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= 61 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 62 | github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 63 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 64 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 65 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 66 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 67 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 68 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 69 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 70 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 71 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 72 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 73 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 74 | github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= 75 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 76 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 77 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 78 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 79 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 80 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 81 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 82 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 83 | github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 84 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 85 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 86 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 87 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 88 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 89 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 90 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 91 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 92 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 93 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 94 | github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= 95 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 96 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 97 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 98 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 99 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 100 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 101 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 102 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 103 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 104 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 105 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 106 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 107 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 108 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 109 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 110 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 111 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 112 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 113 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 114 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 115 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 116 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 117 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 118 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 119 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 120 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 121 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 122 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 123 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 124 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 125 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 126 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 127 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 128 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 129 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 130 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 131 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 132 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 133 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 134 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 135 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 136 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 137 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 138 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 139 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 140 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 141 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 142 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 143 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 144 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 145 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 146 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 147 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 148 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 149 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 150 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 151 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 152 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 153 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 154 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= 155 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= 156 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= 157 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 158 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 159 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.3 h1:BGNSrTRW4rwfhJiFwvwF4XQ0Y72Jj9YEgxVrtovbD5o= 160 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.3/go.mod h1:VHn7KgNsRriXa4mcgtkpR00OXyQY6g67JWMvn+R27A4= 161 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 162 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 163 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 164 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 165 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 166 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 167 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 168 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 169 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 170 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 171 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 172 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 173 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 174 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 175 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 176 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 177 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 178 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 179 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 180 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 181 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 182 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 183 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 184 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 185 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 186 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 187 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 188 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 189 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 190 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 191 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 192 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 193 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 194 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= 195 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= 196 | github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= 197 | github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= 198 | github.com/onsi/gomega v1.22.1 h1:pY8O4lBfsHKZHM/6nrxkhVPUznOlIu3quZcKP/M20KI= 199 | github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= 200 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 201 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 202 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 203 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 204 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 205 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 206 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 207 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 208 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 209 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 210 | github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= 211 | github.com/prometheus/client_golang v1.13.1 h1:3gMjIY2+/hzmqhtUC/aQNYldJA6DtH3CgQvwS+02K1c= 212 | github.com/prometheus/client_golang v1.13.1/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= 213 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 214 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 215 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 216 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 217 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 218 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 219 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 220 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 221 | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 222 | github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= 223 | github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= 224 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 225 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 226 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 227 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 228 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 229 | github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= 230 | github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= 231 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 232 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 233 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 234 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 235 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 236 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 237 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 238 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 239 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 240 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 241 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 242 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 243 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 244 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 245 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 246 | github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y= 247 | github.com/urfave/cli/v2 v2.10.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= 248 | github.com/warehouse-13/safety v0.0.0-20230120170710-60c7451457c5 h1:1uTkby9abQ0d0RJIYrKxZsfDZoWdWEb/KoaCnDqImtI= 249 | github.com/warehouse-13/safety v0.0.0-20230120170710-60c7451457c5/go.mod h1:hTlI1j1CXNEYrNoGbw7bAPj1/maOkuvGgoi63donJ5I= 250 | github.com/weaveworks-liquidmetal/flintlock/api v0.0.0-20221117153111-bd29de31356f h1:xHtKo4eiR1pRZUrXxlGF3KDuoa6xYjBGuPP47M9eVWM= 251 | github.com/weaveworks-liquidmetal/flintlock/api v0.0.0-20221117153111-bd29de31356f/go.mod h1:JPML9O56MoPKGX97jfj++BtuFFS84jm4T+jWQBjO5Uc= 252 | github.com/weaveworks-liquidmetal/flintlock/api v0.0.0-20230113160655-b1354ef6d578 h1:6LaBzWSOaaFI3fJ1oDDlfnlAmtPisrslVF+ZnmSKxr0= 253 | github.com/weaveworks-liquidmetal/flintlock/api v0.0.0-20230113160655-b1354ef6d578/go.mod h1:JPML9O56MoPKGX97jfj++BtuFFS84jm4T+jWQBjO5Uc= 254 | github.com/weaveworks-liquidmetal/flintlock/client v0.0.0-20220722132608-982d429ba641 h1:c5GlWX7UuBOQwx3Xfw4Jnd3VCHvChpkWaSVJhEbQcdo= 255 | github.com/weaveworks-liquidmetal/flintlock/client v0.0.0-20220722132608-982d429ba641/go.mod h1:d65mpsT+pbMnMJZhUqeNtt7lcQnsX8cowl9dTRkniSI= 256 | github.com/weaveworks-liquidmetal/flintlock/client v0.0.0-20230113160655-b1354ef6d578 h1:mBms3Efnv+YVbfMxN7/rHGveua8oteJwWreYfeYYTRE= 257 | github.com/weaveworks-liquidmetal/flintlock/client v0.0.0-20230113160655-b1354ef6d578/go.mod h1:d65mpsT+pbMnMJZhUqeNtt7lcQnsX8cowl9dTRkniSI= 258 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 259 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 260 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 261 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 262 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 263 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 264 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 265 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 266 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 267 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 268 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 269 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 270 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 271 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 272 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 273 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 274 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 275 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 276 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 277 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 278 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 279 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 280 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 281 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 282 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 283 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 284 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 285 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 286 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 287 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 288 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 289 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 290 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 291 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 292 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 293 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 294 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 295 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 296 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 297 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 298 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 299 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 300 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 301 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 302 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 303 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 304 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 305 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 306 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 307 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 308 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 309 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 310 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 311 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 312 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 313 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 314 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 315 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 316 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 317 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 318 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 319 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 320 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 321 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 322 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 323 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 324 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 325 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 326 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 327 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 328 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 329 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 330 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 331 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 332 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 333 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 334 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 335 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 336 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 337 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 338 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 339 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 340 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 341 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 342 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 343 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 344 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 345 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 346 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 347 | golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= 348 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 349 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 350 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 351 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 352 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 353 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 354 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 355 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 356 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 357 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 358 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= 359 | golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= 360 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 361 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 362 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 363 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 364 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 365 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 366 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 367 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 368 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 369 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 370 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 371 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 372 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 373 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 374 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 375 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 376 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 377 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 378 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 379 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 380 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 381 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 382 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 383 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 384 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 385 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 386 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 387 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 388 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 389 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 390 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 391 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 392 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 393 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 394 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 395 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 396 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 397 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 398 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 399 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 400 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 401 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 402 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 403 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 404 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 405 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 406 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 407 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 408 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 409 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 410 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 411 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 412 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 413 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 414 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 415 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 416 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 417 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 418 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 419 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 420 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 421 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 422 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 423 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 424 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 425 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 426 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 427 | golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= 428 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 429 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 430 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 431 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 432 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 433 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 434 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 435 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 436 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 437 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 438 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 439 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 440 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 441 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 442 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 443 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 444 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 445 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 446 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 447 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 448 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 449 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 450 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 451 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 452 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 453 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 454 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 455 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 456 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 457 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 458 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 459 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 460 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 461 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 462 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 463 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 464 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 465 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 466 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 467 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 468 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 469 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 470 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 471 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 472 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 473 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 474 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 475 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 476 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 477 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 478 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 479 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 480 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 481 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 482 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 483 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 484 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 485 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 486 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 487 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 488 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 489 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 490 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 491 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 492 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 493 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 494 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 495 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 496 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 497 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 498 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 499 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 500 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 501 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 502 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 503 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 504 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 505 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 506 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 507 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 508 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 509 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 510 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 511 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 512 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 513 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 514 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 515 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 516 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 517 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 518 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 519 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 520 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 521 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 522 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 523 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 524 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 525 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 526 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 527 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 528 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 529 | google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 530 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 531 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 532 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 533 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 534 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 535 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 536 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 537 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 538 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 539 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 540 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 541 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 542 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 543 | google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I= 544 | google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= 545 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 546 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 547 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 548 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 549 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 550 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 551 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 552 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 553 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 554 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 555 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 556 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 557 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 558 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 559 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 560 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 561 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 562 | google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= 563 | google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U= 564 | google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= 565 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 566 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 567 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 568 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 569 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 570 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 571 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 572 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 573 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 574 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 575 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 576 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 577 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 578 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 579 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 580 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 581 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 582 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 583 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 584 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 585 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 586 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 587 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 588 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 589 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 590 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 591 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 592 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 593 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 594 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 595 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 596 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 597 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 598 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 599 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 600 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 601 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 602 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 603 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 604 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 605 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 606 | k8s.io/utils v0.0.0-20230115233650-391b47cb4029 h1:L8zDtT4jrxj+TaQYD0k8KNlr556WaVQylDXswKmX+dE= 607 | k8s.io/utils v0.0.0-20230115233650-391b47cb4029/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 608 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 609 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 610 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 611 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/warehouse-13/hammertime/pkg/command" 8 | ) 9 | 10 | func main() { 11 | app := command.NewApp(os.Stdout) 12 | 13 | if err := app.Run(os.Args); err != nil { 14 | log.Fatal(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pkg/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/weaveworks-liquidmetal/flintlock/api/services/microvm/v1alpha1" 7 | "github.com/weaveworks-liquidmetal/flintlock/api/types" 8 | "google.golang.org/grpc" 9 | "google.golang.org/protobuf/types/known/emptypb" 10 | "k8s.io/utils/pointer" 11 | 12 | "github.com/warehouse-13/hammertime/pkg/dialler" 13 | ) 14 | 15 | //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate 16 | 17 | // Client is a wrapper around a v1alpha1.MicroVMClient. 18 | type Client struct { 19 | v1alpha1.MicroVMClient 20 | Conn *grpc.ClientConn 21 | } 22 | 23 | //counterfeiter:generate -o fakeclient/ . FlintlockClient 24 | type FlintlockClient interface { 25 | Create(mvm *types.MicroVMSpec) (*v1alpha1.CreateMicroVMResponse, error) 26 | Get(uid string) (*v1alpha1.GetMicroVMResponse, error) 27 | List(name, ns string) (*v1alpha1.ListMicroVMsResponse, error) 28 | Delete(uid string) (*emptypb.Empty, error) 29 | Close() error 30 | } 31 | 32 | // New returns a new flintlock Client. 33 | func New(address, basicAuthToken string) (FlintlockClient, error) { 34 | conn, err := dialler.New(address, basicAuthToken, nil) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return &Client{v1alpha1.NewMicroVMClient(conn), conn}, nil 40 | } 41 | 42 | func (c *Client) Close() error { 43 | return c.Conn.Close() 44 | } 45 | 46 | // Create creates a new Microvm with the MicroVMClient. 47 | func (c *Client) Create(mvm *types.MicroVMSpec) (*v1alpha1.CreateMicroVMResponse, error) { 48 | createReq := v1alpha1.CreateMicroVMRequest{ 49 | Microvm: mvm, 50 | } 51 | 52 | return c.CreateMicroVM(context.Background(), &createReq) 53 | } 54 | 55 | // Get fetches a Microvm with the MicroVMClient by the given ID. 56 | func (c *Client) Get(uid string) (*v1alpha1.GetMicroVMResponse, error) { 57 | getReq := v1alpha1.GetMicroVMRequest{ 58 | Uid: uid, 59 | } 60 | 61 | return c.GetMicroVM(context.Background(), &getReq) 62 | } 63 | 64 | // List fetches Microvms filtered by name and namespace. 65 | func (c *Client) List(name, ns string) (*v1alpha1.ListMicroVMsResponse, error) { 66 | listReq := v1alpha1.ListMicroVMsRequest{ 67 | Namespace: ns, 68 | Name: pointer.String(name), 69 | } 70 | 71 | return c.ListMicroVMs(context.Background(), &listReq) 72 | } 73 | 74 | // Delete deletes a Microvm by the given id. 75 | func (c *Client) Delete(uid string) (*emptypb.Empty, error) { 76 | delReq := v1alpha1.DeleteMicroVMRequest{ 77 | Uid: uid, 78 | } 79 | 80 | return c.DeleteMicroVM(context.Background(), &delReq) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/client/fakeclient/fake_flintlock_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package fakeclient 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/warehouse-13/hammertime/pkg/client" 8 | "github.com/weaveworks-liquidmetal/flintlock/api/services/microvm/v1alpha1" 9 | "github.com/weaveworks-liquidmetal/flintlock/api/types" 10 | "google.golang.org/protobuf/types/known/emptypb" 11 | ) 12 | 13 | type FakeFlintlockClient struct { 14 | CloseStub func() error 15 | closeMutex sync.RWMutex 16 | closeArgsForCall []struct { 17 | } 18 | closeReturns struct { 19 | result1 error 20 | } 21 | closeReturnsOnCall map[int]struct { 22 | result1 error 23 | } 24 | CreateStub func(*types.MicroVMSpec) (*v1alpha1.CreateMicroVMResponse, error) 25 | createMutex sync.RWMutex 26 | createArgsForCall []struct { 27 | arg1 *types.MicroVMSpec 28 | } 29 | createReturns struct { 30 | result1 *v1alpha1.CreateMicroVMResponse 31 | result2 error 32 | } 33 | createReturnsOnCall map[int]struct { 34 | result1 *v1alpha1.CreateMicroVMResponse 35 | result2 error 36 | } 37 | DeleteStub func(string) (*emptypb.Empty, error) 38 | deleteMutex sync.RWMutex 39 | deleteArgsForCall []struct { 40 | arg1 string 41 | } 42 | deleteReturns struct { 43 | result1 *emptypb.Empty 44 | result2 error 45 | } 46 | deleteReturnsOnCall map[int]struct { 47 | result1 *emptypb.Empty 48 | result2 error 49 | } 50 | GetStub func(string) (*v1alpha1.GetMicroVMResponse, error) 51 | getMutex sync.RWMutex 52 | getArgsForCall []struct { 53 | arg1 string 54 | } 55 | getReturns struct { 56 | result1 *v1alpha1.GetMicroVMResponse 57 | result2 error 58 | } 59 | getReturnsOnCall map[int]struct { 60 | result1 *v1alpha1.GetMicroVMResponse 61 | result2 error 62 | } 63 | ListStub func(string, string) (*v1alpha1.ListMicroVMsResponse, error) 64 | listMutex sync.RWMutex 65 | listArgsForCall []struct { 66 | arg1 string 67 | arg2 string 68 | } 69 | listReturns struct { 70 | result1 *v1alpha1.ListMicroVMsResponse 71 | result2 error 72 | } 73 | listReturnsOnCall map[int]struct { 74 | result1 *v1alpha1.ListMicroVMsResponse 75 | result2 error 76 | } 77 | invocations map[string][][]interface{} 78 | invocationsMutex sync.RWMutex 79 | } 80 | 81 | func (fake *FakeFlintlockClient) Close() error { 82 | fake.closeMutex.Lock() 83 | ret, specificReturn := fake.closeReturnsOnCall[len(fake.closeArgsForCall)] 84 | fake.closeArgsForCall = append(fake.closeArgsForCall, struct { 85 | }{}) 86 | stub := fake.CloseStub 87 | fakeReturns := fake.closeReturns 88 | fake.recordInvocation("Close", []interface{}{}) 89 | fake.closeMutex.Unlock() 90 | if stub != nil { 91 | return stub() 92 | } 93 | if specificReturn { 94 | return ret.result1 95 | } 96 | return fakeReturns.result1 97 | } 98 | 99 | func (fake *FakeFlintlockClient) CloseCallCount() int { 100 | fake.closeMutex.RLock() 101 | defer fake.closeMutex.RUnlock() 102 | return len(fake.closeArgsForCall) 103 | } 104 | 105 | func (fake *FakeFlintlockClient) CloseCalls(stub func() error) { 106 | fake.closeMutex.Lock() 107 | defer fake.closeMutex.Unlock() 108 | fake.CloseStub = stub 109 | } 110 | 111 | func (fake *FakeFlintlockClient) CloseReturns(result1 error) { 112 | fake.closeMutex.Lock() 113 | defer fake.closeMutex.Unlock() 114 | fake.CloseStub = nil 115 | fake.closeReturns = struct { 116 | result1 error 117 | }{result1} 118 | } 119 | 120 | func (fake *FakeFlintlockClient) CloseReturnsOnCall(i int, result1 error) { 121 | fake.closeMutex.Lock() 122 | defer fake.closeMutex.Unlock() 123 | fake.CloseStub = nil 124 | if fake.closeReturnsOnCall == nil { 125 | fake.closeReturnsOnCall = make(map[int]struct { 126 | result1 error 127 | }) 128 | } 129 | fake.closeReturnsOnCall[i] = struct { 130 | result1 error 131 | }{result1} 132 | } 133 | 134 | func (fake *FakeFlintlockClient) Create(arg1 *types.MicroVMSpec) (*v1alpha1.CreateMicroVMResponse, error) { 135 | fake.createMutex.Lock() 136 | ret, specificReturn := fake.createReturnsOnCall[len(fake.createArgsForCall)] 137 | fake.createArgsForCall = append(fake.createArgsForCall, struct { 138 | arg1 *types.MicroVMSpec 139 | }{arg1}) 140 | stub := fake.CreateStub 141 | fakeReturns := fake.createReturns 142 | fake.recordInvocation("Create", []interface{}{arg1}) 143 | fake.createMutex.Unlock() 144 | if stub != nil { 145 | return stub(arg1) 146 | } 147 | if specificReturn { 148 | return ret.result1, ret.result2 149 | } 150 | return fakeReturns.result1, fakeReturns.result2 151 | } 152 | 153 | func (fake *FakeFlintlockClient) CreateCallCount() int { 154 | fake.createMutex.RLock() 155 | defer fake.createMutex.RUnlock() 156 | return len(fake.createArgsForCall) 157 | } 158 | 159 | func (fake *FakeFlintlockClient) CreateCalls(stub func(*types.MicroVMSpec) (*v1alpha1.CreateMicroVMResponse, error)) { 160 | fake.createMutex.Lock() 161 | defer fake.createMutex.Unlock() 162 | fake.CreateStub = stub 163 | } 164 | 165 | func (fake *FakeFlintlockClient) CreateArgsForCall(i int) *types.MicroVMSpec { 166 | fake.createMutex.RLock() 167 | defer fake.createMutex.RUnlock() 168 | argsForCall := fake.createArgsForCall[i] 169 | return argsForCall.arg1 170 | } 171 | 172 | func (fake *FakeFlintlockClient) CreateReturns(result1 *v1alpha1.CreateMicroVMResponse, result2 error) { 173 | fake.createMutex.Lock() 174 | defer fake.createMutex.Unlock() 175 | fake.CreateStub = nil 176 | fake.createReturns = struct { 177 | result1 *v1alpha1.CreateMicroVMResponse 178 | result2 error 179 | }{result1, result2} 180 | } 181 | 182 | func (fake *FakeFlintlockClient) CreateReturnsOnCall(i int, result1 *v1alpha1.CreateMicroVMResponse, result2 error) { 183 | fake.createMutex.Lock() 184 | defer fake.createMutex.Unlock() 185 | fake.CreateStub = nil 186 | if fake.createReturnsOnCall == nil { 187 | fake.createReturnsOnCall = make(map[int]struct { 188 | result1 *v1alpha1.CreateMicroVMResponse 189 | result2 error 190 | }) 191 | } 192 | fake.createReturnsOnCall[i] = struct { 193 | result1 *v1alpha1.CreateMicroVMResponse 194 | result2 error 195 | }{result1, result2} 196 | } 197 | 198 | func (fake *FakeFlintlockClient) Delete(arg1 string) (*emptypb.Empty, error) { 199 | fake.deleteMutex.Lock() 200 | ret, specificReturn := fake.deleteReturnsOnCall[len(fake.deleteArgsForCall)] 201 | fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { 202 | arg1 string 203 | }{arg1}) 204 | stub := fake.DeleteStub 205 | fakeReturns := fake.deleteReturns 206 | fake.recordInvocation("Delete", []interface{}{arg1}) 207 | fake.deleteMutex.Unlock() 208 | if stub != nil { 209 | return stub(arg1) 210 | } 211 | if specificReturn { 212 | return ret.result1, ret.result2 213 | } 214 | return fakeReturns.result1, fakeReturns.result2 215 | } 216 | 217 | func (fake *FakeFlintlockClient) DeleteCallCount() int { 218 | fake.deleteMutex.RLock() 219 | defer fake.deleteMutex.RUnlock() 220 | return len(fake.deleteArgsForCall) 221 | } 222 | 223 | func (fake *FakeFlintlockClient) DeleteCalls(stub func(string) (*emptypb.Empty, error)) { 224 | fake.deleteMutex.Lock() 225 | defer fake.deleteMutex.Unlock() 226 | fake.DeleteStub = stub 227 | } 228 | 229 | func (fake *FakeFlintlockClient) DeleteArgsForCall(i int) string { 230 | fake.deleteMutex.RLock() 231 | defer fake.deleteMutex.RUnlock() 232 | argsForCall := fake.deleteArgsForCall[i] 233 | return argsForCall.arg1 234 | } 235 | 236 | func (fake *FakeFlintlockClient) DeleteReturns(result1 *emptypb.Empty, result2 error) { 237 | fake.deleteMutex.Lock() 238 | defer fake.deleteMutex.Unlock() 239 | fake.DeleteStub = nil 240 | fake.deleteReturns = struct { 241 | result1 *emptypb.Empty 242 | result2 error 243 | }{result1, result2} 244 | } 245 | 246 | func (fake *FakeFlintlockClient) DeleteReturnsOnCall(i int, result1 *emptypb.Empty, result2 error) { 247 | fake.deleteMutex.Lock() 248 | defer fake.deleteMutex.Unlock() 249 | fake.DeleteStub = nil 250 | if fake.deleteReturnsOnCall == nil { 251 | fake.deleteReturnsOnCall = make(map[int]struct { 252 | result1 *emptypb.Empty 253 | result2 error 254 | }) 255 | } 256 | fake.deleteReturnsOnCall[i] = struct { 257 | result1 *emptypb.Empty 258 | result2 error 259 | }{result1, result2} 260 | } 261 | 262 | func (fake *FakeFlintlockClient) Get(arg1 string) (*v1alpha1.GetMicroVMResponse, error) { 263 | fake.getMutex.Lock() 264 | ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)] 265 | fake.getArgsForCall = append(fake.getArgsForCall, struct { 266 | arg1 string 267 | }{arg1}) 268 | stub := fake.GetStub 269 | fakeReturns := fake.getReturns 270 | fake.recordInvocation("Get", []interface{}{arg1}) 271 | fake.getMutex.Unlock() 272 | if stub != nil { 273 | return stub(arg1) 274 | } 275 | if specificReturn { 276 | return ret.result1, ret.result2 277 | } 278 | return fakeReturns.result1, fakeReturns.result2 279 | } 280 | 281 | func (fake *FakeFlintlockClient) GetCallCount() int { 282 | fake.getMutex.RLock() 283 | defer fake.getMutex.RUnlock() 284 | return len(fake.getArgsForCall) 285 | } 286 | 287 | func (fake *FakeFlintlockClient) GetCalls(stub func(string) (*v1alpha1.GetMicroVMResponse, error)) { 288 | fake.getMutex.Lock() 289 | defer fake.getMutex.Unlock() 290 | fake.GetStub = stub 291 | } 292 | 293 | func (fake *FakeFlintlockClient) GetArgsForCall(i int) string { 294 | fake.getMutex.RLock() 295 | defer fake.getMutex.RUnlock() 296 | argsForCall := fake.getArgsForCall[i] 297 | return argsForCall.arg1 298 | } 299 | 300 | func (fake *FakeFlintlockClient) GetReturns(result1 *v1alpha1.GetMicroVMResponse, result2 error) { 301 | fake.getMutex.Lock() 302 | defer fake.getMutex.Unlock() 303 | fake.GetStub = nil 304 | fake.getReturns = struct { 305 | result1 *v1alpha1.GetMicroVMResponse 306 | result2 error 307 | }{result1, result2} 308 | } 309 | 310 | func (fake *FakeFlintlockClient) GetReturnsOnCall(i int, result1 *v1alpha1.GetMicroVMResponse, result2 error) { 311 | fake.getMutex.Lock() 312 | defer fake.getMutex.Unlock() 313 | fake.GetStub = nil 314 | if fake.getReturnsOnCall == nil { 315 | fake.getReturnsOnCall = make(map[int]struct { 316 | result1 *v1alpha1.GetMicroVMResponse 317 | result2 error 318 | }) 319 | } 320 | fake.getReturnsOnCall[i] = struct { 321 | result1 *v1alpha1.GetMicroVMResponse 322 | result2 error 323 | }{result1, result2} 324 | } 325 | 326 | func (fake *FakeFlintlockClient) List(arg1 string, arg2 string) (*v1alpha1.ListMicroVMsResponse, error) { 327 | fake.listMutex.Lock() 328 | ret, specificReturn := fake.listReturnsOnCall[len(fake.listArgsForCall)] 329 | fake.listArgsForCall = append(fake.listArgsForCall, struct { 330 | arg1 string 331 | arg2 string 332 | }{arg1, arg2}) 333 | stub := fake.ListStub 334 | fakeReturns := fake.listReturns 335 | fake.recordInvocation("List", []interface{}{arg1, arg2}) 336 | fake.listMutex.Unlock() 337 | if stub != nil { 338 | return stub(arg1, arg2) 339 | } 340 | if specificReturn { 341 | return ret.result1, ret.result2 342 | } 343 | return fakeReturns.result1, fakeReturns.result2 344 | } 345 | 346 | func (fake *FakeFlintlockClient) ListCallCount() int { 347 | fake.listMutex.RLock() 348 | defer fake.listMutex.RUnlock() 349 | return len(fake.listArgsForCall) 350 | } 351 | 352 | func (fake *FakeFlintlockClient) ListCalls(stub func(string, string) (*v1alpha1.ListMicroVMsResponse, error)) { 353 | fake.listMutex.Lock() 354 | defer fake.listMutex.Unlock() 355 | fake.ListStub = stub 356 | } 357 | 358 | func (fake *FakeFlintlockClient) ListArgsForCall(i int) (string, string) { 359 | fake.listMutex.RLock() 360 | defer fake.listMutex.RUnlock() 361 | argsForCall := fake.listArgsForCall[i] 362 | return argsForCall.arg1, argsForCall.arg2 363 | } 364 | 365 | func (fake *FakeFlintlockClient) ListReturns(result1 *v1alpha1.ListMicroVMsResponse, result2 error) { 366 | fake.listMutex.Lock() 367 | defer fake.listMutex.Unlock() 368 | fake.ListStub = nil 369 | fake.listReturns = struct { 370 | result1 *v1alpha1.ListMicroVMsResponse 371 | result2 error 372 | }{result1, result2} 373 | } 374 | 375 | func (fake *FakeFlintlockClient) ListReturnsOnCall(i int, result1 *v1alpha1.ListMicroVMsResponse, result2 error) { 376 | fake.listMutex.Lock() 377 | defer fake.listMutex.Unlock() 378 | fake.ListStub = nil 379 | if fake.listReturnsOnCall == nil { 380 | fake.listReturnsOnCall = make(map[int]struct { 381 | result1 *v1alpha1.ListMicroVMsResponse 382 | result2 error 383 | }) 384 | } 385 | fake.listReturnsOnCall[i] = struct { 386 | result1 *v1alpha1.ListMicroVMsResponse 387 | result2 error 388 | }{result1, result2} 389 | } 390 | 391 | func (fake *FakeFlintlockClient) Invocations() map[string][][]interface{} { 392 | fake.invocationsMutex.RLock() 393 | defer fake.invocationsMutex.RUnlock() 394 | fake.closeMutex.RLock() 395 | defer fake.closeMutex.RUnlock() 396 | fake.createMutex.RLock() 397 | defer fake.createMutex.RUnlock() 398 | fake.deleteMutex.RLock() 399 | defer fake.deleteMutex.RUnlock() 400 | fake.getMutex.RLock() 401 | defer fake.getMutex.RUnlock() 402 | fake.listMutex.RLock() 403 | defer fake.listMutex.RUnlock() 404 | copiedInvocations := map[string][][]interface{}{} 405 | for key, value := range fake.invocations { 406 | copiedInvocations[key] = value 407 | } 408 | return copiedInvocations 409 | } 410 | 411 | func (fake *FakeFlintlockClient) recordInvocation(key string, args []interface{}) { 412 | fake.invocationsMutex.Lock() 413 | defer fake.invocationsMutex.Unlock() 414 | if fake.invocations == nil { 415 | fake.invocations = map[string][][]interface{}{} 416 | } 417 | if fake.invocations[key] == nil { 418 | fake.invocations[key] = [][]interface{}{} 419 | } 420 | fake.invocations[key] = append(fake.invocations[key], args) 421 | } 422 | 423 | var _ client.FlintlockClient = new(FakeFlintlockClient) 424 | -------------------------------------------------------------------------------- /pkg/command/app.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | // NewApp is a builder which returns a cli.App. 10 | func NewApp(out io.Writer) *cli.App { 11 | app := cli.NewApp() 12 | 13 | if out != nil { 14 | app.Writer = out 15 | } 16 | 17 | app.Name = "hammertime" 18 | // TODO let's have a usage builder func here #48 19 | app.Usage = "a basic cli client to flintlock" 20 | app.EnableBashCompletion = true 21 | app.Commands = commands() 22 | 23 | return app 24 | } 25 | 26 | func commands() []*cli.Command { 27 | return []*cli.Command{ 28 | createCommand(), 29 | getCommand(), 30 | listCommand(), 31 | deleteCommand(), 32 | versionCommand(), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/command/app_test.go: -------------------------------------------------------------------------------- 1 | package command_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "net" 8 | "testing" 9 | 10 | . "github.com/onsi/gomega" 11 | "github.com/warehouse-13/safety" 12 | "github.com/weaveworks-liquidmetal/flintlock/api/services/microvm/v1alpha1" 13 | "google.golang.org/grpc" 14 | 15 | "github.com/warehouse-13/hammertime/pkg/client" 16 | "github.com/warehouse-13/hammertime/pkg/command" 17 | "github.com/warehouse-13/hammertime/pkg/config" 18 | "github.com/warehouse-13/hammertime/pkg/dialler" 19 | "github.com/warehouse-13/hammertime/pkg/utils" 20 | ) 21 | 22 | func Test_CRUD_noBasicAuth_noTLS(t *testing.T) { 23 | g := NewWithT(t) 24 | 25 | fakeserver := safety.New() 26 | dialer := fakeserver.StartBuf("") 27 | 28 | t.Cleanup(func() { 29 | fakeserver.Stop() 30 | }) 31 | 32 | cfg := &config.Config{ 33 | ClientConfig: config.ClientConfig{ 34 | ClientBuilderFunc: cl(dialer), 35 | }, 36 | } 37 | 38 | buf := &bytes.Buffer{} 39 | w := utils.NewWriter(buf) 40 | 41 | g.Expect(command.CreateFn(w, cfg)).To(Succeed()) 42 | 43 | out := &v1alpha1.CreateMicroVMResponse{} 44 | g.Expect(json.Unmarshal(buf.Bytes(), out)).To(Succeed()) 45 | 46 | cfg.UUID = *out.Microvm.Spec.Uid 47 | 48 | g.Expect(command.GetFn(w, cfg)).To(Succeed()) 49 | g.Expect(command.ListFn(w, cfg)).To(Succeed()) 50 | g.Expect(command.DeleteFn(w, cfg)).To(Succeed()) 51 | } 52 | 53 | func Test_CRUD_basicAuth_noTLS(t *testing.T) { 54 | g := NewWithT(t) 55 | 56 | basicAuthToken := "secret" 57 | 58 | fakeserver := safety.New() 59 | dialer := fakeserver.StartBuf(basicAuthToken) 60 | 61 | t.Cleanup(func() { 62 | fakeserver.Stop() 63 | }) 64 | 65 | cfg := &config.Config{ 66 | ClientConfig: config.ClientConfig{ 67 | ClientBuilderFunc: cl(dialer), 68 | }, 69 | Token: basicAuthToken, 70 | } 71 | 72 | buf := &bytes.Buffer{} 73 | w := utils.NewWriter(buf) 74 | 75 | g.Expect(command.CreateFn(w, cfg)).To(Succeed()) 76 | 77 | out := &v1alpha1.CreateMicroVMResponse{} 78 | g.Expect(json.Unmarshal(buf.Bytes(), out)).To(Succeed()) 79 | 80 | cfg.UUID = *out.Microvm.Spec.Uid 81 | 82 | g.Expect(command.GetFn(w, cfg)).To(Succeed()) 83 | g.Expect(command.ListFn(w, cfg)).To(Succeed()) 84 | g.Expect(command.DeleteFn(w, cfg)).To(Succeed()) 85 | } 86 | 87 | func Test_basicAuth_failsWithNoClientToken(t *testing.T) { 88 | g := NewWithT(t) 89 | 90 | basicAuthToken := "secret" 91 | 92 | fakeserver := safety.New() 93 | dialer := fakeserver.StartBuf(basicAuthToken) 94 | 95 | t.Cleanup(func() { 96 | fakeserver.Stop() 97 | }) 98 | 99 | cfg := &config.Config{ 100 | ClientConfig: config.ClientConfig{ 101 | ClientBuilderFunc: cl(dialer), 102 | }, 103 | Token: "", 104 | } 105 | 106 | buf := &bytes.Buffer{} 107 | w := utils.NewWriter(buf) 108 | 109 | g.Expect(command.CreateFn(w, cfg)).To(MatchError(ContainSubstring("unauthenticated"))) 110 | } 111 | 112 | func cl(dialer func(context.Context, string) (net.Conn, error)) func(string, string) (client.FlintlockClient, error) { 113 | return func(string, token string) (client.FlintlockClient, error) { 114 | opt := []grpc.DialOption{grpc.WithContextDialer(dialer)} 115 | conn, err := dialler.New("bufnet", token, opt) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | return &client.Client{ 121 | MicroVMClient: v1alpha1.NewMicroVMClient(conn), 122 | Conn: conn, 123 | }, nil 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /pkg/command/create.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/urfave/cli/v2" 7 | "github.com/weaveworks-liquidmetal/flintlock/api/types" 8 | 9 | "github.com/warehouse-13/hammertime/pkg/client" 10 | "github.com/warehouse-13/hammertime/pkg/config" 11 | "github.com/warehouse-13/hammertime/pkg/defaults" 12 | "github.com/warehouse-13/hammertime/pkg/flags" 13 | "github.com/warehouse-13/hammertime/pkg/microvm" 14 | "github.com/warehouse-13/hammertime/pkg/utils" 15 | ) 16 | 17 | func createCommand() *cli.Command { 18 | cfg := &config.Config{ 19 | ClientConfig: config.ClientConfig{ 20 | ClientBuilderFunc: client.New, 21 | }, 22 | } 23 | 24 | w := utils.NewWriter(os.Stdout) 25 | 26 | return &cli.Command{ 27 | Name: "create", 28 | Usage: "create a new microvm", 29 | Aliases: []string{"c"}, 30 | Before: flags.ParseFlags(cfg), 31 | Flags: flags.CLIFlags( 32 | flags.WithGRPCAddressFlag(), 33 | flags.WithNameAndNamespaceFlags(true), 34 | flags.WithJSONSpecFlag(), 35 | flags.WithSSHKeyFlag(), 36 | flags.WithQuietFlag(), 37 | flags.WithBasicAuthFlag(), 38 | ), 39 | Action: func(c *cli.Context) error { 40 | return CreateFn(w, cfg) 41 | }, 42 | } 43 | } 44 | 45 | func CreateFn(w utils.Writer, cfg *config.Config) error { 46 | client, err := cfg.ClientBuilderFunc(cfg.GRPCAddress, cfg.Token) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | defer client.Close() 52 | 53 | var mvm *types.MicroVMSpec 54 | 55 | if utils.IsSet(cfg.JSONFile) { 56 | mvm, err = utils.LoadSpecFromFile(cfg.JSONFile) 57 | if err != nil { 58 | return err 59 | } 60 | } else { 61 | mvm, err = newMicroVM(cfg.MvmName, cfg.MvmNamespace, cfg.SSHKeyPath) 62 | if err != nil { 63 | return err 64 | } 65 | } 66 | 67 | res, err := client.Create(mvm) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | if cfg.Silent { 73 | return nil 74 | } 75 | 76 | return w.PrettyPrint(res) 77 | } 78 | 79 | func newMicroVM(name, namespace, sshPath string) (*types.MicroVMSpec, error) { 80 | mvm := defaults.BaseMicroVM() 81 | 82 | metaData, err := microvm.CreateMetadata(name, namespace) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | userData, err := microvm.CreateUserData(name, sshPath) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | mvm.Id = name 93 | mvm.Namespace = namespace 94 | mvm.Metadata = map[string]string{ 95 | "meta-data": metaData, 96 | "user-data": userData, 97 | } 98 | 99 | return mvm, nil 100 | } 101 | -------------------------------------------------------------------------------- /pkg/command/create_test.go: -------------------------------------------------------------------------------- 1 | package command_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "os" 8 | "testing" 9 | 10 | . "github.com/onsi/gomega" 11 | "github.com/weaveworks-liquidmetal/flintlock/api/services/microvm/v1alpha1" 12 | "github.com/weaveworks-liquidmetal/flintlock/api/types" 13 | 14 | "github.com/warehouse-13/hammertime/pkg/client/fakeclient" 15 | "github.com/warehouse-13/hammertime/pkg/command" 16 | "github.com/warehouse-13/hammertime/pkg/config" 17 | "github.com/warehouse-13/hammertime/pkg/utils" 18 | ) 19 | 20 | func Test_CreateFn(t *testing.T) { 21 | g := NewWithT(t) 22 | 23 | var ( 24 | testName = "foo" 25 | testNamespace = "bar" 26 | ) 27 | 28 | mockClient := new(fakeclient.FakeFlintlockClient) 29 | cfg := &config.Config{ 30 | ClientConfig: config.ClientConfig{ 31 | ClientBuilderFunc: testClient(mockClient, nil), 32 | }, 33 | MvmName: testName, 34 | MvmNamespace: testNamespace, 35 | } 36 | 37 | buf := &bytes.Buffer{} 38 | w := utils.NewWriter(buf) 39 | 40 | resp := createResponse(testName, testNamespace) 41 | mockClient.CreateReturns(resp, nil) 42 | g.Expect(command.CreateFn(w, cfg)).To(Succeed()) 43 | 44 | input := mockClient.CreateArgsForCall(0) 45 | g.Expect(input.Id).To(Equal(testName)) 46 | g.Expect(input.Namespace).To(Equal(testNamespace)) 47 | 48 | out := &v1alpha1.CreateMicroVMResponse{} 49 | g.Expect(json.Unmarshal(buf.Bytes(), out)).To(Succeed()) 50 | 51 | g.Expect(out.Microvm).To(Equal(resp.Microvm)) 52 | } 53 | 54 | func Test_CreateFn_silent(t *testing.T) { 55 | g := NewWithT(t) 56 | 57 | var ( 58 | testName = "foo" 59 | testNamespace = "bar" 60 | ) 61 | 62 | mockClient := new(fakeclient.FakeFlintlockClient) 63 | cfg := &config.Config{ 64 | ClientConfig: config.ClientConfig{ 65 | ClientBuilderFunc: testClient(mockClient, nil), 66 | }, 67 | MvmName: testName, 68 | MvmNamespace: testNamespace, 69 | Silent: true, 70 | } 71 | 72 | buf := &bytes.Buffer{} 73 | w := utils.NewWriter(buf) 74 | 75 | mockClient.CreateReturns(createResponse(testName, testNamespace), nil) 76 | g.Expect(command.CreateFn(w, cfg)).To(Succeed()) 77 | 78 | input := mockClient.CreateArgsForCall(0) 79 | g.Expect(input.Id).To(Equal(testName)) 80 | g.Expect(input.Namespace).To(Equal(testNamespace)) 81 | 82 | g.Expect(buf.String()).To(BeEmpty()) 83 | } 84 | 85 | func Test_CreateFn_clientFails(t *testing.T) { 86 | g := NewWithT(t) 87 | 88 | mockClient := new(fakeclient.FakeFlintlockClient) 89 | cfg := &config.Config{ 90 | ClientConfig: config.ClientConfig{ 91 | ClientBuilderFunc: testClient(mockClient, nil), 92 | }, 93 | } 94 | 95 | mockClient.CreateReturns(nil, errors.New("error")) 96 | g.Expect(command.CreateFn(utils.NewWriter(nil), cfg)).NotTo(Succeed()) 97 | } 98 | 99 | func Test_CreateFn_clientBuilderFails(t *testing.T) { 100 | g := NewWithT(t) 101 | 102 | mockClient := new(fakeclient.FakeFlintlockClient) 103 | cfg := &config.Config{ 104 | ClientConfig: config.ClientConfig{ 105 | ClientBuilderFunc: testClient(mockClient, errors.New("unusable")), 106 | }, 107 | } 108 | 109 | g.Expect(command.CreateFn(utils.NewWriter(nil), cfg)).NotTo(Succeed()) 110 | } 111 | 112 | func Test_CreateFn_withFile(t *testing.T) { 113 | g := NewWithT(t) 114 | 115 | var ( 116 | testName = "fname" 117 | testNamespace = "fns" 118 | ) 119 | 120 | spec := &types.MicroVMSpec{ 121 | Id: testName, 122 | Namespace: testNamespace, 123 | } 124 | 125 | tempFile, err := writeFile(spec) 126 | g.Expect(err).NotTo(HaveOccurred()) 127 | 128 | t.Cleanup(func() { 129 | g.Expect(os.RemoveAll(tempFile.Name())).To(Succeed()) 130 | }) 131 | 132 | mockClient := new(fakeclient.FakeFlintlockClient) 133 | cfg := &config.Config{ 134 | ClientConfig: config.ClientConfig{ 135 | ClientBuilderFunc: testClient(mockClient, nil), 136 | }, 137 | JSONFile: tempFile.Name(), 138 | } 139 | 140 | buf := &bytes.Buffer{} 141 | w := utils.NewWriter(buf) 142 | 143 | resp := createResponse(testName, testNamespace) 144 | mockClient.CreateReturns(resp, nil) 145 | g.Expect(command.CreateFn(w, cfg)).To(Succeed()) 146 | 147 | input := mockClient.CreateArgsForCall(0) 148 | g.Expect(input.Id).To(Equal(testName)) 149 | g.Expect(input.Namespace).To(Equal(testNamespace)) 150 | 151 | out := &v1alpha1.CreateMicroVMResponse{} 152 | g.Expect(json.Unmarshal(buf.Bytes(), out)).To(Succeed()) 153 | 154 | g.Expect(out.Microvm).To(Equal(resp.Microvm)) 155 | } 156 | 157 | func Test_CreateFn_withFile_fails(t *testing.T) { 158 | g := NewWithT(t) 159 | 160 | mockClient := new(fakeclient.FakeFlintlockClient) 161 | cfg := &config.Config{ 162 | ClientConfig: config.ClientConfig{ 163 | ClientBuilderFunc: testClient(mockClient, nil), 164 | }, 165 | JSONFile: "noexist", 166 | } 167 | 168 | g.Expect(command.CreateFn(utils.NewWriter(nil), cfg)).NotTo(Succeed()) 169 | } 170 | -------------------------------------------------------------------------------- /pkg/command/delete.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/urfave/cli/v2" 8 | 9 | "github.com/warehouse-13/hammertime/pkg/client" 10 | "github.com/warehouse-13/hammertime/pkg/config" 11 | "github.com/warehouse-13/hammertime/pkg/flags" 12 | "github.com/warehouse-13/hammertime/pkg/utils" 13 | ) 14 | 15 | func deleteCommand() *cli.Command { 16 | cfg := &config.Config{ 17 | ClientConfig: config.ClientConfig{ 18 | ClientBuilderFunc: client.New, 19 | }, 20 | } 21 | 22 | w := utils.NewWriter(os.Stdout) 23 | 24 | return &cli.Command{ 25 | Name: "delete", 26 | Usage: "delete a microvmd", 27 | Aliases: []string{"d"}, 28 | Before: flags.ParseFlags(cfg), 29 | Flags: flags.CLIFlags( 30 | flags.WithGRPCAddressFlag(), 31 | flags.WithNameAndNamespaceFlags(false), 32 | flags.WithIDFlag(), 33 | flags.WithJSONSpecFlag(), 34 | flags.WithAllFlag(), 35 | flags.WithQuietFlag(), 36 | flags.WithBasicAuthFlag(), 37 | ), 38 | Action: func(c *cli.Context) error { 39 | return DeleteFn(w, cfg) 40 | }, 41 | } 42 | } 43 | 44 | func DeleteFn(w utils.Writer, cfg *config.Config) error { 45 | client, err := cfg.ClientBuilderFunc(cfg.GRPCAddress, cfg.Token) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | defer client.Close() 51 | 52 | if utils.IsSet(cfg.JSONFile) { 53 | var err error 54 | 55 | cfg.UUID, cfg.MvmName, cfg.MvmNamespace, err = utils.ProcessFile(cfg.JSONFile) 56 | if err != nil { 57 | return err 58 | } 59 | } 60 | 61 | // If it is possible to delete by set UUID, do that and exit 62 | if utils.IsSet(cfg.UUID) { 63 | return deleteMvm(w, client, cfg.UUID, cfg.Silent) 64 | } 65 | 66 | // If UUID is not present, make sure that required spec is set 67 | if missingSpec(cfg) { 68 | return fmt.Errorf("required: --namespace, --name") 69 | } 70 | 71 | // Get all microvms 72 | list, err := client.List(cfg.MvmName, cfg.MvmNamespace) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | // Do not auto-delete multple mvms, inform and exit 78 | if len(list.Microvm) > 1 && doNotDeleteAll(cfg) { 79 | w.Printf("%d MicroVMs found under %s/%s:\n", len(list.Microvm), cfg.MvmNamespace, cfg.MvmName) 80 | 81 | for _, mvm := range list.Microvm { 82 | w.Print(*mvm.Spec.Uid) 83 | } 84 | 85 | w.Print("\nTo delete all microvms in this list, re-run command with `--all`.") 86 | 87 | return nil 88 | } 89 | 90 | // By this point we assume the user wants everything dead 91 | for _, mvm := range list.Microvm { 92 | if err := deleteMvm(w, client, *mvm.Spec.Uid, cfg.Silent); err != nil { 93 | return err 94 | } 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func deleteMvm(w utils.Writer, c client.FlintlockClient, u string, s bool) error { //nolint: varnamelen // acceptable 101 | res, err := c.Delete(u) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | if s { 107 | return nil 108 | } 109 | 110 | return w.PrettyPrint(res) 111 | } 112 | 113 | func missingSpec(cfg *config.Config) bool { 114 | return !cfg.DeleteAll && (!utils.IsSet(cfg.MvmName) || !utils.IsSet(cfg.MvmNamespace)) 115 | } 116 | 117 | func doNotDeleteAll(cfg *config.Config) bool { 118 | return utils.IsSet(cfg.MvmName) && utils.IsSet(cfg.MvmNamespace) && !cfg.DeleteAll 119 | } 120 | -------------------------------------------------------------------------------- /pkg/command/delete_test.go: -------------------------------------------------------------------------------- 1 | package command_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "testing" 9 | 10 | . "github.com/onsi/gomega" 11 | "github.com/weaveworks-liquidmetal/flintlock/api/types" 12 | "k8s.io/utils/pointer" 13 | 14 | "github.com/warehouse-13/hammertime/pkg/client/fakeclient" 15 | "github.com/warehouse-13/hammertime/pkg/command" 16 | "github.com/warehouse-13/hammertime/pkg/config" 17 | "github.com/warehouse-13/hammertime/pkg/utils" 18 | ) 19 | 20 | func Test_DeleteFn(t *testing.T) { 21 | g := NewWithT(t) 22 | 23 | var testUid = "123abc" 24 | 25 | mockClient := new(fakeclient.FakeFlintlockClient) 26 | cfg := &config.Config{ 27 | ClientConfig: config.ClientConfig{ 28 | ClientBuilderFunc: testClient(mockClient, nil), 29 | }, 30 | UUID: testUid, 31 | } 32 | 33 | buf := &bytes.Buffer{} 34 | w := utils.NewWriter(buf) 35 | 36 | mockClient.DeleteReturns(deleteResponse(), nil) 37 | g.Expect(command.DeleteFn(w, cfg)).To(Succeed()) 38 | 39 | input := mockClient.DeleteArgsForCall(0) 40 | g.Expect(input).To(Equal(testUid)) 41 | 42 | g.Expect(buf.String()).To(Equal("{}\n")) 43 | } 44 | 45 | func Test_DeleteFn_silent(t *testing.T) { 46 | g := NewWithT(t) 47 | 48 | var testUid = "123abc" 49 | 50 | mockClient := new(fakeclient.FakeFlintlockClient) 51 | cfg := &config.Config{ 52 | ClientConfig: config.ClientConfig{ 53 | ClientBuilderFunc: testClient(mockClient, nil), 54 | }, 55 | UUID: testUid, 56 | Silent: true, 57 | } 58 | 59 | buf := &bytes.Buffer{} 60 | w := utils.NewWriter(buf) 61 | 62 | mockClient.DeleteReturns(deleteResponse(), nil) 63 | g.Expect(command.DeleteFn(w, cfg)).To(Succeed()) 64 | 65 | input := mockClient.DeleteArgsForCall(0) 66 | g.Expect(input).To(Equal(testUid)) 67 | 68 | g.Expect(buf.String()).To(BeEmpty()) 69 | } 70 | 71 | func Test_DeleteFn_noUid_noDeleteAll_nameNotSet(t *testing.T) { 72 | g := NewWithT(t) 73 | 74 | var testNamespace = "bar" 75 | 76 | mockClient := new(fakeclient.FakeFlintlockClient) 77 | cfg := &config.Config{ 78 | ClientConfig: config.ClientConfig{ 79 | ClientBuilderFunc: testClient(mockClient, nil), 80 | }, 81 | MvmNamespace: testNamespace, 82 | } 83 | 84 | err := command.DeleteFn(utils.NewWriter(nil), cfg) 85 | g.Expect(err).To(MatchError("required: --namespace, --name")) 86 | } 87 | 88 | func Test_DeleteFn_noUid_noDeleteAll_namespaceNotSet(t *testing.T) { 89 | g := NewWithT(t) 90 | 91 | var testName = "foo" 92 | 93 | mockClient := new(fakeclient.FakeFlintlockClient) 94 | cfg := &config.Config{ 95 | ClientConfig: config.ClientConfig{ 96 | ClientBuilderFunc: testClient(mockClient, nil), 97 | }, 98 | MvmName: testName, 99 | } 100 | 101 | err := command.DeleteFn(utils.NewWriter(nil), cfg) 102 | g.Expect(err).To(MatchError("required: --namespace, --name")) 103 | } 104 | 105 | func Test_DeleteFn_withFile(t *testing.T) { 106 | g := NewWithT(t) 107 | 108 | var testUid = "123abc" 109 | 110 | spec := &types.MicroVMSpec{ 111 | Uid: pointer.String(testUid), 112 | } 113 | 114 | tempFile, err := writeFile(spec) 115 | g.Expect(err).NotTo(HaveOccurred()) 116 | 117 | t.Cleanup(func() { 118 | g.Expect(os.RemoveAll(tempFile.Name())).To(Succeed()) 119 | }) 120 | 121 | mockClient := new(fakeclient.FakeFlintlockClient) 122 | cfg := &config.Config{ 123 | ClientConfig: config.ClientConfig{ 124 | ClientBuilderFunc: testClient(mockClient, nil), 125 | }, 126 | JSONFile: tempFile.Name(), 127 | } 128 | 129 | buf := &bytes.Buffer{} 130 | w := utils.NewWriter(buf) 131 | 132 | mockClient.DeleteReturns(deleteResponse(), nil) 133 | g.Expect(command.DeleteFn(w, cfg)).To(Succeed()) 134 | 135 | input := mockClient.DeleteArgsForCall(0) 136 | g.Expect(input).To(Equal(testUid)) 137 | 138 | g.Expect(buf.String()).To(Equal("{}\n")) 139 | } 140 | 141 | func Test_DeleteFn_withFile_fails(t *testing.T) { 142 | g := NewWithT(t) 143 | 144 | mockClient := new(fakeclient.FakeFlintlockClient) 145 | cfg := &config.Config{ 146 | ClientConfig: config.ClientConfig{ 147 | ClientBuilderFunc: testClient(mockClient, nil), 148 | }, 149 | JSONFile: "noexist", 150 | } 151 | 152 | g.Expect(command.DeleteFn(utils.NewWriter(nil), cfg)).NotTo(Succeed()) 153 | } 154 | 155 | func Test_DeleteFn_noUid_noDeleteAll_oneMatch(t *testing.T) { 156 | g := NewWithT(t) 157 | 158 | var ( 159 | testName = "foo" 160 | testNamespace = "bar" 161 | ) 162 | 163 | mockClient := new(fakeclient.FakeFlintlockClient) 164 | cfg := &config.Config{ 165 | ClientConfig: config.ClientConfig{ 166 | ClientBuilderFunc: testClient(mockClient, nil), 167 | }, 168 | MvmName: testName, 169 | MvmNamespace: testNamespace, 170 | } 171 | 172 | buf := &bytes.Buffer{} 173 | w := utils.NewWriter(buf) 174 | 175 | resp := listResponse(1, testName, testName) 176 | mockClient.ListReturns(resp, nil) 177 | mockClient.DeleteReturns(deleteResponse(), nil) 178 | 179 | g.Expect(command.DeleteFn(w, cfg)).To(Succeed()) 180 | 181 | input := mockClient.DeleteArgsForCall(0) 182 | g.Expect(input).To(Equal(*resp.Microvm[0].Spec.Uid)) 183 | 184 | g.Expect(buf.String()).To(Equal("{}\n")) 185 | } 186 | 187 | func Test_DeleteFn_noUid_noDeleteAll_multipleMatches(t *testing.T) { 188 | g := NewWithT(t) 189 | 190 | var ( 191 | testName = "foo" 192 | testNamespace = "bar" 193 | ) 194 | 195 | mockClient := new(fakeclient.FakeFlintlockClient) 196 | cfg := &config.Config{ 197 | ClientConfig: config.ClientConfig{ 198 | ClientBuilderFunc: testClient(mockClient, nil), 199 | }, 200 | MvmName: testName, 201 | MvmNamespace: testNamespace, 202 | } 203 | 204 | buf := &bytes.Buffer{} 205 | w := utils.NewWriter(buf) 206 | 207 | resp := listResponse(2, testName, testName) 208 | mockClient.ListReturns(resp, nil) 209 | 210 | g.Expect(command.DeleteFn(w, cfg)).To(Succeed()) 211 | 212 | g.Expect(mockClient.DeleteCallCount()).To(BeZero()) 213 | 214 | g.Expect(buf.String()).To(ContainSubstring(fmt.Sprintf("2 MicroVMs found under %s/%s:", testNamespace, testName))) 215 | } 216 | 217 | func Test_DeleteFn_noUid_deleteAll(t *testing.T) { 218 | g := NewWithT(t) 219 | 220 | var ( 221 | testName = "foo" 222 | testNamespace = "bar" 223 | mvmCount = 2 224 | ) 225 | 226 | mockClient := new(fakeclient.FakeFlintlockClient) 227 | cfg := &config.Config{ 228 | ClientConfig: config.ClientConfig{ 229 | ClientBuilderFunc: testClient(mockClient, nil), 230 | }, 231 | MvmName: testName, 232 | MvmNamespace: testNamespace, 233 | DeleteAll: true, 234 | } 235 | 236 | buf := &bytes.Buffer{} 237 | w := utils.NewWriter(buf) 238 | 239 | resp := listResponse(mvmCount, testName, testName) 240 | mockClient.ListReturns(resp, nil) 241 | mockClient.DeleteReturns(deleteResponse(), nil) 242 | 243 | g.Expect(command.DeleteFn(w, cfg)).To(Succeed()) 244 | 245 | g.Expect(mockClient.DeleteCallCount()).To(Equal(mvmCount)) 246 | 247 | input := mockClient.DeleteArgsForCall(0) 248 | g.Expect(input).To(Equal(*resp.Microvm[0].Spec.Uid)) 249 | input = mockClient.DeleteArgsForCall(1) 250 | g.Expect(input).To(Equal(*resp.Microvm[1].Spec.Uid)) 251 | 252 | g.Expect(buf.String()).To(Equal("{}\n{}\n")) 253 | } 254 | 255 | func Test_DeleteFn_noUid_deleteAll_silent(t *testing.T) { 256 | g := NewWithT(t) 257 | 258 | var ( 259 | testName = "foo" 260 | testNamespace = "bar" 261 | mvmCount = 2 262 | ) 263 | 264 | mockClient := new(fakeclient.FakeFlintlockClient) 265 | cfg := &config.Config{ 266 | ClientConfig: config.ClientConfig{ 267 | ClientBuilderFunc: testClient(mockClient, nil), 268 | }, 269 | MvmName: testName, 270 | MvmNamespace: testNamespace, 271 | DeleteAll: true, 272 | Silent: true, 273 | } 274 | 275 | buf := &bytes.Buffer{} 276 | w := utils.NewWriter(buf) 277 | 278 | resp := listResponse(mvmCount, testName, testName) 279 | mockClient.ListReturns(resp, nil) 280 | mockClient.DeleteReturns(deleteResponse(), nil) 281 | 282 | g.Expect(command.DeleteFn(w, cfg)).To(Succeed()) 283 | 284 | g.Expect(mockClient.DeleteCallCount()).To(Equal(mvmCount)) 285 | 286 | input := mockClient.DeleteArgsForCall(0) 287 | g.Expect(input).To(Equal(*resp.Microvm[0].Spec.Uid)) 288 | input = mockClient.DeleteArgsForCall(1) 289 | g.Expect(input).To(Equal(*resp.Microvm[1].Spec.Uid)) 290 | 291 | g.Expect(buf.String()).To(BeEmpty()) 292 | } 293 | 294 | func Test_DeleteFn_clientFails(t *testing.T) { 295 | g := NewWithT(t) 296 | 297 | mockClient := new(fakeclient.FakeFlintlockClient) 298 | cfg := &config.Config{ 299 | ClientConfig: config.ClientConfig{ 300 | ClientBuilderFunc: testClient(mockClient, nil), 301 | }, 302 | } 303 | 304 | mockClient.DeleteReturns(nil, errors.New("error")) 305 | g.Expect(command.DeleteFn(utils.NewWriter(nil), cfg)).NotTo(Succeed()) 306 | } 307 | 308 | func Test_DeleteFn_clientBuilderFails(t *testing.T) { 309 | g := NewWithT(t) 310 | 311 | mockClient := new(fakeclient.FakeFlintlockClient) 312 | cfg := &config.Config{ 313 | ClientConfig: config.ClientConfig{ 314 | ClientBuilderFunc: testClient(mockClient, errors.New("unusable")), 315 | }, 316 | } 317 | 318 | g.Expect(command.DeleteFn(utils.NewWriter(nil), cfg)).NotTo(Succeed()) 319 | } 320 | -------------------------------------------------------------------------------- /pkg/command/get.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/urfave/cli/v2" 8 | "github.com/weaveworks-liquidmetal/flintlock/api/types" 9 | 10 | "github.com/warehouse-13/hammertime/pkg/client" 11 | "github.com/warehouse-13/hammertime/pkg/config" 12 | "github.com/warehouse-13/hammertime/pkg/flags" 13 | "github.com/warehouse-13/hammertime/pkg/utils" 14 | ) 15 | 16 | func getCommand() *cli.Command { 17 | cfg := &config.Config{ 18 | ClientConfig: config.ClientConfig{ 19 | ClientBuilderFunc: client.New, 20 | }, 21 | } 22 | 23 | w := utils.NewWriter(os.Stdout) 24 | 25 | return &cli.Command{ 26 | Name: "get", 27 | Usage: "get an existing microvm", 28 | Aliases: []string{"g"}, 29 | Before: flags.ParseFlags(cfg), 30 | Flags: flags.CLIFlags( 31 | flags.WithGRPCAddressFlag(), 32 | flags.WithNameAndNamespaceFlags(true), 33 | flags.WithJSONSpecFlag(), 34 | flags.WithStateFlag(), 35 | flags.WithIDFlag(), 36 | flags.WithBasicAuthFlag(), 37 | ), 38 | Action: func(c *cli.Context) error { 39 | return GetFn(w, cfg) 40 | }, 41 | } 42 | } 43 | 44 | func GetFn(w utils.Writer, cfg *config.Config) error { 45 | if utils.IsSet(cfg.JSONFile) { 46 | var err error 47 | 48 | cfg.UUID, cfg.MvmName, cfg.MvmNamespace, err = utils.ProcessFile(cfg.JSONFile) 49 | if err != nil { 50 | return err 51 | } 52 | } 53 | 54 | res, err := findMicrovm(cfg) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | if len(res) == 1 { 60 | if cfg.State { 61 | w.Print(res[0].Status.State) 62 | 63 | return nil 64 | } 65 | 66 | return w.PrettyPrint(res[0]) 67 | } 68 | 69 | if len(res) > 1 { 70 | w.Printf("%d MicroVMs found under %s/%s:\n", len(res), cfg.MvmNamespace, cfg.MvmName) 71 | 72 | for _, mvm := range res { 73 | w.Print(*mvm.Spec.Uid) 74 | } 75 | 76 | return nil 77 | } 78 | 79 | return fmt.Errorf("MicroVM %s/%s not found", cfg.MvmNamespace, cfg.MvmName) 80 | } 81 | 82 | func findMicrovm(cfg *config.Config) ([]*types.MicroVM, error) { 83 | client, err := cfg.ClientBuilderFunc(cfg.GRPCAddress, cfg.Token) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | defer client.Close() 89 | 90 | if utils.IsSet(cfg.UUID) { 91 | res, err := client.Get(cfg.UUID) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | return []*types.MicroVM{res.Microvm}, nil 97 | } 98 | 99 | res, err := client.List(cfg.MvmName, cfg.MvmNamespace) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | return res.Microvm, nil 105 | } 106 | -------------------------------------------------------------------------------- /pkg/command/get_test.go: -------------------------------------------------------------------------------- 1 | package command_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "testing" 10 | 11 | . "github.com/onsi/gomega" 12 | "github.com/weaveworks-liquidmetal/flintlock/api/types" 13 | "k8s.io/utils/pointer" 14 | 15 | "github.com/warehouse-13/hammertime/pkg/client/fakeclient" 16 | "github.com/warehouse-13/hammertime/pkg/command" 17 | "github.com/warehouse-13/hammertime/pkg/config" 18 | "github.com/warehouse-13/hammertime/pkg/utils" 19 | ) 20 | 21 | func Test_GetFn(t *testing.T) { 22 | g := NewWithT(t) 23 | 24 | var ( 25 | testName = "foo" 26 | testNamespace = "bar" 27 | testUid = "abc123" 28 | ) 29 | 30 | mockClient := new(fakeclient.FakeFlintlockClient) 31 | cfg := &config.Config{ 32 | ClientConfig: config.ClientConfig{ 33 | ClientBuilderFunc: testClient(mockClient, nil), 34 | }, 35 | UUID: testUid, 36 | } 37 | 38 | buf := &bytes.Buffer{} 39 | w := utils.NewWriter(buf) 40 | 41 | resp := getResponse(testName, testNamespace, testUid) 42 | mockClient.GetReturns(resp, nil) 43 | g.Expect(command.GetFn(w, cfg)).To(Succeed()) 44 | 45 | inUid := mockClient.GetArgsForCall(0) 46 | g.Expect(inUid).To(Equal(testUid)) 47 | 48 | out := &types.MicroVM{} 49 | g.Expect(json.Unmarshal(buf.Bytes(), out)).To(Succeed()) 50 | 51 | g.Expect(out).To(Equal(resp.Microvm)) 52 | } 53 | 54 | func Test_GetFn_state(t *testing.T) { 55 | g := NewWithT(t) 56 | 57 | var ( 58 | testName = "foo" 59 | testNamespace = "bar" 60 | testUid = "abc123" 61 | ) 62 | 63 | mockClient := new(fakeclient.FakeFlintlockClient) 64 | cfg := &config.Config{ 65 | ClientConfig: config.ClientConfig{ 66 | ClientBuilderFunc: testClient(mockClient, nil), 67 | }, 68 | UUID: testUid, 69 | State: true, 70 | } 71 | 72 | buf := &bytes.Buffer{} 73 | w := utils.NewWriter(buf) 74 | 75 | mockClient.GetReturns(getResponse(testName, testNamespace, testUid), nil) 76 | g.Expect(command.GetFn(w, cfg)).To(Succeed()) 77 | 78 | inUid := mockClient.GetArgsForCall(0) 79 | g.Expect(inUid).To(Equal(testUid)) 80 | 81 | g.Expect(buf.String()).To(Equal("CREATED\n")) 82 | } 83 | 84 | func Test_GetFn_uidNotSet(t *testing.T) { 85 | g := NewWithT(t) 86 | 87 | var ( 88 | testName = "foo" 89 | testNamespace = "bar" 90 | ) 91 | 92 | mockClient := new(fakeclient.FakeFlintlockClient) 93 | cfg := &config.Config{ 94 | ClientConfig: config.ClientConfig{ 95 | ClientBuilderFunc: testClient(mockClient, nil), 96 | }, 97 | MvmName: testName, 98 | MvmNamespace: testNamespace, 99 | } 100 | 101 | buf := &bytes.Buffer{} 102 | w := utils.NewWriter(buf) 103 | 104 | resp := listResponse(1, testName, testName) 105 | mockClient.ListReturns(resp, nil) 106 | g.Expect(command.GetFn(w, cfg)).To(Succeed()) 107 | 108 | g.Expect(mockClient.GetCallCount()).To(BeZero()) 109 | inName, inNamespace := mockClient.ListArgsForCall(0) 110 | g.Expect(inName).To(Equal(testName)) 111 | g.Expect(inNamespace).To(Equal(testNamespace)) 112 | 113 | out := &types.MicroVM{} 114 | g.Expect(json.Unmarshal(buf.Bytes(), out)).To(Succeed()) 115 | 116 | g.Expect(out).To(Equal(resp.Microvm[0])) 117 | } 118 | 119 | func Test_GetFn_uidNotSet_state(t *testing.T) { 120 | g := NewWithT(t) 121 | 122 | var ( 123 | testName = "foo" 124 | testNamespace = "bar" 125 | ) 126 | 127 | mockClient := new(fakeclient.FakeFlintlockClient) 128 | cfg := &config.Config{ 129 | ClientConfig: config.ClientConfig{ 130 | ClientBuilderFunc: testClient(mockClient, nil), 131 | }, 132 | MvmName: testName, 133 | MvmNamespace: testNamespace, 134 | State: true, 135 | } 136 | 137 | buf := &bytes.Buffer{} 138 | w := utils.NewWriter(buf) 139 | 140 | mockClient.ListReturns(listResponse(1, testName, testName), nil) 141 | g.Expect(command.GetFn(w, cfg)).To(Succeed()) 142 | 143 | g.Expect(mockClient.GetCallCount()).To(BeZero()) 144 | inName, inNamespace := mockClient.ListArgsForCall(0) 145 | g.Expect(inName).To(Equal(testName)) 146 | g.Expect(inNamespace).To(Equal(testNamespace)) 147 | 148 | g.Expect(buf.String()).To(Equal("CREATED\n")) 149 | } 150 | 151 | func Test_GetFn_uidNotSet_multipleMatches(t *testing.T) { 152 | g := NewWithT(t) 153 | 154 | var ( 155 | testName = "foo" 156 | testNamespace = "bar" 157 | ) 158 | 159 | mockClient := new(fakeclient.FakeFlintlockClient) 160 | cfg := &config.Config{ 161 | ClientConfig: config.ClientConfig{ 162 | ClientBuilderFunc: testClient(mockClient, nil), 163 | }, 164 | MvmName: testName, 165 | MvmNamespace: testNamespace, 166 | } 167 | 168 | buf := &bytes.Buffer{} 169 | w := utils.NewWriter(buf) 170 | 171 | mockClient.ListReturns(listResponse(2, testName, testNamespace), nil) 172 | g.Expect(command.GetFn(w, cfg)).To(Succeed()) 173 | 174 | g.Expect(mockClient.GetCallCount()).To(BeZero()) 175 | inName, inNamespace := mockClient.ListArgsForCall(0) 176 | g.Expect(inName).To(Equal(testName)) 177 | g.Expect(inNamespace).To(Equal(testNamespace)) 178 | 179 | g.Expect(buf.String()).To(ContainSubstring(fmt.Sprintf("2 MicroVMs found under %s/%s", testNamespace, testName))) 180 | } 181 | 182 | func Test_GetFn_withFile(t *testing.T) { 183 | g := NewWithT(t) 184 | 185 | var ( 186 | testName = "fname" 187 | testNamespace = "fns" 188 | testUid = "abc123" 189 | ) 190 | 191 | spec := &types.MicroVMSpec{ 192 | Id: testName, 193 | Namespace: testNamespace, 194 | Uid: pointer.String(testUid), 195 | } 196 | 197 | tempFile, err := writeFile(spec) 198 | g.Expect(err).NotTo(HaveOccurred()) 199 | 200 | t.Cleanup(func() { 201 | g.Expect(os.RemoveAll(tempFile.Name())).To(Succeed()) 202 | }) 203 | 204 | mockClient := new(fakeclient.FakeFlintlockClient) 205 | cfg := &config.Config{ 206 | ClientConfig: config.ClientConfig{ 207 | ClientBuilderFunc: testClient(mockClient, nil), 208 | }, 209 | JSONFile: tempFile.Name(), 210 | } 211 | 212 | buf := &bytes.Buffer{} 213 | w := utils.NewWriter(buf) 214 | 215 | resp := getResponse(testName, testNamespace, testUid) 216 | mockClient.GetReturns(resp, nil) 217 | g.Expect(command.GetFn(w, cfg)).To(Succeed()) 218 | 219 | inUid := mockClient.GetArgsForCall(0) 220 | g.Expect(inUid).To(Equal(testUid)) 221 | 222 | out := &types.MicroVM{} 223 | g.Expect(json.Unmarshal(buf.Bytes(), out)).To(Succeed()) 224 | 225 | g.Expect(out).To(Equal(resp.Microvm)) 226 | } 227 | 228 | func Test_GetFn_withFile_fails(t *testing.T) { 229 | g := NewWithT(t) 230 | 231 | mockClient := new(fakeclient.FakeFlintlockClient) 232 | cfg := &config.Config{ 233 | ClientConfig: config.ClientConfig{ 234 | ClientBuilderFunc: testClient(mockClient, nil), 235 | }, 236 | JSONFile: "noexist", 237 | } 238 | 239 | g.Expect(command.GetFn(utils.NewWriter(nil), cfg)).NotTo(Succeed()) 240 | } 241 | 242 | func Test_GetFn_nothingFound(t *testing.T) { 243 | g := NewWithT(t) 244 | 245 | var ( 246 | testName = "foo" 247 | testNamespace = "bar" 248 | ) 249 | 250 | mockClient := new(fakeclient.FakeFlintlockClient) 251 | cfg := &config.Config{ 252 | ClientConfig: config.ClientConfig{ 253 | ClientBuilderFunc: testClient(mockClient, nil), 254 | }, 255 | MvmName: testName, 256 | MvmNamespace: testNamespace, 257 | } 258 | 259 | mockClient.ListReturns(listResponse(0, "", ""), nil) 260 | err := command.GetFn(utils.NewWriter(nil), cfg) 261 | g.Expect(err).To(MatchError(ContainSubstring(fmt.Sprintf("MicroVM %s/%s not found", testNamespace, testName)))) 262 | } 263 | 264 | func Test_GetFn_clientFails(t *testing.T) { 265 | g := NewWithT(t) 266 | 267 | var testUid = "abc123" 268 | 269 | mockClient := new(fakeclient.FakeFlintlockClient) 270 | cfg := &config.Config{ 271 | ClientConfig: config.ClientConfig{ 272 | ClientBuilderFunc: testClient(mockClient, nil), 273 | }, 274 | UUID: testUid, 275 | } 276 | 277 | mockClient.GetReturns(nil, errors.New("error")) 278 | g.Expect(command.GetFn(utils.NewWriter(nil), cfg)).NotTo(Succeed()) 279 | } 280 | 281 | func Test_GetFn_clientBuilderFails(t *testing.T) { 282 | g := NewWithT(t) 283 | 284 | mockClient := new(fakeclient.FakeFlintlockClient) 285 | cfg := &config.Config{ 286 | ClientConfig: config.ClientConfig{ 287 | ClientBuilderFunc: testClient(mockClient, errors.New("unusable")), 288 | }, 289 | } 290 | 291 | g.Expect(command.GetFn(utils.NewWriter(nil), cfg)).NotTo(Succeed()) 292 | } 293 | -------------------------------------------------------------------------------- /pkg/command/helpers_test.go: -------------------------------------------------------------------------------- 1 | package command_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "math/rand" 8 | "os" 9 | "time" 10 | 11 | "github.com/warehouse-13/hammertime/pkg/client" 12 | "github.com/weaveworks-liquidmetal/flintlock/api/services/microvm/v1alpha1" 13 | "github.com/weaveworks-liquidmetal/flintlock/api/types" 14 | "google.golang.org/protobuf/types/known/emptypb" 15 | "k8s.io/utils/pointer" 16 | ) 17 | 18 | func testClient(c client.FlintlockClient, err error) func(string, string) (client.FlintlockClient, error) { 19 | return func(string, string) (client.FlintlockClient, error) { 20 | return c, err 21 | } 22 | } 23 | 24 | func writeFile(spec *types.MicroVMSpec) (*os.File, error) { 25 | tempFile, err := ioutil.TempFile("", "getfn_test") 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | dat, err := json.Marshal(spec) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if err := ioutil.WriteFile(tempFile.Name(), dat, 0755); err != nil { 36 | return nil, err 37 | } 38 | 39 | return tempFile, nil 40 | } 41 | 42 | func createResponse(name, namespace string) *v1alpha1.CreateMicroVMResponse { 43 | return &v1alpha1.CreateMicroVMResponse{ 44 | Microvm: &types.MicroVM{ 45 | Spec: &types.MicroVMSpec{ 46 | Id: name, 47 | Namespace: namespace, 48 | }, 49 | }, 50 | } 51 | } 52 | 53 | func deleteResponse() *emptypb.Empty { 54 | return &emptypb.Empty{} 55 | } 56 | 57 | func getResponse(name, namespace, uid string) *v1alpha1.GetMicroVMResponse { 58 | return &v1alpha1.GetMicroVMResponse{ 59 | Microvm: &types.MicroVM{ 60 | Spec: &types.MicroVMSpec{ 61 | Id: name, 62 | Namespace: namespace, 63 | Uid: pointer.String(uid), 64 | }, 65 | Status: &types.MicroVMStatus{ 66 | State: types.MicroVMStatus_CREATED, 67 | }, 68 | }, 69 | } 70 | } 71 | 72 | func listResponse(count int, name, namespace string) *v1alpha1.ListMicroVMsResponse { 73 | mvms := []*types.MicroVM{} 74 | 75 | for i := 0; i < count; i++ { 76 | mvm := &types.MicroVM{ 77 | Spec: &types.MicroVMSpec{ 78 | Id: name, 79 | Namespace: namespace, 80 | Uid: pointer.String(randomString(10)), 81 | }, 82 | Status: &types.MicroVMStatus{ 83 | State: types.MicroVMStatus_CREATED, 84 | }, 85 | } 86 | 87 | mvms = append(mvms, mvm) 88 | } 89 | 90 | return &v1alpha1.ListMicroVMsResponse{ 91 | Microvm: mvms, 92 | } 93 | } 94 | 95 | func randomString(length int) string { 96 | rand.Seed(time.Now().UnixNano()) 97 | b := make([]byte, length) 98 | rand.Read(b) 99 | return fmt.Sprintf("%x", b)[:length] 100 | } 101 | -------------------------------------------------------------------------------- /pkg/command/list.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/urfave/cli/v2" 7 | 8 | "github.com/warehouse-13/hammertime/pkg/client" 9 | "github.com/warehouse-13/hammertime/pkg/config" 10 | "github.com/warehouse-13/hammertime/pkg/flags" 11 | "github.com/warehouse-13/hammertime/pkg/utils" 12 | ) 13 | 14 | func listCommand() *cli.Command { 15 | cfg := &config.Config{ 16 | ClientConfig: config.ClientConfig{ 17 | ClientBuilderFunc: client.New, 18 | }, 19 | } 20 | 21 | w := utils.NewWriter(os.Stdout) 22 | 23 | return &cli.Command{ 24 | Name: "list", 25 | Usage: "list microvms", 26 | Aliases: []string{"l"}, 27 | Before: flags.ParseFlags(cfg), 28 | Flags: flags.CLIFlags( 29 | flags.WithGRPCAddressFlag(), 30 | flags.WithNameAndNamespaceFlags(false), 31 | flags.WithBasicAuthFlag(), 32 | ), 33 | Action: func(c *cli.Context) error { 34 | return ListFn(w, cfg) 35 | }, 36 | } 37 | } 38 | 39 | func ListFn(w utils.Writer, cfg *config.Config) error { 40 | client, err := cfg.ClientBuilderFunc(cfg.GRPCAddress, cfg.Token) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | defer client.Close() 46 | 47 | res, err := client.List(cfg.MvmName, cfg.MvmNamespace) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return w.PrettyPrint(res) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/command/list_test.go: -------------------------------------------------------------------------------- 1 | package command_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "testing" 8 | 9 | . "github.com/onsi/gomega" 10 | "github.com/weaveworks-liquidmetal/flintlock/api/services/microvm/v1alpha1" 11 | 12 | "github.com/warehouse-13/hammertime/pkg/client/fakeclient" 13 | "github.com/warehouse-13/hammertime/pkg/command" 14 | "github.com/warehouse-13/hammertime/pkg/config" 15 | "github.com/warehouse-13/hammertime/pkg/utils" 16 | ) 17 | 18 | func Test_ListFn(t *testing.T) { 19 | g := NewWithT(t) 20 | 21 | var ( 22 | testName = "foo" 23 | testNamespace = "bar" 24 | ) 25 | 26 | mockClient := new(fakeclient.FakeFlintlockClient) 27 | cfg := &config.Config{ 28 | ClientConfig: config.ClientConfig{ 29 | ClientBuilderFunc: testClient(mockClient, nil), 30 | }, 31 | MvmName: testName, 32 | MvmNamespace: testNamespace, 33 | } 34 | 35 | buf := &bytes.Buffer{} 36 | w := utils.NewWriter(buf) 37 | 38 | resp := listResponse(2, testName, testNamespace) 39 | mockClient.ListReturns(resp, nil) 40 | g.Expect(command.ListFn(w, cfg)).To(Succeed()) 41 | 42 | inName, inNamespace := mockClient.ListArgsForCall(0) 43 | g.Expect(inName).To(Equal(testName)) 44 | g.Expect(inNamespace).To(Equal(testNamespace)) 45 | 46 | out := &v1alpha1.ListMicroVMsResponse{} 47 | g.Expect(json.Unmarshal(buf.Bytes(), out)).To(Succeed()) 48 | 49 | g.Expect(out.Microvm).To(Equal(resp.Microvm)) 50 | g.Expect(out.Microvm).To(HaveLen(2)) 51 | } 52 | 53 | func Test_ListFn_clientFails(t *testing.T) { 54 | g := NewWithT(t) 55 | 56 | mockClient := new(fakeclient.FakeFlintlockClient) 57 | cfg := &config.Config{ 58 | ClientConfig: config.ClientConfig{ 59 | ClientBuilderFunc: testClient(mockClient, nil), 60 | }, 61 | } 62 | 63 | mockClient.ListReturns(nil, errors.New("error")) 64 | g.Expect(command.ListFn(utils.NewWriter(nil), cfg)).NotTo(Succeed()) 65 | } 66 | 67 | func Test_ListFn_clientBuilderFails(t *testing.T) { 68 | g := NewWithT(t) 69 | 70 | mockClient := new(fakeclient.FakeFlintlockClient) 71 | cfg := &config.Config{ 72 | ClientConfig: config.ClientConfig{ 73 | ClientBuilderFunc: testClient(mockClient, errors.New("unusable")), 74 | }, 75 | } 76 | 77 | g.Expect(command.ListFn(utils.NewWriter(nil), cfg)).NotTo(Succeed()) 78 | } 79 | -------------------------------------------------------------------------------- /pkg/command/version.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/urfave/cli/v2" 7 | 8 | "github.com/warehouse-13/hammertime/pkg/utils" 9 | "github.com/warehouse-13/hammertime/pkg/version" 10 | ) 11 | 12 | func versionCommand() *cli.Command { 13 | return &cli.Command{ 14 | Name: "version", 15 | Usage: "print the version number for hammertime", 16 | Aliases: []string{"v"}, 17 | Flags: []cli.Flag{ 18 | &cli.BoolFlag{ 19 | Name: "long", 20 | Value: false, 21 | Aliases: []string{"l"}, 22 | Usage: "print the long version information", 23 | }, 24 | }, 25 | Action: VersionFn, 26 | } 27 | } 28 | 29 | type versionInfo struct { 30 | PackageName string 31 | Version string 32 | CommitHash string 33 | BuildDate string 34 | } 35 | 36 | func VersionFn(ctx *cli.Context) error { 37 | w := utils.NewWriter(os.Stdout) 38 | 39 | if ctx.Bool("long") { 40 | info := versionInfo{ 41 | version.PackageName, 42 | version.Version, 43 | version.CommitHash, 44 | version.BuildDate, 45 | } 46 | 47 | return w.PrettyPrint(info) 48 | } 49 | 50 | w.Print(version.Version) 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/warehouse-13/hammertime/pkg/client" 5 | ) 6 | 7 | type Config struct { 8 | // GRPCAddress is the flintlock server address. 9 | GRPCAddress string 10 | // MvmName is the name of the Microvm. 11 | MvmName string 12 | // MvmNamespace is the namespace of the Microvm. 13 | MvmNamespace string 14 | // JSONFile is the path to a file containing a Microvm Spec in json. 15 | JSONFile string 16 | // SSHKeyPath is the path to a file containing a public key. Added for 17 | // creating/using a Microvm with SSH access. 18 | SSHKeyPath string 19 | // State reports on only the state of a Microvm. Can only be used with `get`. 20 | State bool 21 | // DeleteAll configures all microvms to be deleted. Can only be used with `delete`. 22 | DeleteAll bool 23 | // Silent stops the response from being printed. Can only be used with `create` and `delete`. 24 | Silent bool 25 | // UUID is the id of a created Microvm. 26 | UUID string 27 | // Token used for basic auth 28 | Token string 29 | 30 | ClientConfig 31 | } 32 | 33 | type ClientConfig struct { 34 | ClientBuilderFunc func(string, string) (client.FlintlockClient, error) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/defaults/defaults.go: -------------------------------------------------------------------------------- 1 | package defaults 2 | 3 | import ( 4 | "github.com/weaveworks-liquidmetal/flintlock/api/types" 5 | "k8s.io/utils/pointer" 6 | ) 7 | 8 | const ( 9 | // DialTarget is the default address which the client will attempt to contact 10 | // the flintlock server on. 11 | DialTarget = "127.0.0.1:9090" 12 | // MvmName is the default name to use when creating a Microvm. 13 | MvmName = "mvm0" 14 | // MvmNamespace is the default name to use when creating a Microvm. 15 | MvmNamespace = "ns0" 16 | ) 17 | 18 | const ( 19 | // KernelImage is the default MVM kernel image. 20 | KernelImage = "ghcr.io/weaveworks-liquidmetal/kernel-bin:5.10.77" 21 | // ModulesImage is the default MVM kernel image. 22 | ModulesImage = "ghcr.io/weaveworks-liquidmetal/kernel-modules:5.10.77" 23 | // OSImage is the default MVM OS image. 24 | OSImage = "ghcr.io/weaveworks-liquidmetal/capmvm-k8s-os:1.23.5" 25 | 26 | kernelFilename = "boot/vmlinux" 27 | modulesPath = "/lib/modules/5.10.77" 28 | ) 29 | 30 | func BaseMicroVM() *types.MicroVMSpec { 31 | return &types.MicroVMSpec{ 32 | Vcpu: 2, //nolint: gomnd // we don't care 33 | MemoryInMb: 2048, //nolint: gomnd // we don't care 34 | Kernel: &types.Kernel{ 35 | Image: KernelImage, 36 | Filename: pointer.String(kernelFilename), 37 | AddNetworkConfig: true, 38 | }, 39 | RootVolume: &types.Volume{ 40 | Id: "root", 41 | IsReadOnly: false, 42 | Source: &types.VolumeSource{ 43 | ContainerSource: pointer.String(OSImage), 44 | }, 45 | }, 46 | AdditionalVolumes: []*types.Volume{ 47 | { 48 | Id: "modules", 49 | IsReadOnly: false, 50 | Source: &types.VolumeSource{ 51 | ContainerSource: pointer.String(ModulesImage), 52 | }, 53 | MountPoint: pointer.String(modulesPath), 54 | }, 55 | }, 56 | Interfaces: []*types.NetworkInterface{ 57 | { 58 | DeviceId: "eth1", 59 | Type: 0, 60 | }, 61 | }, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pkg/dialler/auth.go: -------------------------------------------------------------------------------- 1 | package dialler 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | ) 7 | 8 | type basicAuth struct { 9 | token string 10 | } 11 | 12 | func basic(t string) basicAuth { 13 | return basicAuth{token: t} 14 | } 15 | 16 | func (b basicAuth) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) { 17 | enc := base64.StdEncoding.EncodeToString([]byte(b.token)) 18 | 19 | return map[string]string{ 20 | "authorization": "Basic " + enc, 21 | }, nil 22 | } 23 | 24 | func (basicAuth) RequireTransportSecurity() bool { 25 | return false 26 | } 27 | -------------------------------------------------------------------------------- /pkg/dialler/dialler.go: -------------------------------------------------------------------------------- 1 | package dialler 2 | 3 | import ( 4 | "google.golang.org/grpc" 5 | "google.golang.org/grpc/credentials/insecure" 6 | ) 7 | 8 | // New process the dial config and returns a grpc.ClientConn. The caller is 9 | // responsible for closing the connection. 10 | func New(address, basicAuthToken string, opts []grpc.DialOption) (*grpc.ClientConn, error) { 11 | // TODO this needs to be tidied up when adding TLS #47 12 | dialOpts := opts 13 | 14 | dialOpts = append(dialOpts, 15 | grpc.WithTransportCredentials(insecure.NewCredentials()), 16 | ) 17 | 18 | if basicAuthToken != "" { 19 | dialOpts = append(dialOpts, grpc.WithPerRPCCredentials( 20 | basic(basicAuthToken), 21 | )) 22 | } 23 | 24 | return grpc.Dial( 25 | address, 26 | dialOpts..., 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/flags/flags.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | 6 | "github.com/warehouse-13/hammertime/pkg/config" 7 | "github.com/warehouse-13/hammertime/pkg/defaults" 8 | ) 9 | 10 | // WithFlagsFunc can be used with CLIFlags to build a list of flags for a 11 | // command. 12 | type WithFlagsFunc func() []cli.Flag 13 | 14 | // CLIFlags takes a list of WithFlagsFunc options and returns a list of flags 15 | // for a command. 16 | func CLIFlags(options ...WithFlagsFunc) []cli.Flag { 17 | flags := []cli.Flag{} 18 | 19 | for _, group := range options { 20 | flags = append(flags, group()...) 21 | } 22 | 23 | return flags 24 | } 25 | 26 | // WithGRPCAddressFlag adds the flintlock GRPC address flag to the command. 27 | func WithGRPCAddressFlag() WithFlagsFunc { 28 | return func() []cli.Flag { 29 | return []cli.Flag{ 30 | &cli.StringFlag{ 31 | Name: "grpc-address", 32 | Value: defaults.DialTarget, 33 | Aliases: []string{"a"}, 34 | Usage: "flintlock server address + port", 35 | }, 36 | } 37 | } 38 | } 39 | 40 | // WithNameAndNamespaceFlags adds the name and namespace flags to the command. 41 | func WithNameAndNamespaceFlags(withDefaults bool) WithFlagsFunc { 42 | nameFlag := &cli.StringFlag{ 43 | Name: "name", 44 | Aliases: []string{"n"}, 45 | Usage: "microvm name", 46 | } 47 | namespaceFlag := &cli.StringFlag{ 48 | Name: "namespace", 49 | Aliases: []string{"ns"}, 50 | Usage: "microvm namespace", 51 | } 52 | 53 | if withDefaults { 54 | nameFlag.Value = defaults.MvmName 55 | namespaceFlag.Value = defaults.MvmNamespace 56 | } 57 | 58 | return func() []cli.Flag { 59 | return []cli.Flag{ 60 | nameFlag, 61 | namespaceFlag, 62 | } 63 | } 64 | } 65 | 66 | // WithJSONSpecFlag adds the json file flag to the command. 67 | func WithJSONSpecFlag() WithFlagsFunc { 68 | return func() []cli.Flag { 69 | return []cli.Flag{ 70 | &cli.StringFlag{ 71 | Name: "file", 72 | Aliases: []string{"f"}, 73 | Usage: "path to json file containing full flintlock spec. will override other flags", 74 | }, 75 | } 76 | } 77 | } 78 | 79 | // WithSSHKeyFlag adds the public-key-path flag to the command. 80 | func WithSSHKeyFlag() WithFlagsFunc { 81 | return func() []cli.Flag { 82 | return []cli.Flag{ 83 | &cli.StringFlag{ 84 | Name: "public-key-path", 85 | Aliases: []string{"k"}, 86 | Usage: "path to file containing public SSH key to be added to root user", 87 | }, 88 | } 89 | } 90 | } 91 | 92 | // WithIDFlag adds the id flag to the command. 93 | func WithIDFlag() WithFlagsFunc { 94 | return func() []cli.Flag { 95 | return []cli.Flag{ 96 | &cli.StringFlag{ 97 | Name: "id", 98 | Aliases: []string{"i"}, 99 | Usage: "microvm uuid", 100 | }, 101 | } 102 | } 103 | } 104 | 105 | // WithStateFlag adds the boolean state flag to the command. 106 | func WithStateFlag() WithFlagsFunc { 107 | return func() []cli.Flag { 108 | return []cli.Flag{ 109 | &cli.BoolFlag{ 110 | Name: "state", 111 | Value: false, 112 | Aliases: []string{"s"}, 113 | Usage: "print just the state of the microvm", 114 | }, 115 | } 116 | } 117 | } 118 | 119 | // WithAllFlag adds the boolean all flag to the command. 120 | func WithAllFlag() WithFlagsFunc { 121 | return func() []cli.Flag { 122 | return []cli.Flag{ 123 | &cli.BoolFlag{ 124 | Name: "all", 125 | Usage: "delete all microvms (filter with --name and --namespace)", 126 | }, 127 | } 128 | } 129 | } 130 | 131 | // WithQuietFlag adds a silent flag to the command. 132 | func WithQuietFlag() WithFlagsFunc { 133 | return func() []cli.Flag { 134 | return []cli.Flag{ 135 | &cli.BoolFlag{ 136 | Name: "quiet", 137 | Aliases: []string{"q"}, 138 | Usage: "silence the output on the command", 139 | }, 140 | } 141 | } 142 | } 143 | 144 | func WithBasicAuthFlag() WithFlagsFunc { 145 | return func() []cli.Flag { 146 | return []cli.Flag{ 147 | &cli.StringFlag{ 148 | Name: "token", 149 | Aliases: []string{"t"}, 150 | Usage: "provide a token if basic auth is set on the server", 151 | }, 152 | } 153 | } 154 | } 155 | 156 | // ParseFlags processes all flags on the CLI context and builds a config object 157 | // which will be used in the command's action. 158 | func ParseFlags(cfg *config.Config) cli.BeforeFunc { 159 | return func(ctx *cli.Context) error { 160 | cfg.GRPCAddress = ctx.String("grpc-address") 161 | cfg.Token = ctx.String("token") 162 | 163 | cfg.MvmName = ctx.String("name") 164 | cfg.MvmNamespace = ctx.String("namespace") 165 | 166 | cfg.JSONFile = ctx.String("file") 167 | cfg.SSHKeyPath = ctx.String("public-key-path") 168 | 169 | cfg.State = ctx.Bool("state") 170 | cfg.DeleteAll = ctx.Bool("all") 171 | cfg.Silent = ctx.Bool("quiet") 172 | 173 | cfg.UUID = ctx.String("id") 174 | 175 | return nil 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /pkg/microvm/data.go: -------------------------------------------------------------------------------- 1 | package microvm 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/weaveworks-liquidmetal/flintlock/client/cloudinit/instance" 9 | "github.com/weaveworks-liquidmetal/flintlock/client/cloudinit/userdata" 10 | "gopkg.in/yaml.v2" 11 | 12 | "github.com/warehouse-13/hammertime/pkg/utils" 13 | ) 14 | 15 | func CreateUserData(name, sshPath string) (string, error) { 16 | defaultUser := userdata.User{ 17 | Name: "root", 18 | } 19 | 20 | if utils.IsSet(sshPath) { 21 | sshKey, err := getKeyFromPath(sshPath) 22 | if err != nil { 23 | return "", err 24 | } 25 | 26 | defaultUser.SSHAuthorizedKeys = []string{ 27 | sshKey, 28 | } 29 | } 30 | 31 | // TODO: remove the boot command temporary fix after image-builder #6 32 | userData := &userdata.UserData{ 33 | HostName: name, 34 | Users: []userdata.User{ 35 | defaultUser, 36 | }, 37 | FinalMessage: "The Liquid Metal booted system is good to go after $UPTIME seconds", 38 | BootCommands: []string{ 39 | "ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf", 40 | }, 41 | } 42 | 43 | data, err := yaml.Marshal(userData) 44 | if err != nil { 45 | return "", fmt.Errorf("marshalling bootstrap data: %w", err) 46 | } 47 | 48 | dataWithHeader := append([]byte("#cloud-config\n"), data...) 49 | 50 | return base64.StdEncoding.EncodeToString(dataWithHeader), nil 51 | } 52 | 53 | func CreateMetadata(name, ns string) (string, error) { 54 | metadata := instance.New( 55 | instance.WithInstanceID(fmt.Sprintf("%s/%s", ns, name)), 56 | instance.WithLocalHostname(name), 57 | instance.WithPlatform("liquid_metal"), 58 | ) 59 | 60 | userMeta, err := yaml.Marshal(metadata) 61 | if err != nil { 62 | return "", fmt.Errorf("unable to marshal metadata: %w", err) 63 | } 64 | 65 | return base64.StdEncoding.EncodeToString(userMeta), nil 66 | } 67 | 68 | func getKeyFromPath(path string) (string, error) { 69 | key, err := os.ReadFile(path) 70 | if err != nil { 71 | return "", err 72 | } 73 | 74 | return string(key), nil 75 | } 76 | -------------------------------------------------------------------------------- /pkg/microvm/data_test.go: -------------------------------------------------------------------------------- 1 | package microvm_test 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "io/fs" 7 | "io/ioutil" 8 | "os" 9 | "testing" 10 | 11 | . "github.com/onsi/gomega" 12 | "github.com/warehouse-13/hammertime/pkg/microvm" 13 | "github.com/weaveworks-liquidmetal/flintlock/client/cloudinit/userdata" 14 | "gopkg.in/yaml.v2" 15 | ) 16 | 17 | func Test_CreateUserData(t *testing.T) { 18 | g := NewWithT(t) 19 | 20 | var ( 21 | testName = "foo" 22 | testPath = "" 23 | ) 24 | 25 | out, err := microvm.CreateUserData(testName, testPath) 26 | g.Expect(err).NotTo(HaveOccurred()) 27 | 28 | dat, err := base64.StdEncoding.DecodeString(out) 29 | g.Expect(err).NotTo(HaveOccurred()) 30 | generated := &userdata.UserData{} 31 | g.Expect(yaml.Unmarshal(dat, generated)).To(Succeed()) 32 | 33 | g.Expect(generated.HostName).To(Equal(testName)) 34 | g.Expect(generated.Users).To(HaveLen(1)) 35 | g.Expect(generated.Users[0].Name).To(Equal("root")) 36 | g.Expect(generated.Users[0].SSHAuthorizedKeys).To(HaveLen(0)) 37 | } 38 | 39 | func Test_CreateUserData_withSSHKey(t *testing.T) { 40 | g := NewWithT(t) 41 | 42 | var ( 43 | testName = "foo" 44 | testPath = "userdata_test" 45 | testKey = "this is a key" 46 | ) 47 | 48 | tempFile, err := ioutil.TempFile("", testPath) 49 | g.Expect(err).NotTo(HaveOccurred()) 50 | 51 | g.Expect(ioutil.WriteFile(tempFile.Name(), []byte(testKey), 0755)).To(Succeed()) 52 | 53 | t.Cleanup(func() { 54 | g.Expect(os.RemoveAll(tempFile.Name())).To(Succeed()) 55 | }) 56 | 57 | out, err := microvm.CreateUserData(testName, tempFile.Name()) 58 | g.Expect(err).NotTo(HaveOccurred()) 59 | 60 | dat, err := base64.StdEncoding.DecodeString(out) 61 | g.Expect(err).NotTo(HaveOccurred()) 62 | generated := &userdata.UserData{} 63 | g.Expect(yaml.Unmarshal(dat, generated)).To(Succeed()) 64 | 65 | g.Expect(generated.HostName).To(Equal(testName)) 66 | g.Expect(generated.Users).To(HaveLen(1)) 67 | g.Expect(generated.Users[0].Name).To(Equal("root")) 68 | g.Expect(generated.Users[0].SSHAuthorizedKeys).To(HaveLen(1)) 69 | g.Expect(generated.Users[0].SSHAuthorizedKeys[0]).To(Equal(testKey)) 70 | } 71 | 72 | func Test_CreateUserData_withSSHKey_readFileFails(t *testing.T) { 73 | g := NewWithT(t) 74 | 75 | var ( 76 | testName = "foo" 77 | testPath = "noexist" 78 | ) 79 | 80 | _, err := microvm.CreateUserData(testName, testPath) 81 | g.Expect(err).To(HaveOccurred()) 82 | g.Expect(err).To(BeAssignableToTypeOf(&fs.PathError{})) 83 | } 84 | 85 | func Test_CreateMetadata(t *testing.T) { 86 | g := NewWithT(t) 87 | 88 | var ( 89 | testName = "foo" 90 | testNamespace = "bar" 91 | ) 92 | 93 | out, err := microvm.CreateMetadata(testName, testNamespace) 94 | g.Expect(err).NotTo(HaveOccurred()) 95 | 96 | dat, err := base64.StdEncoding.DecodeString(out) 97 | g.Expect(err).NotTo(HaveOccurred()) 98 | generated := map[string]string{} 99 | g.Expect(yaml.Unmarshal(dat, generated)).To(Succeed()) 100 | 101 | g.Expect(generated).To(HaveKeyWithValue("instance_id", fmt.Sprintf("%s/%s", testNamespace, testName))) 102 | g.Expect(generated).To(HaveKeyWithValue("local_hostname", testName)) 103 | g.Expect(generated).To(HaveKeyWithValue("platform", "liquid_metal")) 104 | } 105 | -------------------------------------------------------------------------------- /pkg/utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/weaveworks-liquidmetal/flintlock/api/types" 9 | ) 10 | 11 | // ProcessFile will open the given file and process the JSON into a MicroVMSpec. 12 | func ProcessFile(file string) (string, string, string, error) { 13 | var uid, name, namespace string 14 | 15 | spec, err := LoadSpecFromFile(file) 16 | if err != nil { 17 | return "", "", "", err 18 | } 19 | 20 | if spec.Uid == nil && (!IsSet(spec.Id) && !IsSet(spec.Namespace)) { 21 | return "", "", "", fmt.Errorf("required: uuid or name/namespace") 22 | } 23 | 24 | if spec.Uid != nil { 25 | uid = *spec.Uid 26 | } 27 | 28 | name = spec.Id 29 | namespace = spec.Namespace 30 | 31 | return uid, name, namespace, nil 32 | } 33 | 34 | func LoadSpecFromFile(file string) (*types.MicroVMSpec, error) { 35 | dat, err := os.ReadFile(file) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | var spec *types.MicroVMSpec 41 | if err := json.Unmarshal(dat, &spec); err != nil { 42 | return nil, err 43 | } 44 | 45 | return spec, nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/utils/file_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "encoding/json" 5 | "io/fs" 6 | "io/ioutil" 7 | "os" 8 | "testing" 9 | 10 | . "github.com/onsi/gomega" 11 | "github.com/weaveworks-liquidmetal/flintlock/api/types" 12 | "k8s.io/utils/pointer" 13 | 14 | "github.com/warehouse-13/hammertime/pkg/utils" 15 | ) 16 | 17 | func Test_ProcessFile(t *testing.T) { 18 | g := NewWithT(t) 19 | 20 | tempFile, err := ioutil.TempFile("", "utils_test") 21 | g.Expect(err).NotTo(HaveOccurred()) 22 | 23 | t.Cleanup(func() { 24 | g.Expect(os.RemoveAll(tempFile.Name())).To(Succeed()) 25 | }) 26 | 27 | var ( 28 | testName = "foo" 29 | testNs = "bar" 30 | testUid = "baz" 31 | ) 32 | 33 | type testData struct { 34 | name string 35 | ns string 36 | uid *string 37 | } 38 | 39 | tt := []struct { 40 | test string 41 | filename string 42 | input testData 43 | expected func(*WithT, testData, testData, error) 44 | }{ 45 | { 46 | test: "when all values are set in the file, returns all", 47 | filename: tempFile.Name(), 48 | input: testData{ 49 | name: testName, 50 | ns: testNs, 51 | uid: &testUid, 52 | }, 53 | expected: func(g *WithT, in, out testData, err error) { 54 | g.Expect(err).NotTo(HaveOccurred()) 55 | g.Expect(out.name).To(Equal(in.name)) 56 | g.Expect(out.ns).To(Equal(in.ns)) 57 | g.Expect(out.uid).To(Equal(in.uid)) 58 | }, 59 | }, 60 | { 61 | test: "when just name and ns are set in the file, returns name and ns", 62 | filename: tempFile.Name(), 63 | input: testData{ 64 | name: testName, 65 | ns: testNs, 66 | uid: pointer.String(""), 67 | }, 68 | expected: func(g *WithT, in, out testData, err error) { 69 | g.Expect(err).NotTo(HaveOccurred()) 70 | g.Expect(out.name).To(Equal(in.name)) 71 | g.Expect(out.ns).To(Equal(in.ns)) 72 | g.Expect(out.uid).To(Equal(in.uid)) 73 | }, 74 | }, 75 | { 76 | test: "when no values are set in the file, returns an error", 77 | filename: tempFile.Name(), 78 | input: testData{}, 79 | expected: func(g *WithT, in, out testData, err error) { 80 | g.Expect(err).To(MatchError(ContainSubstring("required"))) 81 | }, 82 | }, 83 | { 84 | test: "when the file fails to load, returns an error", 85 | filename: "noexist", 86 | input: testData{}, 87 | expected: func(g *WithT, in, out testData, err error) { 88 | g.Expect(err).To(BeAssignableToTypeOf(&fs.PathError{})) 89 | }, 90 | }, 91 | } 92 | 93 | for _, tc := range tt { 94 | t.Run(tc.test, func(t *testing.T) { 95 | spec := &types.MicroVMSpec{ 96 | Id: tc.input.name, 97 | Namespace: tc.input.ns, 98 | Uid: tc.input.uid, 99 | } 100 | 101 | dat, err := json.Marshal(spec) 102 | g.Expect(err).NotTo(HaveOccurred()) 103 | 104 | g.Expect(ioutil.WriteFile(tempFile.Name(), dat, 0755)).To(Succeed()) 105 | 106 | outUid, outN, outNs, err := utils.ProcessFile(tc.filename) 107 | 108 | out := testData{outN, outNs, &outUid} 109 | 110 | tc.expected(g, tc.input, out, err) 111 | }) 112 | } 113 | } 114 | 115 | func Test_LoadSpecFromFile(t *testing.T) { 116 | g := NewWithT(t) 117 | 118 | tempFile, err := ioutil.TempFile("", "utils_test") 119 | g.Expect(err).NotTo(HaveOccurred()) 120 | 121 | t.Cleanup(func() { 122 | g.Expect(os.RemoveAll(tempFile.Name())).To(Succeed()) 123 | }) 124 | 125 | type testData struct { 126 | name string 127 | ns string 128 | uid *string 129 | } 130 | 131 | tt := []struct { 132 | test string 133 | filename string 134 | input string 135 | expected func(*WithT, *types.MicroVMSpec, error) 136 | }{ 137 | { 138 | test: "happy path", 139 | filename: tempFile.Name(), 140 | input: `{"name": "bar"}`, 141 | expected: func(g *WithT, out *types.MicroVMSpec, err error) { 142 | g.Expect(err).NotTo(HaveOccurred()) 143 | g.Expect(out).To(BeAssignableToTypeOf(&types.MicroVMSpec{})) 144 | }, 145 | }, 146 | { 147 | test: "if the file contains non-json data, returns an error", 148 | filename: tempFile.Name(), 149 | input: `foo`, 150 | expected: func(g *WithT, out *types.MicroVMSpec, err error) { 151 | g.Expect(err).To(HaveOccurred()) 152 | g.Expect(out).To(BeNil()) 153 | }, 154 | }, 155 | { 156 | test: "if the file cannot be loaded, returns an error", 157 | filename: "noexist", 158 | expected: func(g *WithT, out *types.MicroVMSpec, err error) { 159 | g.Expect(err).To(HaveOccurred()) 160 | g.Expect(out).To(BeNil()) 161 | }, 162 | }, 163 | } 164 | 165 | for _, tc := range tt { 166 | t.Run(tc.test, func(t *testing.T) { 167 | g.Expect(ioutil.WriteFile(tempFile.Name(), []byte(tc.input), 0755)).To(Succeed()) 168 | 169 | out, err := utils.LoadSpecFromFile(tc.filename) 170 | tc.expected(g, out, err) 171 | }) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /pkg/utils/print.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | type Writer struct { 11 | out io.Writer 12 | } 13 | 14 | // NewWriter returns a new Writer instance. If out is nil (which it can't really 15 | // be unless explicitly set so), it will default to os.Stdout. 16 | func NewWriter(out io.Writer) Writer { 17 | if out == nil { 18 | out = os.Stdout 19 | } 20 | 21 | return Writer{out: out} 22 | } 23 | 24 | // Print will write the given string to the Writer's out. 25 | func (w Writer) Print(output interface{}) { 26 | fmt.Fprintln(w.out, output) 27 | } 28 | 29 | // Printf will write the given string(s) to the Writer's out and apply given formatting. 30 | func (w Writer) Printf(format string, output ...interface{}) { 31 | fmt.Fprintf(w.out, format, output...) 32 | } 33 | 34 | // PrettyPrint will write the given object the out writer in nice JSON. 35 | func (w Writer) PrettyPrint(response interface{}) error { 36 | resJSON, err := json.MarshalIndent(response, "", " ") 37 | if err != nil { 38 | return err 39 | } 40 | 41 | fmt.Fprintf(w.out, "%s\n", string(resJSON)) 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /pkg/utils/string.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // IsSet returns true if the value of flag is not empty. 4 | func IsSet(flag string) bool { 5 | return flag != "" 6 | } 7 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // PackageName is the name of the package, all commands fall under this name. 4 | const PackageName = "hammertime" 5 | 6 | var ( 7 | Version = "undefined" // Specifies the cli version 8 | BuildDate = "undefined" // Specifies the build date 9 | CommitHash = "undefined" // Specifies the git commit hash 10 | ) 11 | -------------------------------------------------------------------------------- /test/integration/README.md: -------------------------------------------------------------------------------- 1 | ## Integration tests 2 | 3 | The integration tests are very high level. 4 | They only test the core user stories. 5 | For more granular tests for every config and flag variation, add a unit test. 6 | 7 | ### How to run 8 | 9 | ``` 10 | make int 11 | ``` 12 | 13 | To run against a real flintlock instance, start your flintlockd server then run: 14 | 15 | ``` 16 | TEST_SERVER="192.168.0.31:9091" make int 17 | ``` 18 | -------------------------------------------------------------------------------- /test/integration/integration_suite_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | "github.com/onsi/gomega/gbytes" 12 | "github.com/onsi/gomega/gexec" 13 | "github.com/warehouse-13/safety" 14 | ) 15 | 16 | var ( 17 | cliBin string 18 | address string 19 | token = "secret" 20 | ) 21 | 22 | func TestIntegration(t *testing.T) { 23 | RegisterFailHandler(Fail) 24 | var fakeserver *safety.FakeServer 25 | 26 | BeforeSuite(func() { 27 | var err error 28 | cliBin, err = gexec.Build("github.com/warehouse-13/hammertime") 29 | Expect(err).NotTo(HaveOccurred()) 30 | Expect(os.Setenv("AUTH_TOKEN", token)).To(Succeed()) 31 | 32 | if remote_test_server := os.Getenv("TEST_SERVER"); remote_test_server != "" { 33 | address = remote_test_server 34 | fmt.Fprintf(GinkgoWriter, "Using real Flintlock server at %s: tests may take a little longer", address) 35 | } else { 36 | fakeserver = safety.New() 37 | address = fakeserver.Start("") 38 | } 39 | 40 | // Sometimes the server doesn't start immediately, so we check that an 41 | // endpoint is reachable before we carry on with the test. 42 | getCmd := command{action: "get", args: []string{"--id", "foobar"}} 43 | getSession := executeCommand(getCmd) 44 | Eventually(getSession, "10s").Should(gexec.Exit(1)) 45 | Eventually(getSession.Err).Should(gbytes.Say("rpc error")) 46 | }) 47 | 48 | AfterSuite(func() { 49 | if fakeserver != nil { 50 | Expect(fakeserver.Stop()).To(Succeed()) 51 | } 52 | 53 | gexec.CleanupBuildArtifacts() 54 | }) 55 | 56 | RunSpecs(t, "Integration Suite") 57 | } 58 | 59 | type command struct { 60 | action string 61 | args []string 62 | } 63 | 64 | func executeCommand(command command) *gexec.Session { 65 | var args = []string{command.action, "--grpc-address", address, "--token", token} 66 | cmd := exec.Command(cliBin, append(args, command.args...)...) 67 | session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 68 | Expect(err).NotTo(HaveOccurred()) 69 | 70 | return session 71 | } 72 | -------------------------------------------------------------------------------- /test/integration/integration_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "encoding/json" 5 | "os/exec" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | "github.com/onsi/gomega/gbytes" 10 | "github.com/onsi/gomega/gexec" 11 | "github.com/weaveworks-liquidmetal/flintlock/api/services/microvm/v1alpha1" 12 | "github.com/weaveworks-liquidmetal/flintlock/api/types" 13 | ) 14 | 15 | const ( 16 | timeout = "30s" 17 | interval = "5s" 18 | ) 19 | 20 | var _ = Describe("Integration", func() { 21 | var ( 22 | defaultName = "mvm0" 23 | defaultNamespace = "ns0" 24 | 25 | createSession *gexec.Session 26 | created1 v1alpha1.CreateMicroVMResponse 27 | ) 28 | 29 | BeforeEach(func() { 30 | createSession = create() 31 | Eventually(createSession, timeout, interval).Should(gexec.Exit(0)) 32 | Expect(json.Unmarshal(createSession.Out.Contents(), &created1)).To(Succeed()) 33 | }) 34 | 35 | AfterEach(func() { 36 | _ = delete("--all") 37 | }) 38 | 39 | It("creating a MicroVM", func() { 40 | Expect(created1.Microvm.Spec.Id).To(Equal(defaultName)) 41 | Expect(created1.Microvm.Spec.Namespace).To(Equal(defaultNamespace)) 42 | }) 43 | 44 | It("getting a MicroVM", func() { 45 | session := get("--id", *created1.Microvm.Spec.Uid) 46 | Eventually(session, timeout, interval).Should(gexec.Exit(0)) 47 | 48 | var getResult types.MicroVM 49 | Expect(json.Unmarshal(session.Out.Contents(), &getResult)).To(Succeed()) 50 | Expect(getResult.Spec.Id).To(Equal(created1.Microvm.Spec.Id)) 51 | }) 52 | 53 | It("listing MicroVMs", func() { 54 | session := list("--namespace", defaultNamespace, "--name", defaultName) 55 | Eventually(session, timeout, interval).Should(gexec.Exit(0)) 56 | 57 | var list1 v1alpha1.ListMicroVMsResponse 58 | Expect(json.Unmarshal(session.Out.Contents(), &list1)).To(Succeed()) 59 | Expect(list1.Microvm).To(HaveLen(1)) 60 | }) 61 | 62 | It("deleting a MicroVM", func() { 63 | Eventually(func(g Gomega) { 64 | session := delete("--id", *created1.Microvm.Spec.Uid) 65 | g.Expect(session.Wait()).To(gexec.Exit(0)) 66 | }, timeout, interval).Should(Succeed()) 67 | 68 | Eventually(func(g Gomega) { 69 | session := get("--id", *created1.Microvm.Spec.Uid) 70 | g.Expect(session.Wait()).To(gexec.Exit(1)) 71 | g.Expect(session.Err).To(gbytes.Say("rpc error")) 72 | }, timeout, interval).Should(Succeed()) 73 | 74 | Expect(listAll()).To(Equal(0)) 75 | }) 76 | }) 77 | 78 | func create(opts ...string) *gexec.Session { 79 | args := []string{"create", "--grpc-address", address, "--token", token} 80 | args = append(args, opts...) 81 | 82 | return runCmd(exec.Command(cliBin, args...)) 83 | } 84 | 85 | func get(opts ...string) *gexec.Session { 86 | args := []string{"get", "--grpc-address", address, "--token", token} 87 | args = append(args, opts...) 88 | 89 | return runCmd(exec.Command(cliBin, args...)) 90 | } 91 | 92 | func list(opts ...string) *gexec.Session { 93 | args := []string{"list", "--grpc-address", address, "--token", token} 94 | args = append(args, opts...) 95 | 96 | return runCmd(exec.Command(cliBin, args...)) 97 | } 98 | 99 | func delete(opts ...string) *gexec.Session { 100 | args := []string{"delete", "--grpc-address", address, "--token", token} 101 | args = append(args, opts...) 102 | 103 | return runCmd(exec.Command(cliBin, args...)) 104 | } 105 | 106 | func runCmd(cmd *exec.Cmd) *gexec.Session { 107 | session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) 108 | Expect(err).NotTo(HaveOccurred()) 109 | 110 | return session 111 | } 112 | 113 | func listAll() int { 114 | session := list() 115 | Eventually(session, timeout, interval).Should(gexec.Exit(0)) 116 | 117 | var list v1alpha1.ListMicroVMsResponse 118 | Expect(json.Unmarshal(session.Out.Contents(), &list)).To(Succeed()) 119 | return len(list.Microvm) 120 | } 121 | --------------------------------------------------------------------------------