├── .github └── workflows │ ├── go.yaml │ └── goreleaser.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── cmd ├── db.go ├── db │ ├── create.go │ ├── create_test.go │ ├── delete.go │ ├── delete_test.go │ ├── get.go │ ├── get_test.go │ ├── list.go │ ├── list_test.go │ ├── park.go │ ├── park_test.go │ ├── resize.go │ ├── resize_test.go │ ├── secBundle.go │ ├── secBundle_test.go │ ├── tiers.go │ ├── tiers_test.go │ ├── unpark.go │ └── unpark_test.go ├── db_test.go ├── login.go ├── login_test.go ├── root.go └── root_test.go ├── go.mod ├── go.sum ├── main.go ├── pkg ├── authenticated_client.go ├── conf.go ├── conf_test.go ├── const.go ├── env │ └── env.go ├── errors.go ├── errors_test.go ├── httputils │ ├── client.go │ └── client_test.go ├── login.go ├── login_test.go ├── strfmt.go ├── strfmt_test.go ├── testdata │ ├── empty.json │ ├── missing-id.json │ ├── missing-name.json │ ├── missing-secret.json │ ├── sa.json │ ├── with_empty_token │ │ └── .config │ │ │ └── astra │ │ │ └── prod_token │ ├── with_invalid_sa │ │ └── .config │ │ │ └── astra │ │ │ └── prod_sa.json │ ├── with_invalid_token │ │ └── .config │ │ │ └── astra │ │ │ └── prod_token │ └── with_token │ │ └── .config │ │ └── astra │ │ └── prod_token └── tests │ ├── client.go │ └── client_test.go ├── script ├── all ├── bootstrap ├── build ├── cibuild ├── clean ├── cover-html ├── docker ├── install-astra.sh ├── lint ├── package ├── setup ├── test └── update └── snapcraft.yaml /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | # Trigger the workflow on push or pull request, 3 | # but only for the main branch 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | jobs: 11 | ci: 12 | runs-on: ubuntu-18.04 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-go@v2 16 | with: 17 | stable: 'false' 18 | go-version: '1.17' 19 | - name: ci 20 | run: ./script/cibuild 21 | coverage: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Install Go 25 | if: success() 26 | uses: actions/setup-go@v2 27 | with: 28 | go-version: '1.17' 29 | - name: Checkout code 30 | uses: actions/checkout@v2 31 | - name: Install dependencies 32 | run: | 33 | go mod download 34 | - name: Run Unit tests 35 | run: | 36 | go test -race -covermode atomic -coverprofile=covprofile ./... 37 | - name: Install goveralls 38 | env: 39 | GO111MODULE: off 40 | run: go get github.com/mattn/goveralls 41 | - name: Send coverage 42 | env: 43 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | run: goveralls -coverprofile=covprofile -service=github 45 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | goreleaser: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - 15 | name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | - 20 | name: Set up Go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: 1.17 24 | - 25 | name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v2 27 | with: 28 | # either 'goreleaser' (default) or 'goreleaser-pro' 29 | distribution: goreleaser 30 | version: latest 31 | args: release --rm-dist 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Go 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | bin/ 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | .DS_Store 19 | 20 | dist/ 21 | 22 | .vagrant 23 | 24 | *.swp 25 | Vagrantfile 26 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | issues: 2 | exclude: 3 | SA1019 4 | linters: 5 | disable-all: true 6 | enable: 7 | - bodyclose 8 | - deadcode 9 | - depguard 10 | - dogsled 11 | - dupl 12 | - errcheck 13 | - exhaustive 14 | - funlen 15 | - goconst 16 | - gocritic 17 | - gocyclo 18 | - gofmt 19 | - goimports 20 | - revive 21 | - gomnd 22 | - goprintffuncname 23 | - gosimple 24 | - govet 25 | - ineffassign 26 | - misspell 27 | - nakedret 28 | - nolintlint 29 | - rowserrcheck 30 | - exportloopref 31 | - staticcheck 32 | - structcheck 33 | - typecheck 34 | - unconvert 35 | - unparam 36 | - unused 37 | - varcheck 38 | - whitespace 39 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | builds: 8 | - id: astra 9 | binary: astra 10 | goos: 11 | - windows 12 | - linux 13 | goarch: 14 | - amd64 15 | - arm64 16 | - id: astra-osx-amd 17 | binary: astra 18 | goos: 19 | - darwin 20 | goarch: 21 | - amd64 22 | - id: astra-osx-arm 23 | binary: astra 24 | goos: 25 | - darwin 26 | goarch: 27 | - arm64 28 | changelog: 29 | sort: asc 30 | filters: 31 | exclude: 32 | - '^docs:' 33 | - '^test:' 34 | release: 35 | draft: true 36 | extra_files: 37 | - glob: ./dist/*.zip 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # astra-cli 2 | 3 | Apache 2.0 licensed Astra Cloud Management CLI 4 | 5 | [![.github/workflows/go.yaml](https://github.com/datastax-labs/astra-cli/actions/workflows/go.yaml/badge.svg)](https://github.com/datastax-labs/astra-cli/actions/workflows/go.yaml) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/datastax-labs/astra-cli)](https://goreportcard.com/report/github.com/datastax-labs/astra-cli) 7 | [![GitHub go.mod Go version of a Go module](https://img.shields.io/github/go-mod/go-version/datastax-labs/astra-cli)](https://img.shields.io/github/go-mod/go-version/datastax-labs/astra-cli) 8 | [![Latest Version](https://img.shields.io/github/v/release/datastax-labs/astra-cli?include_prereleases)](https://github.com/datastax-labs/astra-cli/releases) 9 | [![Coverage Status](https://coveralls.io/repos/github/datastax-labs/astra-cli/badge.svg)](https://coveralls.io/github/datastax-labs/astra-cli) 10 | 11 | ## status 12 | 13 | Ready for production 14 | 15 | ## How to install - install script 16 | 17 | * /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/datastax-labs/astra-cli/main/script/install-astra.sh)" 18 | * astra login 19 | 20 | ## How to install - Release Binaries 21 | 22 | * download a [release](https://github.com/datastax-labs/astra-cli/releases) 23 | * tar zxvf 24 | * cd 25 | * ./astra 26 | 27 | ## How to install - From Source 28 | 29 | * Install [Go 1.17](https://golang.org/dl/) 30 | * run `git clone git@github.com:datastax-labs/astra-cli.git` 31 | * run `./scripts/build` or `go build -o ./bin/astra .` 32 | 33 | ## How to use 34 | 35 | * login 36 | * execute commands on your database 37 | 38 | ### login with token 39 | 40 | After creating a token with rights to use the devops api 41 | 42 | ``` 43 | astra login --token "changed" 44 | Login information saved 45 | ``` 46 | ### login service account 47 | 48 | After creating a service account on the Astra page 49 | 50 | ``` 51 | astra login --id "changed" --name "changed" --secret "changed" 52 | Login information saved 53 | ``` 54 | 55 | ## login service account with json 56 | 57 | ``` 58 | astra login --json '{"clientId":"changed","clientName":"change@me.com","clientSecret":"changed"}' 59 | Login information saved 60 | 61 | ``` 62 | 63 | ### creating database 64 | 65 | ``` 66 | astra db create -v --keyspace myks --name mydb 67 | ............ 68 | database 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b created 69 | ``` 70 | 71 | ### get secure connection bundle 72 | 73 | ``` 74 | astra db secBundle 3c577e51-4ff5-4551-86a4-41d475c61822 -d external -l external.zip 75 | file external.zip saved 12072 bytes written 76 | astra db secBundle 3c577e51-4ff5-4551-86a4-41d475c61822 -d internal -l internal.zip 77 | file internal.zip saved 12066 bytes written 78 | astra db secBundle 3c577e51-4ff5-4551-86a4-41d475c61822 -d proxy-internal -l proxy-internal.zip 79 | file proxy-internal.zip saved 348 bytes written 80 | astra db secBundle 3c577e51-4ff5-4551-86a4-41d475c61822 -d proxy-external -l proxy-external.zip 81 | file proxy-external.zip saved 339 bytes written 82 | ``` 83 | 84 | ### get secure connection bundle URLs 85 | 86 | ``` 87 | astra db secBundle 3c577e51-4ff5-4551-86a4-41d475c61822 -o list 88 | external bundle: changed 89 | internal bundle: changed 90 | external proxy: changed 91 | internal proxy: changed 92 | ``` 93 | 94 | ### listing databases 95 | 96 | ``` 97 | astra db list 98 | name id status 99 | mydb 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b ACTIVE 100 | ``` 101 | 102 | ### listing databases in json 103 | 104 | ``` 105 | astra db list -o json 106 | [ 107 | { 108 | "id": "2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b", 109 | "orgId": "changed", 110 | "ownerId": "changed", 111 | "info": { 112 | "name": "mydb", 113 | "keyspace": "myks", 114 | "cloudProvider": "GCP", 115 | "tier": "developer", 116 | "capacityUnits": 1, 117 | "region": "us-east1", 118 | "user": "dbuser", 119 | "password": "", 120 | "additionalKeyspaces": null, 121 | "cost": null 122 | }, 123 | "creationTime": "2021-02-24T17:23:19Z", 124 | "terminationTime": "0001-01-01T00:00:00Z", 125 | "status": "ACTIVE", 126 | "storage": { 127 | "nodeCount": 1, 128 | "replicationFactor": 1, 129 | "totalStorage": 5, 130 | "usedStorage": 0 131 | }, 132 | "availableActions": [ 133 | "park", 134 | "getCreds", 135 | "resetPassword", 136 | "terminate", 137 | "addKeyspace", 138 | "removeKeyspace", 139 | "addTable" 140 | ], 141 | "message": "", 142 | "studioUrl": "https://2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b-us-east1.studio.astra.datastax.com", 143 | "grafanaUrl": "https://2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b-us-east1.dashboard.astra.datastax.com/d/cloud/dse-cluster-condensed?refresh=30s\u0026orgId=1\u0026kiosk=tv", 144 | "cqlshUrl": "https://2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b-us-east1.apps.astra.datastax.com/cqlsh", 145 | "graphUrl": "", 146 | "dataEndpointUrl": "https://2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b-us-east1.apps.astra.datastax.com/api/rest" 147 | } 148 | ] 149 | ``` 150 | ### getting database by id 151 | 152 | ``` 153 | astra db get 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b 154 | name id status 155 | mydb 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b ACTIVE 156 | ``` 157 | 158 | ### getting database by id in json 159 | 160 | ``` 161 | astra db get 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b -o json 162 | { 163 | "id": "2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b", 164 | "orgId": "changed", 165 | "ownerId": "changed", 166 | "info": { 167 | "name": "mydb", 168 | "keyspace": "myks", 169 | "cloudProvider": "GCP", 170 | "tier": "developer", 171 | "capacityUnits": 1, 172 | "region": "us-east1", 173 | "user": "dbuser", 174 | "password": "", 175 | "additionalKeyspaces": null, 176 | "cost": null 177 | }, 178 | "creationTime": "2021-02-24T17:23:19Z", 179 | "terminationTime": "0001-01-01T00:00:00Z", 180 | "status": "ACTIVE", 181 | "storage": { 182 | "nodeCount": 1, 183 | "replicationFactor": 1, 184 | "totalStorage": 5, 185 | "usedStorage": 0 186 | }, 187 | "availableActions": [ 188 | "park", 189 | "getCreds", 190 | "resetPassword", 191 | "terminate", 192 | "addKeyspace", 193 | "removeKeyspace", 194 | "addTable" 195 | ], 196 | "message": "", 197 | "studioUrl": "https://2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b-us-east1.studio.astra.datastax.com", 198 | "grafanaUrl": "https://2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b-us-east1.dashboard.astra.datastax.com/d/cloud/dse-cluster-condensed?refresh=30s\u0026orgId=1\u0026kiosk=tv", 199 | "cqlshUrl": "https://2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b-us-east1.apps.astra.datastax.com/cqlsh", 200 | "graphUrl": "", 201 | "dataEndpointUrl": "https://2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b-us-east1.apps.astra.datastax.com/api/rest" 202 | } 203 | ``` 204 | 205 | 206 | ### parking database 207 | 208 | NOTE: Does not work on serverless 209 | 210 | ``` 211 | astra db park -v 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b 212 | starting to park database 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b 213 | ........... 214 | database 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b parked 215 | ``` 216 | 217 | ### unparking database 218 | 219 | NOTE: Does not work on serverless 220 | 221 | ``` 222 | astra db unpark -v 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b 223 | starting to unpark database 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b 224 | ........... 225 | database 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b unparked 226 | ``` 227 | 228 | ### deleting database 229 | 230 | ``` 231 | astra db delete -v 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b 232 | starting to delete database 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b 233 | database 2c3bc0d6-5e3e-4d77-81c8-d95a35bdc58b deleted 234 | ``` 235 | 236 | ### resizing 237 | 238 | I did not have a paid account to verify this works, but you can see it succesfully starts the process 239 | 240 | ``` 241 | astra db resize -v 72c4d35b-1875-495a-b5f1-97329d90b6c5 2 242 | unable to unpark '72c4d35b-1875-495a-b5f1-97329d90b6c5' with error expected status code 2xx but had: 400 error was [map[ID:2.000009e+06 message:resizing is not supported for this database tier]] 243 | ``` 244 | 245 | 246 | -------------------------------------------------------------------------------- /cmd/db.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package cmd contains all fo the commands for the cli 16 | package cmd 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/datastax-labs/astra-cli/cmd/db" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | func init() { 27 | dbCmd.AddCommand(db.CreateCmd) 28 | dbCmd.AddCommand(db.DeleteCmd) 29 | dbCmd.AddCommand(db.ParkCmd) 30 | dbCmd.AddCommand(db.UnparkCmd) 31 | dbCmd.AddCommand(db.ResizeCmd) 32 | dbCmd.AddCommand(db.GetCmd) 33 | dbCmd.AddCommand(db.ListCmd) 34 | dbCmd.AddCommand(db.TiersCmd) 35 | dbCmd.AddCommand(db.SecBundleCmd) 36 | } 37 | 38 | var dbCmd = &cobra.Command{ 39 | Use: "db", 40 | Short: "Shows all the db commands", 41 | Long: `Shows all other db commands. Create, Delete, Get information on your databases`, 42 | Run: func(cobraCmd *cobra.Command, args []string) { 43 | if err := executeDB(cobraCmd.Usage); err != nil { 44 | os.Exit(1) 45 | } 46 | }, 47 | } 48 | 49 | func executeDB(usage func() error) error { 50 | if err := usage(); err != nil { 51 | return fmt.Errorf("warn unable to show usage %v", err) 52 | } 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /cmd/db/create.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db is where the Astra DB commands are 16 | package db 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/datastax-labs/astra-cli/pkg" 23 | astraops "github.com/datastax/astra-client-go/v2/astra" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | var createDbName string 28 | var createDbKeyspace string 29 | var createDbRegion string 30 | var createDbTier string 31 | var createDbCloudProvider string 32 | 33 | func init() { 34 | CreateCmd.Flags().StringVarP(&createDbName, "name", "n", "", "name to give to the Astra Database") 35 | CreateCmd.Flags().StringVarP(&createDbKeyspace, "keyspace", "k", "", "keyspace user to give to the Astra Database") 36 | CreateCmd.Flags().StringVarP(&createDbRegion, "region", "r", "us-east1", "region to give to the Astra Database") 37 | CreateCmd.Flags().StringVarP(&createDbTier, "tier", "t", "serverless", "tier to give to the Astra Database") 38 | CreateCmd.Flags().StringVarP(&createDbCloudProvider, "cloudProvider", "l", "GCP", "cloud provider flag to give to the Astra Database") 39 | } 40 | 41 | // CreateCmd creates a database in Astra 42 | var CreateCmd = &cobra.Command{ 43 | Use: "create", 44 | Short: "creates a database by id", 45 | Long: `creates a database by id`, 46 | Run: func(cobraCmd *cobra.Command, args []string) { 47 | creds := &pkg.Creds{} 48 | err := executeCreate(creds.Login) 49 | if err != nil { 50 | fmt.Fprintln(os.Stderr, err) 51 | os.Exit(1) 52 | } 53 | }, 54 | } 55 | 56 | func executeCreate(makeClient func() (pkg.Client, error)) error { 57 | client, err := makeClient() 58 | if err != nil { 59 | return fmt.Errorf("unable to login with error %v", err) 60 | } 61 | createDb := astraops.DatabaseInfoCreate{ 62 | Name: createDbName, 63 | Keyspace: createDbKeyspace, 64 | CapacityUnits: 1, // we only support 1 CU on initial creation as of Feb 14 2022 65 | Region: createDbRegion, 66 | Tier: astraops.Tier(createDbTier), 67 | CloudProvider: astraops.CloudProvider(createDbCloudProvider), 68 | } 69 | db, err := client.CreateDb(createDb) 70 | if err != nil { 71 | return fmt.Errorf("unable to create '%v' with error %v", createDb, err) 72 | } 73 | fmt.Printf("database %v created\n", db.Id) 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /cmd/db/create_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db is where the Astra DB commands are 16 | package db 17 | 18 | import ( 19 | "fmt" 20 | "testing" 21 | 22 | "github.com/datastax-labs/astra-cli/pkg" 23 | tests "github.com/datastax-labs/astra-cli/pkg/tests" 24 | astraops "github.com/datastax/astra-client-go/v2/astra" 25 | ) 26 | 27 | func TestCreateGetsId(t *testing.T) { 28 | expectedID := "createID1234" 29 | // setting package variables by hand, there be dragons 30 | mockClient := &tests.MockClient{ 31 | Databases: []astraops.Database{ 32 | { 33 | Id: expectedID, 34 | }, 35 | }, 36 | } 37 | err := executeCreate(func() (pkg.Client, error) { 38 | return mockClient, nil 39 | }) 40 | if err != nil { 41 | t.Fatalf("unexpected error '%v'", err) 42 | } 43 | 44 | if len(mockClient.Calls()) != 1 { 45 | t.Fatalf("expected 1 call but was %v", len(mockClient.Calls())) 46 | } 47 | } 48 | func TestCreateLoginFails(t *testing.T) { 49 | // setting package variables by hand, there be dragons 50 | mockClient := &tests.MockClient{} 51 | err := executeCreate(func() (pkg.Client, error) { 52 | return mockClient, fmt.Errorf("service down") 53 | }) 54 | if err == nil { 55 | t.Fatal("expected error") 56 | } 57 | 58 | expected := "unable to login with error service down" 59 | if err.Error() != expected { 60 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 61 | } 62 | if len(mockClient.Calls()) != 0 { 63 | t.Fatalf("expected 0 call but was %v", len(mockClient.Calls())) 64 | } 65 | } 66 | 67 | func TestCreateFails(t *testing.T) { 68 | // setting package variables by hand, there be dragons 69 | mockClient := &tests.MockClient{ 70 | ErrorQueue: []error{fmt.Errorf("service down")}, 71 | } 72 | err := executeCreate(func() (pkg.Client, error) { 73 | return mockClient, nil 74 | }) 75 | if err == nil { 76 | t.Fatal("expected error") 77 | } 78 | 79 | if len(mockClient.Calls()) != 1 { 80 | t.Fatalf("expected 1 call but was %v", len(mockClient.Calls())) 81 | } 82 | } 83 | 84 | func TestCreateSetsName(t *testing.T) { 85 | mockClient := &tests.MockClient{} 86 | createDbName = "mydb" 87 | err := executeCreate(func() (pkg.Client, error) { 88 | return mockClient, nil 89 | }) 90 | if err != nil { 91 | t.Fatalf("unexpected error '%v'", err) 92 | } 93 | arg0 := mockClient.Call(0).(astraops.DatabaseInfoCreate) 94 | if arg0.Name != createDbName { 95 | t.Errorf("expected '%v' but was '%v'", arg0.Name, createDbName) 96 | } 97 | } 98 | 99 | func TestCreateSetsKeyspace(t *testing.T) { 100 | mockClient := &tests.MockClient{} 101 | createDbKeyspace = "myKeyspace" 102 | err := executeCreate(func() (pkg.Client, error) { 103 | return mockClient, nil 104 | }) 105 | if err != nil { 106 | t.Fatalf("unexpected error '%v'", err) 107 | } 108 | arg0 := mockClient.Call(0).(astraops.DatabaseInfoCreate) 109 | if arg0.Keyspace != createDbKeyspace { 110 | t.Errorf("expected '%v' but was '%v'", arg0.Keyspace, createDbKeyspace) 111 | } 112 | } 113 | 114 | func TestCreateSetsRegion(t *testing.T) { 115 | mockClient := &tests.MockClient{} 116 | createDbRegion = "EU-West1" 117 | err := executeCreate(func() (pkg.Client, error) { 118 | return mockClient, nil 119 | }) 120 | if err != nil { 121 | t.Fatalf("unexpected error '%v'", err) 122 | } 123 | arg0 := mockClient.Call(0).(astraops.DatabaseInfoCreate) 124 | if arg0.Region != createDbRegion { 125 | t.Errorf("expected '%v' but was '%v'", arg0.Region, createDbRegion) 126 | } 127 | } 128 | 129 | func TestCreateSetsTier(t *testing.T) { 130 | mockClient := &tests.MockClient{} 131 | createDbTier = "afdfdf" 132 | err := executeCreate(func() (pkg.Client, error) { 133 | return mockClient, nil 134 | }) 135 | if err != nil { 136 | t.Fatalf("unexpected error '%v'", err) 137 | } 138 | arg0 := mockClient.Call(0).(astraops.DatabaseInfoCreate) 139 | if arg0.Tier != astraops.Tier(createDbTier) { 140 | t.Errorf("expected '%v' but was '%v'", arg0.Tier, createDbTier) 141 | } 142 | } 143 | 144 | func TestCreateSetsProvider(t *testing.T) { 145 | mockClient := &tests.MockClient{} 146 | createDbCloudProvider = "ryanscloud" 147 | err := executeCreate(func() (pkg.Client, error) { 148 | return mockClient, nil 149 | }) 150 | if err != nil { 151 | t.Fatalf("unexpected error '%v'", err) 152 | } 153 | arg0 := mockClient.Call(0).(astraops.DatabaseInfoCreate) 154 | if arg0.CloudProvider != astraops.CloudProvider(createDbCloudProvider) { 155 | t.Errorf("expected '%v' but was '%v'", arg0.CloudProvider, createDbCloudProvider) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /cmd/db/delete.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db provides the sub-commands for the db command 16 | package db 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/datastax-labs/astra-cli/pkg" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | // DeleteCmd provides the delete database command 27 | var DeleteCmd = &cobra.Command{ 28 | Use: "delete ", 29 | Short: "delete database by databaseID", 30 | Long: `deletes a database from your Astra account by ID`, 31 | Args: cobra.ExactArgs(1), 32 | Run: func(cmd *cobra.Command, args []string) { 33 | creds := &pkg.Creds{} 34 | msg, err := executeDelete(args, creds.Login) 35 | if err != nil { 36 | fmt.Fprintln(os.Stderr, err) 37 | os.Exit(1) 38 | } 39 | fmt.Fprintln(os.Stdout, msg) 40 | }, 41 | } 42 | 43 | func executeDelete(args []string, makeClient func() (pkg.Client, error)) (string, error) { 44 | client, err := makeClient() 45 | if err != nil { 46 | return "", fmt.Errorf("unable to login with error '%v'", err) 47 | } 48 | id := args[0] 49 | fmt.Printf("starting to delete database %v\n", id) 50 | if err := client.Terminate(id, false); err != nil { 51 | return "", fmt.Errorf("unable to delete '%s' with error %v", id, err) 52 | } 53 | return fmt.Sprintf("database %v deleted", id), nil 54 | } 55 | -------------------------------------------------------------------------------- /cmd/db/delete_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db is where the Astra DB commands are 16 | package db 17 | 18 | import ( 19 | "fmt" 20 | "testing" 21 | 22 | "github.com/datastax-labs/astra-cli/pkg" 23 | tests "github.com/datastax-labs/astra-cli/pkg/tests" 24 | ) 25 | 26 | func TestDelete(t *testing.T) { 27 | mockClient := &tests.MockClient{} 28 | id := "123" 29 | msg, err := executeDelete([]string{id}, func() (pkg.Client, error) { 30 | return mockClient, nil 31 | }) 32 | if err != nil { 33 | t.Fatalf("unexpected error '%v'", err) 34 | } 35 | if len(mockClient.Calls()) != 1 { 36 | t.Fatalf("expected 1 call but was %v", len(mockClient.Calls())) 37 | } 38 | if id != mockClient.Call(0) { 39 | t.Errorf("expected '%v' but was '%v'", id, mockClient.Call(0)) 40 | } 41 | expected := "database 123 deleted" 42 | if expected != msg { 43 | t.Errorf("expected '%v' but was '%v'", expected, msg) 44 | } 45 | } 46 | 47 | func TestDeleteLoginError(t *testing.T) { 48 | mockClient := &tests.MockClient{} 49 | id := "123" 50 | msg, err := executeDelete([]string{id}, func() (pkg.Client, error) { 51 | return mockClient, fmt.Errorf("unable to login") 52 | }) 53 | if err == nil { 54 | t.Fatalf("should be returning an error and is not") 55 | } 56 | expectedError := "unable to login with error 'unable to login'" 57 | if err.Error() != expectedError { 58 | t.Errorf("expected '%v' but was '%v'", expectedError, err) 59 | } 60 | if len(mockClient.Calls()) != 0 { 61 | t.Fatalf("expected no calls but was %v", len(mockClient.Calls())) 62 | } 63 | 64 | if "" != msg { 65 | t.Errorf("expected empty but was '%v'", msg) 66 | } 67 | } 68 | 69 | func TestDeleteError(t *testing.T) { 70 | mockClient := &tests.MockClient{ 71 | ErrorQueue: []error{fmt.Errorf("timeout error")}, 72 | } 73 | id := "123" 74 | msg, err := executeDelete([]string{id}, func() (pkg.Client, error) { 75 | return mockClient, nil 76 | }) 77 | if err == nil { 78 | t.Fatal("expected error but none came") 79 | } 80 | if len(mockClient.Calls()) != 1 { 81 | t.Fatalf("expected 1 call but was %v", len(mockClient.Calls())) 82 | } 83 | if id != mockClient.Call(0) { 84 | t.Errorf("expected '%v' but was '%v'", id, mockClient.Call(0)) 85 | } 86 | expected := "" 87 | if expected != msg { 88 | t.Errorf("expected '%v' but was '%v'", expected, msg) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cmd/db/get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db provides the sub-commands for the db command 16 | package db 17 | 18 | import ( 19 | "bytes" 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | 24 | "github.com/datastax-labs/astra-cli/pkg" 25 | astraops "github.com/datastax/astra-client-go/v2/astra" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | var getFmt string 31 | 32 | func init() { 33 | GetCmd.Flags().StringVarP(&getFmt, "output", "o", "text", "Output format for report default is text") 34 | } 35 | 36 | // GetCmd provides the get database command 37 | var GetCmd = &cobra.Command{ 38 | Use: "get ", 39 | Short: "get database by databaseID", 40 | Long: `gets a database from your Astra account by ID`, 41 | Args: cobra.ExactArgs(1), 42 | Run: func(cobraCmd *cobra.Command, args []string) { 43 | creds := &pkg.Creds{} 44 | txt, err := executeGet(args, creds.Login) 45 | if err != nil { 46 | fmt.Fprintf(os.Stderr, "unable to login with error %v\n", err) 47 | os.Exit(1) 48 | } 49 | fmt.Println(txt) 50 | }, 51 | } 52 | 53 | func executeGet(args []string, login func() (pkg.Client, error)) (string, error) { 54 | client, err := login() 55 | if err != nil { 56 | return "", fmt.Errorf("unable to login with error %v", err) 57 | } 58 | id := args[0] 59 | var db astraops.Database 60 | if db, err = client.FindDb(id); err != nil { 61 | return "", fmt.Errorf("unable to get '%s' with error %v", id, err) 62 | } 63 | switch getFmt { 64 | case pkg.TextFormat: 65 | var rows [][]string 66 | rows = append(rows, []string{"name", "id", "status"}) 67 | rows = append(rows, []string{*db.Info.Name, db.Id, string(db.Status)}) 68 | var buf bytes.Buffer 69 | err = pkg.WriteRows(&buf, rows) 70 | if err != nil { 71 | return "", fmt.Errorf("unexpected error writing out text %v", err) 72 | } 73 | return buf.String(), nil 74 | case pkg.JSONFormat: 75 | b, err := json.MarshalIndent(db, "", " ") 76 | if err != nil { 77 | return "", fmt.Errorf("unexpected error marshaling to json: '%v', Try -output text instead", err) 78 | } 79 | return string(b), nil 80 | default: 81 | return "", fmt.Errorf("-o %q is not valid option", getFmt) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cmd/db/get_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db is where the Astra DB commands are 16 | package db 17 | 18 | import ( 19 | "encoding/json" 20 | "errors" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/datastax-labs/astra-cli/pkg" 25 | tests "github.com/datastax-labs/astra-cli/pkg/tests" 26 | astraops "github.com/datastax/astra-client-go/v2/astra" 27 | ) 28 | 29 | func TestGet(t *testing.T) { 30 | getFmt = pkg.JSONFormat 31 | dbs := []astraops.Database{ 32 | {Id: "1"}, 33 | {Id: "2"}, 34 | } 35 | jsonTxt, err := executeGet([]string{"1"}, func() (pkg.Client, error) { 36 | return &tests.MockClient{ 37 | Databases: dbs, 38 | }, nil 39 | }) 40 | if err != nil { 41 | t.Fatalf("unexpected error %v", err) 42 | } 43 | var fromServer astraops.Database 44 | err = json.Unmarshal([]byte(jsonTxt), &fromServer) 45 | if err != nil { 46 | t.Fatalf("unexpected error with json %v with text %v", err, jsonTxt) 47 | } 48 | if fromServer.Id != dbs[0].Id { 49 | t.Errorf("expected '%v' but was '%v'", dbs[0].Id, fromServer.Id) 50 | } 51 | } 52 | 53 | func TestGetFindDbFails(t *testing.T) { 54 | getFmt = pkg.JSONFormat 55 | dbs := []astraops.Database{} 56 | jsonTxt, err := executeGet([]string{"1"}, func() (pkg.Client, error) { 57 | return &tests.MockClient{ 58 | Databases: dbs, 59 | ErrorQueue: []error{errors.New("cant find db")}, 60 | }, nil 61 | }) 62 | if err == nil { 63 | t.Fatal("expected error") 64 | } 65 | expected := "unable to get '1' with error cant find db" 66 | if err.Error() != expected { 67 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 68 | } 69 | if jsonTxt != "" { 70 | t.Errorf("expected '%v' but was '%v'", "", jsonTxt) 71 | } 72 | } 73 | 74 | func TestGetFailedLogin(t *testing.T) { 75 | // setting package variables by hand, there be dragons 76 | mockClient := &tests.MockClient{} 77 | mockClient.ErrorQueue = []error{} 78 | id := "12345" 79 | msg, err := executeGet([]string{id}, func() (pkg.Client, error) { 80 | return mockClient, errors.New("no db") 81 | }) 82 | if err == nil { 83 | t.Fatalf("expected error") 84 | } 85 | expectedErr := tests.LoginError 86 | if err.Error() != expectedErr { 87 | t.Errorf("expected '%v' but was '%v'", expectedErr, err) 88 | } 89 | expected := "" 90 | if msg != expected { 91 | t.Errorf("expected '%v' but was '%v'", expected, msg) 92 | } 93 | } 94 | 95 | func TestGetText(t *testing.T) { 96 | getFmt = pkg.TextFormat 97 | dbs := []astraops.Database{ 98 | { 99 | Id: "1", 100 | Info: astraops.DatabaseInfo{ 101 | Name: astraops.StringPtr("A"), 102 | }, 103 | Status: astraops.StatusEnumACTIVE, 104 | }, 105 | { 106 | Id: "2", 107 | Info: astraops.DatabaseInfo{ 108 | Name: astraops.StringPtr("B"), 109 | }, 110 | Status: astraops.StatusEnumTERMINATING, 111 | }, 112 | } 113 | txt, err := executeGet([]string{"1"}, func() (pkg.Client, error) { 114 | return &tests.MockClient{ 115 | Databases: dbs, 116 | }, nil 117 | }) 118 | if err != nil { 119 | t.Fatalf("unexpected error %v", err) 120 | } 121 | expected := strings.Join([]string{ 122 | "name id status", 123 | "A 1 ACTIVE", 124 | }, 125 | "\n") 126 | if txt != expected { 127 | t.Errorf("expected '%v' but was '%v'", expected, txt) 128 | } 129 | } 130 | 131 | func TestGetInvalidFmt(t *testing.T) { 132 | getFmt = "badham" 133 | _, err := executeGet([]string{"abc"}, func() (pkg.Client, error) { 134 | return &tests.MockClient{}, nil 135 | }) 136 | if err == nil { 137 | t.Fatalf("unexpected error %v", err) 138 | } 139 | expected := "-o \"badham\" is not valid option" 140 | if err.Error() != expected { 141 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /cmd/db/list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db provides the sub-commands for the db command 16 | package db 17 | 18 | import ( 19 | "bytes" 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | 24 | "github.com/datastax-labs/astra-cli/pkg" 25 | astraops "github.com/datastax/astra-client-go/v2/astra" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | var limit int 31 | var include string 32 | var provider string 33 | var startingAfter string 34 | var listFmt string 35 | 36 | func init() { 37 | defaultLimit := 1000 38 | ListCmd.Flags().IntVarP(&limit, "limit", "l", defaultLimit, "limit of databases retrieved") 39 | ListCmd.Flags().StringVarP(&include, "include", "i", "", "the type of filter to apply") 40 | ListCmd.Flags().StringVarP(&provider, "provider", "p", "", "provider to filter by") 41 | ListCmd.Flags().StringVarP(&startingAfter, "startingAfter", "a", "", "timestamp filter, ie only show databases created after this timestamp") 42 | ListCmd.Flags().StringVarP(&listFmt, "output", "o", "text", "Output format for report default is json") 43 | } 44 | 45 | // ListCmd provides the list databases command 46 | var ListCmd = &cobra.Command{ 47 | Use: "list", 48 | Short: "lists all databases", 49 | Long: `lists all databases in your Astra account`, 50 | Run: func(cmd *cobra.Command, args []string) { 51 | creds := &pkg.Creds{} 52 | msg, err := executeList(creds.Login) 53 | if err != nil { 54 | fmt.Fprintf(os.Stderr, "%v\n", err) 55 | os.Exit(1) 56 | } 57 | fmt.Println(msg) 58 | }, 59 | } 60 | 61 | func executeList(login func() (pkg.Client, error)) (string, error) { 62 | client, err := login() 63 | if err != nil { 64 | return "", fmt.Errorf("unable to login with error '%v'", err) 65 | } 66 | var dbs []astraops.Database 67 | if dbs, err = client.ListDb(include, provider, startingAfter, limit); err != nil { 68 | return "", fmt.Errorf("unable to get list of dbs with error '%v'", err) 69 | } 70 | switch listFmt { 71 | case pkg.TextFormat: 72 | var rows [][]string 73 | rows = append(rows, []string{"name", "id", "status"}) 74 | for _, db := range dbs { 75 | rows = append(rows, []string{*db.Info.Name, db.Id, string(db.Status)}) 76 | } 77 | var out bytes.Buffer 78 | err = pkg.WriteRows(&out, rows) 79 | if err != nil { 80 | return "", fmt.Errorf("unexpected error writing text output '%v'", err) 81 | } 82 | return out.String(), nil 83 | case pkg.JSONFormat: 84 | b, err := json.MarshalIndent(dbs, "", " ") 85 | if err != nil { 86 | return "", fmt.Errorf("unexpected error marshaling to json: '%v', Try -output text instead", err) 87 | } 88 | return string(b), nil 89 | default: 90 | return "", fmt.Errorf("-o %q is not valid option", listFmt) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /cmd/db/list_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db is where the Astra DB commands are 16 | package db 17 | 18 | import ( 19 | "encoding/json" 20 | "errors" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/datastax-labs/astra-cli/pkg" 25 | tests "github.com/datastax-labs/astra-cli/pkg/tests" 26 | astraops "github.com/datastax/astra-client-go/v2/astra" 27 | ) 28 | 29 | func TestList(t *testing.T) { 30 | listFmt = pkg.JSONFormat 31 | dbs := []astraops.Database{ 32 | {Id: "1"}, 33 | {Id: "2"}, 34 | } 35 | jsonTxt, err := executeList(func() (pkg.Client, error) { 36 | return &tests.MockClient{ 37 | Databases: dbs, 38 | }, nil 39 | }) 40 | if err != nil { 41 | t.Fatalf("unexpected error %v", err) 42 | } 43 | var fromServer []astraops.Database 44 | err = json.Unmarshal([]byte(jsonTxt), &fromServer) 45 | if err != nil { 46 | t.Fatalf("unexpected error with json %v with text %v", err, jsonTxt) 47 | } 48 | if len(fromServer) != len(dbs) { 49 | t.Errorf("expected '%v' but was '%v'", len(dbs), len(fromServer)) 50 | } 51 | if fromServer[0].Id != dbs[0].Id { 52 | t.Errorf("expected '%v' but was '%v'", dbs[0].Id, fromServer[0].Id) 53 | } 54 | if fromServer[1].Id != dbs[1].Id { 55 | t.Errorf("expected '%v' but was '%v'", dbs[1].Id, fromServer[1].Id) 56 | } 57 | } 58 | 59 | func TestListText(t *testing.T) { 60 | listFmt = pkg.TextFormat 61 | dbs := []astraops.Database{ 62 | { 63 | Id: "1", 64 | Info: astraops.DatabaseInfo{ 65 | Name: astraops.StringPtr("A"), 66 | }, 67 | Status: astraops.StatusEnumACTIVE, 68 | }, 69 | { 70 | Id: "2", 71 | Info: astraops.DatabaseInfo{ 72 | Name: astraops.StringPtr("B"), 73 | }, 74 | Status: astraops.StatusEnumTERMINATING, 75 | }, 76 | } 77 | txt, err := executeList(func() (pkg.Client, error) { 78 | return &tests.MockClient{ 79 | Databases: dbs, 80 | }, nil 81 | }) 82 | if err != nil { 83 | t.Fatalf("unexpected error %v", err) 84 | } 85 | expected := strings.Join([]string{ 86 | "name id status", 87 | "A 1 ACTIVE", 88 | "B 2 TERMINATING", 89 | }, 90 | "\n") 91 | if txt != expected { 92 | t.Errorf("expected '%v' but was '%v'", expected, txt) 93 | } 94 | } 95 | 96 | func TestListInvalidFmt(t *testing.T) { 97 | listFmt = "listham" 98 | _, err := executeList(func() (pkg.Client, error) { 99 | return &tests.MockClient{}, nil 100 | }) 101 | if err == nil { 102 | t.Fatalf("unexpected error %v", err) 103 | } 104 | expected := "-o \"listham\" is not valid option" 105 | if err.Error() != expected { 106 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 107 | } 108 | } 109 | 110 | func TestListFails(t *testing.T) { 111 | getFmt = pkg.JSONFormat 112 | dbs := []astraops.Database{} 113 | jsonTxt, err := executeList(func() (pkg.Client, error) { 114 | return &tests.MockClient{ 115 | Databases: dbs, 116 | ErrorQueue: []error{errors.New("cant find db")}, 117 | }, nil 118 | }) 119 | if err == nil { 120 | t.Fatal("expected error") 121 | } 122 | expected := "unable to get list of dbs with error 'cant find db'" 123 | if err.Error() != expected { 124 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 125 | } 126 | if jsonTxt != "" { 127 | t.Errorf("expected '%v' but was '%v'", "", jsonTxt) 128 | } 129 | } 130 | 131 | func TestListFailedLogin(t *testing.T) { 132 | // setting package variables by hand, there be dragons 133 | mockClient := &tests.MockClient{} 134 | mockClient.ErrorQueue = []error{errors.New("no db")} 135 | msg, err := executeList(func() (pkg.Client, error) { 136 | return mockClient, nil 137 | }) 138 | if err == nil { 139 | t.Fatalf("expected error") 140 | } 141 | expectedErr := "unable to get list of dbs with error 'no db'" 142 | if err.Error() != expectedErr { 143 | t.Errorf("expected '%v' but was '%v'", expectedErr, err) 144 | } 145 | expected := "" 146 | if msg != expected { 147 | t.Errorf("expected '%v' but was '%v'", expected, msg) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /cmd/db/park.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db is where the Astra DB commands are 16 | package db 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/datastax-labs/astra-cli/pkg" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | // ParkCmd provides parking support for classic database tiers in Astra 27 | var ParkCmd = &cobra.Command{ 28 | Use: "park ", 29 | Short: "parks the database specified, does not work with serverless", 30 | Long: `parks the database specified, only works on classic tier databases and can take a very long time to park (20-30 minutes)`, 31 | Args: cobra.ExactArgs(1), 32 | Run: func(cobraCmd *cobra.Command, args []string) { 33 | creds := &pkg.Creds{} 34 | msg, err := executePark(args, creds.Login) 35 | if err != nil { 36 | fmt.Fprintln(os.Stderr, err) 37 | os.Exit(1) 38 | } 39 | fmt.Println(msg) 40 | }, 41 | } 42 | 43 | // executePark parks the database with the specified ID. If no ID is provided 44 | // the command will error out 45 | func executePark(args []string, makeClient func() (pkg.Client, error)) (string, error) { 46 | client, err := makeClient() 47 | if err != nil { 48 | return "", fmt.Errorf("unable to login with error %v", err) 49 | } 50 | id := args[0] 51 | fmt.Printf("starting to park database %v\n", id) 52 | if err := client.Park(id); err != nil { 53 | return "", fmt.Errorf("unable to park '%s' with error %v", id, err) 54 | } 55 | return fmt.Sprintf("database %v parked", id), nil 56 | } 57 | -------------------------------------------------------------------------------- /cmd/db/park_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db is where the Astra DB commands are 16 | package db 17 | 18 | import ( 19 | "errors" 20 | "testing" 21 | 22 | "github.com/datastax-labs/astra-cli/pkg" 23 | tests "github.com/datastax-labs/astra-cli/pkg/tests" 24 | ) 25 | 26 | func TestPark(t *testing.T) { 27 | // setting package variables by hand, there be dragons 28 | mockClient := &tests.MockClient{} 29 | id := "abcd" 30 | msg, err := executePark([]string{id}, func() (pkg.Client, error) { 31 | return mockClient, nil 32 | }) 33 | if err != nil { 34 | t.Fatalf("unexpected error '%v'", err) 35 | } 36 | 37 | if len(mockClient.Calls()) != 1 { 38 | t.Fatalf("expected 1 call but was %v", len(mockClient.Calls())) 39 | } 40 | if id != mockClient.Call(0) { 41 | t.Errorf("expected '%v' but was '%v'", id, mockClient.Call(0)) 42 | } 43 | expected := "database abcd parked" 44 | if msg != expected { 45 | t.Errorf("expected '%v' but was '%v'", expected, msg) 46 | } 47 | } 48 | 49 | func TestParkFailedLogin(t *testing.T) { 50 | // setting package variables by hand, there be dragons 51 | mockClient := &tests.MockClient{} 52 | id := "abcd" 53 | msg, err := executePark([]string{id}, func() (pkg.Client, error) { 54 | return mockClient, errors.New("bad login") 55 | }) 56 | if err == nil { 57 | t.Fatalf("expected error") 58 | } 59 | expectedErr := "unable to login with error bad login" 60 | if err.Error() != expectedErr { 61 | t.Errorf("expected '%v' but was '%v'", expectedErr, err) 62 | } 63 | expected := "" 64 | if msg != expected { 65 | t.Errorf("expected '%v' but was '%v'", expected, msg) 66 | } 67 | } 68 | 69 | func TestParkFailed(t *testing.T) { 70 | // setting package variables by hand, there be dragons 71 | mockClient := &tests.MockClient{} 72 | mockClient.ErrorQueue = []error{errors.New("unable to park")} 73 | id := "123" 74 | msg, err := executePark([]string{id}, func() (pkg.Client, error) { 75 | return mockClient, nil 76 | }) 77 | if err == nil { 78 | t.Fatalf("expected error") 79 | } 80 | expectedErr := "unable to park '123' with error unable to park" 81 | if err.Error() != expectedErr { 82 | t.Errorf("expected '%v' but was '%v'", expectedErr, err) 83 | } 84 | expected := "" 85 | if msg != expected { 86 | t.Errorf("expected '%v' but was '%v'", expected, msg) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /cmd/db/resize.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db is where the Astra DB commands are 16 | package db 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | "strconv" 22 | 23 | "github.com/datastax-labs/astra-cli/pkg" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | const noRequiredArgs = 2 28 | 29 | // ResizeCmd provides the resize database command 30 | var ResizeCmd = &cobra.Command{ 31 | Use: "resize ", 32 | Short: "Resizes a database by id with the specified capacity unit", 33 | Long: "Resizes a database by id with the specified capacity unit. Note does not work on serverless.", 34 | Args: cobra.ExactArgs(noRequiredArgs), 35 | Run: func(cobraCmd *cobra.Command, args []string) { 36 | creds := &pkg.Creds{} 37 | err := executeResize(args, creds.Login) 38 | if err != nil { 39 | fmt.Fprintf(os.Stderr, "unable to resize with error %v\n", err) 40 | os.Exit(1) 41 | } 42 | }, 43 | } 44 | 45 | // executeResize resizes the database with the specified ID with the specified size. If no ID is provided 46 | // the command will error out 47 | func executeResize(args []string, makeClient func() (pkg.Client, error)) error { 48 | client, err := makeClient() 49 | if err != nil { 50 | return fmt.Errorf("unable to login with error %v", err) 51 | } 52 | id := args[0] 53 | capacityUnitRaw := args[1] 54 | defaultCapacity := 10 55 | bits := 32 56 | capacityUnit, err := strconv.ParseInt(capacityUnitRaw, defaultCapacity, bits) 57 | if err != nil { 58 | return &pkg.ParseError{ 59 | Args: args, 60 | Err: fmt.Errorf("unable to parse capacity unit '%s' with error %v", capacityUnitRaw, err), 61 | } 62 | } 63 | if err := client.Resize(id, int(capacityUnit)); err != nil { 64 | return fmt.Errorf("unable to resize '%s' with error %v", id, err) 65 | } 66 | fmt.Printf("resize database %v submitted with size %v\n", id, capacityUnit) 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /cmd/db/resize_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db is where the Astra DB commands are 16 | package db 17 | 18 | import ( 19 | "errors" 20 | "testing" 21 | 22 | "github.com/datastax-labs/astra-cli/pkg" 23 | tests "github.com/datastax-labs/astra-cli/pkg/tests" 24 | ) 25 | 26 | func TestResize(t *testing.T) { 27 | // setting package variables by hand, there be dragons 28 | mockClient := &tests.MockClient{} 29 | id := "resizeId1" 30 | size := "100" 31 | err := executeResize([]string{id, size}, func() (pkg.Client, error) { 32 | return mockClient, nil 33 | }) 34 | if err != nil { 35 | t.Fatalf("unexpected error '%v'", err) 36 | } 37 | 38 | if len(mockClient.Calls()) != 1 { 39 | t.Fatalf("expected 1 call but was %v", len(mockClient.Calls())) 40 | } 41 | actualID := mockClient.Call(0).([]interface{})[0] 42 | if id != actualID { 43 | t.Errorf("expected '%v' but was '%v'", id, actualID) 44 | } 45 | actualSize := mockClient.Call(0).([]interface{})[1].(int) 46 | if 100 != actualSize { 47 | t.Errorf("expected '%v' but was '%v'", size, actualSize) 48 | } 49 | } 50 | 51 | func TestResizeParseError(t *testing.T) { 52 | // setting package variables by hand, there be dragons 53 | mockClient := &tests.MockClient{} 54 | id := "resizeparseId" 55 | size := "poppaoute" 56 | err := executeResize([]string{id, size}, func() (pkg.Client, error) { 57 | return mockClient, nil 58 | }) 59 | if err == nil { 60 | t.Fatal("expected error") 61 | } 62 | expectedError := "Unable to parse command line with args: resizeparseId, poppaoute. Nested error was 'unable to parse capacity unit 'poppaoute' with error strconv.ParseInt: parsing \"poppaoute\": invalid syntax'" 63 | if err.Error() != expectedError { 64 | t.Errorf("expected '%v' but was '%v'", expectedError, err.Error()) 65 | } 66 | if len(mockClient.Calls()) != 0 { 67 | t.Fatalf("expected 0 call but was %v", len(mockClient.Calls())) 68 | } 69 | } 70 | 71 | func TestResizeFailed(t *testing.T) { 72 | // setting package variables by hand, there be dragons 73 | mockClient := &tests.MockClient{} 74 | mockClient.ErrorQueue = []error{errors.New("no db")} 75 | id := "12389" 76 | err := executeResize([]string{id, "100"}, func() (pkg.Client, error) { 77 | return mockClient, nil 78 | }) 79 | if err == nil { 80 | t.Fatalf("expected error") 81 | } 82 | expectedErr := "unable to resize '12389' with error no db" 83 | if err.Error() != expectedErr { 84 | t.Errorf("expected '%v' but was '%v'", expectedErr, err) 85 | } 86 | } 87 | 88 | func TestResizeFailedLogin(t *testing.T) { 89 | // setting package variables by hand, there be dragons 90 | mockClient := &tests.MockClient{} 91 | mockClient.ErrorQueue = []error{} 92 | id := "12390" 93 | err := executeResize([]string{id, "100"}, func() (pkg.Client, error) { 94 | return mockClient, errors.New("no db") 95 | }) 96 | if err == nil { 97 | t.Fatalf("expected error") 98 | } 99 | expectedErr := "unable to login with error no db" 100 | if err.Error() != expectedErr { 101 | t.Errorf("expected '%v' but was '%v'", expectedErr, err) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /cmd/db/secBundle.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db provides the sub-commands for the db command 16 | package db 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | "os" 22 | 23 | "github.com/datastax-labs/astra-cli/pkg" 24 | "github.com/datastax-labs/astra-cli/pkg/httputils" 25 | astraops "github.com/datastax/astra-client-go/v2/astra" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | var secBundleFmt string 31 | var secBundleLoc string 32 | var secBundleDownloadType string 33 | 34 | func init() { 35 | SecBundleCmd.Flags().StringVarP(&secBundleFmt, "output", "o", "zip", "Output format for report default is zip") 36 | SecBundleCmd.Flags().StringVarP(&secBundleDownloadType, "download-type", "d", "external", "Bundle type to download external, internal, proxy-external and proxy-internal available. Only works with -o zip") 37 | SecBundleCmd.Flags().StringVarP(&secBundleLoc, "location", "l", "secureBundle.zip", "location of bundle to download to if using zip format. ignore if using json") 38 | } 39 | 40 | // SecBundleCmd provides the secBundle database command 41 | var SecBundleCmd = &cobra.Command{ 42 | Use: "secBundle ", 43 | Short: "get secure bundle by databaseID", 44 | Long: `gets the secure connetion bundle for the database from your Astra account by ID`, 45 | Args: cobra.ExactArgs(1), 46 | Run: func(cobraCmd *cobra.Command, args []string) { 47 | creds := &pkg.Creds{} 48 | out, err := executeSecBundle(args, creds.Login) 49 | if err != nil { 50 | fmt.Fprintf(os.Stderr, "%v\n", err) 51 | os.Exit(1) 52 | } 53 | fmt.Println(out) 54 | }, 55 | } 56 | 57 | func executeSecBundle(args []string, login func() (pkg.Client, error)) (string, error) { 58 | client, err := login() 59 | if err != nil { 60 | return "", fmt.Errorf("unable to login with error %v", err) 61 | } 62 | id := args[0] 63 | var secBundle astraops.CredsURL 64 | if secBundle, err = client.GetSecureBundle(id); err != nil { 65 | return "", fmt.Errorf("unable to get '%s' with error %v", id, err) 66 | } 67 | switch secBundleFmt { 68 | case "zip": 69 | var urlToDownload string 70 | switch secBundleDownloadType { 71 | case "external": 72 | urlToDownload = secBundle.DownloadURL 73 | case "internal": 74 | urlToDownload = *secBundle.DownloadURLInternal 75 | case "proxy-external": 76 | urlToDownload = *secBundle.DownloadURLMigrationProxy 77 | case "proxy-internal": 78 | urlToDownload = *secBundle.DownloadURLMigrationProxyInternal 79 | default: 80 | return "", fmt.Errorf("invalid download type %s passed. valid options are 'external', 'internal', 'proxy-external', 'proxy-internal'", secBundleDownloadType) 81 | } 82 | bytesWritten, err := httputils.DownloadZip(urlToDownload, secBundleLoc) 83 | if err != nil { 84 | return "", fmt.Errorf("error outputing zip format '%v'", err) 85 | } 86 | return fmt.Sprintf("file %v saved %v bytes written", secBundleLoc, bytesWritten), nil 87 | case pkg.JSONFormat: 88 | b, err := json.MarshalIndent(secBundle, "", " ") 89 | if err != nil { 90 | return "", fmt.Errorf("unexpected error marshaling to json: '%v', Try -output text instead", err) 91 | } 92 | return string(b), nil 93 | case "list": 94 | return fmt.Sprintf(` 95 | external bundle: %s 96 | internal bundle: %s 97 | external proxy: %s 98 | internal proxy: %s 99 | `, secBundle.DownloadURL, *secBundle.DownloadURLInternal, *secBundle.DownloadURLMigrationProxy, *secBundle.DownloadURLMigrationProxyInternal), nil 100 | default: 101 | return "", fmt.Errorf("-o %q is not valid option", secBundleFmt) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /cmd/db/secBundle_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db is where the Astra DB commands are 16 | package db 17 | 18 | import ( 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "net/http" 23 | "net/http/httptest" 24 | "os" 25 | "path" 26 | "testing" 27 | 28 | "github.com/datastax-labs/astra-cli/pkg" 29 | tests "github.com/datastax-labs/astra-cli/pkg/tests" 30 | astraops "github.com/datastax/astra-client-go/v2/astra" 31 | ) 32 | 33 | func TestSecBundle(t *testing.T) { 34 | id := "secId123" 35 | secBundleLoc = "my_loc" 36 | secBundleFmt = "json" 37 | bundle := astraops.CredsURL{ 38 | DownloadURL: "abcd", 39 | DownloadURLInternal: astraops.StringPtr("wyz"), 40 | DownloadURLMigrationProxy: astraops.StringPtr("opu"), 41 | DownloadURLMigrationProxyInternal: astraops.StringPtr("zert"), 42 | } 43 | jsonTxt, err := executeSecBundle([]string{id}, func() (pkg.Client, error) { 44 | return &tests.MockClient{ 45 | Bundle: bundle, 46 | }, nil 47 | }) 48 | if err != nil { 49 | t.Fatalf("unexpected error %v", err) 50 | } 51 | // after we went to the newer api with it's heavy use of pointers we lost easy comparison, here I convert 52 | // the struct into json text for comparison 53 | bundleTxt, err := json.MarshalIndent(bundle, "", " ") 54 | if err != nil { 55 | t.Fatalf("unexpected error with json %v", err) 56 | } 57 | if string(bundleTxt) != jsonTxt { 58 | t.Errorf("expected '%v' but was '%v", string(bundleTxt), jsonTxt) 59 | } 60 | } 61 | 62 | func TestSecBundleZip(t *testing.T) { 63 | zipContent := "zip file content" 64 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 65 | fmt.Fprintln(w, zipContent) 66 | })) 67 | defer ts.Close() 68 | tmpDir := t.TempDir() 69 | zipFile := path.Join(tmpDir, "bundle.zip") 70 | defer func() { 71 | if err := os.Remove(zipFile); err != nil { 72 | t.Logf("unable to remove '%v' in test due to error '%v'", zipFile, err) 73 | } 74 | }() 75 | id := "abc" 76 | secBundleLoc = zipFile 77 | secBundleFmt = "zip" 78 | bundle := astraops.CredsURL{ 79 | DownloadURL: ts.URL, 80 | DownloadURLInternal: astraops.StringPtr("wyz"), 81 | DownloadURLMigrationProxy: astraops.StringPtr("opu"), 82 | DownloadURLMigrationProxyInternal: astraops.StringPtr("zert"), 83 | } 84 | msg, err := executeSecBundle([]string{id}, func() (pkg.Client, error) { 85 | return &tests.MockClient{ 86 | Bundle: bundle, 87 | }, nil 88 | }) 89 | if err != nil { 90 | t.Fatalf("unexpected error %v", err) 91 | } 92 | expected := fmt.Sprintf("file %v saved 17 bytes written", zipFile) 93 | if msg != expected { 94 | t.Errorf("expected '%v' but was '%v'", expected, msg) 95 | } 96 | } 97 | 98 | func TestSecBundleInvalidFmt(t *testing.T) { 99 | id := "abc" 100 | secBundleFmt = "ham" 101 | bundle := astraops.CredsURL{ 102 | DownloadURL: "url", 103 | DownloadURLInternal: astraops.StringPtr("wyz"), 104 | DownloadURLMigrationProxy: astraops.StringPtr("opu"), 105 | DownloadURLMigrationProxyInternal: astraops.StringPtr("zert"), 106 | } 107 | _, err := executeSecBundle([]string{id}, func() (pkg.Client, error) { 108 | return &tests.MockClient{ 109 | Bundle: bundle, 110 | }, nil 111 | }) 112 | if err == nil { 113 | t.Fatalf("unexpected error %v", err) 114 | } 115 | expected := "-o \"ham\" is not valid option" 116 | if err.Error() != expected { 117 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 118 | } 119 | } 120 | 121 | func TestSecBundleFailed(t *testing.T) { 122 | // setting package variables by hand, there be dragons 123 | mockClient := &tests.MockClient{} 124 | mockClient.ErrorQueue = []error{errors.New("no db")} 125 | id := "12390" 126 | _, err := executeSecBundle([]string{id}, func() (pkg.Client, error) { 127 | return mockClient, nil 128 | }) 129 | if err == nil { 130 | t.Fatalf("expected error") 131 | } 132 | expectedErr := "unable to get '12390' with error no db" 133 | if err.Error() != expectedErr { 134 | t.Errorf("expected '%v' but was '%v'", expectedErr, err) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /cmd/db/tiers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db provides the sub-commands for the db command 16 | package db 17 | 18 | import ( 19 | "bytes" 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | 24 | "github.com/datastax-labs/astra-cli/pkg" 25 | astraops "github.com/datastax/astra-client-go/v2/astra" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | var tiersFmt string 31 | 32 | func init() { 33 | TiersCmd.Flags().StringVarP(&tiersFmt, "output", "o", "text", "Output format for report default is json") 34 | } 35 | 36 | // TiersCmd is the command to list availability data in Astra 37 | var TiersCmd = &cobra.Command{ 38 | Use: "tiers", 39 | Short: "List all available tiers on the Astra DevOps API", 40 | Long: `List all available tiers on the Astra DevOps API. Each tier is a combination of costs, size, region, and name`, 41 | Run: func(cmd *cobra.Command, args []string) { 42 | creds := &pkg.Creds{} 43 | msg, err := executeTiers(creds.Login) 44 | if err != nil { 45 | fmt.Fprintf(os.Stderr, "%v\n", err) 46 | os.Exit(1) 47 | } 48 | fmt.Println(msg) 49 | }, 50 | } 51 | 52 | func executeTiers(login func() (pkg.Client, error)) (string, error) { 53 | var tiers []astraops.AvailableRegionCombination 54 | client, err := login() 55 | if err != nil { 56 | return "", fmt.Errorf("unable to login with error %v", err) 57 | } 58 | if tiers, err = client.GetTierInfo(); err != nil { 59 | return "", fmt.Errorf("unable to get tiers with error %v", err) 60 | } 61 | switch tiersFmt { 62 | case pkg.TextFormat: 63 | var rows [][]string 64 | rows = append(rows, []string{"name", "cloud", "region", "db (used)/(limit)", "cap (used)/(limit)", "cost per month", "cost per minute"}) 65 | for _, tier := range tiers { 66 | var costMonthRaw float64 67 | if tier.Cost.CostPerMonthCents != nil { 68 | costMonthRaw = *tier.Cost.CostPerMonthCents 69 | } 70 | var costMinRaw float64 71 | if tier.Cost.CostPerMinCents != nil { 72 | costMinRaw = *tier.Cost.CostPerMinCents 73 | } 74 | 75 | divisor := 100.0 76 | var costMonth float64 77 | if costMonthRaw > 0.0 { 78 | costMonth = costMonthRaw / divisor 79 | } 80 | var costMin float64 81 | if costMinRaw > 0.0 { 82 | costMin = costMinRaw / divisor 83 | } 84 | rows = append(rows, []string{ 85 | string(tier.Tier), 86 | string(tier.CloudProvider), 87 | tier.Region, 88 | fmt.Sprintf("%v/%v", tier.DatabaseCountUsed, tier.DatabaseCountLimit), 89 | fmt.Sprintf("%v/%v", tier.CapacityUnitsUsed, tier.CapacityUnitsLimit), 90 | fmt.Sprintf("$%.2f", costMonth), 91 | fmt.Sprintf("$%.2f", costMin)}) 92 | } 93 | var buf bytes.Buffer 94 | err = pkg.WriteRows(&buf, rows) 95 | if err != nil { 96 | return "", fmt.Errorf("unexpected error writing text output %v", err) 97 | } 98 | return buf.String(), nil 99 | case pkg.JSONFormat: 100 | b, err := json.MarshalIndent(tiers, "", " ") 101 | if err != nil { 102 | return "", fmt.Errorf("unexpected error marshaling to json: '%v', Try -format text instead", err) 103 | } 104 | return string(b), nil 105 | default: 106 | return "", fmt.Errorf("-o %q is not valid option", tiersFmt) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /cmd/db/tiers_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db is where the Astra DB commands are 16 | package db 17 | 18 | import ( 19 | "encoding/json" 20 | "errors" 21 | "reflect" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/datastax-labs/astra-cli/pkg" 26 | tests "github.com/datastax-labs/astra-cli/pkg/tests" 27 | astraops "github.com/datastax/astra-client-go/v2/astra" 28 | ) 29 | 30 | func TestTiers(t *testing.T) { 31 | tiersFmt = "json" 32 | tier1 := astraops.AvailableRegionCombination{ 33 | Tier: "abd", 34 | } 35 | tier2 := astraops.AvailableRegionCombination{ 36 | Tier: "xyz", 37 | } 38 | jsonTxt, err := executeTiers(func() (pkg.Client, error) { 39 | return &tests.MockClient{ 40 | Tiers: []astraops.AvailableRegionCombination{ 41 | tier1, 42 | tier2, 43 | }, 44 | }, nil 45 | }) 46 | if err != nil { 47 | t.Fatalf("unexpected error %v", err) 48 | } 49 | var fromServer []astraops.AvailableRegionCombination 50 | err = json.Unmarshal([]byte(jsonTxt), &fromServer) 51 | if err != nil { 52 | t.Fatalf("unexpected error with json %v", err) 53 | } 54 | expected := []astraops.AvailableRegionCombination{ 55 | tier1, 56 | tier2, 57 | } 58 | if !reflect.DeepEqual(fromServer, expected) { 59 | t.Errorf("expected '%v' but was '%v'", expected, fromServer) 60 | } 61 | } 62 | 63 | func TestTiersText(t *testing.T) { 64 | tiersFmt = "text" 65 | var costPerMonthCents = 10.0 66 | var costPerMinCents = 1.0 67 | tier1 := astraops.AvailableRegionCombination{ 68 | Tier: "tier1", 69 | CloudProvider: "cloud1", 70 | Region: "region1", 71 | DatabaseCountUsed: 1, 72 | DatabaseCountLimit: 1, 73 | Cost: astraops.Costs{ 74 | CostPerMonthCents: &costPerMonthCents, 75 | CostPerMinCents: &costPerMinCents, 76 | }, 77 | CapacityUnitsUsed: 1, 78 | CapacityUnitsLimit: 1, 79 | } 80 | var costPerMonthCents2 = 20.0 81 | var costPerMinCents2 = 2.0 82 | tier2 := astraops.AvailableRegionCombination{ 83 | Tier: "tier2", 84 | CloudProvider: "cloud2", 85 | Region: "region2", 86 | Cost: astraops.Costs{ 87 | CostPerMonthCents: &costPerMonthCents2, 88 | CostPerMinCents: &costPerMinCents2, 89 | }, 90 | CapacityUnitsUsed: 2, 91 | CapacityUnitsLimit: 2, 92 | } 93 | msg, err := executeTiers(func() (pkg.Client, error) { 94 | return &tests.MockClient{ 95 | Tiers: []astraops.AvailableRegionCombination{ 96 | tier1, 97 | tier2, 98 | }, 99 | }, nil 100 | }) 101 | if err != nil { 102 | t.Fatalf("unexpected error %v", err) 103 | } 104 | expected := strings.Join([]string{ 105 | "name cloud region db (used)/(limit) cap (used)/(limit) cost per month cost per minute", 106 | "tier1 cloud1 region1 1/1 1/1 $0.10 $0.01", 107 | "tier2 cloud2 region2 0/0 2/2 $0.20 $0.02", 108 | }, "\n") 109 | if msg != expected { 110 | t.Errorf("expected '%v' but was '%v'", expected, msg) 111 | } 112 | } 113 | 114 | func TestTiersTextWithNoCost(t *testing.T) { 115 | tiersFmt = "text" 116 | tier1 := astraops.AvailableRegionCombination{ 117 | Tier: "tier1", 118 | CloudProvider: "cloud1", 119 | Region: "region1", 120 | DatabaseCountUsed: 1, 121 | DatabaseCountLimit: 1, 122 | CapacityUnitsUsed: 1, 123 | CapacityUnitsLimit: 1, 124 | } 125 | tier2 := astraops.AvailableRegionCombination{ 126 | Tier: "tier2", 127 | CloudProvider: "cloud2", 128 | Region: "region2", 129 | CapacityUnitsUsed: 2, 130 | CapacityUnitsLimit: 2, 131 | } 132 | msg, err := executeTiers(func() (pkg.Client, error) { 133 | return &tests.MockClient{ 134 | Tiers: []astraops.AvailableRegionCombination{ 135 | tier1, 136 | tier2, 137 | }, 138 | }, nil 139 | }) 140 | if err != nil { 141 | t.Fatalf("unexpected error %v", err) 142 | } 143 | expected := strings.Join([]string{ 144 | "name cloud region db (used)/(limit) cap (used)/(limit) cost per month cost per minute", 145 | "tier1 cloud1 region1 1/1 1/1 $0.00 $0.00", 146 | "tier2 cloud2 region2 0/0 2/2 $0.00 $0.00", 147 | }, "\n") 148 | if msg != expected { 149 | t.Errorf("expected '%v' but was '%v'", expected, msg) 150 | } 151 | } 152 | 153 | func TestTiersnvalidFmt(t *testing.T) { 154 | tiersFmt = "ham" 155 | _, err := executeTiers(func() (pkg.Client, error) { 156 | return &tests.MockClient{}, nil 157 | }) 158 | if err == nil { 159 | t.Fatalf("expected error") 160 | } 161 | expected := "-o \"ham\" is not valid option" 162 | if err.Error() != expected { 163 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 164 | } 165 | } 166 | 167 | func TestTiersFailedLogin(t *testing.T) { 168 | // setting package variables by hand, there be dragons 169 | mockClient := &tests.MockClient{} 170 | mockClient.ErrorQueue = []error{} 171 | _, err := executeTiers(func() (pkg.Client, error) { 172 | return mockClient, errors.New("no db") 173 | }) 174 | if err == nil { 175 | t.Fatalf("expected error") 176 | } 177 | expectedErr := "unable to login with error no db" 178 | if err.Error() != expectedErr { 179 | t.Errorf("expected '%v' but was '%v'", expectedErr, err) 180 | } 181 | } 182 | 183 | func TestTiersFailed(t *testing.T) { 184 | // setting package variables by hand, there be dragons 185 | mockClient := &tests.MockClient{} 186 | mockClient.ErrorQueue = []error{errors.New("no db")} 187 | _, err := executeTiers(func() (pkg.Client, error) { 188 | return mockClient, nil 189 | }) 190 | if err == nil { 191 | t.Fatalf("expected error") 192 | } 193 | expectedErr := "unable to get tiers with error no db" 194 | if err.Error() != expectedErr { 195 | t.Errorf("expected '%v' but was '%v'", expectedErr, err) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /cmd/db/unpark.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db is where the Astra DB commands are 16 | package db 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/datastax-labs/astra-cli/pkg" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | // UnparkCmd provides unparking support for classic database tiers in Astra 27 | var UnparkCmd = &cobra.Command{ 28 | Use: "unpark ", 29 | Short: "parks the database specified, does not work with serverless", 30 | Long: `parks the database specified, only works on classic tier databases and can take a very long time to park (20-30 minutes)`, 31 | Args: cobra.ExactArgs(1), 32 | Run: func(cobraCmd *cobra.Command, args []string) { 33 | creds := &pkg.Creds{} 34 | err := executeUnpark(args, creds.Login) 35 | if err != nil { 36 | fmt.Fprintln(os.Stderr, err) 37 | os.Exit(1) 38 | } 39 | }, 40 | } 41 | 42 | // executeUnpark unparks the database with the specified ID. If no ID is provided 43 | // the command will error out 44 | func executeUnpark(args []string, makeClient func() (pkg.Client, error)) error { 45 | client, err := makeClient() 46 | if err != nil { 47 | return fmt.Errorf("unable to login with error %v", err) 48 | } 49 | id := args[0] 50 | fmt.Printf("starting to unpark database %v\n", id) 51 | if err := client.Unpark(id); err != nil { 52 | return fmt.Errorf("unable to unpark '%s' with error %v", id, err) 53 | } 54 | fmt.Printf("database %v unparked\n", id) 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /cmd/db/unpark_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package db is where the Astra DB commands are 16 | package db 17 | 18 | import ( 19 | "errors" 20 | "testing" 21 | 22 | "github.com/datastax-labs/astra-cli/pkg" 23 | tests "github.com/datastax-labs/astra-cli/pkg/tests" 24 | ) 25 | 26 | func TestUnpark(t *testing.T) { 27 | // setting package variables by hand, there be dragons 28 | mockClient := &tests.MockClient{} 29 | id := "unparkID123" 30 | err := executeUnpark([]string{id}, func() (pkg.Client, error) { 31 | return mockClient, nil 32 | }) 33 | if err != nil { 34 | t.Fatalf("unexpected error '%v'", err) 35 | } 36 | 37 | if len(mockClient.Calls()) != 1 { 38 | t.Fatalf("expected 1 call but was %v", len(mockClient.Calls())) 39 | } 40 | if id != mockClient.Call(0) { 41 | t.Errorf("expected '%v' but was '%v'", id, mockClient.Call(0)) 42 | } 43 | } 44 | 45 | func TestUnparkFailedLogin(t *testing.T) { 46 | // setting package variables by hand, there be dragons 47 | mockClient := &tests.MockClient{} 48 | id := "unpark136" 49 | err := executeUnpark([]string{id}, func() (pkg.Client, error) { 50 | return mockClient, errors.New("bad login") 51 | }) 52 | if err == nil { 53 | t.Fatalf("expected error") 54 | } 55 | expectedErr := "unable to login with error bad login" 56 | if err.Error() != expectedErr { 57 | t.Errorf("expected '%v' but was '%v'", expectedErr, err) 58 | } 59 | } 60 | 61 | func TestUnparkFailed(t *testing.T) { 62 | // setting package variables by hand, there be dragons 63 | mockClient := &tests.MockClient{} 64 | mockClient.ErrorQueue = []error{errors.New("unable to unpark")} 65 | id := "123" 66 | err := executeUnpark([]string{id}, func() (pkg.Client, error) { 67 | return mockClient, nil 68 | }) 69 | if err == nil { 70 | t.Fatalf("expected error") 71 | } 72 | expectedErr := "unable to unpark '123' with error unable to unpark" 73 | if err.Error() != expectedErr { 74 | t.Errorf("expected '%v' but was '%v'", expectedErr, err) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /cmd/db_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package cmd contains all fo the commands for the cli 16 | package cmd 17 | 18 | import ( 19 | "bytes" 20 | "errors" 21 | "io/ioutil" 22 | "testing" 23 | ) 24 | 25 | func TestDBUsageFails(t *testing.T) { 26 | fails := func() error { 27 | return errors.New("error showing usage") 28 | } 29 | err := executeDB(fails) 30 | if err == nil { 31 | t.Fatal("there is supposed to be an error") 32 | } 33 | expected := "warn unable to show usage error showing usage" 34 | if err.Error() != expected { 35 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 36 | } 37 | } 38 | 39 | func TestDBUsage(t *testing.T) { 40 | fails := func() error { 41 | return nil 42 | } 43 | err := executeDB(fails) 44 | if err != nil { 45 | t.Fatalf("unexpected eror %v", err) 46 | } 47 | } 48 | 49 | func TestDBShowHelp(t *testing.T) { 50 | clientJSON = "" 51 | authToken = "" 52 | clientName = "" 53 | clientSecret = "" 54 | clientID = "" 55 | originalOut := RootCmd.OutOrStderr() 56 | defer func() { 57 | RootCmd.SetOut(originalOut) 58 | RootCmd.SetArgs([]string{}) 59 | }() 60 | b := bytes.NewBufferString("") 61 | RootCmd.SetOut(b) 62 | RootCmd.SetArgs([]string{"db"}) 63 | err := RootCmd.Execute() 64 | if err != nil { 65 | t.Errorf("unexpected error '%v'", err) 66 | } 67 | out, err := ioutil.ReadAll(b) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | expected := dbCmd.UsageString() 72 | 73 | if string(out) != expected { 74 | t.Errorf("expected\n'%q'\nbut was\n'%q'", expected, string(out)) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /cmd/login.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package cmd is the entry point for all of the commands 16 | package cmd 17 | 18 | import ( 19 | "bufio" 20 | "encoding/json" 21 | "fmt" 22 | "io/fs" 23 | "os" 24 | 25 | "github.com/datastax-labs/astra-cli/pkg" 26 | 27 | "github.com/spf13/cobra" 28 | ) 29 | 30 | var clientID string 31 | var clientName string 32 | var clientSecret string 33 | var clientJSON string 34 | var authToken string 35 | 36 | func init() { 37 | loginCmd.Flags().StringVarP(&authToken, "token", "t", "", "authtoken generated with enough rights to perform the devops actions. Generated from the Astra site") 38 | loginCmd.Flags().StringVarP(&clientJSON, "json", "j", "", "copy the json for service account from the Astra site") 39 | loginCmd.Flags().StringVarP(&clientSecret, "secret", "s", "", "clientSecret from service account. Ignored if -json flag is used.") 40 | loginCmd.Flags().StringVarP(&clientName, "name", "n", "", "clientName from service account. Ignored if -json flag is used.") 41 | loginCmd.Flags().StringVarP(&clientID, "id", "i", "", "clientId from service account. Ignored if -json flag is used.") 42 | } 43 | 44 | const ( 45 | CriticalError = 1 46 | WriteError = 2 47 | CannotFindHome = 3 48 | JSONError = 4 49 | ) 50 | 51 | var loginCmd = &cobra.Command{ 52 | Use: "login", 53 | Short: "Stores credentials for the cli to use in other commands to operate on the Astra DevOps API", 54 | Long: `Token or service account is saved in .config/astra/ for use by the other commands`, 55 | Run: func(cobraCmd *cobra.Command, args []string) { 56 | exitCode, err := executeLogin(args, func() (string, pkg.ConfFiles, error) { 57 | return pkg.GetHome(os.UserHomeDir) 58 | }, cobraCmd.Usage) 59 | if err != nil { 60 | fmt.Printf("%v\n", err) 61 | } 62 | if exitCode != 0 { 63 | os.Exit(exitCode) 64 | } 65 | }, 66 | } 67 | 68 | func getTokenFromInput(usageFunc func() error) (string, int, error) { 69 | fmt.Print("token:") 70 | reader := bufio.NewReader(os.Stdin) 71 | var token string 72 | var err error 73 | tries := 0 74 | maxTries := 3 75 | for token == "" { 76 | if tries == maxTries { 77 | break 78 | } 79 | token, err = reader.ReadString('\n') 80 | if err != nil { 81 | return "", CriticalError, fmt.Errorf("error reading input %v", err) 82 | } 83 | if token == "" { 84 | fmt.Println("the token was empty try again") 85 | } else { 86 | return token, 0, nil 87 | } 88 | tries++ 89 | } 90 | if err := usageFunc(); err != nil { 91 | return "", CriticalError, fmt.Errorf("cannot show usage %v", err) 92 | } 93 | return "", CriticalError, fmt.Errorf("you must enter a token exiting") 94 | } 95 | 96 | func executeLogin(args []string, getHome func() (string, pkg.ConfFiles, error), usageFunc func() error) (int, error) { 97 | if clientJSON == "" && clientID == "" && clientName == "" && clientSecret == "" && authToken == "" { 98 | token, retcode, err := getTokenFromInput(usageFunc) 99 | if err != nil { 100 | return retcode, err 101 | } 102 | authToken = token 103 | } 104 | confDir, confFiles, err := getHome() 105 | if err != nil { 106 | return CannotFindHome, err 107 | } 108 | switch { 109 | case authToken != "": 110 | if err := makeConf(confDir, confFiles.TokenPath, authToken); err != nil { 111 | return WriteError, err 112 | } 113 | return 0, nil 114 | case clientJSON != "": 115 | return executeLoginJSON(args, confDir, confFiles) 116 | default: 117 | if clientID == "" { 118 | return JSONError, &pkg.ParseError{ 119 | Args: args, 120 | Err: fmt.Errorf("clientId missing"), 121 | } 122 | } 123 | if clientName == "" { 124 | return JSONError, &pkg.ParseError{ 125 | Args: args, 126 | Err: fmt.Errorf("clientName missing"), 127 | } 128 | } 129 | if clientSecret == "" { 130 | return JSONError, &pkg.ParseError{ 131 | Args: args, 132 | Err: fmt.Errorf("clientSecret missing"), 133 | } 134 | } 135 | clientJSON = fmt.Sprintf("{\"clientId\":\"%v\",\"clientName\":\"%v\",\"clientSecret\":\"%v\"}", clientID, clientName, clientSecret) 136 | if err := makeConf(confDir, confFiles.SaPath, clientJSON); err != nil { 137 | return WriteError, err 138 | } 139 | return 0, nil 140 | } 141 | } 142 | 143 | func executeLoginJSON(args []string, confDir string, confFiles pkg.ConfFiles) (int, error) { 144 | var clientInfo pkg.ClientInfo 145 | err := json.Unmarshal([]byte(clientJSON), &clientInfo) 146 | if err != nil { 147 | return JSONError, fmt.Errorf("unable to serialize the json into a valid login due to error %s", err) 148 | } 149 | if len(clientInfo.ClientName) == 0 { 150 | return JSONError, &pkg.ParseError{ 151 | Args: args, 152 | Err: fmt.Errorf("clientName missing"), 153 | } 154 | } 155 | if len(clientInfo.ClientID) == 0 { 156 | return JSONError, &pkg.ParseError{ 157 | Args: args, 158 | Err: fmt.Errorf("clientId missing"), 159 | } 160 | } 161 | if len(clientInfo.ClientSecret) == 0 { 162 | return JSONError, &pkg.ParseError{ 163 | Args: args, 164 | Err: fmt.Errorf("clientSecret missing"), 165 | } 166 | } 167 | if err := makeConf(confDir, confFiles.SaPath, clientJSON); err != nil { 168 | return WriteError, err 169 | } 170 | return 0, nil 171 | } 172 | 173 | func makeConf(confDir, confFile, content string) error { 174 | var rwxPerm fs.FileMode = 0700 175 | if err := os.MkdirAll(confDir, rwxPerm); err != nil { 176 | return fmt.Errorf("unable to get make config directory with error %s", err) 177 | } 178 | f, err := os.Create(confFile) 179 | if err != nil { 180 | return fmt.Errorf("unable to create the login file due to error %s", err) 181 | } 182 | defer func() { 183 | if err = f.Close(); err != nil { 184 | fmt.Printf("failed unable to write file with error %s\n", err) 185 | } 186 | }() 187 | writer := bufio.NewWriter(f) 188 | // safe to write after validation 189 | _, err = writer.Write([]byte(content)) 190 | if err != nil { 191 | return fmt.Errorf("error writing file") 192 | } 193 | err = writer.Flush() 194 | if err != nil { 195 | return fmt.Errorf("error finishing file") 196 | } 197 | fmt.Printf("Login information saved at %v\n", confFile) 198 | return nil 199 | } 200 | -------------------------------------------------------------------------------- /cmd/login_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package cmd contains all fo the commands for the cli 16 | package cmd 17 | 18 | import ( 19 | "fmt" 20 | "io" 21 | "os" 22 | "path" 23 | "testing" 24 | 25 | "github.com/datastax-labs/astra-cli/pkg" 26 | ) 27 | 28 | const testJSON = `{"clientId":"deeb55bd-2a55-4988-a345-d8fdddd0e0c9","clientName":"me@example.com","clientSecret":"6ae15bff-1435-430f-975b-9b3d9914b698"}` 29 | const testSecret = "jljlajef" 30 | const testName = "me@me.com" 31 | const testID = "abd278332" 32 | 33 | func usageFunc() error { 34 | return nil 35 | } 36 | func TestLoginCmdJson(t *testing.T) { 37 | clientJSON = testJSON 38 | defer func() { 39 | clientJSON = "" 40 | }() 41 | dir := path.Join(t.TempDir(), "config") 42 | f := path.Join(dir, "mytempFile") 43 | exitCode, err := executeLogin([]string{"--json", clientJSON}, func() (string, pkg.ConfFiles, error) { 44 | return dir, pkg.ConfFiles{ 45 | SaPath: f, 46 | }, nil 47 | }, usageFunc) 48 | defer os.RemoveAll(dir) 49 | if err != nil { 50 | t.Fatalf("unexpected error %v", err) 51 | } 52 | if exitCode != 0 { 53 | t.Fatalf("unexpected exit code %v", exitCode) 54 | } 55 | fd, err := os.Open(f) 56 | if err != nil { 57 | t.Fatalf("unexpected error %v", err) 58 | } 59 | b, err := io.ReadAll(fd) 60 | if err != nil { 61 | t.Fatalf("unexpected error %v", err) 62 | } 63 | if string(b) == "" { 64 | t.Error("login file was empty") 65 | } 66 | if clientJSON != string(b) { 67 | t.Errorf("expected '%v' but was '%v'", clientJSON, string(b)) 68 | } 69 | } 70 | 71 | func TestLoginCmdJsonInvalidPerms(t *testing.T) { 72 | clientJSON = testJSON 73 | authToken = "" 74 | clientID = "" 75 | clientName = "" 76 | clientSecret = "" 77 | defer func() { 78 | clientJSON = "" 79 | }() 80 | dir := t.TempDir() 81 | inaccessible := path.Join(dir, "inaccessible") 82 | err := os.Mkdir(inaccessible, 0400) 83 | if err != nil { 84 | t.Fatalf("unexpected error %v", err) 85 | } 86 | defer os.RemoveAll(inaccessible) 87 | f := path.Join(inaccessible, "mytempFile") 88 | exitCode, err := executeLogin([]string{"--json", clientJSON}, func() (string, pkg.ConfFiles, error) { 89 | return dir, pkg.ConfFiles{ 90 | SaPath: f, 91 | }, nil 92 | }, usageFunc) 93 | defer os.RemoveAll(dir) 94 | if err == nil { 95 | t.Error("expected error") 96 | } 97 | if exitCode != WriteError { 98 | t.Errorf("unexpected exit code %v", exitCode) 99 | } 100 | expected := fmt.Sprintf("unable to create the login file due to error open %v: permission denied", f) 101 | if err.Error() != expected { 102 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 103 | } 104 | } 105 | 106 | func TestArgs(t *testing.T) { 107 | clientID = `deeb55bd-2a55-4988-a345-d8fdddd0e0c9` 108 | clientName = "me@example.com" 109 | clientSecret = "fortestargs" 110 | defer func() { 111 | clientID = "" 112 | clientSecret = "" 113 | clientName = "" 114 | }() 115 | 116 | dir := path.Join(t.TempDir(), "config") 117 | f := path.Join(dir, "mytempFile") 118 | exitCode, err := executeLogin([]string{"--clientId", clientID, "--clientName", clientName, "--clientSecret", clientSecret}, func() (string, pkg.ConfFiles, error) { 119 | return dir, pkg.ConfFiles{ 120 | SaPath: f, 121 | }, nil 122 | }, usageFunc) 123 | defer os.RemoveAll(dir) 124 | if err != nil { 125 | t.Fatalf("unexpected error %v", err) 126 | } 127 | if exitCode != 0 { 128 | t.Fatalf("unexpected exit code %v", exitCode) 129 | } 130 | fd, err := os.Open(f) 131 | if err != nil { 132 | t.Fatalf("unexpected error %v", err) 133 | } 134 | b, err := io.ReadAll(fd) 135 | if err != nil { 136 | t.Fatalf("unexpected error %v", err) 137 | } 138 | if string(b) == "" { 139 | t.Error("login file was empty") 140 | } 141 | expected := `{"clientId":"deeb55bd-2a55-4988-a345-d8fdddd0e0c9","clientName":"me@example.com","clientSecret":"fortestargs"}` 142 | if expected != string(b) { 143 | t.Errorf("expected\n%v\nactual:\n%v", expected, string(b)) 144 | } 145 | } 146 | func TestArgsWithNoPermission(t *testing.T) { 147 | clientJSON = "" 148 | authToken = "" 149 | clientID = `deeb55bd-2a55-4988-a345-d8fdddd0e0c9` 150 | clientName = "me@example.com" 151 | clientSecret = "fortestargsnoperm" 152 | defer func() { 153 | clientID = "" 154 | clientSecret = "" 155 | clientName = "" 156 | }() 157 | 158 | dir := t.TempDir() 159 | inaccessible := path.Join(dir, "inaccessible") 160 | err := os.Mkdir(inaccessible, 0400) 161 | if err != nil { 162 | t.Fatalf("unexpected error %v", err) 163 | } 164 | defer os.RemoveAll(inaccessible) 165 | f := path.Join(inaccessible, "mytempFile") 166 | exitCode, err := executeLogin([]string{"--clientId", clientID, "--clientName", clientName, "--clientSecret", clientSecret}, func() (string, pkg.ConfFiles, error) { 167 | return dir, pkg.ConfFiles{ 168 | SaPath: f, 169 | }, nil 170 | }, usageFunc) 171 | defer os.RemoveAll(dir) 172 | if err == nil { 173 | t.Error("expected error") 174 | } 175 | if exitCode != WriteError { 176 | t.Errorf("unexpected exit code %v", exitCode) 177 | } 178 | expected := fmt.Sprintf("unable to create the login file due to error open %v: permission denied", f) 179 | if err.Error() != expected { 180 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 181 | } 182 | } 183 | 184 | func TestLoginArgsMissingId(t *testing.T) { 185 | clientJSON = "" 186 | authToken = "" 187 | clientName = testName 188 | clientSecret = testSecret 189 | clientID = "" 190 | defer func() { 191 | clientSecret = "" 192 | clientName = "" 193 | }() 194 | exitCode, err := executeLogin([]string{"--clientId", clientID, "--clientName", clientName, "--clientSecret", clientSecret}, func() (string, pkg.ConfFiles, error) { 195 | return "", pkg.ConfFiles{}, nil 196 | }, usageFunc) 197 | if err == nil { 198 | t.Error("expected error") 199 | } 200 | expected := `Unable to parse command line with args: --clientId, , --clientName, me@me.com, --clientSecret, jljlajef. Nested error was 'clientId missing'` 201 | if err.Error() != expected { 202 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 203 | } 204 | if exitCode != JSONError { 205 | t.Errorf("unexpected exit code %v", exitCode) 206 | } 207 | } 208 | 209 | func TestLoginArgsMissingName(t *testing.T) { 210 | clientJSON = "" 211 | authToken = "" 212 | clientName = "" 213 | clientSecret = testSecret 214 | clientID = testID 215 | defer func() { 216 | clientSecret = "" 217 | clientID = "" 218 | }() 219 | exitCode, err := executeLogin([]string{"--clientId", clientID, "--clientName", clientName, "--clientSecret", clientSecret}, func() (string, pkg.ConfFiles, error) { 220 | return "", pkg.ConfFiles{}, nil 221 | }, usageFunc) 222 | if err == nil { 223 | t.Errorf("expected error") 224 | } 225 | expected := `Unable to parse command line with args: --clientId, abd278332, --clientName, , --clientSecret, jljlajef. Nested error was 'clientName missing'` 226 | if err.Error() != expected { 227 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 228 | } 229 | if exitCode != JSONError { 230 | t.Errorf("unexpected exit code %v", exitCode) 231 | } 232 | } 233 | 234 | func TestLoginArgsMissingSecret(t *testing.T) { 235 | clientJSON = "" 236 | authToken = "" 237 | clientName = testName 238 | clientSecret = "" 239 | clientID = testID 240 | defer func() { 241 | clientName = "" 242 | clientID = "" 243 | }() 244 | exitCode, err := executeLogin([]string{"--clientId", clientID, "--clientName", clientName, "--clientSecret", clientSecret}, func() (string, pkg.ConfFiles, error) { 245 | return "", pkg.ConfFiles{}, nil 246 | }, usageFunc) 247 | if err == nil { 248 | t.Error("expected error") 249 | } 250 | expected := `Unable to parse command line with args: --clientId, abd278332, --clientName, me@me.com, --clientSecret, . Nested error was 'clientSecret missing'` 251 | if err.Error() != expected { 252 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 253 | } 254 | if exitCode != JSONError { 255 | t.Errorf("unexpected exit code %v", exitCode) 256 | } 257 | } 258 | 259 | func TestLoginHomeError(t *testing.T) { 260 | clientJSON = "invalidjson" 261 | defer func() { clientJSON = "" }() 262 | exitCode, err := executeLogin([]string{}, func() (string, pkg.ConfFiles, error) { 263 | return "", pkg.ConfFiles{}, fmt.Errorf("big error") 264 | }, usageFunc) 265 | if err == nil { 266 | t.Logf("expected error there was none and exit code was %v", exitCode) 267 | t.FailNow() 268 | } 269 | expected := "big error" 270 | if err.Error() != expected { 271 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 272 | } 273 | if exitCode != CannotFindHome { 274 | t.Errorf("unexpected exit code %v", exitCode) 275 | } 276 | } 277 | 278 | func TestLoginCmdJsonMissignId(t *testing.T) { 279 | clientJSON = `{"clientId":"","clientName":"me@example.com","clientSecret":"6ae15bff-1435-430f-975b-9b3d9914b698"}` 280 | defer func() { 281 | clientJSON = "" 282 | }() 283 | dir := path.Join(t.TempDir(), "config") 284 | f := path.Join(dir, "mytempFile") 285 | exitCode, err := executeLogin([]string{"--json", clientJSON}, func() (string, pkg.ConfFiles, error) { 286 | return dir, pkg.ConfFiles{ 287 | SaPath: f, 288 | }, nil 289 | }, usageFunc) 290 | defer os.RemoveAll(dir) 291 | if err == nil { 292 | t.Error("expected error") 293 | } 294 | expected := `Unable to parse command line with args: --json, {"clientId":"","clientName":"me@example.com","clientSecret":"6ae15bff-1435-430f-975b-9b3d9914b698"}. Nested error was 'clientId missing'` 295 | if err.Error() != expected { 296 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 297 | } 298 | if exitCode != JSONError { 299 | t.Errorf("unexpected exit code %v", exitCode) 300 | } 301 | } 302 | 303 | func TestLoginCmdJsonMissignName(t *testing.T) { 304 | clientJSON = `{"clientId":"deeb55bd-2a55-4988-a345-d8fdddd0e0c9","clientName":"","clientSecret":"6ae15bff-1435-430f-975b-9b3d9914b698"}` 305 | defer func() { 306 | clientJSON = "" 307 | }() 308 | dir := path.Join(t.TempDir(), "config") 309 | f := path.Join(dir, "mytempFile") 310 | exitCode, err := executeLogin([]string{"--json", clientJSON}, func() (string, pkg.ConfFiles, error) { 311 | return dir, pkg.ConfFiles{ 312 | SaPath: f, 313 | }, nil 314 | }, usageFunc) 315 | defer os.RemoveAll(dir) 316 | if err == nil { 317 | t.Errorf("expected error") 318 | } 319 | expected := `Unable to parse command line with args: --json, {"clientId":"deeb55bd-2a55-4988-a345-d8fdddd0e0c9","clientName":"","clientSecret":"6ae15bff-1435-430f-975b-9b3d9914b698"}. Nested error was 'clientName missing'` 320 | if err.Error() != expected { 321 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 322 | } 323 | if exitCode != JSONError { 324 | t.Errorf("unexpected exit code %v", exitCode) 325 | } 326 | } 327 | 328 | func TestLoginCmdJsonMissignSecret(t *testing.T) { 329 | clientJSON = `{"clientId":"deeb55bd-2a55-4988-a345-d8fdddd0e0c9","clientName":"me@example.com","clientSecret":""}` 330 | defer func() { 331 | clientJSON = "" 332 | }() 333 | dir := path.Join(t.TempDir(), "config") 334 | f := path.Join(dir, "mytempFile") 335 | exitCode, err := executeLogin([]string{"--json", clientJSON}, func() (string, pkg.ConfFiles, error) { 336 | return dir, pkg.ConfFiles{ 337 | SaPath: f, 338 | }, nil 339 | }, usageFunc) 340 | defer os.RemoveAll(dir) 341 | if err == nil { 342 | t.Error("expected error") 343 | } 344 | expected := `Unable to parse command line with args: --json, {"clientId":"deeb55bd-2a55-4988-a345-d8fdddd0e0c9","clientName":"me@example.com","clientSecret":""}. Nested error was 'clientSecret missing'` 345 | if err.Error() != expected { 346 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 347 | } 348 | if exitCode != JSONError { 349 | t.Errorf("unexpected exit code %v", exitCode) 350 | } 351 | } 352 | 353 | func TestLoginCmdJsonInvalid(t *testing.T) { 354 | clientJSON = `invalidtext` 355 | defer func() { 356 | clientJSON = "" 357 | }() 358 | dir := path.Join(t.TempDir(), "config") 359 | f := path.Join(dir, "mytempFile") 360 | exitCode, err := executeLogin([]string{"--json", clientJSON}, func() (string, pkg.ConfFiles, error) { 361 | return dir, pkg.ConfFiles{ 362 | SaPath: f, 363 | }, nil 364 | }, usageFunc) 365 | defer os.RemoveAll(dir) 366 | if err == nil { 367 | t.Errorf("expected error") 368 | } 369 | if exitCode != JSONError { 370 | t.Errorf("unexpected exit code %v", exitCode) 371 | } 372 | } 373 | 374 | func TestLoginToken(t *testing.T) { 375 | authToken = `6ae15bff-1435-430f-975b-9b3d9914b698` 376 | defer func() { 377 | authToken = "" 378 | }() 379 | dir := path.Join(t.TempDir(), "config") 380 | f := path.Join(dir, "mytempFile") 381 | exitCode, err := executeLogin([]string{"--token", authToken}, func() (string, pkg.ConfFiles, error) { 382 | return dir, pkg.ConfFiles{ 383 | TokenPath: f, 384 | }, nil 385 | }, usageFunc) 386 | defer os.RemoveAll(dir) 387 | if err != nil { 388 | t.Fatalf("unexpected error %v", err) 389 | } 390 | if exitCode != 0 { 391 | t.Fatalf("unexpected exit code %v", exitCode) 392 | } 393 | fd, err := os.Open(f) 394 | if err != nil { 395 | t.Fatalf("unexpected error %v", err) 396 | } 397 | b, err := io.ReadAll(fd) 398 | if err != nil { 399 | t.Fatalf("unexpected error %v", err) 400 | } 401 | if string(b) == "" { 402 | t.Error("login file was empty") 403 | } 404 | if authToken != string(b) { 405 | t.Errorf("expected '%v' but was '%v'", authToken, string(b)) 406 | } 407 | } 408 | 409 | func TestLoginTokenInvalidPerms(t *testing.T) { 410 | authToken = `6ae15bff-1435-430f-975b-9b3d9914b698` 411 | defer func() { 412 | authToken = "" 413 | }() 414 | dir := t.TempDir() 415 | inaccessible := path.Join(dir, "inaccessible") 416 | err := os.Mkdir(inaccessible, 0400) 417 | if err != nil { 418 | t.Fatalf("unexpected error %v", err) 419 | } 420 | defer os.RemoveAll(inaccessible) 421 | f := path.Join(inaccessible, "mytempFile") 422 | exitCode, err := executeLogin([]string{"--token", authToken}, func() (string, pkg.ConfFiles, error) { 423 | return dir, pkg.ConfFiles{ 424 | TokenPath: f, 425 | }, nil 426 | }, usageFunc) 427 | defer os.RemoveAll(dir) 428 | if err == nil { 429 | t.Error("expected error") 430 | } 431 | if exitCode != WriteError { 432 | t.Errorf("unexpected exit code %v", exitCode) 433 | } 434 | expected := fmt.Sprintf("unable to create the login file due to error open %v: permission denied", f) 435 | if err.Error() != expected { 436 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 437 | } 438 | } 439 | 440 | func TestMakeConf(t *testing.T) { 441 | content := "mycontent" 442 | dir := t.TempDir() 443 | f := path.Join(dir, "mytempFile") 444 | err := makeConf(dir, f, content) 445 | if err != nil { 446 | t.Fatalf("unable to create conf with error %v", err) 447 | } 448 | fd, err := os.Open(f) 449 | if err != nil { 450 | t.Fatalf("unable to read conf with error %v", err) 451 | } 452 | b, err := io.ReadAll(fd) 453 | if err != nil { 454 | t.Fatalf("unable to read conf with error %v", err) 455 | } 456 | str := string(b) 457 | if str != content { 458 | t.Errorf("expected '%v' but was '%v'", content, str) 459 | } 460 | } 461 | 462 | func TestMakeConfWithInaccessibleDir(t *testing.T) { 463 | dir := t.TempDir() 464 | inaccessible := path.Join(dir, "inaccessible") 465 | err := os.Mkdir(inaccessible, 0400) 466 | if err != nil { 467 | t.Fatalf("unexpected error %v", err) 468 | } 469 | defer os.RemoveAll(inaccessible) 470 | f := path.Join(inaccessible, "mytempFile") 471 | err = makeConf(inaccessible, f, "mycontent") 472 | if err == nil { 473 | t.Fatalf("expected error") 474 | } 475 | expected := fmt.Sprintf("unable to create the login file due to error open %v: permission denied", f) 476 | if err.Error() != expected { 477 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 478 | } 479 | } 480 | 481 | func TestMakeConfWithNonMakeableDir(t *testing.T) { 482 | dir := t.TempDir() 483 | inaccessible := path.Join(dir, "inaccessible") 484 | err := os.Mkdir(inaccessible, 0400) 485 | if err != nil { 486 | t.Fatalf("unexpected error %v", err) 487 | } 488 | defer os.RemoveAll(inaccessible) 489 | newDir := path.Join(inaccessible, "new") 490 | f := path.Join(newDir, "mytempFile") 491 | err = makeConf(newDir, f, "mycontent") 492 | if err == nil { 493 | t.Fatalf("expected error") 494 | } 495 | expected := fmt.Sprintf("unable to get make config directory with error mkdir %v: permission denied", newDir) 496 | if err.Error() != expected { 497 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package cmd contains all fo the commands for the cli 16 | package cmd 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/datastax-labs/astra-cli/pkg" 23 | "github.com/datastax-labs/astra-cli/pkg/env" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | func init() { 28 | RootCmd.PersistentFlags().BoolVarP(&env.Verbose, "verbose", "v", false, "turns on verbose logging") 29 | RootCmd.PersistentFlags().StringVarP(&pkg.Env, "env", "e", "prod", "environment to automate, other options are test and dev") 30 | RootCmd.AddCommand(loginCmd) 31 | RootCmd.AddCommand(dbCmd) 32 | } 33 | 34 | // RootCmd is the entry point for the whole app 35 | var RootCmd = &cobra.Command{ 36 | Use: "astra-cli", 37 | Short: "An easy to use client for automating DataStax Astra", 38 | Long: `Manage and provision databases on DataStax Astra 39 | Complete documentation is available at https://github.com/datastax-labs/astra-cli`, 40 | Run: func(cobraCmd *cobra.Command, args []string) { 41 | if err := executeRoot(cobraCmd.Usage); err != nil { 42 | os.Exit(1) 43 | } 44 | }, 45 | } 46 | 47 | func executeRoot(usage func() error) error { 48 | if err := usage(); err != nil { 49 | return fmt.Errorf("warn unable to show usage %v", err) 50 | } 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /cmd/root_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package cmd contains all fo the commands for the cli 16 | package cmd 17 | 18 | import ( 19 | "bytes" 20 | "errors" 21 | "io/ioutil" 22 | "testing" 23 | ) 24 | 25 | func TestRootUsageFails(t *testing.T) { 26 | fails := func() error { 27 | return errors.New("error showing usage") 28 | } 29 | err := executeRoot(fails) 30 | if err == nil { 31 | t.Fatal("there is supposed to be an error") 32 | } 33 | expected := "warn unable to show usage error showing usage" 34 | if err.Error() != expected { 35 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 36 | } 37 | } 38 | 39 | func TestRootShowHelp(t *testing.T) { 40 | originalOut := RootCmd.OutOrStderr() 41 | defer func() { 42 | RootCmd.SetOut(originalOut) 43 | }() 44 | b := bytes.NewBufferString("") 45 | RootCmd.SetOut(b) 46 | RootCmd.SetArgs([]string{}) 47 | err := RootCmd.Execute() 48 | if err != nil { 49 | t.Errorf("unexpected error '%v'", err) 50 | } 51 | out, err := ioutil.ReadAll(b) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | expected := RootCmd.UsageString() 56 | if string(out) != expected { 57 | t.Errorf("expected\n'%q'\nbut was\n'%q'", expected, string(out)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/datastax-labs/astra-cli 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/datastax/astra-client-go/v2 v2.2.12 7 | github.com/spf13/cobra v1.2.1 8 | ) 9 | 10 | require ( 11 | github.com/deepmap/oapi-codegen v1.9.1 // indirect 12 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 13 | github.com/spf13/pflag v1.0.5 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/datastax-labs/astra-cli/cmd" 22 | ) 23 | 24 | func main() { 25 | if err := cmd.RootCmd.Execute(); err != nil { 26 | fmt.Fprintf(os.Stderr, "unhandled error executing command %v", err) 27 | os.Exit(1) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/authenticated_client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package pkg is the top level package for shared libraries 16 | package pkg 17 | 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "io/ioutil" 23 | "log" 24 | "net" 25 | "net/http" 26 | "os" 27 | "strings" 28 | "time" 29 | 30 | "github.com/datastax-labs/astra-cli/pkg/env" 31 | "github.com/datastax/astra-client-go/v2/astra" 32 | ) 33 | 34 | // Error when the api has an error this is the structure 35 | type Error struct { 36 | // API specific error code 37 | ID int 38 | // User-friendly description of error 39 | Message string 40 | } 41 | 42 | // ErrorResponse when the API has an error 43 | type ErrorResponse struct { 44 | Errors []Error 45 | } 46 | 47 | func closeBody(res *http.Response) { 48 | if err := res.Body.Close(); err != nil { 49 | fmt.Fprintf(os.Stderr, "unable to close request body '%v'", err) 50 | } 51 | } 52 | 53 | func handleErrors(body []byte, statusCode string) error { 54 | var errorStrings []string 55 | 56 | var resObj ErrorResponse 57 | err := json.Unmarshal(body, &resObj) 58 | if err != nil { 59 | return fmt.Errorf("CRITIAL ERROR unable to decode error response with error: '%v'. body was '%v' and http status was %v", err, string(body), statusCode) 60 | } 61 | for _, e := range resObj.Errors { 62 | errorString := fmt.Sprintf("(%v:%v)", e.ID, e.Message) 63 | errorStrings = append(errorStrings, errorString) 64 | } 65 | return fmt.Errorf("%v with status code: %v", strings.Join(errorStrings, ", "), statusCode) 66 | } 67 | 68 | // FormatErrors puts the API errors into a well formatted text output 69 | func FormatErrors(es []Error) string { 70 | var formatted []string 71 | for _, e := range es { 72 | formatted = append(formatted, fmt.Sprintf("ID: %v Text: '%v'", e.ID, e.Message)) 73 | } 74 | return strings.Join(formatted, ", ") 75 | } 76 | 77 | // AuthenticatedClient has a token and the methods to query the Astra DevOps API 78 | type AuthenticatedClient struct { 79 | token string 80 | client *http.Client 81 | astraclient *astra.ClientWithResponses 82 | timeoutSeconds int 83 | verbose bool 84 | } 85 | 86 | func newHTTPClient() *http.Client { 87 | expectTimeout := 1 88 | defaultTimeout := 10 89 | connections := 10 90 | return &http.Client{ 91 | Timeout: time.Duration(defaultTimeout) * time.Second, 92 | Transport: &http.Transport{ 93 | MaxIdleConns: connections, 94 | MaxConnsPerHost: connections, 95 | MaxIdleConnsPerHost: connections, 96 | Dial: (&net.Dialer{ 97 | Timeout: time.Duration(defaultTimeout) * time.Second, 98 | KeepAlive: time.Duration(defaultTimeout) * time.Second, 99 | }).Dial, 100 | TLSHandshakeTimeout: time.Duration(defaultTimeout) * time.Second, 101 | ResponseHeaderTimeout: time.Duration(defaultTimeout) * time.Second, 102 | ExpectContinueTimeout: time.Duration(expectTimeout) * time.Second, 103 | }, 104 | } 105 | } 106 | 107 | func timeoutContext(timeSeconds int) (context.Context, context.CancelFunc) { 108 | return context.WithDeadline( 109 | context.Background(), 110 | time.Now().Add(time.Duration(timeSeconds)*time.Second), 111 | ) 112 | } 113 | 114 | func AuthenticateToken(token string, verbose bool) (*AuthenticatedClient, error) { 115 | astraClient, err := astra.NewClientWithResponses(apiURL(), func(c *astra.Client) error { 116 | c.RequestEditors = append(c.RequestEditors, func(ctx context.Context, req *http.Request) error { 117 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 118 | return nil 119 | }) 120 | return nil 121 | }) 122 | if err != nil { 123 | return &AuthenticatedClient{}, fmt.Errorf("unexpected error setting up devops api client: %v", err) 124 | } 125 | timeout := 10 126 | authenticatedClient := &AuthenticatedClient{ 127 | verbose: verbose, 128 | timeoutSeconds: timeout, 129 | astraclient: astraClient, 130 | client: newHTTPClient(), 131 | token: fmt.Sprintf("Bearer %s", token), 132 | } 133 | return authenticatedClient, nil 134 | } 135 | 136 | func Authenticate(clientInfo ClientInfo, verbose bool) (*AuthenticatedClient, error) { 137 | timeout := 10 138 | tokenInput := astra.AuthenticateServiceAccountTokenJSONRequestBody{ 139 | ClientId: clientInfo.ClientID, 140 | ClientName: clientInfo.ClientName, 141 | ClientSecret: clientInfo.ClientSecret, 142 | } 143 | astraClientTmp, err := astra.NewClientWithResponses(apiURL()) 144 | if err != nil { 145 | return &AuthenticatedClient{}, fmt.Errorf("unexpected error setting up devops api client: %v", err) 146 | } 147 | ctx, cancel := timeoutContext(timeout) 148 | defer cancel() 149 | response, err := astraClientTmp.AuthenticateServiceAccountTokenWithResponse(ctx, tokenInput) 150 | if err != nil { 151 | return &AuthenticatedClient{}, fmt.Errorf("unexpected error logging into devops api client: %v", err) 152 | } 153 | if response.StatusCode() != http.StatusOK { 154 | return &AuthenticatedClient{}, fmt.Errorf("unexpected error logging into devops api client: %v - %v", response.StatusCode(), response.Status()) 155 | } 156 | token := response.JSON200.Token 157 | astraClient, err := astra.NewClientWithResponses(apiURL(), func(c *astra.Client) error { 158 | c.RequestEditors = append(c.RequestEditors, func(ctx context.Context, req *http.Request) error { 159 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *token)) 160 | return nil 161 | }) 162 | return nil 163 | }) 164 | 165 | if err != nil { 166 | return &AuthenticatedClient{}, fmt.Errorf("unexpected error logging into devops api client: %v", err) 167 | } 168 | authenticatedClient := &AuthenticatedClient{ 169 | token: fmt.Sprintf("Bearer %s", *token), 170 | verbose: verbose, 171 | timeoutSeconds: timeout, 172 | astraclient: astraClient, 173 | client: newHTTPClient(), 174 | } 175 | if err != nil { 176 | return &AuthenticatedClient{}, fmt.Errorf("unexpected error authenticating: %v", err) 177 | } 178 | 179 | return authenticatedClient, nil 180 | } 181 | 182 | func apiURL() string { 183 | if env.Verbose { 184 | log.Printf("env is %v", Env) 185 | } 186 | var url string 187 | switch Env { 188 | case "dev": 189 | url = "https://api.dev.cloud.datastax.com" 190 | case "test": 191 | url = "https://api.test.cloud.datastax.com" 192 | default: 193 | url = "https://api.astra.datastax.com" 194 | } 195 | if env.Verbose { 196 | log.Printf("api url is %v", url) 197 | } 198 | return url 199 | } 200 | 201 | func dbURL() string { 202 | url := fmt.Sprintf("%v/v2/databases", apiURL()) 203 | if env.Verbose { 204 | log.Printf("db url is %v", url) 205 | } 206 | return url 207 | } 208 | 209 | var Env = "prod" 210 | 211 | func (a *AuthenticatedClient) ctx() (context.Context, context.CancelFunc) { 212 | return timeoutContext(a.timeoutSeconds) 213 | } 214 | 215 | func (a *AuthenticatedClient) setHeaders(req *http.Request) { 216 | req.Header.Set("Accept", "application/json") 217 | req.Header.Set("Authorization", a.token) 218 | req.Header.Set("Content-Type", "application/json") 219 | } 220 | 221 | // WaitUntil will keep checking the database for the requested status until it is available. Eventually it will timeout if the operation is not 222 | // yet complete. 223 | // * @param id string - the database id to find 224 | // * @param tries int - number of attempts 225 | // * @param intervalSeconds int - seconds to wait between tries 226 | // * @param status StatusEnum - status to wait for 227 | // @returns (Database, error) 228 | func (a *AuthenticatedClient) WaitUntil(id string, tries int, intervalSeconds int, status ...astra.StatusEnum) (astra.Database, error) { 229 | for i := 0; i < tries; i++ { 230 | time.Sleep(time.Duration(intervalSeconds) * time.Second) 231 | db, err := a.FindDb(id) 232 | if err != nil { 233 | if a.verbose { 234 | log.Printf("db %s not able to be found with error '%v' trying again %v more times", id, err, tries-i-1) 235 | } else { 236 | fmt.Print(".") 237 | } 238 | continue 239 | } 240 | 241 | if db.Status == astra.StatusEnumERROR { 242 | return db, fmt.Errorf("database %v in error status, exiting", id) 243 | } 244 | var statusStrings []string 245 | for _, s := range status { 246 | if db.Status == s { 247 | return db, nil 248 | } 249 | statusStrings = append(statusStrings, fmt.Sprintf("%v", s)) 250 | } 251 | if a.verbose { 252 | log.Printf("db %s in state(s) %v but expected %v trying again %v more times", id, db.Status, strings.Join(statusStrings, ", "), tries-i-1) 253 | } else { 254 | fmt.Print(".") 255 | } 256 | } 257 | return astra.Database{}, fmt.Errorf("unable to find db id %s with status %s after %v seconds", id, status, intervalSeconds*tries) 258 | } 259 | 260 | // ListDb find all databases that match the parameters 261 | // * @param "include" (optional.string) - Allows filtering so that databases in listed states are returned 262 | // * @param "provider" (optional.string) - Allows filtering so that databases from a given provider are returned 263 | // * @param "startingAfter" (optional.string) - Optional parameter for pagination purposes. Used as this value for starting retrieving a specific page of results 264 | // * @param "limit" (optional.int) - Optional parameter for pagination purposes. Specify the number of items for one page of data 265 | // @return ([]Database, error) 266 | func (a *AuthenticatedClient) ListDb(include string, provider string, startingAfter string, limit int) ([]astra.Database, error) { 267 | var params astra.ListDatabasesParams 268 | if len(include) > 0 { 269 | astraInclude := astra.ListDatabasesParamsInclude(include) 270 | params.Include = &astraInclude 271 | } 272 | if len(provider) > 0 { 273 | astraProvider := astra.ListDatabasesParamsProvider(provider) 274 | params.Provider = &astraProvider 275 | } 276 | if len(startingAfter) > 0 { 277 | params.StartingAfter = astra.StringPtr(startingAfter) 278 | } 279 | if limit > 0 { 280 | limitInt := limit 281 | params.Limit = &limitInt 282 | } 283 | ctx, cancel := a.ctx() 284 | defer cancel() 285 | dbs, err := a.astraclient.ListDatabasesWithResponse(ctx, ¶ms) 286 | if err != nil { 287 | return []astra.Database{}, fmt.Errorf("unexpected error listing databases '%v'", err) 288 | } 289 | if dbs.StatusCode() != http.StatusOK { 290 | return []astra.Database{}, handleErrors(dbs.Body, dbs.Status()) 291 | } 292 | 293 | return *dbs.JSON200, nil 294 | } 295 | 296 | // CreateDb creates a database in Astra, username and password fields are required only on legacy tiers and waits until it is in a created state 297 | // * @param createDb Definition of new database 298 | // @return (Database, error) 299 | func (a *AuthenticatedClient) CreateDb(createDb astra.DatabaseInfoCreate) (astra.Database, error) { 300 | ctx, cancel := a.ctx() 301 | defer cancel() 302 | response, err := a.astraclient.CreateDatabaseWithResponse(ctx, astra.CreateDatabaseJSONRequestBody(createDb)) 303 | if err != nil { 304 | return astra.Database{}, err 305 | } 306 | if response.StatusCode() != http.StatusCreated { 307 | return astra.Database{}, handleErrors(response.Body, response.Status()) 308 | } 309 | id := response.HTTPResponse.Header.Get("location") 310 | 311 | tries := 90 312 | interval := 30 313 | db, err := a.WaitUntil(id, tries, interval, astra.StatusEnumACTIVE) 314 | if err != nil { 315 | return db, fmt.Errorf("waiting for status check on create db failed because '%v'", err) 316 | } 317 | return db, nil 318 | } 319 | 320 | // FindDb Returns specified database 321 | // * @param databaseID string representation of the database ID 322 | // @return (Database, error) 323 | func (a *AuthenticatedClient) FindDb(databaseID string) (astra.Database, error) { 324 | ctx, cancel := a.ctx() 325 | defer cancel() 326 | dbs, err := a.astraclient.GetDatabaseWithResponse(ctx, astra.DatabaseIdParam(databaseID)) 327 | if err != nil { 328 | return astra.Database{}, fmt.Errorf("failed creating request to find db with id %s with: %w", databaseID, err) 329 | } 330 | if dbs.StatusCode() != http.StatusOK { 331 | return astra.Database{}, handleErrors(dbs.Body, dbs.Status()) 332 | } 333 | return *dbs.JSON200, nil 334 | } 335 | 336 | // AddKeyspaceToDb Adds keyspace into database 337 | // * @param databaseID string representation of the database ID 338 | // * @param keyspaceName Name of database keyspace 339 | // @return error 340 | func (a *AuthenticatedClient) AddKeyspaceToDb(databaseID string, keyspaceName string) error { 341 | ctx, cancel := a.ctx() 342 | defer cancel() 343 | res, err := a.astraclient.AddKeyspaceWithResponse(ctx, astra.DatabaseIdParam(databaseID), astra.KeyspaceNameParam(keyspaceName)) 344 | if err != nil { 345 | return fmt.Errorf("failed creating request to add keyspace to db with id %s with: %w", databaseID, err) 346 | } 347 | if res.StatusCode() != http.StatusOK { 348 | return handleErrors(res.Body, res.Status()) 349 | } 350 | return nil 351 | } 352 | 353 | // GetSecureBundle Returns a temporary URL to download a zip file with certificates for connecting to the database. 354 | // The URL expires after five minutes.<p>There are two types of the secure bundle URL: <ul> 355 | // * @param databaseID string representation of the database ID 356 | // @return (SecureBundle, error) 357 | func (a *AuthenticatedClient) GetSecureBundle(databaseID string) (astra.CredsURL, error) { 358 | ctx, cancel := a.ctx() 359 | defer cancel() 360 | res, err := a.astraclient.GenerateSecureBundleURLWithResponse(ctx, astra.DatabaseIdParam(databaseID)) 361 | 362 | if err != nil { 363 | return astra.CredsURL{}, fmt.Errorf("failed get secure bundle for database id %s with: %w", databaseID, err) 364 | } 365 | if res.StatusCode() != http.StatusOK { 366 | return astra.CredsURL{}, handleErrors(res.Body, res.Status()) 367 | } 368 | return *res.JSON200, nil 369 | } 370 | 371 | // Terminate deletes the database at the specified id and will block until it shows up as deleted or is removed from the system 372 | // * @param databaseID string representation of the database ID 373 | // * @param "PreparedStateOnly" - For internal use only. Used to safely terminate prepared databases 374 | // @return error 375 | func (a *AuthenticatedClient) Terminate(id string, preparedStateOnly bool) error { 376 | ctx, cancel := a.ctx() 377 | defer cancel() 378 | res, err := a.astraclient.TerminateDatabaseWithResponse(ctx, astra.DatabaseIdParam(id), &astra.TerminateDatabaseParams{ 379 | PreparedStateOnly: &preparedStateOnly, 380 | }) 381 | if err != nil { 382 | return err 383 | } 384 | if res.StatusCode() != http.StatusAccepted { 385 | return handleErrors(res.Body, res.Status()) 386 | } 387 | tries := 30 388 | interval := 10 389 | _, err = a.WaitUntil(id, tries, interval, astra.StatusEnumTERMINATED, astra.StatusEnumTERMINATING, astra.StatusEnumUNKNOWN) 390 | return err 391 | } 392 | 393 | // ParkAsync parks the database at the specified id. Note you cannot park a serverless database 394 | // * @param databaseID string representation of the database ID 395 | // @return error 396 | func (a *AuthenticatedClient) ParkAsync(databaseID string) error { 397 | req, err := http.NewRequest("POST", fmt.Sprintf("%s/%s/park", dbURL(), databaseID), http.NoBody) 398 | if err != nil { 399 | return fmt.Errorf("failed creating request to park db with id %s with: %w", databaseID, err) 400 | } 401 | a.setHeaders(req) 402 | res, err := a.client.Do(req) 403 | if err != nil { 404 | return fmt.Errorf("failed to park database id %s with: %w", databaseID, err) 405 | } 406 | defer closeBody(res) 407 | if res.StatusCode != http.StatusAccepted { 408 | defer res.Body.Close() 409 | b, err := ioutil.ReadAll(res.Body) 410 | if err != nil { 411 | return fmt.Errorf("unable to read response body for park operation to db %v with error '%v'. http status of request was %v", databaseID, err, res.StatusCode) 412 | } 413 | return handleErrors(b, res.Status) 414 | } 415 | return nil 416 | } 417 | 418 | // Park parks the database at the specified id and will block until the database is parked 419 | // * @param databaseID string representation of the database ID 420 | // @return error 421 | func (a *AuthenticatedClient) Park(databaseID string) error { 422 | err := a.ParkAsync(databaseID) 423 | if err != nil { 424 | return fmt.Errorf("park db failed because '%v'", err) 425 | } 426 | tries := 30 427 | interval := 30 428 | _, err = a.WaitUntil(databaseID, tries, interval, astra.StatusEnumPARKED) 429 | if err != nil { 430 | return fmt.Errorf("unable to check status for park db because of error '%v'", err) 431 | } 432 | return nil 433 | } 434 | 435 | // UnparkAsync unparks the database at the specified id. NOTE you cannot unpark a serverless database 436 | // * @param databaseID String representation of the database ID 437 | // @return error 438 | func (a *AuthenticatedClient) UnparkAsync(databaseID string) error { 439 | req, err := http.NewRequest("POST", fmt.Sprintf("%s/%s/unpark", dbURL(), databaseID), http.NoBody) 440 | if err != nil { 441 | return fmt.Errorf("failed creating request to unpark db with id %s with: %w", databaseID, err) 442 | } 443 | a.setHeaders(req) 444 | res, err := a.client.Do(req) 445 | if err != nil { 446 | return fmt.Errorf("failed to unpark database id %s with: %w", databaseID, err) 447 | } 448 | defer closeBody(res) 449 | if res.StatusCode != http.StatusAccepted { 450 | defer res.Body.Close() 451 | b, err := ioutil.ReadAll(res.Body) 452 | if err != nil { 453 | return fmt.Errorf("unable to read response body for unpark operation to db %v with error '%v'. http status of request was %v", databaseID, err, res.StatusCode) 454 | } 455 | return handleErrors(b, res.Status) 456 | } 457 | return nil 458 | } 459 | 460 | // Unpark unparks the database at the specified id and will block until the database is unparked 461 | // * @param databaseID String representation of the database ID 462 | // @return error 463 | func (a *AuthenticatedClient) Unpark(databaseID string) error { 464 | err := a.UnparkAsync(databaseID) 465 | if err != nil { 466 | return fmt.Errorf("unpark db failed because '%v'", err) 467 | } 468 | tries := 60 469 | interval := 30 470 | _, err = a.WaitUntil(databaseID, tries, interval, astra.StatusEnumACTIVE) 471 | if err != nil { 472 | return fmt.Errorf("unable to check status for unpark db because of error '%v'", err) 473 | } 474 | return nil 475 | } 476 | 477 | // Resize a database. Total number of capacity units desired should be specified. Reducing a size of a database is not supported at this time. Note you cannot resize a serverless database 478 | // * @param databaseID string representation of the database ID 479 | // * @param capacityUnits int32 containing capacityUnits key with a value greater than the current number of capacity units (max increment of 3 additional capacity units) 480 | // @return error 481 | func (a *AuthenticatedClient) Resize(databaseID string, capacityUnits int) error { 482 | ctx, cancel := a.ctx() 483 | defer cancel() 484 | res, err := a.astraclient.ResizeDatabaseWithResponse(ctx, astra.DatabaseIdParam(databaseID), astra.ResizeDatabaseJSONRequestBody{ 485 | CapacityUnits: &capacityUnits, 486 | }) 487 | if err != nil { 488 | return fmt.Errorf("failed to resize database for database id %s with: %w", databaseID, err) 489 | } 490 | if res.StatusCode() != http.StatusAccepted { 491 | return handleErrors(res.Body, res.Status()) 492 | } 493 | return nil 494 | } 495 | 496 | // ResetPassword changes the password for the database at the specified id 497 | // * @param databaseID string representation of the database ID 498 | // * @param username string containing username 499 | // * @param password string containing password. The specified password will be updated for the specified database user 500 | // @return error 501 | func (a *AuthenticatedClient) ResetPassword(databaseID, username, password string) error { 502 | ctx, cancel := a.ctx() 503 | defer cancel() 504 | res, err := a.astraclient.ResetPasswordWithResponse(ctx, astra.DatabaseIdParam(databaseID), astra.ResetPasswordJSONRequestBody{ 505 | Username: astra.StringPtr(username), 506 | Password: astra.StringPtr(password), 507 | }) 508 | if err != nil { 509 | return fmt.Errorf("failed to reset password for database id %s with: %w", databaseID, err) 510 | } 511 | 512 | if res.StatusCode() != http.StatusOK { 513 | return handleErrors(res.Body, res.Status()) 514 | } 515 | return nil 516 | } 517 | 518 | // GetTierInfo Returns all supported tier, cloud, region, count, and capacitity combinations 519 | // @return ([]TierInfo, error) 520 | func (a *AuthenticatedClient) GetTierInfo() ([]astra.AvailableRegionCombination, error) { 521 | ctx, cancel := a.ctx() 522 | defer cancel() 523 | res, err := a.astraclient.ListAvailableRegionsWithResponse(ctx) 524 | if err != nil { 525 | return []astra.AvailableRegionCombination{}, fmt.Errorf("failed listing tier info with: %w", err) 526 | } 527 | 528 | if res.StatusCode() != http.StatusOK { 529 | return []astra.AvailableRegionCombination{}, handleErrors(res.Body, res.Status()) 530 | } 531 | return *res.JSON200, nil 532 | } 533 | -------------------------------------------------------------------------------- /pkg/conf.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package pkg is the top level package for shared libraries 16 | package pkg 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | "io" 22 | "os" 23 | "path" 24 | "strings" 25 | ) 26 | 27 | // ClientInfo provides access to 28 | type ClientInfo struct { 29 | ClientSecret string 30 | ClientName string 31 | ClientID string 32 | } 33 | 34 | // ConfFiles supports both formats of credentials and will say if the token one is present 35 | type ConfFiles struct { 36 | TokenPath string 37 | SaPath string 38 | } 39 | 40 | // HasServiceAccount returns true if there is a service account file present and accessible 41 | func (c ConfFiles) HasServiceAccount() (bool, error) { 42 | if _, err := os.Stat(c.SaPath); err != nil { 43 | if os.IsNotExist(err) { 44 | return false, nil 45 | } 46 | return false, fmt.Errorf("warning error of %v is unexpected", err) 47 | } 48 | return true, nil 49 | } 50 | 51 | // HasToken returns true if there is a token file present and accessible 52 | func (c ConfFiles) HasToken() (bool, error) { 53 | if _, err := os.Stat(c.TokenPath); err != nil { 54 | if os.IsNotExist(err) { 55 | return false, nil 56 | } 57 | return false, fmt.Errorf("warning error of %v is unexpected", err) 58 | } 59 | return true, nil 60 | } 61 | 62 | // GetHome returns the configuration directory and file 63 | // error will return if there is no user home folder 64 | func GetHome(getHome func() (string, error)) (confDir string, confFiles ConfFiles, err error) { 65 | var home string 66 | home, err = getHome() 67 | if err != nil { 68 | return "", ConfFiles{}, fmt.Errorf("unable to get user home directory with error '%s'", err) 69 | } 70 | confDir = path.Join(home, ".config", "astra") 71 | 72 | tokenFile := path.Join(confDir, PathWithEnv("token")) 73 | saFile := path.Join(confDir, PathWithEnv("sa.json")) 74 | return confDir, ConfFiles{ 75 | TokenPath: tokenFile, 76 | SaPath: saFile, 77 | }, nil 78 | } 79 | 80 | func PathWithEnv(f string) string { 81 | if strings.Contains(f, string(os.PathSeparator)) { 82 | tokens := strings.Split(f, string(os.PathSeparator)) 83 | tokenLen := len(tokens) 84 | if tokenLen > 0 { 85 | last := tokens[tokenLen-1] 86 | tokens[tokenLen-1] = Env + "_" + last 87 | return strings.Join(tokens, string(os.PathSeparator)) 88 | } 89 | } 90 | return Env + "_" + f 91 | } 92 | 93 | // ReadToken retrieves the login from the specified json file 94 | func ReadToken(tokenFile string) (string, error) { 95 | f, err := os.Open(tokenFile) 96 | if err != nil { 97 | return "", &FileNotFoundError{ 98 | Path: tokenFile, 99 | Err: fmt.Errorf("unable to read login file with error '%w'", err), 100 | } 101 | } 102 | defer func() { 103 | if err := f.Close(); err != nil { 104 | fmt.Printf("warning unable to close %v with error '%v'", tokenFile, err) 105 | } 106 | }() 107 | b, err := io.ReadAll(f) 108 | if err != nil { 109 | return "", fmt.Errorf("unable to read login file '%s' with error '%w'", tokenFile, err) 110 | } 111 | if len(b) == 0 { 112 | return "", fmt.Errorf("token file '%s' is empty", tokenFile) 113 | } 114 | token := strings.Trim(string(b), "\n") 115 | if !strings.HasPrefix(token, "AstraCS") { 116 | return "", fmt.Errorf("missing prefix 'AstraCS' in token file '%s'", tokenFile) 117 | } 118 | return token, nil 119 | } 120 | 121 | // ReadLogin retrieves the login from the specified json file 122 | func ReadLogin(saJSONFile string) (ClientInfo, error) { 123 | f, err := os.Open(saJSONFile) 124 | if err != nil { 125 | return ClientInfo{}, &FileNotFoundError{ 126 | Path: saJSONFile, 127 | Err: fmt.Errorf("unable to read login file with error %w", err), 128 | } 129 | } 130 | defer func() { 131 | if err := f.Close(); err != nil { 132 | fmt.Printf("warning unable to close %v with error %v", saJSONFile, err) 133 | } 134 | }() 135 | b, err := io.ReadAll(f) 136 | if err != nil { 137 | return ClientInfo{}, fmt.Errorf("unable to read login file %s with error %w", saJSONFile, err) 138 | } 139 | var clientInfo ClientInfo 140 | err = json.Unmarshal(b, &clientInfo) 141 | if err != nil { 142 | return ClientInfo{}, &JSONParseError{ 143 | Original: string(b), 144 | Err: fmt.Errorf("unable to parse json from login file %s with error %s", saJSONFile, err), 145 | } 146 | } 147 | if clientInfo.ClientID == "" { 148 | return ClientInfo{}, fmt.Errorf("Invalid service account: Client ID for service account is empty for file '%v'", saJSONFile) 149 | } 150 | if clientInfo.ClientName == "" { 151 | return ClientInfo{}, fmt.Errorf("Invalid service account: Client name for service account is empty for file '%v'", saJSONFile) 152 | } 153 | if clientInfo.ClientSecret == "" { 154 | return ClientInfo{}, fmt.Errorf("Invalid service account: Client secret for service account is empty for file '%v'", saJSONFile) 155 | } 156 | return clientInfo, err 157 | } 158 | -------------------------------------------------------------------------------- /pkg/conf_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package pkg is the top level package for shared libraries 16 | package pkg 17 | 18 | import ( 19 | "errors" 20 | "testing" 21 | ) 22 | 23 | func TestPathWithEnvWhenPath(t *testing.T) { 24 | newPath := PathWithEnv("/test/sa.json") 25 | if newPath != "/test/prod_sa.json" { 26 | t.Errorf("expected path of /test/prod_sa.json but was %v", newPath) 27 | } 28 | } 29 | 30 | func TestPathWithEnvWhenNoPath(t *testing.T) { 31 | newPath := PathWithEnv("sa.json") 32 | if newPath != "prod_sa.json" { 33 | t.Errorf("expected path of prod_sa.json but was %v", newPath) 34 | } 35 | } 36 | 37 | func TestReadLogin(t *testing.T) { 38 | c, err := ReadLogin("testdata/sa.json") 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | name := "me@example.com" 43 | if c.ClientName != name { 44 | t.Errorf("expected %v but was %v", name, c.ClientName) 45 | } 46 | id := "deeb55bd-2a55-4988-a345-d8fdddd0e0c9" 47 | if c.ClientID != id { 48 | t.Errorf("expected %v but was %v", id, c.ClientID) 49 | } 50 | testClientSec := "6ae15bff-1435-430f-975b-9b3d9914b698" 51 | if c.ClientSecret != testClientSec { 52 | t.Errorf("expected %v but was %v", testClientSec, c.ClientSecret) 53 | } 54 | } 55 | 56 | func TestReadLoginWithNoFile(t *testing.T) { 57 | _, err := ReadLogin("testdata/not-a-real-file.json") 58 | if err == nil { 59 | t.Fatal("expected an error but there was none") 60 | } 61 | var e *FileNotFoundError 62 | if !errors.As(err, &e) { 63 | t.Errorf("expected %T but was %T", e, err) 64 | } 65 | } 66 | 67 | func TestReadLoginWithEmptyFile(t *testing.T) { 68 | _, err := ReadLogin("testdata/empty.json") 69 | if err == nil { 70 | t.Fatal("expected an error but there was none") 71 | } 72 | var e *JSONParseError 73 | if !errors.As(err, &e) { 74 | t.Errorf("expected %T but was %T", e, err) 75 | } 76 | } 77 | 78 | func TestUnableToGetHomeFolder(t *testing.T) { 79 | _, _, err := GetHome(func() (string, error) { return "", errors.New("unable to get home") }) 80 | if err == nil { 81 | t.Fatal("expected error but none was present") 82 | } 83 | } 84 | 85 | func TestReadTokenWithNoFile(t *testing.T) { 86 | _, err := ReadToken("testdata/notthere") 87 | if err == nil { 88 | t.Fatal("expected an error but there was none") 89 | } 90 | var e *FileNotFoundError 91 | if !errors.As(err, &e) { 92 | t.Errorf("expected %T but was %T", e, err) 93 | } 94 | } 95 | 96 | func TestMissingId(t *testing.T) { 97 | _, err := ReadLogin("testdata/missing-id.json") 98 | if err == nil { 99 | t.Fatal("expected an error but there was none") 100 | } 101 | expected := "Invalid service account: Client ID for service account is empty for file 'testdata/missing-id.json'" 102 | if err.Error() != expected { 103 | t.Errorf("expected '%v' but was '%v'", expected, err) 104 | } 105 | } 106 | 107 | func TestMissingName(t *testing.T) { 108 | _, err := ReadLogin("testdata/missing-name.json") 109 | if err == nil { 110 | t.Fatal("expected an error but there was none") 111 | } 112 | expected := "Invalid service account: Client name for service account is empty for file 'testdata/missing-name.json'" 113 | if err.Error() != expected { 114 | t.Errorf("expected '%v' but was '%v'", expected, err) 115 | } 116 | } 117 | 118 | func TestMissingSecret(t *testing.T) { 119 | _, err := ReadLogin("testdata/missing-secret.json") 120 | if err == nil { 121 | t.Fatal("expected an error but there was none") 122 | } 123 | expected := "Invalid service account: Client secret for service account is empty for file 'testdata/missing-secret.json'" 124 | if err.Error() != expected { 125 | t.Errorf("expected '%v' but was '%v'", expected, err) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /pkg/const.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package pkg is the top level package for shared libraries 16 | package pkg 17 | 18 | const ( 19 | // JSONFormat is for the command line flag -o 20 | JSONFormat = "json" 21 | // TextFormat is for the command line flag -o 22 | TextFormat = "text" 23 | ) 24 | -------------------------------------------------------------------------------- /pkg/env/env.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package env is the package where global environment configuration goes 16 | package env 17 | 18 | // Verbose sets the verbose mode for the command 19 | var Verbose bool 20 | -------------------------------------------------------------------------------- /pkg/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package pkg is the top level package for shared libraries 16 | package pkg 17 | 18 | import ( 19 | "fmt" 20 | "strings" 21 | ) 22 | 23 | // ParseError is used to indicate there is an error in the command line args 24 | type ParseError struct { 25 | Args []string 26 | Err error 27 | } 28 | 29 | // Error outpus the error with the args provided, if there are no args that becomes the error 30 | func (p *ParseError) Error() string { 31 | if len(p.Args) == 0 { 32 | return "no args provided" 33 | } 34 | return fmt.Sprintf("Unable to parse command line with args: %v. Nested error was '%v'", strings.Join(p.Args, ", "), p.Err) 35 | } 36 | 37 | // JSONParseError when unable to read JSON 38 | type JSONParseError struct { 39 | Original string 40 | Err error 41 | } 42 | 43 | // Error returns the error string 44 | func (j *JSONParseError) Error() string { 45 | return fmt.Sprintf("JSON parsing error for json '%v' with error '%v'", j.Original, j.Err) 46 | } 47 | 48 | // FileNotFoundError when unable to read file 49 | type FileNotFoundError struct { 50 | Path string 51 | Err error 52 | } 53 | 54 | // Error returns the error string 55 | func (j *FileNotFoundError) Error() string { 56 | return fmt.Sprintf("Unable to find file '%v' with error: '%s'", j.Path, j.Err) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/errors_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package pkg is the top level package for shared libraries 16 | package pkg 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | "testing" 22 | ) 23 | 24 | func TestParseErrorNoArgs(t *testing.T) { 25 | parseError := ParseError{ 26 | Err: errors.New("bogus error"), 27 | } 28 | 29 | expected := "no args provided" 30 | if parseError.Error() != expected { 31 | t.Errorf("expected '%v' but was '%v'", expected, parseError.Error()) 32 | } 33 | } 34 | 35 | func TestParseError(t *testing.T) { 36 | parseError := ParseError{ 37 | Args: []string{"a", "b"}, 38 | Err: errors.New("bogus error"), 39 | } 40 | 41 | expected := "Unable to parse command line with args: a, b. Nested error was 'bogus error'" 42 | if parseError.Error() != expected { 43 | t.Errorf("expected '%v' but was '%v'", expected, parseError.Error()) 44 | } 45 | } 46 | 47 | func TestFileNotFoundError(t *testing.T) { 48 | fileErr := FileNotFoundError{ 49 | Path: "/a/b/C", 50 | Err: fmt.Errorf("Bogus Error"), 51 | } 52 | expected := "Unable to find file '/a/b/C' with error: 'Bogus Error'" 53 | if fileErr.Error() != expected { 54 | t.Errorf("expected '%v' but was '%v'", expected, fileErr.Error()) 55 | } 56 | } 57 | 58 | func TestJSONParseError(t *testing.T) { 59 | fileErr := JSONParseError{ 60 | Original: "invalid string", 61 | Err: fmt.Errorf("Bogus Error"), 62 | } 63 | expected := "JSON parsing error for json 'invalid string' with error 'Bogus Error'" 64 | if fileErr.Error() != expected { 65 | t.Errorf("expected '%v' but was '%v'", expected, fileErr.Error()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/httputils/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package httputils provides common http functions and utilities 16 | package httputils 17 | 18 | import ( 19 | "fmt" 20 | "io" 21 | "net" 22 | "net/http" 23 | "os" 24 | "time" 25 | ) 26 | 27 | const connections = 10 28 | const standardTimeOut = 5 * time.Second 29 | const dialTimeout = 10 * time.Second 30 | const expectContinueResponse = 1 * time.Second 31 | 32 | // NewHTTPClient fires up client with 'better' defaults 33 | func NewHTTPClient() *http.Client { 34 | return &http.Client{ 35 | Timeout: standardTimeOut, 36 | Transport: &http.Transport{ 37 | MaxIdleConns: connections, 38 | MaxConnsPerHost: connections, 39 | MaxIdleConnsPerHost: connections, 40 | Dial: (&net.Dialer{ 41 | Timeout: dialTimeout, 42 | KeepAlive: dialTimeout, 43 | }).Dial, 44 | TLSHandshakeTimeout: standardTimeOut, 45 | ResponseHeaderTimeout: standardTimeOut, 46 | ExpectContinueTimeout: expectContinueResponse, 47 | }, 48 | } 49 | } 50 | 51 | // DownloadZip pulls down the URL listed and saves it to the specified location 52 | func DownloadZip(downloadURL string, secBundleLoc string) (int64, error) { 53 | httpClient := NewHTTPClient() 54 | res, err := httpClient.Get(downloadURL) 55 | if err != nil { 56 | return -1, fmt.Errorf("unable to download zip with error %v", err) 57 | } 58 | defer func() { 59 | err = res.Body.Close() 60 | if err != nil { 61 | fmt.Fprintf(os.Stderr, "Warn: error closing http response body %v\n for request %v with status code %v\n", err, downloadURL, res.StatusCode) 62 | } 63 | }() 64 | f, err := os.Create(secBundleLoc) 65 | if err != nil { 66 | return -1, fmt.Errorf("unable to create file to save too %v", err) 67 | } 68 | defer func() { 69 | err = f.Close() 70 | if err != nil { 71 | fmt.Fprintf(os.Stderr, "Warn: error closing file %v for file %v\n", err, secBundleLoc) 72 | } 73 | }() 74 | i, err := io.Copy(f, res.Body) 75 | if err != nil { 76 | return -1, fmt.Errorf("unable to copy downloaded file to %v", err) 77 | } 78 | return i, nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/httputils/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package httputils provides common http functions and utilities 16 | package httputils 17 | 18 | import ( 19 | "fmt" 20 | "net/http" 21 | "net/http/httptest" 22 | "os" 23 | "path" 24 | "testing" 25 | ) 26 | 27 | func TestDownloadUrl(t *testing.T) { 28 | zipContent := "zip file content" 29 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 30 | fmt.Fprintln(w, zipContent) 31 | })) 32 | defer ts.Close() 33 | tmpDir := t.TempDir() 34 | zipFile := path.Join(tmpDir, "bundle.zip") 35 | bytesWritten, err := DownloadZip(ts.URL, zipFile) 36 | if bytesWritten == 0 { 37 | t.Fatal("Expected bytes to be written but none were") 38 | } 39 | 40 | if err != nil { 41 | t.Fatalf("Unexpected error test '%v'", err) 42 | } 43 | 44 | b, err := os.ReadFile(zipFile) 45 | if err != nil { 46 | t.Fatalf("Unexpected error reading file '%v'", err) 47 | } 48 | expectedZipContent := fmt.Sprintf("%v\n", zipContent) 49 | if expectedZipContent != string(b) { 50 | t.Errorf("expected/actual \n'%q'\n'%q'", expectedZipContent, string(b)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/login.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package pkg is the top level package for shared libraries 16 | package pkg 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/datastax-labs/astra-cli/pkg/env" 23 | astraops "github.com/datastax/astra-client-go/v2/astra" 24 | ) 25 | 26 | // LoginService provides interface to implement logins and produce an Client 27 | type LoginService interface { 28 | Login() (Client, error) 29 | } 30 | 31 | // Client is the abstraction for client interactions. Allows alternative db management clients 32 | type Client interface { 33 | CreateDb(astraops.DatabaseInfoCreate) (astraops.Database, error) 34 | Terminate(string, bool) error 35 | FindDb(string) (astraops.Database, error) 36 | ListDb(string, string, string, int) ([]astraops.Database, error) 37 | Park(string) error 38 | Unpark(string) error 39 | Resize(string, int) error 40 | GetSecureBundle(string) (astraops.CredsURL, error) 41 | GetTierInfo() ([]astraops.AvailableRegionCombination, error) 42 | } 43 | 44 | // Creds knows how handle and store credentials 45 | type Creds struct { 46 | GetHomeFunc func() (string, error) // optional. If not specified os.UserHomeDir is used for log base directory to find creds 47 | } 48 | 49 | // Login logs into the Astra DevOps API using the local configuration provided by the 'astra-cli login' command 50 | func (c *Creds) Login() (Client, error) { 51 | getHome := c.GetHomeFunc 52 | if getHome == nil { 53 | getHome = os.UserHomeDir 54 | } 55 | confDir, confFile, err := GetHome(getHome) 56 | if err != nil { 57 | return &AuthenticatedClient{}, fmt.Errorf("unable to read conf dir with error '%v'", err) 58 | } 59 | hasToken, err := confFile.HasToken() 60 | if err != nil { 61 | return &AuthenticatedClient{}, fmt.Errorf("unable to read token file '%v' with error '%v'", confFile.TokenPath, err) 62 | } 63 | var client *AuthenticatedClient 64 | if hasToken { 65 | token, err := ReadToken(confFile.TokenPath) 66 | if err != nil { 67 | return &AuthenticatedClient{}, fmt.Errorf("found token at '%v' but unable to read token with error '%v'", confFile.TokenPath, err) 68 | } 69 | return AuthenticateToken(token, env.Verbose) 70 | } 71 | hasSa, err := confFile.HasServiceAccount() 72 | if err != nil { 73 | return &AuthenticatedClient{}, fmt.Errorf("unable to read service account file '%v' with error '%v'", confFile.SaPath, err) 74 | } 75 | if !hasSa { 76 | return &AuthenticatedClient{}, fmt.Errorf("unable to access any file for directory `%v`, run astra-cli login first", confDir) 77 | } 78 | clientInfo, err := ReadLogin(confFile.SaPath) 79 | if err != nil { 80 | return &AuthenticatedClient{}, err 81 | } 82 | client, err = Authenticate(clientInfo, env.Verbose) 83 | if err != nil { 84 | return &AuthenticatedClient{}, fmt.Errorf("authenticate failed with error %v", err) 85 | } 86 | return client, nil 87 | } 88 | -------------------------------------------------------------------------------- /pkg/login_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package pkg is the top level package for shared libraries 16 | package pkg 17 | 18 | import ( 19 | "fmt" 20 | "path" 21 | "testing" 22 | ) 23 | 24 | func TestUnableToReadHomeDir(t *testing.T) { 25 | noPath := func() (string, error) { return "", fmt.Errorf("unexpected error") } 26 | creds := &Creds{ 27 | GetHomeFunc: noPath, 28 | } 29 | _, err := creds.Login() 30 | if err == nil { 31 | t.Fatal("expected an error on an empty path") 32 | } 33 | expected := "unable to read conf dir with error 'unable to get user home directory with error 'unexpected error''" 34 | if err.Error() != expected { 35 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 36 | } 37 | } 38 | func TestMissingConfigFolder(t *testing.T) { 39 | noPath := func() (string, error) { return "", nil } 40 | creds := &Creds{ 41 | GetHomeFunc: noPath, 42 | } 43 | _, err := creds.Login() 44 | if err == nil { 45 | t.Fatal("expected an error on an empty path") 46 | } 47 | expected := "unable to access any file for directory `.config/astra`, run astra-cli login first" 48 | if err.Error() != expected { 49 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 50 | } 51 | } 52 | 53 | func TestLoginWithInvalidTokenFile(t *testing.T) { 54 | invalid := func() (string, error) { return path.Join("testdata", "with_invalid_token"), nil } 55 | creds := &Creds{ 56 | GetHomeFunc: invalid, 57 | } 58 | _, err := creds.Login() 59 | if err == nil { 60 | t.Fatal("expected an error on an empty path") 61 | } 62 | expected := "found token at 'testdata/with_invalid_token/.config/astra/prod_token' but unable to read token with error 'missing prefix 'AstraCS' in token file 'testdata/with_invalid_token/.config/astra/prod_token''" 63 | if err.Error() != expected { 64 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 65 | } 66 | } 67 | 68 | func TestLoginWithEmptyTokenFile(t *testing.T) { 69 | invalid := func() (string, error) { return path.Join("testdata", "with_empty_token"), nil } 70 | creds := &Creds{ 71 | GetHomeFunc: invalid, 72 | } 73 | _, err := creds.Login() 74 | if err == nil { 75 | t.Fatal("expected an error on an empty path") 76 | } 77 | expected := "found token at 'testdata/with_empty_token/.config/astra/prod_token' but unable to read token with error 'token file 'testdata/with_empty_token/.config/astra/prod_token' is empty'" 78 | if err.Error() != expected { 79 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 80 | } 81 | } 82 | 83 | func TestLoginValidToken(t *testing.T) { 84 | valid := func() (string, error) { return path.Join("testdata", "with_token"), nil } 85 | creds := &Creds{ 86 | GetHomeFunc: valid, 87 | } 88 | _, err := creds.Login() 89 | if err != nil { 90 | t.Fatalf("unexpected error '%v'", err) 91 | } 92 | } 93 | 94 | func TestLoginWithInvalidSA(t *testing.T) { 95 | invalid := func() (string, error) { return path.Join("testdata", "with_invalid_sa"), nil } 96 | creds := &Creds{ 97 | GetHomeFunc: invalid, 98 | } 99 | _, err := creds.Login() 100 | if err == nil { 101 | t.Fatal("expected an error on an empty path") 102 | } 103 | expected := "Invalid service account: Client ID for service account is empty for file 'testdata/with_invalid_sa/.config/astra/prod_sa.json'" 104 | if err.Error() != expected { 105 | t.Errorf("expected '%v' but was '%v'", expected, err.Error()) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /pkg/strfmt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package pkg is the top level package for shared libraries 16 | package pkg 17 | 18 | import ( 19 | "fmt" 20 | "io" 21 | "strings" 22 | "text/tabwriter" 23 | ) 24 | 25 | // WriteRows outputs a flexiable right aligned tabwriter 26 | func WriteRows(w io.Writer, rows [][]string) error { 27 | tw := tabwriter.NewWriter(w, 0, 0, 1, ' ', 0) 28 | for i, row := range rows { 29 | rowStr := strings.Join(row, "\t") 30 | if i > 0 { 31 | fmt.Fprint(tw, "\n") 32 | } 33 | fmt.Fprint(tw, rowStr) 34 | } 35 | return tw.Flush() 36 | } 37 | -------------------------------------------------------------------------------- /pkg/strfmt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package pkg is the top level package for shared libraries 16 | package pkg 17 | 18 | import ( 19 | "bytes" 20 | "strings" 21 | "testing" 22 | ) 23 | 24 | func TestTabWriterLayout(t *testing.T) { 25 | w := bytes.NewBufferString("") 26 | rows := [][]string{ 27 | { 28 | "abc", "def", "ghi", 29 | }, 30 | { 31 | "", "123456", "1", 32 | }, 33 | } 34 | err := WriteRows(w, rows) 35 | if err != nil { 36 | t.Fatalf("unexpected error %v", err) 37 | } 38 | expected1 := "abc def ghi" 39 | expected2 := " 123456 1" 40 | expected := strings.Join([]string{expected1, expected2}, "\n") 41 | if w.String() != expected { 42 | t.Errorf("expected/actual \n'%v'\n'%v'", expected, w.String()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/testdata/empty.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datastax-labs/astra-cli/2584f839263f04f87cb753e7788da9f098f420fc/pkg/testdata/empty.json -------------------------------------------------------------------------------- /pkg/testdata/missing-id.json: -------------------------------------------------------------------------------- 1 | {"clientId":"","clientName":"me@example.com","clientSecret":"6ae15bff-1435-430f-975b-9b3d9914b698"} -------------------------------------------------------------------------------- /pkg/testdata/missing-name.json: -------------------------------------------------------------------------------- 1 | {"clientId":"deeb55bd-2a55-4988-a345-d8fdddd0e0c9","clientName":"","clientSecret":"6ae15bff-1435-430f-975b-9b3d9914b698"} 2 | -------------------------------------------------------------------------------- /pkg/testdata/missing-secret.json: -------------------------------------------------------------------------------- 1 | {"clientId":"deeb55bd-2a55-4988-a345-d8fdddd0e0c9","clientName":"me@example.com","clientSecret":""} -------------------------------------------------------------------------------- /pkg/testdata/sa.json: -------------------------------------------------------------------------------- 1 | {"clientId":"deeb55bd-2a55-4988-a345-d8fdddd0e0c9","clientName":"me@example.com","clientSecret":"6ae15bff-1435-430f-975b-9b3d9914b698"} 2 | 3 | -------------------------------------------------------------------------------- /pkg/testdata/with_empty_token/.config/astra/prod_token: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datastax-labs/astra-cli/2584f839263f04f87cb753e7788da9f098f420fc/pkg/testdata/with_empty_token/.config/astra/prod_token -------------------------------------------------------------------------------- /pkg/testdata/with_invalid_sa/.config/astra/prod_sa.json: -------------------------------------------------------------------------------- 1 | {"clientId":"","clientName":"","clientSecret":""} -------------------------------------------------------------------------------- /pkg/testdata/with_invalid_token/.config/astra/prod_token: -------------------------------------------------------------------------------- 1 | thisisinvalid -------------------------------------------------------------------------------- /pkg/testdata/with_token/.config/astra/prod_token: -------------------------------------------------------------------------------- 1 | AstraCS:thisisavalidtoken -------------------------------------------------------------------------------- /pkg/tests/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package test is for test utilies and mocks 16 | package test 17 | 18 | import astraops "github.com/datastax/astra-client-go/v2/astra" 19 | 20 | // LoginError is a pretty common error message 21 | const LoginError = "unable to login with error no db" 22 | 23 | // MockClient is used for testing 24 | type MockClient struct { 25 | ErrorQueue []error 26 | calls []interface{} 27 | Databases []astraops.Database 28 | Tiers []astraops.AvailableRegionCombination 29 | Bundle astraops.CredsURL 30 | } 31 | 32 | // getError pops the next error stored off the stack 33 | func (c *MockClient) getError() error { 34 | var err error 35 | if len(c.ErrorQueue) > 0 { 36 | err = c.ErrorQueue[0] 37 | c.ErrorQueue[0] = nil 38 | c.ErrorQueue = c.ErrorQueue[1:] 39 | } 40 | return err 41 | } 42 | 43 | // getError pops the next db object stored off the stack 44 | func (c *MockClient) getDb() astraops.Database { 45 | var db astraops.Database 46 | if len(c.Databases) > 0 { 47 | db = c.Databases[0] 48 | c.Databases = c.Databases[1:] 49 | } 50 | return db 51 | } 52 | 53 | // Call returns a call at the specified index 54 | func (c *MockClient) Call(index int) interface{} { 55 | return c.calls[index] 56 | } 57 | 58 | // Calls returns all calls made in order 59 | func (c *MockClient) Calls() []interface{} { 60 | return c.calls 61 | } 62 | 63 | // CreateDb returns the next error and the next db created 64 | func (c *MockClient) CreateDb(db astraops.DatabaseInfoCreate) (astraops.Database, error) { 65 | c.calls = append(c.calls, db) 66 | return c.getDb(), c.getError() 67 | } 68 | 69 | // Terminate returns the next error and stores the id used, internal is ignored 70 | func (c *MockClient) Terminate(id string, internal bool) error { 71 | c.calls = append(c.calls, id) 72 | return c.getError() 73 | } 74 | 75 | // FindDb returns the next database and next error, the id call is stored 76 | func (c *MockClient) FindDb(id string) (astraops.Database, error) { 77 | c.calls = append(c.calls, id) 78 | return c.getDb(), c.getError() 79 | } 80 | 81 | // ListDb returns all databases and stores the arguments as an interface array 82 | func (c *MockClient) ListDb(include string, provider string, startingAfter string, limit int) ([]astraops.Database, error) { 83 | c.calls = append(c.calls, []interface{}{ 84 | include, 85 | provider, 86 | startingAfter, 87 | limit, 88 | }) 89 | return c.Databases, c.getError() 90 | } 91 | 92 | // Unpark returns the next error, the id call is stored 93 | func (c *MockClient) Unpark(id string) error { 94 | c.calls = append(c.calls, id) 95 | return c.getError() 96 | } 97 | 98 | // Park returns the next error, the id call is stored 99 | func (c *MockClient) Park(id string) error { 100 | c.calls = append(c.calls, id) 101 | return c.getError() 102 | } 103 | 104 | // Resize returns the next error, the id call and size is stored 105 | func (c *MockClient) Resize(id string, size int) error { 106 | c.calls = append(c.calls, []interface{}{id, size}) 107 | return c.getError() 108 | } 109 | 110 | // GetSecureBundle returns the next error, the secured bundle stored, and the id call is stored 111 | func (c *MockClient) GetSecureBundle(id string) (astraops.CredsURL, error) { 112 | c.calls = append(c.calls, id) 113 | return c.Bundle, c.getError() 114 | } 115 | 116 | // GetTierInfo returns the next error, and the tierinfo objects stored 117 | func (c *MockClient) GetTierInfo() ([]astraops.AvailableRegionCombination, error) { 118 | return c.Tiers, c.getError() 119 | } 120 | -------------------------------------------------------------------------------- /pkg/tests/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 DataStax 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package test is for test utilies and mocks 16 | package test 17 | 18 | import ( 19 | "errors" 20 | "testing" 21 | 22 | astraops "github.com/datastax/astra-client-go/v2/astra" 23 | ) 24 | 25 | func TestGetError(t *testing.T) { 26 | client := &MockClient{ 27 | ErrorQueue: []error{ 28 | errors.New("error 1"), 29 | errors.New("error 2"), 30 | errors.New("error 3"), 31 | }, 32 | } 33 | err := client.getError() 34 | if err.Error() != "error 1" { 35 | t.Errorf("expected 'error 1' but was '%v'", err.Error()) 36 | } 37 | err = client.getError() 38 | if err.Error() != "error 2" { 39 | t.Errorf("expected 'error 2' but was '%v'", err.Error()) 40 | } 41 | err = client.getError() 42 | if err.Error() != "error 3" { 43 | t.Errorf("expected 'error 3' but was '%v'", err.Error()) 44 | } 45 | err = client.getError() 46 | if err != nil { 47 | t.Errorf("expected nil but was '%v'", err.Error()) 48 | } 49 | } 50 | 51 | func TestGetDB(t *testing.T) { 52 | client := &MockClient{ 53 | Databases: []astraops.Database{ 54 | {Id: "1"}, 55 | {Id: "2"}, 56 | {Id: "3"}, 57 | }, 58 | } 59 | id := client.getDb().Id 60 | if id != "1" { 61 | t.Errorf("expected '1' but was '%v'", id) 62 | } 63 | id = client.getDb().Id 64 | if id != "2" { 65 | t.Errorf("expected '2' but was '%v'", id) 66 | } 67 | id = client.getDb().Id 68 | if id != "3" { 69 | t.Errorf("expected '3' but was '%v'", id) 70 | } 71 | id = client.getDb().Id 72 | if id != "" { 73 | t.Errorf("expected '' but was '%v'", id) 74 | } 75 | } 76 | 77 | func TestPark(t *testing.T) { 78 | client := &MockClient{} 79 | id := "123" 80 | err := client.Park(id) 81 | if err != nil { 82 | t.Fatal("unexpected error") 83 | } 84 | if client.Call(0) != id { 85 | t.Errorf("expected '%v' but was '%v'", id, client.Call(0)) 86 | } 87 | if len(client.Calls()) != 1 { 88 | t.Errorf("expected '%v' but was '%v'", 1, len(client.Calls())) 89 | } 90 | } 91 | 92 | func TestUnpark(t *testing.T) { 93 | client := &MockClient{} 94 | id := "parkid" 95 | err := client.Unpark(id) 96 | if err != nil { 97 | t.Fatal("unexpected error") 98 | } 99 | if client.Call(0) != id { 100 | t.Errorf("expected '%v' but was '%v'", id, client.Call(0)) 101 | } 102 | if len(client.Calls()) != 1 { 103 | t.Errorf("expected '%v' but was '%v'", 1, len(client.Calls())) 104 | } 105 | } 106 | 107 | func TestTerminate(t *testing.T) { 108 | client := &MockClient{} 109 | id := "termid" 110 | err := client.Terminate(id, false) 111 | if err != nil { 112 | t.Fatal("unexpected error") 113 | } 114 | if client.Call(0) != id { 115 | t.Errorf("expected '%v' but was '%v'", id, client.Call(0)) 116 | } 117 | if len(client.Calls()) != 1 { 118 | t.Errorf("expected '%v' but was '%v'", 1, len(client.Calls())) 119 | } 120 | } 121 | 122 | func TestGetSecurteBundleId(t *testing.T) { 123 | url := "myurl" 124 | client := &MockClient{ 125 | Bundle: astraops.CredsURL{ 126 | DownloadURL: url, 127 | }, 128 | } 129 | id := "secid" 130 | bundle, err := client.GetSecureBundle(id) 131 | if err != nil { 132 | t.Fatal("unexpected error") 133 | } 134 | if bundle.DownloadURL != url { 135 | t.Errorf("expected '%v' but was '%v'", url, bundle.DownloadURL) 136 | } 137 | if client.Call(0) != id { 138 | t.Errorf("expected '%v' but was '%v'", id, client.Call(0)) 139 | } 140 | if len(client.Calls()) != 1 { 141 | t.Errorf("expected '%v' but was '%v'", 1, len(client.Calls())) 142 | } 143 | } 144 | 145 | func TestFindDb(t *testing.T) { 146 | id := "DSQ" 147 | 148 | client := &MockClient{ 149 | Databases: []astraops.Database{ 150 | {Id: id}, 151 | {Id: "fakeid"}, 152 | }, 153 | } 154 | db, err := client.FindDb(id) 155 | if err != nil { 156 | t.Fatal("unexpected error") 157 | } 158 | if db.Id != id { 159 | t.Errorf("expected '%v' but was '%v'", id, db.Id) 160 | } 161 | if client.Call(0) != id { 162 | t.Errorf("expected '%v' but was '%v'", id, client.Call(0)) 163 | } 164 | if len(client.Calls()) != 1 { 165 | t.Errorf("expected '%v' but was '%v'", 1, len(client.Calls())) 166 | } 167 | } 168 | 169 | func TestCreateDb(t *testing.T) { 170 | id := "DSQ" 171 | 172 | client := &MockClient{ 173 | Databases: []astraops.Database{ 174 | {Id: id}, 175 | {Id: "fakeid"}, 176 | }, 177 | } 178 | db, err := client.CreateDb(astraops.DatabaseInfoCreate{ 179 | Name: "myname", 180 | }) 181 | if err != nil { 182 | t.Fatal("unexpected error") 183 | } 184 | if db.Id != id { 185 | t.Errorf("expected '%v' but was '%v'", id, db.Id) 186 | } 187 | if client.Call(0).(astraops.DatabaseInfoCreate).Name != "myname" { 188 | t.Errorf("expected '%v' but was '%v'", "myname", client.Call(0).(astraops.DatabaseInfoCreate).Name) 189 | } 190 | if len(client.Calls()) != 1 { 191 | t.Errorf("expected '%v' but was '%v'", 1, len(client.Calls())) 192 | } 193 | } 194 | 195 | func TestResize(t *testing.T) { 196 | client := &MockClient{} 197 | id := "987" 198 | size := 10 199 | err := client.Resize(id, size) 200 | if err != nil { 201 | t.Fatal("unexpected error") 202 | } 203 | actual := client.Call(0).([]interface{}) 204 | if actual[0].(string) != id { 205 | t.Errorf("expected '%v' but was '%v'", id, actual[0]) 206 | } 207 | if actual[1].(int) != size { 208 | t.Errorf("expected '%v' but was '%v'", size, actual[1]) 209 | } 210 | if len(client.Calls()) != 1 { 211 | t.Errorf("expected '%v' but was '%v'", 1, len(client.Calls())) 212 | } 213 | } 214 | 215 | func TestTiers(t *testing.T) { 216 | client := &MockClient{ 217 | Tiers: []astraops.AvailableRegionCombination{ 218 | {Tier: "abc"}, 219 | }, 220 | } 221 | tiers, err := client.GetTierInfo() 222 | if err != nil { 223 | t.Fatal("unexpected error") 224 | } 225 | 226 | if tiers[0].Tier != "abc" { 227 | t.Errorf("expected '%v' but was '%v'", "abc", tiers[0].Tier) 228 | } 229 | 230 | if len(client.Calls()) != 0 { 231 | t.Errorf("expected '%v' but was '%v'", 0, len(client.Calls())) 232 | } 233 | } 234 | 235 | func TestListdDb(t *testing.T) { 236 | id1 := "1" 237 | id2 := "2" 238 | include := "filter" 239 | provider := "gcp" 240 | starting := "today" 241 | limit := 1000 242 | client := &MockClient{ 243 | Databases: []astraops.Database{ 244 | {Id: id1}, 245 | {Id: id2}, 246 | }, 247 | } 248 | dbs, err := client.ListDb(include, provider, starting, limit) 249 | if err != nil { 250 | t.Fatal("unexpected error") 251 | } 252 | if len(dbs) != 2 { 253 | t.Errorf("expected '%v' but was '%v'", 2, len(dbs)) 254 | } 255 | calls := client.Calls() 256 | if len(calls) != 1 { 257 | t.Errorf("expected '%v' but was '%v'", 1, len(calls)) 258 | } 259 | args := calls[0].([]interface{}) 260 | actualInclude := args[0].(string) 261 | if actualInclude != include { 262 | t.Errorf("expected '%v' but was '%v'", include, actualInclude) 263 | } 264 | actualProvider := args[1].(string) 265 | if actualProvider != provider { 266 | t.Errorf("expected '%v' but was '%v'", provider, actualProvider) 267 | } 268 | actualStarting := args[2].(string) 269 | if actualStarting != starting { 270 | t.Errorf("expected '%v' but was '%v'", starting, actualStarting) 271 | } 272 | actualLimit := args[3].(int) 273 | if actualLimit != limit { 274 | t.Errorf("expected '%v' but was '%v'", limit, actualLimit) 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /script/all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 3 | 4 | $DIR/lint 5 | $DIR/test 6 | $DIR/clean 7 | $DIR/build 8 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 DataStax 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the « License »); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an « AS IS » BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # script/bootstrap: Resolve all dependencies that the application requires to 17 | # run. 18 | 19 | 20 | 21 | 22 | if ! command -v go &> /dev/null 23 | then 24 | echo "os $(uname -s) arch $(uname -m)" 25 | if [ "$(uname -s)" = "Darwin" ]; then 26 | echo "install via homebrew" 27 | brew update 28 | brew install go 29 | fi 30 | 31 | if [ "$(uname -s)" = "Linux" ] && [ "$(uname -m)" = "armv7l" ]; then 32 | echo "arm found installing go" 33 | curl -L -O https://golang.org/dl/go1.16.linux-armv6l.tar.gz 34 | sudo tar -C /usr/local -xzf go1.16.linux-armv6l.tar.gz 35 | echo "add ‘export PATH=\$PATH:/usr/local/go/bin’ to your .bashrc" 36 | rm go1.16.linux-armv6l.tar.gz 37 | fi 38 | 39 | if [ "$(uname -s)" = "Linux" ] && [ "$(uname -m)" = "amd64" ]; then 40 | echo "amd64 found installing go" 41 | curl -L -O https://golang.org/dl/go1.16.linux-amd64.tar.gz 42 | sudo tar -C /usr/local -xzf go1.16.linux-amd64.tar.gz 43 | echo "add 'export PATH=\$PATH:/usr/local/go/bin' to your .bashrc" 44 | rm go1.16.linux-arm64.tar.gz 45 | fi 46 | 47 | else 48 | echo "go installed skipping" 49 | fi 50 | 51 | if ! command -v golangci-lint &> /dev/null 52 | then 53 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.43.0 54 | else 55 | echo "golangci-lint installed skipping" 56 | fi 57 | -------------------------------------------------------------------------------- /script/build: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/bash 3 | 4 | 5 | # Copyright 2022 DataStax 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the « License »); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an « AS IS » BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | rm -fr ./bin 20 | mkdir ./bin 21 | go build -o bin/astra . 22 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 DataStax 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the « License »); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an « AS IS » BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 16 | 17 | # script/cibuild: Setup environment for CI to run tests. This is primarily 18 | # designed to run on the continuous integration server. 19 | 20 | 21 | $DIR/setup && \ 22 | $DIR/lint && \ 23 | $DIR/test && \ 24 | $DIR/build -------------------------------------------------------------------------------- /script/clean: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -fr ./bin 4 | mkdir ./bin 5 | -------------------------------------------------------------------------------- /script/cover-html: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | t="/tmp/go-cover.$$.tmp" 4 | go test -race -covermode=atomic -coverprofile=$t ./... && go tool cover -html=$t && unlink $t 5 | -------------------------------------------------------------------------------- /script/docker: -------------------------------------------------------------------------------- 1 | podman build -t ghcr.io/datastax-labs/astra-cli:latest . -------------------------------------------------------------------------------- /script/install-astra.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2022 DataStax 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the « License »); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an « AS IS » BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # script/install-astra.sh: dynamically install the correct binary according to the platform 17 | 18 | EXE=astra 19 | OS=$(echo `uname`|tr '[:upper:]' '[:lower:]') 20 | 21 | ARCH=$(uname -m) 22 | if [ "$ARCH" = "x86_64" ]; then 23 | ARCH="amd64" 24 | fi 25 | 26 | VERSION=$(curl --silent "https://api.github.com/repos/datastax-labs/astra-cli/releases/latest" | grep tag_name | sed -nr 's/"tag_name": "(.+)",/\1/p' | xargs) 27 | VERSION_SHORT=${VERSION:1} 28 | 29 | echo "installing $OS $ARCH $VERSION" 30 | ARC_FOLDER=$EXE-cli_${VERSION_SHORT}_${OS}_${ARCH} 31 | ARC=$(echo "${ARC_FOLDER}.tar.gz") 32 | 33 | url=https://github.com/datastax-labs/astra-cli/releases/download/$VERSION/$ARC 34 | curl -o $ARC -L $url 35 | mkdir -p $ARC_FOLDER 36 | tar zxvf $ARC -C $ARC_FOLDER 37 | sudo mv $ARC_FOLDER/$EXE /usr/local/bin/$EXE 38 | 39 | rm -fr $ARC 40 | rm -fr $ARC_FOLDER 41 | -------------------------------------------------------------------------------- /script/lint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 DataStax 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the « License »); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an « AS IS » BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # script/lint: verify no obvious bugs or layout problems are found 17 | 18 | gofmt -s -w . && \ 19 | golangci-lint run 20 | -------------------------------------------------------------------------------- /script/package: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 DataStax 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the « License »); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an « AS IS » BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # script/package: build and tgz all supported platforms and architectures 17 | 18 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 19 | $DIR/clean 20 | VERSION=$(git describe --abbrev=0 --tags) 21 | ORIG=$(git branch --show-current) 22 | echo "packaging $VERSION$" 23 | git checkout $VERSION 24 | GOOS=darwin GOARCH=amd64 go build -o bin/astra . 25 | tar czvf ./bin/astra-$VERSION-darwin-amd64.tgz ./bin/astra 26 | GOOS=darwin GOARCH=arm64 go build -o bin/astra . 27 | tar czvf ./bin/astra-$VERSION-darwin-arm64.tgz ./bin/astra 28 | GOOS=linux GOARCH=amd64 go build -o bin/astra . 29 | tar czvf ./bin/astra-$VERSION-linux-amd64.tgz ./bin/astra 30 | GOOS=linux GOARCH=arm64 go build -o bin/astra . 31 | tar czvf ./bin/astra-$VERSION-linux-arm64.tgz ./bin/astra 32 | git checkout $ORIG 33 | -------------------------------------------------------------------------------- /script/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 DataStax 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the « License »); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an « AS IS » BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # script/setup: Set up application for the first time after cloning, or set it 17 | # back to the initial first unused state. 18 | 19 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 20 | $DIR/bootstrap && \ 21 | go mod verify -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 DataStax 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the « License »); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an « AS IS » BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # script/test: Run test suite for application. 17 | 18 | 19 | t="/tmp/go-cover.$$.tmp" 20 | go test -race -covermode=atomic -coverprofile=$t ./... && go tool cover -func=$t 21 | last=$? 22 | unlink $t || true 23 | if [ "$last" = "0" ]; then 24 | echo "successfully ran" 25 | else 26 | (exit 1) 27 | fi 28 | 29 | -------------------------------------------------------------------------------- /script/update: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 DataStax 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the « License »); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an « AS IS » BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # script/update: Update application to run for its current checkout. 17 | 18 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 19 | $DIR/bootstrap 20 | go mod tidy 21 | 22 | -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: astra-cli 2 | version: git 3 | summary: Apache 2.0 licensed DataStax Astra management CLI 4 | description: | 5 | Automates provisioning services in DataStax Astra. 6 | Currently supporting AstraDB classic and serverless databases 7 | confinement: devmode 8 | base: core18 9 | parts: 10 | astra-cli: 11 | plugin: go 12 | go-importpath: github.com/datastax-labs/astra-cli 13 | source: . 14 | source-type: git 15 | build-packages: 16 | - gcc 17 | apps: 18 | astra-cli: 19 | command: bin/astra 20 | --------------------------------------------------------------------------------