├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── .yardopts ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── acceptance ├── google │ └── cloud │ │ └── gemserver │ │ ├── backend │ │ ├── key_test.rb │ │ └── storage_sync_test.rb │ │ ├── cli │ │ ├── cloud_sql_test.rb │ │ └── project_test.rb │ │ ├── configuration_test.rb │ │ └── gcs_test.rb ├── helper.rb └── perf │ └── perf_test.rb ├── bin ├── console ├── google-cloud-gemserver └── setup ├── docs ├── app.yaml.example ├── authentication.md ├── configuration.md ├── key.md ├── running-locally.md └── usage_example.md ├── google-cloud-gemserver.gemspec ├── integration ├── google-cloud-storage-1.1.0.gem ├── integration_test.rb └── private-test-gem-0.1.0.gem ├── lib ├── google │ └── cloud │ │ ├── gemserver.rb │ │ └── gemserver │ │ ├── authentication.rb │ │ ├── backend.rb │ │ ├── backend │ │ ├── gemstash_server.rb │ │ ├── key.rb │ │ ├── stats.rb │ │ └── storage_sync.rb │ │ ├── cli.rb │ │ ├── cli │ │ ├── cloud_sql.rb │ │ ├── project.rb │ │ ├── request.rb │ │ └── server.rb │ │ ├── configuration.rb │ │ ├── gcs.rb │ │ └── version.rb └── patched │ ├── configuration.rb │ ├── dependencies.rb │ ├── env.rb │ ├── gem_pusher.rb │ ├── gem_yanker.rb │ ├── storage.rb │ └── web.rb ├── secrets ├── app.yaml.enc └── service-account-key.json.enc └── test ├── google └── cloud │ └── gemserver │ ├── authentication_test.rb │ ├── backend │ ├── key_test.rb │ └── stats_test.rb │ └── cli │ ├── cloud_sql_test.rb │ ├── project_test.rb │ ├── request_test.rb │ └── server_test.rb └── helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /integration/gem_install/**/* 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | mygems 12 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - "acceptance/**/*" 4 | - "integration/**/*" 5 | - "google-cloud-gemserver.gemspec" 6 | - "lib/google/cloud/**/*" 7 | - "Rakefile" 8 | - "support/**/*" 9 | - "test/**/*" 10 | - "lib/patched/**/*" 11 | 12 | Documentation: 13 | Enabled: false 14 | 15 | Style/StringLiterals: 16 | EnforcedStyle: double_quotes 17 | Style/MethodDefParentheses: 18 | EnforcedStyle: require_no_parentheses 19 | Style/NumericLiterals: 20 | Enabled: false 21 | Layout/SpaceAroundOperators: 22 | Enabled: false 23 | Layout/EmptyLines: 24 | Enabled: false 25 | Metrics/ClassLength: 26 | Enabled: false 27 | Style/EmptyElse: 28 | Enabled: false 29 | Metrics/BlockLength: 30 | ExcludedMethods: ["class_eval"] 31 | Metrics/CyclomaticComplexity: 32 | Max: 12 33 | Metrics/PerceivedComplexity: 34 | Max: 12 35 | Metrics/AbcSize: 36 | Max: 28 37 | Metrics/MethodLength: 38 | Max: 22 39 | Metrics/ParameterLists: 40 | Enabled: false 41 | Style/RescueModifier: 42 | Enabled: false 43 | Style/ClassVars: 44 | Enabled: false 45 | Style/TrivialAccessors: 46 | Enabled: false 47 | Style/CaseEquality: 48 | Enabled: false 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.1 4 | 5 | script: 6 | 7 | - echo "Install gem dependencies" 8 | - bundle install 9 | 10 | - echo "Install and setup google-cloud-gemserver" 11 | - rake install 12 | - google-cloud-gemserver gen_config 13 | 14 | - echo "Workaround to run tests with GOOGLE_APPLICATION_CREDENTIALS set before it has been decrypted" 15 | - export TMP_CREDS=$GOOGLE_APPLICATION_CREDENTIALS 16 | - unset GOOGLE_APPLICATION_CREDENTIALS 17 | 18 | - echo "Run unit tests" 19 | - bundle exec rake test 20 | 21 | - echo "reset GOOGLE_APPLICATION_CREDENTIALS" 22 | - export GOOGLE_APPLICATION_CREDENTIALS=$TMP_CREDS 23 | 24 | # You need to be a project member to run acceptance and integration tests 25 | - if [ -z ${PROJECT_MEMBER+x} ]; then 26 | echo "User is not a project member. Skipping acceptance and integration tests."; 27 | else 28 | echo "Decrypt secrets"; 29 | echo -n "$GEMSERVER_ENCRYPTION_KEY" | openssl enc -d -aes-256-cbc -salt -in secrets/app.yaml.enc -out secrets/app.yaml -pass stdin; 30 | echo -n "$GEMSERVER_ENCRYPTION_KEY" | openssl enc -d -aes-256-cbc -salt -in secrets/service-account-key.json.enc -out secrets/service-account-key.json -pass stdin; 31 | cp secrets/app.yaml ~/.google_cloud_gemserver/app.yaml; 32 | 33 | echo "Install gcloud SDK"; 34 | export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)"; 35 | echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list; 36 | curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -; 37 | sudo apt-get update && sudo apt-get install google-cloud-sdk; 38 | 39 | echo "Authenticate with gcloud"; 40 | gcloud auth activate-service-account --key-file secrets/service-account-key.json; 41 | 42 | echo "Install and setup Cloud SQL proxy"; 43 | wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64; 44 | mv cloud_sql_proxy.linux.amd64 cloud_sql_proxy; 45 | chmod +x cloud_sql_proxy; 46 | sudo mkdir /cloudsql; 47 | sudo chmod 777 /cloudsql; 48 | 49 | echo "Start cloud_sql_proxy"; 50 | ./cloud_sql_proxy -instances=$CLOUD_SQL_ICN -dir=/cloudsql & 51 | 52 | echo "Setup gemserver keys"; 53 | touch ~/.gem/credentials; 54 | chmod 0600 ~/.gem/credentials; 55 | echo "Writing [colon]GEMSERVER_KEY_NAME[colon] $GEMSERVER_KEY to ~/.gem/credentials in a convoluted way because Travis parses strings with [colon] as mappings"; 56 | echo :$GEMSERVER_KEY_NAME >> ~/.gem/credentials; 57 | sed -i '$s/$/:/' ~/.gem/credentials; 58 | sed -i '$s/$/ '"$GEMSERVER_KEY"'/' ~/.gem/credentials; 59 | bundle config $GEMSERVER_PRIVATE_URL $GEMSERVER_KEY; 60 | 61 | echo "Run acceptance tests"; 62 | gcloud config set project $PROJECT_NAME; 63 | host=$GEMSERVER_URL key=$GEMSERVER_KEY_NAME bundle exec rake acceptance; 64 | 65 | echo "Run integration tests"; 66 | gcloud config set project $PROJECT_NAME; 67 | host=$GEMSERVER_URL key=$GEMSERVER_KEY_NAME bundle exec rake integration; 68 | fi 69 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --title=Google Cloud Gemserver 3 | --markup markdown 4 | 5 | ./lib/**/*.rb 6 | 7 | README.md 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Development 19 | 20 | After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 21 | 22 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 23 | 24 | ### Running tests 25 | 26 | Note: you will need gemstash installed from 27 | [source](https://github.com/bundler/gemstash) to run acceptance and integration 28 | tests. 29 | 30 | 1) Edit your app.yaml to use a separate database to run tests on otherwise 31 | your existing private gems can get deleted. 32 | 2) Generate a test-key with both read/write permissions (my-test-key). 33 | 3) To run unit tests: 34 | `bundle exec rake test` 35 | 3) To run acceptance and performance tests: 36 | Ensure cloud_sql_proxy is running and connected to the CloudSQL instance of 37 | your testing gemserver (dev prefix) in app.yaml. 38 | `bundle exec rake test host=test-gemserver-url.com key=my-test-key` 39 | 4) To run integration tests: 40 | Ensure cloud_sql_proxy is running and connected to the CloudSQL instance of 41 | your testing gemserver (dev prefix) in app.yaml. 42 | `bundle exec rake integration host=test-gemserver-url.com key=my-test-key` 43 | 44 | ### Travis PR Builds 45 | 46 | By default, unit tests will always run for every update to a PR. Acceptance and 47 | integration tests will run after a PR is merged. If tests break, please submit a 48 | separate PR to fix the issues. 49 | 50 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "concurrent-ruby", require: "concurrent" 4 | gem "gemstash", "~> 1.1.0" 5 | gemspec 6 | -------------------------------------------------------------------------------- /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 | # Google::Cloud::Gemserver 2 | 3 | [![Build Status](https://travis-ci.org/GoogleCloudPlatform/google-cloud-gemserver.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/google-cloud-gemserver) 4 | 5 | This gem is a tool that lets you manage, interact with, and deploy a [private gem 6 | server](https://github.com/bundler/gemstash) to a Google Cloud Platform project. 7 | The gemserver acts as a private gem repository for your gems similar 8 | to how rubygems.org works with the exception that pushing and installing gems 9 | are protected operations. 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | ```ruby 16 | gem 'google-cloud-gemserver' 17 | ``` 18 | 19 | And then execute: 20 | 21 | $ bundle 22 | 23 | Or install it yourself as: 24 | 25 | $ gem install google-cloud-gemserver 26 | 27 | ## Usage 28 | 29 | ### Basic Prerequisites 30 | 1) Create a Google Cloud Platform (GCP) project. 31 | 2) Install and setup the [gcloud SDK](https://cloud.google.com/sdk/downloads). Currently, versions 161+ are supported. 32 | 3) Authenticate gcloud by using a [service account](https://cloud.google.com/docs/authentication/getting-started) or [application default credentials](https://developers.google.com/identity/protocols/application-default-credentials). 33 | Using a service account is the recommended method for authentication; application default credentials should only be used for development purporses. Read this [authentication guide](docs/authentication.md) for more information. 34 | 4) Running acceptance or performance tests requires you to have the Cloud SQL proxy running with your Cloud SQL instance. Visit this [link](https://cloud.google.com/sql/docs/mysql/connect-admin-proxy) to learn how to install and run it (steps 3 and 5 can be skipped). 35 | 36 | ### Typical Workflow 37 | 1) Deploy a gemserver by running: `google-cloud-gemserver create --use-proj YOUR_PROJECT_ID`. This deploys the gemserver in a Google App Engine project as the default service. It also creates a new Cloud SQL instance with machine type db-f1-micro. Note that this machine type is only recommended for development / testing and is not under the Cloud SQL SLA coverage. 38 | 2) Generate a key (referred to as my-key) by running `google-cloud-gemserver create-key --use-proj YOUR_PROJECT_ID` for your gemserver. By default, this generates a key with both read and write permissions. For more information about keys, read [this](docs/key.md). 39 | 3) Add this key to your bundle config by running `bundle config http://gemserver-url.com/private/ my-key` where gemserver-url is the same as your project's url, e.g. http://my-project.appspot.com/private/. This is necessary to download gems. 40 | 4) Add this key to your gem credentials as my-key (in ~/.gem/credentials): `:my-key: [KEY]` This is necessary to push gems (if the key has write permission). 41 | 5) Push private gems to the gemserver as described [below](#pushing-gems). 42 | 6) Download private gems by modifying your Gemfile as described 43 | [below](#fetching-gems). 44 | 45 | ### Pushing gems 46 | Note: ensure `my-key` has the read permission and is added in your gem 47 | credentials file (~/.gem/credentials) 48 | `gem push my-gem --key my-key --host http://my-gemserver.com/private/` 49 | 50 | ### Fetching gems 51 | Note: ensure `my-key` has the read permission and is set in your bundle 52 | config by running `bundle config http://my-gemserver.com/private/ my-key` 53 | 54 | 1) Add `source "http://my-gemserver.com"` to the top of your Gemfile 55 | 2) Add the following to your Gemfile: 56 | ``` 57 | source "http://my-gemserver.com/private" do 58 | gem "my-private-gem1" 59 | (other private gems here) 60 | end 61 | ``` 62 | 3) Run `bundle install` 63 | 64 | ### Yanking gems 65 | Note: ensure `my-key` has the write permission and is added in your gem 66 | credentials file (~/.gem/credentials) 67 | `gem push my-gem --key my-key --host http://my-gemserver.com/private/` 68 | 69 | 1) Run `gem yank --key my-key [GEM_NAME] --host 70 | http://my-gemserver.com/private` 71 | 72 | Gems can not be "unyanked" so once a gem has been yanked it cannot be pushed 73 | to the gemserver again with the same name and version. It can be pushed if the 74 | version number is changed, however. 75 | 76 | 77 | ### Gemserver commands 78 | * `google-cloud-gemserver config` 79 | 80 | Usage: 81 | google-cloud-gemserver config 82 | 83 | Displays the config the current deployed gemserver is using (if one is running) 84 | 85 | * `google-cloud-gemserver create` 86 | 87 | Usage: 88 | google-cloud-gemserver create 89 | 90 | Options: 91 | * -g, [--use-proj=USE_PROJ] # Existing project to deploy gemserver to 92 | * -i, [--use-inst=USE_INST] # Existing project to deploy gemserver to 93 | 94 | Creates and deploys the gem server then starts it 95 | 96 | * `google-cloud-gemserver create-key` 97 | 98 | Usage: 99 | google-cloud-gemserver create-key 100 | 101 | Options: 102 | * -r, [--remote=REMOTE] # The gemserver URL, i.e. gemserver.com 103 | * -p, [--permissions=PERMISSIONS] # Options: write, read, both. Default is 104 | both. 105 | * -g, [--use-proj=USE_PROJ] # The GCP project the gemserver was 106 | deployed to. 107 | 108 | Creates an authentication key 109 | 110 | * `google-cloud-gemserver delete-key` 111 | 112 | Usage: 113 | google-cloud-gemserver delete-key 114 | 115 | Options: 116 | * -r, [--remote=REMOTE] # The gemserver URL, i.e. gemserver.com 117 | * -k, [--key=KEY] # The key to delete 118 | * -g, [--use-proj=USE_PROJ] # The GCP project the gemserver was 119 | deployed to. 120 | 121 | Deletes a given key 122 | 123 | * `google-cloud-gemserver delete` 124 | 125 | Usage: 126 | google-cloud-gemserver delete 127 | 128 | Options: 129 | * -g, [--use-proj=USE_PROJ] # Project id of GCP project the gemserver was deployed to. Warning: parent project and CloudSQL instance will also be deleted 130 | 131 | Delete a given gemserver 132 | 133 | * `google-cloud-gemserver start` 134 | 135 | Usage: 136 | google-cloud-gemserver start 137 | 138 | Starts the gem server. This will be run automatically after a deploy. 139 | Running this locally will start the gemserver locally 140 | 141 | * `google-cloud-gemserver stats` 142 | 143 | Usage: 144 | google-cloud-gemserver stats 145 | 146 | Options: 147 | * -r, [--remote=REMOTE] # The gemserver URL, i.e. gemserver.com 148 | * -g, [--use-proj=USE_PROJ] # The GCP project the gemserver was 149 | deployed to. 150 | 151 | Displays statistics on the given gemserver 152 | 153 | * `google-cloud-gemserver update` 154 | 155 | Usage: 156 | google-cloud-gemserver update 157 | 158 | Redeploys the gemserver with the current config file and google-cloud-gemserver gem version (a deploy must have succeeded for 'update' to work) 159 | 160 | * `google-cloud-gemserver gen-config` 161 | 162 | Usage: 163 | google-cloud-gemserver gen-config 164 | 165 | Generates configuration files with default values 166 | 167 | * `google-cloud-gemserver help` 168 | 169 | Usage: 170 | google-cloud-gemserver help [COMMAND] 171 | 172 | Describe available commands or one specific command 173 | 174 | More documentation can be found in the docs [directory](docs/). 175 | 176 | ## Contributing 177 | 178 | Detailed information can be found in [CONTRIBUTING.md](CONTRIBUTING.md). 179 | 180 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "bundler/gem_tasks" 3 | require "rubocop/rake_task" 4 | require "google/cloud/gemserver" 5 | 6 | RuboCop::RakeTask.new 7 | 8 | desc "Run unit tests." 9 | task :test do 10 | ENV["APP_ENV"] = "test" 11 | $LOAD_PATH.unshift "lib", "test" 12 | Google::Cloud::Gemserver::Configuration.new.gen_config 13 | Dir.glob("test/**/*_test.rb").each { |file| require_relative file } 14 | end 15 | 16 | desc "Run acceptance and performance tests." 17 | task :acceptance do 18 | ENV["APP_ENV"] = "dev" 19 | check_config 20 | $LOAD_PATH.unshift "lib", "acceptance" 21 | Google::Cloud::Gemserver::Configuration.new.gen_config 22 | Dir.glob("acceptance/**/*_test.rb").each { |file| require_relative file } 23 | end 24 | 25 | desc "Run integration test." 26 | task :integration do 27 | ENV["APP_ENV"] = "dev" 28 | check_config 29 | $LOAD_PATH.unshift "lib", "integration" 30 | Google::Cloud::Gemserver::Configuration.new.gen_config 31 | Dir.glob("integration/**/*_test.rb").each { |file| require_relative file } 32 | end 33 | 34 | def check_config 35 | abort "Host missing. Run again with host=[your-gemserver-url/private]" unless ENV["host"] 36 | abort "Key missing. Run again with key=[name-of-key in ~/.gem/credentials]" unless ENV["key"] 37 | end 38 | -------------------------------------------------------------------------------- /acceptance/google/cloud/gemserver/backend/key_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | require "fileutils" 17 | require "yaml" 18 | require "gemstash" 19 | 20 | describe Google::Cloud::Gemserver::Backend::Key do 21 | 22 | describe "when generating a new key with all permissions" do 23 | it "must have all permissions" do 24 | all_key = Google::Cloud::Gemserver::Backend::Key.create_key 25 | puts "all_key: #{all_key}" 26 | wont_be_nil Gemstash::Authorization.check(all_key, "fetch") 27 | wont_be_nil Gemstash::Authorization.check(all_key, "push") 28 | wont_be_nil Gemstash::Authorization.check(all_key, "yank") 29 | Google::Cloud::Gemserver::Backend::Key.delete_key all_key 30 | end 31 | end 32 | 33 | describe "when generating a new key with only read permission" do 34 | it "must have the read permission" do 35 | fetch_key = Google::Cloud::Gemserver::Backend::Key.create_key("read") 36 | puts "fetch_key: #{fetch_key}" 37 | wont_be_nil Gemstash::Authorization.check(fetch_key, "fetch") 38 | proc {Gemstash::Authorization.check(fetch_key, "push")} 39 | .must_raise Gemstash::NotAuthorizedError 40 | proc {Gemstash::Authorization.check(fetch_key, "yank")} 41 | .must_raise Gemstash::NotAuthorizedError 42 | Google::Cloud::Gemserver::Backend::Key.delete_key fetch_key 43 | end 44 | end 45 | 46 | describe "when deleting a key" do 47 | it "must succeed in deleting" do 48 | key_to_delete = Google::Cloud::Gemserver::Backend::Key.create_key 49 | wont_be_nil Google::Cloud::Gemserver::Backend::Key.delete_key key_to_delete 50 | end 51 | end 52 | 53 | describe "when mapping permissions from write/read to gemstash perms" do 54 | it "must map to fetch, yank, and push correctly" do 55 | MAPPING = {"write" => ["push", "yank"], "read" => ["fetch"]} 56 | assert MAPPING["write"], 57 | Google::Cloud::Gemserver::Backend::Key.send(:map_perms, "write") 58 | assert MAPPING["read"], 59 | Google::Cloud::Gemserver::Backend::Key.send(:map_perms, "read") 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /acceptance/google/cloud/gemserver/backend/storage_sync_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | require "google/cloud/storage" 17 | 18 | SS = GCG::Backend::StorageSync 19 | 20 | describe Google::Cloud::Gemserver::Backend::StorageSync do 21 | describe ".run" do 22 | it "calls upload_service" do 23 | SS.stub :try_upload, nil do 24 | output = capture_io { SS.run } 25 | assert output[0].empty? || output[0].include?("Running") 26 | end 27 | end 28 | 29 | it "calls download_service do" do 30 | SS.stub :try_download, nil do 31 | SS.run 32 | output = capture_io { SS.run } 33 | assert output[0].empty? || output[0].include?("Running") 34 | end 35 | end 36 | end 37 | 38 | describe ".try_upload" do 39 | upload_mock = Minitest::Mock.new 40 | upload_mock.expect :file_changed?, true, [String] 41 | it "calls file_changed?" do 42 | GCG::GCS.stub :on_gcs?, false do 43 | GCG::GCS.stub :upload, nil do 44 | File.stub :exist?, false do 45 | SS.stub :file_changed?, upload_mock do 46 | SS.try_upload "/tmp/file-that-probably-doesnt-exist" 47 | assert_send([upload_mock, :file_changed?, String]) 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | 55 | describe ".try_download" do 56 | download_mock = Minitest::Mock.new 57 | download_mock.expect :file_changed?, true, [String] 58 | it "calls file_changed?" do 59 | GCG::GCS.stub :sync, nil do 60 | File.stub :exist?, false do 61 | SS.try_download "/tmp/file-that-probably-doesnt-exist" 62 | assert_send([download_mock, :file_changed?, String]) 63 | end 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /acceptance/google/cloud/gemserver/cli/cloud_sql_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | require "google/apis/sqladmin_v1beta4" 17 | require "googleauth" 18 | require "yaml" 19 | 20 | SQL = Google::Cloud::Gemserver::CLI::CloudSQL 21 | CFG = Google::Cloud::Gemserver::Configuration 22 | CSQL = Google::Apis::SqladminV1beta4 23 | SCOPES = ["https://www.googleapis.com/auth/sqlservice.admin"] 24 | 25 | describe Google::Cloud::Gemserver::CLI::CloudSQL do 26 | 27 | let(:auth) { 28 | auth= Google::Auth.get_application_default(SCOPES) 29 | Google::Apis::RequestOptions.default.authorization = auth 30 | } 31 | 32 | let(:service) { 33 | auth 34 | CSQL::SQLAdminService.new 35 | } 36 | 37 | before(:all) do 38 | @config = CFG.new 39 | @app_path = @config.app_path 40 | @config_path = @config.config_path 41 | ConfigHelper.new.copy_configs 42 | end 43 | 44 | after(:all) do 45 | ConfigHelper.new.restore_configs 46 | end 47 | 48 | describe "creating an instance" do 49 | it "must load the configuration" do 50 | sql = SQL.new("testdb") 51 | sql.send(:load_config) 52 | assert_equal CFG::DEV_DB_DEFAULTS[:username], sql.user 53 | assert_equal CFG::DEV_DB_DEFAULTS[:password], sql.pwd 54 | assert_equal CFG::DEV_DB_DEFAULTS[:database], sql.db 55 | assert_equal @config[:proj_id], sql.proj_id 56 | end 57 | 58 | it "must get the instance if it exists" do 59 | sql = SQL.new("testdb") 60 | inst = service.get_instance @config[:proj_id], "testdb" 61 | assert_equal inst.connection_name, sql.send(:instance).connection_name 62 | end 63 | 64 | it "must create an instance if it does not exist" do 65 | sql = SQL.new 66 | sql.run 67 | assert_equal CSQL::DatabaseInstance, sql.send(:instance).class 68 | sql.send(:del_instance) 69 | end 70 | 71 | it "must update the configuration file" do 72 | sql = SQL.new("testdb") 73 | sql.run 74 | config = YAML.load_file @config_path 75 | refute config[:db_connection_options][:socket].empty? 76 | end 77 | end 78 | 79 | describe "deleting an instance" do 80 | it "must delete the instance" do 81 | sql = SQL.new 82 | sql.run 83 | sql.send(:del_instance) 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /acceptance/google/cloud/gemserver/cli/project_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | require "yaml" 17 | 18 | Project = Google::Cloud::Gemserver::CLI::Project 19 | 20 | describe Google::Cloud::Gemserver::CLI::Project do 21 | 22 | before do 23 | @config = Google::Cloud::Gemserver::Configuration.new 24 | @config_path = @config.config_path 25 | end 26 | 27 | let(:reset_config) { 28 | config = YAML.load_file @config_path 29 | config[:proj_id] = "" 30 | File.open(@config_path, 'w') {|f| YAML.dump config, f} 31 | } 32 | 33 | after do 34 | config = YAML.load_file @config_path 35 | config[:proj_id] = ConfigHelper.new.name 36 | File.open(@config_path, 'w') {|f| YAML.dump config, f} 37 | end 38 | 39 | describe "managing a project" do 40 | 41 | before do 42 | Google::Cloud::Gemserver::CLI::Project.class_eval do 43 | private 44 | def prompt_user; end 45 | end 46 | end 47 | 48 | it "must be possible to use an existing project" do 49 | project = Project.new(@config[:proj_id]).create 50 | assert_equal Google::Cloud::ResourceManager::Project, project.class 51 | end 52 | 53 | it "must update the config file correctly" do 54 | reset_config 55 | project = Project.new(@config[:proj_id]).create 56 | config = YAML.load_file @config_path 57 | assert_equal project.project_id, config[:proj_id] 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /acceptance/google/cloud/gemserver/configuration_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | require "yaml" 17 | require "fileutils" 18 | 19 | describe Google::Cloud::Gemserver::Configuration do 20 | before(:all) do 21 | ENV["APP_ENV"] = "dev" 22 | @config = GCG::Configuration.new 23 | ConfigHelper.new.copy_configs 24 | end 25 | 26 | let(:dir) { GCG::Configuration::CONFIG_DIR } 27 | 28 | let(:app_path) { 29 | "#{dir}/app.yaml" 30 | } 31 | 32 | let(:config_path) { 33 | "#{dir}/config.yml" 34 | } 35 | 36 | let(:dev_config_path) { 37 | "#{dir}/dev_config.yml" 38 | } 39 | 40 | let(:the_test_config_path) { 41 | "#{dir}/test_config.yml" 42 | } 43 | 44 | after(:all) do 45 | ConfigHelper.new.restore_configs 46 | end 47 | 48 | describe "managing configurations" do 49 | it "must get the correct config paths" do 50 | @config.stub :config_dir, dir do 51 | ENV["APP_ENV"] = "test" 52 | assert_equal File.expand_path(the_test_config_path), 53 | File.expand_path(@config.config_path) 54 | ENV["APP_ENV"] = "production" 55 | assert_equal File.expand_path(config_path), 56 | File.expand_path( @config.config_path) 57 | ENV["APP_ENV"] = "dev" 58 | assert_equal File.expand_path(dev_config_path), 59 | File.expand_path( @config.config_path) 60 | assert_equal File.expand_path(app_path), 61 | File.expand_path(@config.app_path) 62 | end 63 | end 64 | 65 | it "must access keys in project config correctly" do 66 | @config.stub :config_dir, dir do 67 | assert_equal ConfigHelper.new.name, @config[:proj_id] 68 | end 69 | end 70 | 71 | it "must update project config correctly" do 72 | @config.stub :config_dir, dir do 73 | @config.update_config "ruby", :test 74 | assert_equal "ruby", @config[:test] 75 | end 76 | end 77 | 78 | it "must update app config correctly" do 79 | @config.stub :config_dir, dir do 80 | @config.update_app "ruby", :test 81 | app = YAML.load_file app_path 82 | assert_equal "ruby", app[:test] 83 | end 84 | end 85 | 86 | it "must save project config to GCS" do 87 | @config.stub :config_dir, dir do 88 | @config.save_to_cloud 89 | assert_equal true, GCG::GCS.on_gcs?(GCG::Configuration::GCS_PATH) 90 | GCG::GCS.delete_file GCG::Configuration::GCS_PATH 91 | end 92 | end 93 | 94 | it "must be able to display config" do 95 | @config.stub :config_dir, dir do 96 | @config.save_to_cloud 97 | out, err = capture_io { GCG::Configuration.display_config } 98 | did_output = out.include?("No configuration") || 99 | out.include?("Gemserver is running") 100 | assert_equal true, did_output 101 | GCG::GCS.delete_file GCG::Configuration::GCS_PATH 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /acceptance/google/cloud/gemserver/gcs_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | require "google/cloud/storage" 17 | 18 | GCS = GCG::GCS 19 | 20 | describe Google::Cloud::Gemserver::GCS do 21 | 22 | let(:proj_name) { 23 | ConfigHelper.new.name 24 | } 25 | 26 | before do 27 | @config = Google::Cloud::Gemserver::Configuration.new 28 | @config.update_config proj_name, :proj_id 29 | end 30 | 31 | describe "setting up Google Cloud Storage" do 32 | it "must get a GCS service instance for the current project" do 33 | assert_equal Google::Cloud::Storage::Project, GCS.cs.class 34 | assert_equal proj_name, GCS.cs.project 35 | end 36 | 37 | it "must get a GCS bucket" do 38 | assert_equal proj_name, GCS.bucket.name 39 | end 40 | end 41 | 42 | describe "managing files" do 43 | it "can get a file" do 44 | GCS.upload @config.config_path 45 | assert_equal Google::Cloud::Storage::File, GCS.get_file(@config.config_path).class 46 | GCS.delete_file @config.config_path 47 | end 48 | 49 | it "can get all files" do 50 | files = GCS.files 51 | assert_equal Google::Cloud::Storage::File::List, files.class 52 | end 53 | 54 | it "can upload a file" do 55 | assert_equal @config.config_path, GCS.upload(@config.config_path).name 56 | GCS.delete_file @config.config_path 57 | end 58 | 59 | it "can delete a file" do 60 | GCS.upload @config.config_path 61 | assert_equal true, GCS.delete_file(@config.config_path) 62 | end 63 | 64 | it "can sync a file between host and GCS" do 65 | GCS.upload @config.config_path 66 | assert_equal true, GCS.sync(@config.config_path) 67 | GCS.delete_file @config.config_path 68 | end 69 | 70 | it "can check if a file exists on GCS" do 71 | GCS.upload @config.config_path 72 | assert_equal true, GCS.on_gcs?(@config.config_path) 73 | GCS.delete_file @config.config_path 74 | end 75 | 76 | it "can get extract the dir name from a file path" do 77 | assert_equal "/a/b/c/", GCS.extract_dir("/a/b/c/file.file") 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /acceptance/helper.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | gem "minitest" 16 | 17 | require "minitest/autorun" 18 | require "minitest/rg" 19 | require "google/cloud/gemserver" 20 | 21 | GCG = Google::Cloud::Gemserver 22 | 23 | class ConfigHelper 24 | def initialize 25 | ENV["APP_ENV"] = "dev" 26 | @config = Google::Cloud::Gemserver::Configuration.new 27 | end 28 | 29 | def copy_configs 30 | @config.update_config name, :proj_id 31 | FileUtils.cp @config.app_path, "/tmp/gemserver-app-config.yaml" 32 | FileUtils.cp @config.config_path, "/tmp/gemserver-config.yml" 33 | end 34 | 35 | def restore_configs 36 | FileUtils.rm @config.app_path 37 | FileUtils.rm @config.config_path 38 | FileUtils.cp "/tmp/gemserver-app-config.yaml", @config.app_path 39 | FileUtils.cp "/tmp/gemserver-config.yml", @config.config_path 40 | end 41 | 42 | def name 43 | @config[:proj_id] 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /acceptance/perf/perf_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | require "fileutils" 17 | require "benchmark" 18 | 19 | HOST = ENV["host"].freeze 20 | GEM = "google-cloud-storage".freeze 21 | VER = "1.1.0".freeze 22 | GEM_PATH = "integration/#{GEM}-#{VER}.gem".freeze 23 | KEY = ENV["key"].freeze 24 | 25 | describe Google::Cloud::Gemserver do 26 | let(:gemserver_gemfile) { 27 | gemfile = "source \"http://#{HOST}/private\" do\n" \ 28 | "gem \"#{GEM}\"\nend" 29 | FileUtils.mkpath "integration/gem_install" 30 | File.open("integration/gem_install/Gemfile", "w") do |f| 31 | f.write gemfile 32 | end 33 | } 34 | 35 | let(:rubygems_gemfile) { 36 | gemfile = "source \"http://rubygems.org\" do\n" \ 37 | "gem \"#{GEM}\"\nend" 38 | FileUtils.mkpath "integration/gem_install" 39 | File.open("integration/gem_install/Gemfile", "w") do |f| 40 | f.write gemfile 41 | end 42 | } 43 | 44 | let(:reset) { 45 | env = Google::Cloud::Gemserver::Backend::Stats.new.send(:env) 46 | env.db[:versions].delete 47 | env.db[:rubygems].delete 48 | env.db[:cached_rubygems].delete 49 | env.db[:dependencies].delete 50 | } 51 | 52 | let(:push) { 53 | reset 54 | url = "http://#{HOST}/private" 55 | `gem push --key #{KEY} #{GEM_PATH} --host #{url}` 56 | } 57 | 58 | let(:yank) { 59 | url = "http://#{HOST}/private" 60 | `RUBYGEMS_HOST=#{url} gem yank --key #{KEY} #{GEM} --version #{VER}` 61 | } 62 | 63 | let(:range) { 15..15+GCG::Backend::Key::KEY_LENGTH } 64 | 65 | after(:all) do 66 | reset 67 | end 68 | 69 | it "can push gems" do 70 | Benchmark.bm(7) do |x| 71 | x.report("push: ") { push } 72 | end 73 | end 74 | 75 | it "can yank gems" do 76 | push 77 | Benchmark.bm(7) do |x| 78 | x.report("yank: ") { yank } 79 | end 80 | end 81 | 82 | it "can install gems" do 83 | gemserver_gemfile 84 | Benchmark.bm(7) do |x| 85 | x.report("gemserver install: ") { `cd integration/gem_install && bundle install` } 86 | rubygems_gemfile 87 | x.report("rubygems install: ") { `cd integration/gem_install && bundle install` } 88 | end 89 | end 90 | 91 | it "can create a gemserver key" do 92 | Benchmark.bm(7) do |x| 93 | x.report("create-key: "){ `google-cloud-gemserver create-key -r #{HOST}` } 94 | end 95 | end 96 | 97 | it "can delete a gemserver key" do 98 | raw = `google-cloud-gemserver create-key -r #{HOST}` 99 | key = raw[range].chomp 100 | Benchmark.bm(7) do |x| 101 | x.report("delete-key: "){ `google-cloud-gemserver delete-key -k #{key} -r #{HOST}` } 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "google/cloud/gemserver" 5 | 6 | require "irb" 7 | IRB.start(__FILE__) 8 | -------------------------------------------------------------------------------- /bin/google-cloud-gemserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "bundler/setup" 3 | require "google/cloud/gemserver/cli" 4 | 5 | Google::Cloud::Gemserver::CLI.start ARGV 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /docs/app.yaml.example: -------------------------------------------------------------------------------- 1 | --- 2 | runtime: ruby 3 | env: flex 4 | entrypoint: "./bin/google-cloud-gemserver start" 5 | beta_settings: 6 | # this is set automatically during a deploy 7 | cloud_sql_instances: "" 8 | health_check: 9 | enable_health_check: false # can be changed 10 | automatic_scaling: 11 | min_num_instances: 1 # can be changed 12 | # this can be set to 1 to disable auto scaling 13 | max_num_instances: 5 # can be changed 14 | env_variables: 15 | GEMSERVER_ON_APPENGINE: true 16 | production_db_database: mygems # can be changed 17 | production_db_username: test # can be changed 18 | production_db_password: test # can be changed 19 | production_db_host: localhost # can be changed 20 | # this is set automatically during a deploy 21 | production_db_socket: "" # can be changed 22 | production_db_adapter: cloud_sql 23 | dev_db_database: mygems # can be changed 24 | dev_db_username: test # can be changed 25 | dev_db_password: test # can be changed 26 | dev_db_host: localhost # can be changed 27 | # this must be set manually if using cloud_sql, requires APP_ENV=dev 28 | dev_db_socket: "# this must be set manually" # can be changed 29 | dev_db_adapter: cloud_sql # can be changed 30 | test_db_database: mygems # can be changed 31 | test_db_username: test # can be changed 32 | test_db_password: test # can be changed 33 | test_db_host: localhost # can be changed 34 | test_db_adapter: sqlite3 # can be changed 35 | # general gemserver settings 36 | gen_cache_type: memory 37 | gen_protected_fetch: true 38 | gen_bind: tcp://0.0.0.0:8080 39 | gen_log_file: :stdout 40 | -------------------------------------------------------------------------------- /docs/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | ## Using Service Account (recommended) 4 | 1) Visit the Google Cloud console for your project. 5 | 2) Go to the "IAM & Admin" page and select the "Service accounts" option 6 | 3) Create a service account or select an existing one 7 | 4) Create a key for a chosen service account and download it 8 | 5) Authenticate with gcloud with the service account key by running: 9 | `gcloud auth activate-service-account --key-file [PATH TO SERVICE ACCOUNT KEY]` 10 | 6) Set your "GOOGLE_APPLICATION_CREDENTIALS" environment variable to the path to 11 | your service account key file. For example: 12 | `export GOOGLE_APPLICATION_CREDENTIALS=~/my-project.json` 13 | 14 | ## Using gcloud application-default 15 | Simply run `gcloud auth application-default login` to authenticate yourself with 16 | gcloud. This method is simpler, however, will not work well for a production 17 | environment. It is better for running the gemserver locally. 18 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | There are 4 config files, 3 are for different gemserver environments and 1 is 4 | for the Google App Engine Flex (GAE) deployment. You only need to worry about the 5 | GAE config file (app.yaml) as the other config files are derived from it. 6 | 1) app.yaml (GAE environment, contains all config data) 7 | 2) config.yml ("production" environment, runs on GAE) 8 | 3) dev_config.yml ("development" environment, runs on GAE - useful for acceptance 9 | tests so gems do not get deleted) 10 | 4) test_config.yml ("testing" environment, useful for running the gemserver 11 | locally) 12 | 13 | These files are auto generated with default settings when any `google-cloud-gemserver` command is run. They can be explicitly created by running `google-cloud-gemserver gen-config`. The files are stored at ~/.google_cloud_gemserver by default. The config directory can be changed by setting a GEMSERVER_CONFIG_DIR environment variable to the path of your configuration directory. If that environment variable is set, it will check that directory for configuration files first before checking the default directory. 14 | 15 | Note that the configuration directory is a convenience for gemserver deployment, it is not used by the gemserver on Google App Engine. 16 | 17 | [Here](docs/app.yaml.example) is an example app.yaml file. 18 | 19 | ## Settings 20 | 21 | Below are settings in the app.yaml that can be changed 22 | 23 | * "enable_health_check" - enables Google Cloud App Engine project health checks (default false) 24 | * "min_num_instances" - the minimum number of Google App Engine instances (autoscaled) 25 | * "max_num_instances" - the maximum number of Google App Engine instances (autoscaled) 26 | * "production_db_database" - production database to be used (default "mygems") 27 | * "production_db_username" - non -root user to access the production database (default "test") 28 | * "production_db_password" - password of the new user (default "test") that connects to the production database 29 | * "production_db_host" - the host of the production database (default "localhost") 30 | * "production_db_socket" - socket that the gemserver connects with to the database, for CloudSQL it is always /cloudsql/[cloud-sql-instance-connection-name] 31 | * "production_db_adapter" - the production database adapter (default "cloud_sql") 32 | * "dev_db_database" - development database to be used (default "mygems") 33 | * "dev_db_username" - non -root user to access the production database (default "test") 34 | * "dev_db_password" - password of the new user (default "test") that connects to the development database 35 | * "dev_db_host" - the host of the development database (default "localhost") 36 | * "dev_db_socket" - socket that the gemserver connects with to the database, for CloudSQL it is always /cloudsql/[cloud-sql-instance-connection-name] 37 | * "dev_db_adapter" - the development database adapter (default "cloud_sql") 38 | * "test_db_database" - test database to be used (default "mygems") 39 | * "test_db_username" - non -root user to access the test database (default "test") 40 | * "test_db_password" - password of the new user (default "test") that connects to the test database 41 | * "test_db_host" - the host of the test database (default "localhost") 42 | * "test_db_adapter" - the test database adapter (default "sqlite3") 43 | * `gen_proj_id` - project id of the Google Cloud Platform project the gemserver was deployed to 44 | (does not need to be set for config.yml but must be set for test_config.yml and 45 | dev_config.yml if the database is a Cloud SQL instance) 46 | -------------------------------------------------------------------------------- /docs/key.md: -------------------------------------------------------------------------------- 1 | # Keys 2 | 3 | ## Environment 4 | By default, key creation / deletion is done on the production environment 5 | configuration (config.yml). This is important because your production 6 | CloudSQL instance should be different from your development one. To ensure keys 7 | are generated for the right environment, hence saved in the appropriate 8 | databases, prepend commands with `APP_ENV=dev|test` otherwise the 9 | environment will default to "production." 10 | 11 | ** Note that Cloud SQL proxy will need to be running for key generation and 12 | deletion to work properly. 13 | 14 | ## Creating a key 15 | There are 3 permission settings for a key: 16 | * write (can only push gems) 17 | * read (can only install gems) 18 | * both (read and write) 19 | 20 | To create a key with a desired permission, run: 21 | `google-cloud-gemserver create-key --use-proj PROJECT_ID [--permissions read|write|both]`. By default, if the permissions argument is not given a key will be generated with both (read, write) permissions. 22 | 23 | Note: the `create-key` command requires either the `--use-proj PROJECT_ID` flag 24 | or the `--remote your-gemserver-url` flag, i.e. `--use-proj my-project` or `--remote 25 | my-gemserver.appspot.com`. 26 | 27 | If the current GCP project containing your gemserver is not the one you want 28 | a key generated for, you can pass a "remote" option to target a specific 29 | gemserver, i.e. `google-cloud gemserver create-key --remote 30 | mygemserver.appspot.com`. Note that the key creation will fail if you do not 31 | have access to the GCP project containing the gemserver. 32 | 33 | To be able to push gems with the `gem push` command, a key must be supplied 34 | (i.e. `gem push --key my-key`). Pushing gems will fails unless my-key has write 35 | permissions. The gem command checks the ~/.gem/credentials file for my-key 36 | before attempting to push your gem so ensure that an entry for your key has been 37 | made. For example: 38 | 39 | ``` (~/.gem/credentials) 40 | :my-key: 123abc 41 | ``` 42 | 43 | To be able to download gems with `bundle install`, my-key must have the read 44 | permission. Bundler checks your bundler config for my-key before installing a 45 | gem. Ensure that bundler knows to use my-key for any gem on your gemserver. This 46 | can be done by running: 47 | `bundler config http://[GEMSERVER-DOMAIN].com/private my-key`. For example" 48 | `bundler config http://my-gemserver.appspot.com/private my-key` 49 | 50 | ## Deleting a key 51 | To delete a key called my-key, run: 52 | `google-cloud-gemserver delete-key --key my-key --use-proj PROJECT_ID` 53 | If there is a specific gemserver you want to delete the key from (not the current 54 | GCP project in `gcloud config`, run `google-cloud-gemserver delete-key my-key 55 | --remote mygemserver.appspot.com`. Note that the command will only succeed if 56 | you have access to the GCP project containing that gemserver. 57 | -------------------------------------------------------------------------------- /docs/running-locally.md: -------------------------------------------------------------------------------- 1 | # Running the gemserver 2 | 3 | If you want to use the project and Cloud SQL instance in config.yml, no extra 4 | work is necessary as that is the default setting. Prepend any 5 | `google-cloud-gemserver` command with `APP_ENV=dev` if you want to use the 6 | project and Cloud SQL instance in dev-config.yml. 7 | 8 | # How to run locally 9 | 10 | Unlike the above, you normally want to run a standalone gemserver with no 11 | reliance on backend services when you want to run a gemserver locally. To do so, 12 | prepend `google-cloud-gemserver` commands with `APP_ENV=test`. This creates and 13 | uses a local sqlite3 instance with the default database, username, etc. settings as 14 | defined [here](configuration.md). All that changes is that your gemserver url 15 | becomes "http://localhost:8080" and the sqlite3 instance is used for key/gem 16 | storage. 17 | 18 | ## Pushing gems 19 | Simply run `gem push --key my-key [GEM_PATH] --host 20 | http://localhost:8080/private` while ensuring my-key is set in 21 | ~/.gem/credentials (visit the [key document](key.md) for more details) 22 | 23 | ## Downloading gems 24 | It is the exact same as outlined in the [usage example](usage_example.md) 25 | except the gemserver url is http://localhost:8080. For example, this will become 26 | your Gemfile: 27 | 28 | ``` 29 | source "http://localhost:8080" 30 | source "http://localhost:8080/private" do 31 | gem "gem1" 32 | gem "gem2" 33 | end 34 | ``` 35 | 36 | Again, ensure bundler config knows about my-key by running: 37 | `bundle config http://localhost:8080/private my-key` otherwise gem installation 38 | will fail since bundler does not know about my-key for this source. 39 | -------------------------------------------------------------------------------- /docs/usage_example.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Deployment 4 | 1) Install the gem with `gem install google-cloud-gemserver` 5 | 2) Deploy the gemserver by running `production google-cloud-gemserver create --use-proj 6 | [PROJECT_ID]`. This deploys the gemserver as a Google App Engine project to GCP. 7 | In doing it, it creates a Cloud SQL instance. If you wish to use an existing 8 | Cloud SQL instance run `google-cloud-gemserver create --use-proj [PROJECT_ID] 9 | --use-inst [CLOUD_SQL INSTANCE NAME]` 10 | 11 | Note: If you do not want to deploy to GCP and instead want to use the Cloud SQL 12 | instance in dev_config.yml or sqlite3 database to run the gemserver completely 13 | locally then prepend "APP_ENV=dev" or "APP_ENV=test" to google-cloud-gemserver 14 | commands, accordingly. Read [running locally](running-locally.md) for more 15 | details. 16 | 17 | Your gemserver is now up and running! 18 | 19 | ## Pushing gems 20 | 21 | To push a gem, simply run `gem push --key [key] [path-to-gem] --host 22 | [gemserver-url/private]` 23 | 24 | Here is an example: 25 | 26 | * assume you have deployed a gemserver to a project called my-gemserver. This 27 | project has the following url: http://my-gemserver.appspot.com 28 | * assume you created a key with read and write permissions called my-key (read 29 | more about key [here](key.md) for more information) 30 | 31 | 1) Create a new gem with `bundle gem private-gem` 32 | 2) Edit `private-gem.gemspec` such that the gem can be built and pushed to 33 | arbitrary endpoints by removing the `spec.respond_to?` conditional. 34 | 3) Build the gem with `rake build`. This created a .gem file in pkg/ that we 35 | will push to the gemserver. 36 | 4) Create a key (referred to as my-key) for your gemserver if you have not 37 | already by running `google-cloud-gemserver create-key --use-proj PROJECT_ID`. 38 | 5) Push the gem to your gemserver: `gem push --key my-key 39 | pkg/private-gem-0.1.0.gem --host http://my-gemserver.appspot.com/private` 40 | Note the url has /private at the end; this is important otherwise pushing gems 41 | will fail. 42 | 43 | ## Installing Gems 44 | 45 | Note: same assumptions as above 46 | 47 | 1) Add `source "http://my-gemserver.appspot.com" to the top of your Gemfile. 48 | This lets the gemserver fetch private gem dependencies from rubygems.org if they 49 | are not currently cached. 50 | 2) Wrap private gems within a `source "http://my-gemserver.appspot.com/private 51 | do" block such that private gems are fetched from that source. Again, note the 52 | /private at the end of the url. 53 | 3) Run `bundle install` 54 | 55 | That's all it takes to push gems to the gemserver and later install them. 56 | -------------------------------------------------------------------------------- /google-cloud-gemserver.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | lib = File.expand_path("../lib", __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | require "google/cloud/gemserver/version" 7 | 8 | Gem::Specification.new do |spec| 9 | 10 | spec.name = "google-cloud-gemserver" 11 | spec.version = Google::Cloud::Gemserver::VERSION 12 | spec.authors = ["Arham Ahmed"] 13 | spec.email = ["arhamahmed@google.com"] 14 | 15 | spec.summary = "CLI to manage a private gemserver on Google App Engine" 16 | spec.description = "This gem provides an easy interface to deploy and" \ 17 | "manage a private gem server on Google Cloud Platform." 18 | spec.homepage = "https://github.com/GoogleCloudPlatform/google-cloud-gemserver" 19 | spec.license = "Apache-2.0" 20 | 21 | spec.files = Dir["**/*"].select do |f| 22 | f.match(/^(bin|lib)/) && File.file?(f) 23 | end + ["CONTRIBUTING.md", "README.md", "LICENSE", ".yardopts"] 24 | 25 | spec.executables = "google-cloud-gemserver" 26 | spec.require_paths = ["lib"] 27 | 28 | spec.add_runtime_dependency "thor", "~> 0.19" 29 | spec.add_runtime_dependency "google-cloud-resource_manager", "~> 0.24" 30 | spec.add_runtime_dependency "google-cloud-storage", "~> 1.1.0" 31 | spec.add_runtime_dependency "activesupport", "~> 4.2" 32 | spec.add_runtime_dependency "googleauth", "~> 0.5.3" 33 | 34 | spec.add_development_dependency "mysql2", "~> 0.4" 35 | spec.add_development_dependency "filelock", "~> 1.1.1" 36 | spec.add_development_dependency "rake", "~> 11.0" 37 | spec.add_development_dependency "minitest", "~> 5.10" 38 | spec.add_development_dependency "minitest-autotest", "~> 1.0" 39 | spec.add_development_dependency "minitest-focus", "~> 1.1" 40 | spec.add_development_dependency "minitest-rg", "~> 5.2" 41 | spec.add_development_dependency "rubocop", "<= 0.49.1" 42 | spec.add_development_dependency "yard", "~> 0.9" 43 | end 44 | -------------------------------------------------------------------------------- /integration/google-cloud-storage-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/google-cloud-gemserver/7a246a68405fb28056a75d76130adb8d35b5236d/integration/google-cloud-storage-1.1.0.gem -------------------------------------------------------------------------------- /integration/integration_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | gem "minitest" 16 | require "minitest/autorun" 17 | require "minitest/rg" 18 | require "minitest/focus" 19 | require "google/cloud/gemserver" 20 | require "fileutils" 21 | 22 | HOST = ENV["host"].freeze 23 | GEM = "private-test-gem".freeze 24 | VER = "0.1.0".freeze 25 | GEM_PATH = "integration/#{GEM}-#{VER}.gem".freeze 26 | KEY = ENV["key"].freeze 27 | GCG = Google::Cloud::Gemserver 28 | 29 | describe Google::Cloud::Gemserver do 30 | let(:gemfile) { 31 | gemfile = "source \"http://#{HOST}/private\" do\n" \ 32 | "gem \"#{GEM}\"\nend" 33 | File.open("integration/gem_install/Gemfile", "w") do |f| 34 | f.write gemfile 35 | end 36 | } 37 | 38 | let(:reset) { 39 | env = Google::Cloud::Gemserver::Backend::Stats.new.send(:env) 40 | env.db[:versions].delete 41 | env.db[:rubygems].delete 42 | } 43 | 44 | let(:push) { 45 | url = "http://#{HOST}/private" 46 | `gem push --key #{KEY} #{GEM_PATH} --host #{url}` 47 | } 48 | 49 | let(:yank) { 50 | url = "http://#{HOST}/private" 51 | `RUBYGEMS_HOST=#{url} gem yank --key #{KEY} #{GEM} --version #{VER}` 52 | } 53 | let(:range) { 15..15+GCG::Backend::Key::KEY_LENGTH } 54 | 55 | after(:all) do 56 | reset 57 | end 58 | 59 | it "can push gems" do 60 | reset 61 | refute push.include?("Internal Server Error") 62 | end 63 | 64 | it "can yank gems" do 65 | reset 66 | push 67 | refute yank.include?("Internal Server Error") 68 | end 69 | 70 | it "can install gems" do 71 | gemfile 72 | reset 73 | push 74 | out = `cd integration/gem_install && bundle install` 75 | assert out.include?("Bundle complete!") 76 | end 77 | 78 | it "can get gemserver stats" do 79 | out = `google-cloud-gemserver stats -r #{HOST}` 80 | assert out.include?("Project Information") 81 | assert out.include?("Private Gems") 82 | assert out.include?("Cached Gem Dependencies") 83 | end 84 | 85 | it "can create a gemserver key" do 86 | # response format => Generated key: KEY 87 | out = `google-cloud-gemserver create-key -r #{HOST}` 88 | assert out.size > 16 89 | `google-cloud-gemserver delete-key -k #{out[range].chomp} -r #{HOST}` 90 | out = `google-cloud-gemserver create-key -p both -r #{HOST}` 91 | assert out.size > 16 92 | `google-cloud-gemserver delete-key -k #{out[range].chomp} -r #{HOST}` 93 | out = `google-cloud-gemserver create-key -p write -r #{HOST}` 94 | assert out.size > 16 95 | `google-cloud-gemserver delete-key -k #{out[range].chomp} -r #{HOST}` 96 | out = `google-cloud-gemserver create-key -p read -r #{HOST}` 97 | assert out.size > 16 98 | `google-cloud-gemserver delete-key -k #{out[range].chomp} -r #{HOST}` 99 | end 100 | 101 | it "can delete a gemserver key" do 102 | raw = `google-cloud-gemserver create-key -r #{HOST}` 103 | refute raw.include? "Internal server error" 104 | out = `google-cloud-gemserver delete-key -k #{raw[range].chomp} -r #{HOST}` 105 | assert out.include?("success") 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /integration/private-test-gem-0.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/google-cloud-gemserver/7a246a68405fb28056a75d76130adb8d35b5236d/integration/private-test-gem-0.1.0.gem -------------------------------------------------------------------------------- /lib/google/cloud/gemserver.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | module Google 16 | module Cloud 17 | ## 18 | # 19 | # # Gemserver 20 | # 21 | # Gemserver provides a command line interface to create, manage, and deploy 22 | # a gemserver to a Google Cloud Platform project. 23 | # 24 | module Gemserver 25 | autoload :CLI, "google/cloud/gemserver/cli" 26 | autoload :VERSION, "google/cloud/gemserver/version" 27 | autoload :Configuration, "google/cloud/gemserver/configuration" 28 | autoload :GCS, "google/cloud/gemserver/gcs" 29 | autoload :Authentication, "google/cloud/gemserver/authentication" 30 | autoload :Backend, "google/cloud/gemserver/backend" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/authentication.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "google/cloud/gemserver" 16 | require "json" 17 | require "googleauth" 18 | require "net/http" 19 | require "uri" 20 | 21 | module Google 22 | module Cloud 23 | module Gemserver 24 | ## 25 | # 26 | # # Authentication 27 | # 28 | # Manages the permissions of the currently logged in user with the gcloud 29 | # sdk. 30 | # 31 | class Authentication 32 | 33 | ## 34 | # The project id of the Google App Engine project the gemserver was 35 | # deployed to. 36 | # @return [String] 37 | attr_accessor :proj 38 | 39 | ## 40 | # Creates the Authentication object and sets the project id field. 41 | def initialize 42 | @proj = Configuration.new[:proj_id] 43 | end 44 | 45 | ## 46 | # Checks if the currently logged in user can modify the gemserver 47 | # i.e. create keys. 48 | # 49 | # @return [Boolean] 50 | def can_modify? 51 | user = curr_user 52 | owners.each do |owner| 53 | return true if extract_account(owner) == user 54 | end 55 | editors.each do |editor| 56 | return true if extract_account(editor) == user 57 | end 58 | puts "You are either not authenticated with gcloud or lack access" \ 59 | " to the gemserver." 60 | false 61 | end 62 | 63 | ## 64 | # Generates an access token from a user authenticated by gcloud. 65 | # 66 | # @return [String] 67 | def access_token 68 | return unless can_modify? 69 | scope = ["https://www.googleapis.com/auth/cloud-platform"] 70 | auth = Google::Auth.get_application_default scope 71 | auth.fetch_access_token! 72 | end 73 | 74 | ## 75 | # @private Implicitly checks if the account that generated the token 76 | # has edit permissions on the Google Cloud Platform project by issuing 77 | # a redundant update to the project (update to original settings). 78 | # 79 | # @param [String] The authentication token generated from gcloud. 80 | # 81 | # @return [Boolean] 82 | def validate_token auth_header 83 | token = auth_header.split.drop(1)[0] 84 | 85 | appengine_url = "https://appengine.googleapis.com" 86 | endpoint = "/v1/apps/#{@proj}/services/default?updateMask=split" 87 | version = appengine_version token 88 | split = { 89 | "split" => { 90 | "allocations" => { 91 | version.to_s => 1 92 | } 93 | } 94 | } 95 | res = send_req appengine_url, endpoint, Net::HTTP::Patch, token, split 96 | if check_status(res) 97 | op = JSON.parse(res.body)["name"] 98 | wait_for_op appengine_url, "/v1/#{op}", token 99 | true 100 | else 101 | false 102 | end 103 | end 104 | 105 | private 106 | 107 | ## 108 | # @private Fetches the latest version of the deployed Google App Engine 109 | # instance running the gemserver (default service only). 110 | # 111 | # @param [String] The authentication token generated from gcloud. 112 | # 113 | # @return [String] 114 | def appengine_version token 115 | appengine_url = "https://appengine.googleapis.com" 116 | path = "/v1/apps/#{@proj}/services/default" 117 | res = send_req appengine_url, path, Net::HTTP::Get, token 118 | 119 | fail "Unauthorized" unless check_status(res) 120 | 121 | JSON.parse(res.body)["split"]["allocations"].first[0] 122 | end 123 | 124 | ## 125 | # @private Sends a request to a given URL with given parameters. 126 | # 127 | # @param [String] dom The protocol + domain name of the request. 128 | # 129 | # @param [String] path The path of the URL. 130 | # 131 | # @param [Net::HTTP] type The type of request to be made. 132 | # 133 | # @param [String] token The authentication token used in the header. 134 | # 135 | # @param [Hash] params Additional parameters send in the request body. 136 | # 137 | # @return [Net::HTTPResponse] 138 | def send_req dom, path, type, token, params = nil 139 | uri = URI.parse dom 140 | http = Net::HTTP.new uri.host, uri.port 141 | http.use_ssl = true if dom.include? "https" 142 | 143 | req = type.new path 144 | req["Authorization"] = Signet::OAuth2.generate_bearer_authorization_header token 145 | unless type == Net::HTTP::Get 146 | if params 147 | req["Content-Type"] = "application/json" 148 | req.body = params.to_json 149 | end 150 | end 151 | http.request req 152 | end 153 | 154 | ## 155 | # @private Waits for a project update operation to complete. 156 | # 157 | # @param [String] dom The domain and protocol of the request. 158 | # 159 | # @param [String] path The path of the request containing the operation 160 | # ID. 161 | # 162 | # @param [String] token The authorization token in the request. 163 | # 164 | # @param [Integer] timeout The length of time the operation is polled. 165 | def wait_for_op dom, path, token, timeout = 60 166 | start = Time.now 167 | loop do 168 | if Time.now - start > timeout 169 | fail "Operation at #{path} failed to complete in time" 170 | else 171 | res = send_req dom, path, Net::HTTP::Get, token 172 | if JSON.parse(res.body)["done"] == true 173 | break 174 | end 175 | sleep 1 176 | end 177 | end 178 | end 179 | 180 | ## 181 | # @private Checks if a request response matches a given status code. 182 | # 183 | # @param [Net::HTTPResponse] reponse The response from a request. 184 | # 185 | # @param [Integer] code The desired response code. 186 | # 187 | # @return [Boolean] 188 | def check_status response, code = 200 189 | response.code.to_i == code 190 | end 191 | 192 | ## 193 | # @private Fetches the members with a specific role that have access 194 | # to the Google App Engine project the gemserver was deployed to. 195 | # 196 | # @return [Array] 197 | def members type 198 | yml = YAML.load(run_cmd "gcloud projects get-iam-policy #{@proj}") 199 | yml["bindings"].select do |member_set| 200 | member_set["role"] == type 201 | end[0]["members"] 202 | end 203 | 204 | ## 205 | # @private Fetches members with a role of editor that can access the 206 | # gemserver. 207 | # 208 | # @return [Array] 209 | def editors 210 | members "roles/editor" 211 | end 212 | 213 | ## 214 | # @private Fetches members with a role of owner that can access the 215 | # gemserver. 216 | # 217 | # @return [Array] 218 | def owners 219 | members "roles/owner" 220 | end 221 | 222 | ## 223 | # @private Fetches the active account of the currently logged in user. 224 | # 225 | # @return [String] 226 | def curr_user 227 | raw = run_cmd "gcloud auth list --format json" 228 | JSON.load(raw).map do |i| 229 | return i["account"] if i["status"] == "ACTIVE" 230 | end 231 | abort "You are not authenticated with gcloud" 232 | end 233 | 234 | ## 235 | # @private Parses a gcloud "member" and removes the account prefix. 236 | # 237 | # @param [String] acc The member the account is extracted from. 238 | # 239 | # @return [String] 240 | def extract_account acc 241 | acc[acc.index(":") + 1 .. acc.size] 242 | end 243 | 244 | ## 245 | # @private Runs a given command on the local machine. 246 | # 247 | # @param [String] args The command to be run. 248 | def run_cmd args 249 | `#{args}` 250 | end 251 | end 252 | end 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/backend.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | module Google 16 | module Cloud 17 | module Gemserver 18 | ## 19 | # 20 | # # Backend 21 | # 22 | # Contains services that run on Google App Engine directly leveraging 23 | # tools such as Cloud SQL proxy. 24 | # 25 | module Backend 26 | autoload :GemstashServer, "google/cloud/gemserver/backend/gemstash_server" 27 | autoload :Key, "google/cloud/gemserver/backend/key" 28 | autoload :Stats, "google/cloud/gemserver/backend/stats" 29 | autoload :StorageSync, "google/cloud/gemserver/backend/storage_sync" 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/backend/gemstash_server.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # @https://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 | require "patched/configuration" 16 | require "patched/dependencies" 17 | require "patched/env" 18 | require "patched/gem_pusher" 19 | require "patched/gem_yanker" 20 | require "patched/storage" 21 | require "patched/web" 22 | require "gemstash" 23 | 24 | module Google 25 | module Cloud 26 | module Gemserver 27 | module Backend 28 | ## 29 | # 30 | # # GemstashServer 31 | # 32 | # The class that runs gemstash specific commands and starts the gemstash 33 | # server. Parts of gemstash are monkey-patched with lib/patched for 34 | # compatibility with Google Cloud Platform services such as Cloud Storage 35 | # and Cloud SQL. 36 | module GemstashServer 37 | 38 | ## 39 | # Runs a given command through the gemstash gem. 40 | # 41 | # @param [String] args The argument passed to gemstash. 42 | def self.start args 43 | Gemstash::CLI.start args 44 | end 45 | 46 | ## 47 | # Fetches the gemstash environment given a configuration file. 48 | # 49 | # @param [String] config_path The path to the configuration file. 50 | # 51 | # @return [Gemstash::Env] 52 | def self.env config_path 53 | config = Gemstash::Configuration.new file: config_path 54 | Gemstash::Env.new config 55 | end 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/backend/key.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "google/cloud/gemserver" 16 | require "stringio" 17 | require "fileutils" 18 | require "yaml" 19 | 20 | module Google 21 | module Cloud 22 | module Gemserver 23 | module Backend 24 | ## 25 | # # Key 26 | # 27 | # Manages the creation and deletion of a key used to push gems to the 28 | # gemserver and download them. 29 | # 30 | class Key 31 | ## 32 | # A mapping from gemserver permissions to gemstash permissions. 33 | MAPPING = { 34 | "write" => %w[push yank], 35 | "read" => %w[fetch] 36 | }.freeze 37 | 38 | ## 39 | # Aliases for read and write permissions. 40 | ALL = ["both", "all", "", nil].freeze 41 | 42 | ## 43 | # Path to the credentials file checked when pushing gems to the gem 44 | # server (or any endpoint). 45 | GEM_CREDS = File.expand_path("~/.gem") 46 | 47 | ## 48 | # The length of a key generated by gemstash. 49 | KEY_LENGTH = 32 50 | 51 | ## 52 | # Creates a key with given permissions. 53 | # 54 | # @param permissions [String] The permissions for a key. Optional. 55 | # 56 | # @return [String] 57 | def self.create_key permissions = nil 58 | mapped = map_perms permissions 59 | args = base_args.concat mapped 60 | output = capture_stdout { GemstashServer.start args } 61 | key = parse_key(output) 62 | puts "Created key: #{key}" 63 | key 64 | end 65 | 66 | ## 67 | # Deletes a given key. 68 | # 69 | # @param [String] key The key to delete. 70 | def self.delete_key key 71 | args = [ 72 | "--remove", 73 | "--key=#{key}" 74 | ] 75 | GemstashServer.start base_args.concat(args) 76 | puts "Deleted key: #{key}" 77 | true 78 | end 79 | 80 | ## 81 | # @private Maps read/write permissions to the permissions the 82 | # gemstash gem uses. 83 | # 84 | # @param [String] perms The permissions to be mapped. 85 | def self.map_perms perms 86 | if perms == "write" 87 | MAPPING["write"] 88 | elsif ALL.include? perms 89 | MAPPING["write"] + MAPPING["read"] 90 | else 91 | MAPPING["read"] 92 | end 93 | end 94 | 95 | ## 96 | # @private Temporarily routes stdout to a temporary variable such 97 | # that stdout from gemstash is captured. 98 | # 99 | # @return [String] 100 | def self.capture_stdout 101 | old_stdout = $stdout 102 | $stdout = StringIO.new 103 | yield 104 | $stdout.string 105 | ensure 106 | $stdout = old_stdout 107 | end 108 | 109 | ## 110 | # @private The arguments passed to every gemstash key generation 111 | # command. 112 | # 113 | # @return [Array] 114 | def self.base_args 115 | [ 116 | "authorize", 117 | "--config-file=#{Google::Cloud::Gemserver::Configuration.new.config_path}" 118 | ] 119 | end 120 | 121 | ## 122 | # @private Outputs important information to the user on how they 123 | # should set up their keys so pushing/installing gems works as 124 | # intended. 125 | def self.output_key_info 126 | puts "Note: remember to add this key to ~/.gem/credentials" \ 127 | " so that you are able to push gems to the gemserver." 128 | puts "Note: remember to add this key to your bundle config so " \ 129 | "that `bundle install` works for private gems (bundle config" \ 130 | " http://my-gemserver.appspot.com/private/ my-key" 131 | end 132 | 133 | ## 134 | # Parses the key from output generated from the corresponding key 135 | # creation command in gemstash. 136 | # 137 | # @param [String] output The output to parse. 138 | # 139 | # @return [String] 140 | def self.parse_key output 141 | i = output.index(":") 142 | output[i+2..i+2+KEY_LENGTH].chomp 143 | end 144 | 145 | private_class_method :map_perms 146 | private_class_method :capture_stdout 147 | private_class_method :parse_key 148 | end 149 | end 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/backend/stats.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "google/cloud/gemserver" 16 | require "yaml" 17 | 18 | module Google 19 | module Cloud 20 | module Gemserver 21 | module Backend 22 | ## 23 | # # Stats 24 | # 25 | # Stats provides a set of methods that display detailed information 26 | # about the deployed gemserver. It includes: general Google Cloud 27 | # Platform project information, how long the gemserver has been running 28 | # , what private gems are stored, and what gems have been cached. 29 | # 30 | class Stats 31 | 32 | ## 33 | # The project ID of the project on Google Cloud Platform the 34 | # gemserver was deployed to. 35 | # @return [String] 36 | attr_accessor :proj 37 | 38 | ## 39 | # Initialize a Configuration object and project ID for the Stats 40 | # object enabling it to fetch detailed information about the 41 | # gemserver. 42 | def initialize 43 | @config = Google::Cloud::Gemserver::Configuration.new 44 | @proj = (@config[:proj_id] || nil).freeze 45 | end 46 | 47 | ## 48 | # Displays various sets of information about the gemserver such as 49 | # how long it has been running, currently stored, private gems and 50 | # their status, and cached gems. 51 | def run 52 | resp = "" 53 | resp << log_uptime 54 | resp << log_private_gems 55 | resp << log_cached_gems 56 | end 57 | 58 | ## 59 | # Displays information about the project on Google Cloud 60 | # Platform the gemserver was deployed to. 61 | def log_app_description 62 | return "" if ENV["APP_ENV"] == "test" 63 | puts "Project Information:" 64 | cmd = "gcloud app describe --project #{@proj}" 65 | puts run_cmd(cmd).gsub("\n", "\n\t").prepend "\t" 66 | end 67 | 68 | private 69 | 70 | ## 71 | # @private Displays the time of which the gemserver was deployed. 72 | def log_uptime 73 | return "" unless project 74 | "The gemserver has been running since #{project.created_at}\n" 75 | end 76 | 77 | ## 78 | # @private Displays the private gems stored on the gemserver and 79 | # their status (currently indexed or not). 80 | def log_private_gems 81 | res = "Private Gems:\n" 82 | versions = db :versions 83 | format = "%35s\t%20s\n" 84 | res << sprintf(format, "Gem Name - Version", "Available?") 85 | versions.map do |gem| 86 | res << sprintf(format, gem[:storage_id], gem[:indexed]) 87 | end 88 | puts res 89 | res 90 | end 91 | 92 | ## 93 | # @private Displays the gems cached on the gemserver. 94 | def log_cached_gems 95 | res = "Cached Gem Dependencies:\n" 96 | cached = db :cached_rubygems 97 | format = "%35s\t%20s\n" 98 | res << sprintf(format, "Gem Name - Version", "Date Cached") 99 | cached.map do |gem| 100 | res << sprintf(format, gem[:name], gem[:created_at]) 101 | end 102 | puts res 103 | res 104 | end 105 | 106 | ## 107 | # @private Fetches the Google Cloud Platform project the gemserver 108 | # was deployed to. 109 | # 110 | # @return [Project] 111 | def project 112 | if @proj.nil? 113 | return nil if ENV["APP_ENV"] == "test" 114 | raise ":proj_id not set in config file" 115 | end 116 | Google::Cloud::Gemserver::CLI::Project.new(@proj).send(:project) 117 | end 118 | 119 | ## 120 | # @private Fetches the Environment object currently being used by the 121 | # gemserver. It enables access to the database. 122 | # 123 | # @return [Gemstash::Env] 124 | def env 125 | GemstashServer.env @config.config_path 126 | end 127 | 128 | ## 129 | # @private Retrieves all the rows in the database for a given table. 130 | # 131 | # @param [String] table The table to be read. 132 | # 133 | # @return [Array] 134 | def db table 135 | env.db[table].all 136 | end 137 | 138 | ## 139 | # @private Runs a given command on the local machine. 140 | # 141 | # @param [String] cmd The command to be run. 142 | def run_cmd cmd 143 | `#{cmd}` 144 | end 145 | end 146 | end 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/backend/storage_sync.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "google/cloud/storage" 16 | require "google/cloud/gemserver" 17 | require "fileutils" 18 | require "filelock" 19 | require "digest/md5" 20 | require "singleton" 21 | require "concurrent" 22 | require "forwardable" 23 | 24 | module Google 25 | module Cloud 26 | module Gemserver 27 | module Backend 28 | ## 29 | # # Storage Sync 30 | # 31 | # A set of methods that manage syncing files between the local file 32 | # system that the gemserver runs on (a container on Google App Engine) 33 | # and Google Cloud Storage. By doing so, when the gemserver is restarted 34 | # , for whatever reason, the gems pushed to the gemserver will persist. 35 | # Without such a system in place all gems / files on the gemserver will 36 | # be lost as it runs on a container. 37 | # 38 | class StorageSync 39 | include Concurrent::Async 40 | include Singleton 41 | 42 | ## 43 | # A lock to ensure the .gemstash directory, used to store gem files, is 44 | # created atomically. 45 | DIR_LOCK = File.expand_path("~/gemstash_dir").freeze 46 | 47 | ## 48 | # Extend StorageSync such that it can be called without the 49 | # .instance method. 50 | class << self 51 | extend Forwardable 52 | 53 | ## 54 | # Delegate the run and download_service methods to the Singleton 55 | # via .instance. 56 | def_delegators :instance, :run, :upload_service, :download_service, 57 | :try_upload, :try_download, :file_changed? 58 | end 59 | 60 | ## 61 | # Creates an instance of the Singleton StorageSync class with a 62 | # background thread and asynchronous components to run methods 63 | # asynchronously. 64 | def initialize 65 | super 66 | end 67 | 68 | ## 69 | # Runs a background gem files syncing service to ensure they are up to 70 | # date with respect to the files on Google Cloud Storage. This allows 71 | # allow the gem metadata on the gemserver (in the container) to persist 72 | # in case of situations where the gemserver goes down. 73 | def run 74 | async.sync 75 | end 76 | 77 | ## 78 | # @private Runs the uploader to send updated gem files to Google Cloud 79 | # Storage (source of truth) then updates the rest of the gem files by 80 | # downloading them off Google Cloud Storage. 81 | def sync 82 | upload_service 83 | download_service 84 | end 85 | 86 | ## 87 | # @private The directory used to store gem data. 88 | # 89 | # @return [String] 90 | def gemstash_dir 91 | if ENV["APP_ENV"] == "production" 92 | Configuration::GEMSTASH_DIR 93 | else 94 | File.expand_path("~/.gemstash") 95 | end 96 | end 97 | 98 | ## 99 | # @private Create the directory used to store gem data. 100 | def prepare_dir 101 | Filelock DIR_LOCK do 102 | FileUtils.mkpath gemstash_dir 103 | end 104 | end 105 | 106 | ## 107 | # @private The uploading service that uploads gem files from the local 108 | # file system to Google Cloud Storage. It does not upload any cached 109 | # files. 110 | def upload_service 111 | puts "Running uploading service..." 112 | prepare_dir 113 | 114 | entries = Dir.glob("#{gemstash_dir}/**/*").reject do |e| 115 | e.include? "gem_cache" 116 | end 117 | entries.each do |e| 118 | try_upload e if File.file? e 119 | end 120 | end 121 | 122 | ## 123 | # @private Uploads a file to Google Cloud Storage only if the file 124 | # has yet to be uploaded or has a different hash from the Cloud copy. 125 | # 126 | # @param [String] file The path to the file to be uploaded. 127 | def try_upload file 128 | GCS.upload(file) unless GCS.on_gcs?(file) 129 | return unless file_changed?(file) 130 | Filelock file do 131 | GCS.upload file 132 | end 133 | end 134 | 135 | ## 136 | # @private The downloading service that downloads gem files from Google 137 | # Cloud Storage to the local file system. 138 | def download_service 139 | puts "Running downloading service..." 140 | prepare_dir 141 | 142 | files = GCS.files 143 | return unless files 144 | files.each { |file| try_download file.name } 145 | end 146 | 147 | ## 148 | # @private Downloads a file to the local file sytem from Google Cloud 149 | # Storage only if there is sufficient space and the local copy's hash 150 | # differs from the cloud copy's hash. 151 | # 152 | # @param [String] file Name of the file to download. 153 | def try_download file 154 | total = `df -k /`.split(" ")[11].to_f 155 | used = `df -k /`.split(" ")[12].to_f 156 | usage = used / total 157 | if usage < 0.95 158 | if File.exist? file 159 | Filelock(file) { GCS.sync file if file_changed? file } 160 | else 161 | GCS.copy_to_host file 162 | end 163 | else 164 | raise "Error downloading: disk usage at #{usage}! Increase disk space!" 165 | end 166 | end 167 | 168 | ## 169 | # @private Determines if a file on the local file system has changed 170 | # from the corresponding file on Google Cloud Storage, if it exists. 171 | # 172 | # @param [String] file Name of the file 173 | # 174 | # @return [Boolean] 175 | def file_changed? file 176 | return true unless File.exist? file 177 | return true unless GCS.get_file(file) 178 | gcs_md5 = GCS.get_file(file).md5 179 | local_md5 = Digest::MD5.file(file).base64digest 180 | gcs_md5 != local_md5 181 | end 182 | end 183 | end 184 | end 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/cli.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "google/cloud/gemserver" 16 | require "thor" 17 | 18 | module Google 19 | module Cloud 20 | module Gemserver 21 | ## 22 | # 23 | # # CLI 24 | # 25 | # The command line interface which provides methods to interact with a 26 | # gemserver and deploy it to a given Google Cloud Platform project. 27 | # 28 | class CLI < Thor 29 | autoload :Project, "google/cloud/gemserver/cli/project" 30 | autoload :CloudSQL, "google/cloud/gemserver/cli/cloud_sql" 31 | autoload :Server, "google/cloud/gemserver/cli/server" 32 | autoload :Request, "google/cloud/gemserver/cli/request" 33 | 34 | # Error class thrown when a command that does not exist is run. 35 | class Error < Thor::Error 36 | def initialize cli, message 37 | super cli.set_color(message, :red) 38 | end 39 | end 40 | 41 | def self.start args = ARGV 42 | Configuration.new.gen_config 43 | super 44 | end 45 | 46 | ## 47 | # Starts the gemserver by starting up gemstash. 48 | desc "start", "Starts the gem server. This will be run automatically" \ 49 | " after a deploy. Running this locally will start the gemserver "\ 50 | "locally" 51 | def start 52 | Server.new.start 53 | end 54 | 55 | ## 56 | # Creates a gemserver app and deploys it to a Google Cloud Platform 57 | # project. An existing Google Cloud Platform project must be provided 58 | # through the --use-proj option and an existing Cloud SQL instance may 59 | # be provided through the --use-inst option, otherwise a new one will 60 | # be created. 61 | desc "create", "Creates and deploys the gem server then starts it" 62 | method_option :use_proj, type: :string, aliases: "-g", desc: 63 | "Existing project to deploy gemserver to" 64 | method_option :use_inst, type: :string, aliases: "-i", desc: 65 | "Existing project to deploy gemserver to" 66 | def create 67 | prepare 68 | Server.new.deploy 69 | end 70 | 71 | ## 72 | # Retrieves a Google Cloud Platform instance and informs the user to 73 | # enable necessary APIs for that project. Also creates a Cloud SQL 74 | # instance if one was not provided with the --use-inst option. 75 | desc "prepare", "Uses a project on Google Cloud Platform and deploys"\ 76 | " a gemserver to it." 77 | method_option :use_proj, type: :string, aliases: "-g", desc: 78 | "Existing project to deploy gemserver to" 79 | method_option :use_inst, type: :string, aliases: "-i", desc: 80 | "Existing Cloud SQL instance to us" 81 | def prepare 82 | Project.new(options[:use_proj]).create 83 | CloudSQL.new(options[:use_inst]).run 84 | end 85 | 86 | ## 87 | # Updates the gemserver on Google Cloud Platform to the latest version 88 | # of the gemserver installed on the user's system. 89 | desc "update", "Redeploys the gemserver with the current config file" \ 90 | " and google-cloud-gemserver gem version (a deploy must have " \ 91 | "succeeded for 'update' to work." 92 | def update 93 | Server.new.update 94 | end 95 | 96 | ## 97 | # Deletes a given gemserver provided by the --use-proj option. 98 | # This deletes the Google Cloud Platform project, all associated 99 | # Cloud SQL instances, and all Cloud Storage buckets. 100 | desc "delete", "Delete a given gemserver" 101 | method_option :use_proj, type: :string, aliases: "-g", desc: 102 | "Project id of GCP project the gemserver was deployed to. Warning:"\ 103 | " parent project and CloudSQL instance will also be deleted" 104 | def delete 105 | Server.new.delete options[:use_proj] 106 | end 107 | 108 | ## 109 | # Creates a key used for installing or pushing gems to the given 110 | # gemserver with given permissions provided with the --permissions 111 | # option. By default, a key with all permissions is created. 112 | desc "create_key", "Creates an authentication key" 113 | method_option :permissions, type: :string, aliases: "-p", desc: 114 | "Options: write, read, both. Default is both." 115 | method_option :remote, type: :string, aliases: "-r", desc: 116 | "The gemserver URL, i.e. gemserver.com" 117 | method_option :use_proj, type: :string, aliases: "-g", desc: 118 | "The GCP project the gemserver was deployed to." 119 | def create_key 120 | if ENV["APP_ENV"] == "test" 121 | return Backend::Key.create_key(options[:permissions]) 122 | end 123 | puts Request.new(options[:remote], options[:use_proj]).create_key(options[:permissions]).body 124 | Backend::Key.output_key_info 125 | end 126 | 127 | ## 128 | # Deletes a given key provided by the --key option from the given 129 | # gemserver. 130 | desc "delete_key", "Deletes a given key" 131 | method_option :key, type: :string, aliases: "-k", desc: 132 | "The key to delete" 133 | method_option :remote, type: :string, aliases: "-r", desc: 134 | "The gemserver URL, i.e. gemserver.com" 135 | method_option :use_proj, type: :string, aliases: "-g", desc: 136 | "The GCP project the gemserver was deployed to." 137 | def delete_key 138 | if ENV["APP_ENV"] == "test" 139 | return Backend::Key.delete_key(options[:key]) 140 | end 141 | puts Request.new(options[:remote], options[:use_proj]).delete_key(options[:key]).body 142 | end 143 | 144 | ## 145 | # Displays the configuration used by the currently deployed gemserver. 146 | desc "config", "Displays the config the current deployed gemserver is"\ 147 | " using (if one is running)" 148 | def config 149 | Configuration.display_config 150 | end 151 | 152 | ## 153 | # Displays statistics on the given gemserver such as private gems, 154 | # cached gems, gemserver creation time, etc. 155 | desc "stats", "Displays statistics on the given gemserver" 156 | method_option :remote, type: :string, aliases: "-r", desc: 157 | "The gemserver URL, i.e. gemserver.com" 158 | method_option :use_proj, type: :string, aliases: "-g", desc: 159 | "The GCP project the gemserver was deployed to." 160 | def stats 161 | return Backend::Stats.new.run if ENV["APP_ENV"] == "test" 162 | Backend::Stats.new.log_app_description 163 | puts Request.new(options[:remote], options[:use_proj]).stats.body 164 | end 165 | 166 | desc "gen_config", "Generates configuration files with default" \ 167 | " values" 168 | def gen_config 169 | Configuration.new.gen_config 170 | end 171 | end 172 | end 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/cli/cloud_sql.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "google/cloud/gemserver" 16 | require "securerandom" 17 | require "google/apis/sqladmin_v1beta4" 18 | require "googleauth" 19 | require "yaml" 20 | 21 | module Google 22 | module Cloud 23 | module Gemserver 24 | class CLI 25 | ## 26 | # # CloudSQL 27 | # 28 | # CloudSQL manages the creation of a Cloud SQL instance as well as 29 | # the necessary database and user creation. 30 | # 31 | class CloudSQL 32 | ## 33 | # Permits SQL admin operations with the Cloud SQL API. 34 | SCOPES = ["https://www.googleapis.com/auth/sqlservice.admin"] 35 | .freeze 36 | 37 | ## 38 | # An alias for the SqladminV1beta4 module. 39 | SQL = Google::Apis::SqladminV1beta4 40 | 41 | ## 42 | # The name of the database used to store gemserver data. 43 | # @return [String] 44 | attr_reader :db 45 | 46 | ## 47 | # The name of the default user created to access the database. 48 | # @return [String] 49 | attr_reader :user 50 | 51 | ## 52 | # The password of the default user created to access the database. 53 | # @return [String] 54 | attr_reader :pwd 55 | 56 | ## 57 | # The project ID of the project the gemserver will be deployed to. 58 | # @return [String] 59 | attr_reader :proj_id 60 | 61 | ## 62 | # The name of the Cloud SQL instance. 63 | # @return [String] 64 | attr_reader :inst 65 | 66 | ## 67 | # The Cloud SQL API used to manage Cloud SQL instances. 68 | # @return [Google::Apis::SqladminV1beta4::SQLAdminService] 69 | attr_reader :service 70 | 71 | ## 72 | # Creates a CloudSQL object and loads the necessary configuration 73 | # settings. 74 | # 75 | # @param inst [String] Name of the instance to be used. Optional. 76 | def initialize inst = nil 77 | auth = Google::Auth.get_application_default SCOPES 78 | Google::Apis::RequestOptions.default.authorization = auth 79 | @config = Configuration.new 80 | @service = SQL::SQLAdminService.new 81 | @inst = inst || "instance-#{SecureRandom.uuid}".freeze 82 | @custom = inst ? true : false 83 | load_config 84 | end 85 | 86 | ## 87 | # Prepares a Cloud SQL instance with a database and user. Also saves 88 | # the database settings in the appropriate configuration file. 89 | def run 90 | create_instance do |instance_op| 91 | run_sql_task instance_op if instance_op.class == SQL::Operation 92 | update_root_user 93 | create_db do |db_op| 94 | run_sql_task db_op 95 | create_user 96 | end 97 | end 98 | update_config 99 | end 100 | 101 | private 102 | 103 | ## 104 | # @private Creates a Cloud SQL instance. 105 | def create_instance &block 106 | if @custom 107 | puts "Using existing Cloud SQL instance: #{@inst}" 108 | yield 109 | return instance 110 | end 111 | puts "Creating Cloud SQL instance #{@inst} (this takes a few "\ 112 | "minutes to complete)" 113 | settings = SQL::Settings.new(tier: "db-f1-micro") 114 | payload = SQL::DatabaseInstance.new( 115 | name: @inst, 116 | project: @proj_id, 117 | settings: settings 118 | ) 119 | @service.insert_instance(@proj_id, payload, &block) 120 | end 121 | 122 | ## 123 | # @private Creates a database for a Cloud SQL instance. 124 | def create_db &block 125 | puts "Creating database #{@db}" 126 | db = SQL::Database.new name: @db 127 | @service.insert_database(@proj_id, @inst, db, &block) 128 | end 129 | 130 | ## 131 | # @private Creates a user for a Cloud SQL instance. 132 | def create_user 133 | puts "Creating user #{@user}" 134 | user = SQL::User.new(name: @user, password: @pwd) 135 | run_sql_task @service.insert_user(@proj_id, @inst, user) 136 | end 137 | 138 | ## 139 | # @private Updates the password of the root user if a new Cloud SQL 140 | # instance was created. 141 | def update_root_user 142 | return if @custom 143 | cmd = "gcloud sql users set-password root % --password #{@pwd} "\ 144 | "-i #{@inst} --project #{@proj_id}" 145 | `#{cmd}` 146 | end 147 | 148 | ## 149 | # @private Fetches a Cloud SQL instance. 150 | # 151 | # @return [Google::Apis::SqladminV1beta4::DatabaseInstance 152 | def instance 153 | @service.get_instance @proj_id, @inst 154 | end 155 | 156 | ## 157 | # Deletes the Cloud SQL instance for a gemserver. 158 | def del_instance 159 | puts "Deleting instance: #{@inst}" 160 | @service.delete_instance @proj_id, @inst 161 | end 162 | 163 | ## 164 | # Sets various Cloud SQL settings used to create a Cloud SQL 165 | # instance. 166 | def load_config 167 | @db = @config[:db_connection_options][:database] 168 | @user = @config[:db_connection_options][:username] 169 | @pwd = @config[:db_connection_options][:password] 170 | @proj_id = @config[:proj_id] 171 | end 172 | 173 | ## 174 | # Saves the Cloud SQL configuration in the appropriate configuration 175 | # file and app configuration file. 176 | def update_config 177 | puts "Updating configurations: app.yaml and config.yml " 178 | conn_name = instance.connection_name 179 | @config.update_config "/cloudsql/#{conn_name}", 180 | :db_connection_options, 181 | :socket 182 | @config.update_app conn_name, "beta_settings", "cloud_sql_instances" 183 | end 184 | 185 | ## 186 | # Runs a Cloud SQL task and polls for its completion. 187 | def run_sql_task op 188 | while @service.get_operation(@proj_id, op.name).status != "DONE" 189 | sleep 2 190 | end 191 | end 192 | end 193 | end 194 | end 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/cli/project.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "google/cloud/gemserver" 16 | require "google/cloud/resource_manager" 17 | require "securerandom" 18 | 19 | module Google 20 | module Cloud 21 | module Gemserver 22 | class CLI 23 | ## 24 | # # Project 25 | # 26 | # Holds a reference to a project on Google Cloud Platform. 27 | # 28 | class Project 29 | 30 | ## 31 | # The name of the project on Google Cloud platform; same as ID. 32 | # @return [String] 33 | attr_accessor :proj_name 34 | 35 | ## 36 | # The Configuration object storing the settings used by the Project 37 | # object. 38 | # @return [Configuration] 39 | attr_accessor :config 40 | 41 | ## 42 | # Initializes the project name and Configuration object. 43 | def initialize name = nil 44 | @proj_name = name 45 | @config = Configuration.new 46 | end 47 | 48 | ## 49 | # Fetches a reference to the given project on Google Cloud Platform 50 | # and prompts the user to configure it correctly. 51 | def create 52 | raise "Project name was not provided!" unless @proj_name 53 | begin 54 | @config.update_config @proj_name, :proj_id 55 | create_gae_project 56 | enable_api 57 | enable_billing 58 | project 59 | rescue Google::Cloud::PermissionDeniedError, RuntimeError => e 60 | puts "Permission denied. You might not be authorized with " \ 61 | "gcloud. Read github.com/GoogleCloudPlatform/google-cloud`." \ 62 | "-ruby/google-cloud-gemserver/docs/authentication.md for " \ 63 | "more information on how to get authenticated." 64 | puts "If you still get this error the Cloud Resource Manager " \ 65 | "API might not be enabled." 66 | abort "More details: #{e.message}" 67 | end 68 | end 69 | 70 | private 71 | 72 | ## 73 | # @private Checks if the current Google Cloud Platform project 74 | # contains a Google App Engine project. If not, one is created. 75 | def create_gae_project 76 | return if project_exists? 77 | puts "Required Google App Engine project does not exist." 78 | system "gcloud app create --project #{@proj_name}" 79 | end 80 | 81 | ## 82 | # @private Checks if a Google App Engine project exists. 83 | # 84 | # @return [Boolean] 85 | def project_exists? 86 | system "gcloud app describe --project #{@proj_name} >/dev/null 2>&1" 87 | end 88 | 89 | ## 90 | # @private Prompts the user to press enter. 91 | # 92 | # @return [String] 93 | def prompt_user 94 | puts "\nPress enter to continue..." 95 | STDIN.gets 96 | end 97 | 98 | ## 99 | # @private Fetches a given project on Google Cloud Platform. 100 | # 101 | # @return [Google::Cloud::ResourceManager::Project] 102 | def project 103 | resource_manager = Google::Cloud::ResourceManager.new 104 | resource_manager.project @proj_name 105 | end 106 | 107 | ## 108 | # @private Prompts the user to enable necessary APIs for the 109 | # gemserver to work as intended. 110 | # 111 | # @return [String] 112 | def enable_api 113 | puts "\nEnable the Google Cloud SQL API if it is not already "\ 114 | "enabled by visiting:\n https://console.developers.google.com"\ 115 | "/apis/api/sqladmin.googleapis.com/overview?"\ 116 | "project=#{@proj_name} and clicking \"Enable\"" 117 | puts "\nEnable the Google Cloud Resource manager API if it is not"\ 118 | "already enabled by visiting:\nhttps://console.developers.google"\ 119 | ".com/apis/api/cloudresourcemanager.googleapis.com/overview?"\ 120 | "project=#{@proj_name} and clicking \"Enable\"" 121 | puts "\nEnable the Google App Engine Admin API if it is not "\ 122 | "already enabled by visiting:\nhttps://console.developers.google"\ 123 | ".com/apis/api/appengine.googleapis.com/overview?"\ 124 | "project=#{@proj_name} and clicking \"Enable\"" 125 | prompt_user 126 | end 127 | 128 | ## 129 | # @private Prompts the user to enable billing such that the gemserver 130 | # work as intended. 131 | # 132 | # @return [String] 133 | def enable_billing 134 | puts "\nEnable billing for the project you just created by "\ 135 | "visiting: \nconsole.cloud.google.com/billing?project="\ 136 | "#{@proj_name}\nand setting up a billing account." 137 | prompt_user 138 | end 139 | end 140 | end 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/cli/request.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # @https://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 | require "google/cloud/gemserver" 16 | require "net/http" 17 | require "yaml" 18 | 19 | module Google 20 | module Cloud 21 | module Gemserver 22 | class CLI 23 | ## 24 | # 25 | # # Request 26 | # 27 | # Responsible for sending requests to the gemserver for operations that 28 | # involve the database. Gem operations are done with the 'gem' command 29 | # and are not in the scope of Request. 30 | class Request 31 | 32 | ## 33 | # The HTTP object used to connect to and send requests to the 34 | # gemserver. 35 | # @return [Net::HTTP] 36 | attr_accessor :http 37 | 38 | ## 39 | # Initialize the Backend object by constructing an HTTP object for the 40 | # gemserver. 41 | # 42 | # @param [String] url The URL of the gemserver. Optional. 43 | # 44 | # @param [String] proj_name The name of the Google Cloud Platform the 45 | # gemserver was deployed to. Optional. 46 | def initialize url = nil, proj_name = nil 47 | gemserver_url = url.nil? == true ? remote(proj_name) : url 48 | @http = Net::HTTP.new gemserver_url 49 | end 50 | 51 | ## 52 | # Send a request to the gemserver to create a key with certain 53 | # permissions. 54 | # 55 | # @param [String] permissions The permissions the generated key will 56 | # have (read, write, or both). Optional. 57 | # 58 | # @return [Net::HTTPResponse] 59 | def create_key permissions = nil 60 | send_req Net::HTTP::Post, "/api/v1/key", {permissions: permissions} 61 | end 62 | 63 | ## 64 | # Send a request to the gemserver to delete a key. 65 | # 66 | # @param [String] key The key to delete. 67 | # 68 | # @return [Net::HTTPResponse] 69 | def delete_key key 70 | send_req Net::HTTP::Put, "/api/v1/key", {key: key} 71 | end 72 | 73 | ## 74 | # Send a request to the gemserver to fetch information about stored 75 | # private gems and cached gem dependencies. 76 | # 77 | # @return [Net::HTTPResponse] 78 | def stats 79 | send_req Net::HTTP::Post, "/api/v1/stats" 80 | end 81 | 82 | ## 83 | # Sends a request to the gemserver to ensure it is accessible. 84 | # 85 | # @return [Net::HTTPResponse] 86 | def health 87 | send_req Net::HTTP::Get, "/health" 88 | end 89 | 90 | private 91 | 92 | ## 93 | # @private The URL of the gemserver. 94 | # 95 | # @param [String] proj_name The Google Cloud Platform project the 96 | # gemserver was deployed to. 97 | # 98 | # @return [String] 99 | def remote proj_name 100 | descrip = YAML.load(`gcloud app describe --project #{proj_name}`) 101 | descrip["defaultHostname"] 102 | end 103 | 104 | ## 105 | # @private Makes a request to the gemserver and returns the response. 106 | # 107 | # @param [Net::HTTP] type The type of HTTP request. 108 | # 109 | # @param [String] endpoint The endpoint the request is made to on the 110 | # gemserver. 111 | # 112 | # @param [Object] params The data passed to the gemserver to be 113 | # processed. Optional. 114 | # 115 | # @return [String] 116 | def send_req type, endpoint, params = nil 117 | auth = Google::Cloud::Gemserver::Authentication.new 118 | t = auth.access_token["access_token"] 119 | req = type.new endpoint 120 | req["Authorization"] = Signet::OAuth2.generate_bearer_authorization_header t 121 | if type != Net::HTTP::Get 122 | req.set_form_data(params) if params 123 | end 124 | @http.request req 125 | end 126 | end 127 | end 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/cli/server.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "google/cloud/gemserver" 16 | require "fileutils" 17 | require "yaml" 18 | 19 | module Google 20 | module Cloud 21 | module Gemserver 22 | class CLI 23 | 24 | ## 25 | # # Server 26 | # 27 | # Object responsible for deploying the gemserver to Google Cloud 28 | # Platform and starting it. 29 | # 30 | class Server 31 | 32 | ## 33 | # The Configuration object used to deploy the gemserver 34 | # @return [Configuration] 35 | attr_reader :config 36 | 37 | ## 38 | # Creates a Server instance by initializing a Configuration object 39 | # that will be used to access paths to necessary configuration files. 40 | def initialize 41 | @config = Configuration.new 42 | end 43 | 44 | ## 45 | # Starts the gemserver by starting up the gemstash server with 46 | # predefined options. 47 | def start 48 | path = if ENV["APP_ENV"] == "production" 49 | Configuration::GAE_PATH 50 | else 51 | @config.config_path 52 | end 53 | args = [ 54 | "start", 55 | "--no-daemonize", 56 | "--config-file=#{path}" 57 | ].freeze 58 | Google::Cloud::Gemserver::Backend::StorageSync.download_service 59 | Google::Cloud::Gemserver::Backend::GemstashServer.start args 60 | end 61 | 62 | ## 63 | # Deploys the gemserver to Google Cloud Platform if the app 64 | # environment variable is set to "production." Otherwise, the 65 | # gemserver is started locally. 66 | def deploy 67 | begin 68 | return start if ["test", "dev"].include? ENV["APP_ENV"] 69 | deploy_to_gae 70 | setup_default_keys 71 | ensure 72 | cleanup 73 | end 74 | end 75 | 76 | ## 77 | # Updates the gemserver on a Google Cloud Platform project by 78 | # redeploying it. 79 | def update 80 | return unless Configuration.deployed? 81 | puts "Updating gemserver..." 82 | deploy_to_gae 83 | end 84 | 85 | ## 86 | # Deletes a given gemserver by its parent project's ID. 87 | # 88 | # @param [String] proj_id The project ID of the project the gemserver 89 | # was deployed to. 90 | def delete proj_id 91 | return unless Configuration.deployed? 92 | full_delete = user_input("This will delete the entire Google Cloud"\ 93 | " Platform project #{proj_id}. Continue"\ 94 | " deletion? (Y|n, default n) If no, all relevant resources will"\ 95 | " be deleted besides the parent GCP project.").downcase 96 | if full_delete == "y" 97 | puts "Deleting gemserver with parent project" 98 | system "gcloud projects delete #{proj_id}" 99 | else 100 | @config.delete_from_cloud 101 | del_gcs_files 102 | inst = @config.app["beta_settings"]["cloud_sql_instances"] 103 | .split(":").pop 104 | puts "Deleting child Cloud SQL instance #{inst}..." 105 | params = "delete #{inst} --project #{proj_id}" 106 | status = system "gcloud beta sql instances #{params}" 107 | fail "Unable to delete instance" unless status 108 | puts "The Cloud SQL instance has been deleted. Visit:\n "\ 109 | "https://console.cloud.google.com/appengine/settings?project="\ 110 | "#{proj_id} and click \"Disable Application\" to delete the "\ 111 | "Google App Engine application the gemserver was deployed to." 112 | end 113 | end 114 | 115 | private 116 | 117 | ## 118 | # Deploys the gemserver to Google App Engine and uploads the 119 | # configuration file used by the gemserver to Google Cloud Storage 120 | # for later convenience. 121 | def deploy_to_gae 122 | puts "Beginning gemserver deployment..." 123 | prepare_dir 124 | path = "#{Configuration::SERVER_PATH}/app.yaml" 125 | flags = "-q --project #{@config[:proj_id]}" 126 | status = system "gcloud app deploy #{path} #{flags}" 127 | fail "Gemserver deployment failed. " unless status 128 | wait_until_server_accessible 129 | @config.save_to_cloud 130 | display_next_steps 131 | end 132 | 133 | ## 134 | # @private Deletes all gem data files on Google Cloud Storage. 135 | def del_gcs_files 136 | puts "Deleting all gem data on Google Cloud Storage..." 137 | gem_files = GCS.files Configuration::GEMSTASH_DIR 138 | gem_files.each { |f| f.delete } 139 | end 140 | 141 | ## 142 | # @private Creates a key with all permissions and sets it in the 143 | # necessary configurations (gem credentials and bundle config). 144 | def setup_default_keys 145 | should_create = user_input("Would you like to setup a default " \ 146 | "key? [Y/n] (default yes)") 147 | return if should_create.downcase == "n" 148 | gemserver_url = remote 149 | res = Request.new(gemserver_url).create_key 150 | abort "Error generating key" unless res.code.to_i == 200 151 | key = Backend::Key.send :parse_key, res.body 152 | abort "Invalid key" unless valid_key? key 153 | puts "Generated key: #{key}" 154 | set_bundle key, gemserver_url 155 | set_gem_credentials key 156 | end 157 | 158 | ## 159 | # @private Sets a given key in the bundle config used by bundler for 160 | # installing gems. 161 | # 162 | # @param [String] key The key to be added to the bundle config. 163 | # @param [String] gemserver_url The URL of the gemserver. 164 | def set_bundle key, gemserver_url 165 | puts "Updating bundle config" 166 | run_cmd "bundle config http://#{gemserver_url}/private #{key}" 167 | end 168 | 169 | ## 170 | # @private Sets a given key in the gem credentials file used by 171 | # Rubygems.org 172 | # 173 | # @param [String] key The key to be added to the credentials. 174 | def set_gem_credentials key 175 | key_name = sanitize_name(user_input("Updating bundle config. Enter"\ 176 | " a name for your key (default is \"master-gemserver-key\"")) 177 | key_name = key_name.empty? == true ? Configuration::DEFAULT_KEY_NAME : key_name 178 | puts "Updating #{Configuration::CREDS_PATH}" 179 | 180 | FileUtils.touch Configuration::CREDS_PATH 181 | keys = YAML.load_file(Configuration::CREDS_PATH) || {} 182 | 183 | if keys[key_name.to_sym].nil? 184 | system "echo \":#{key_name}: #{key}\" >> #{Configuration::CREDS_PATH}" 185 | else 186 | puts "The key name \"#{key_name}\" already exists. Please update"\ 187 | " #{Configuration::CREDS_PATH} manually to replace the key or" \ 188 | " manually enter a different name into the file for your key:" \ 189 | " #{key}." 190 | end 191 | end 192 | 193 | ## 194 | # @private Checks if a key is valid by its length and value. 195 | # 196 | # @param [String] key The key to be validated. 197 | # 198 | # @return [Boolean] 199 | def valid_key? key 200 | size = key.size == Backend::Key::KEY_LENGTH 201 | m_size = key.gsub(/[^0-9a-z]/i, "").size == Backend::Key::KEY_LENGTH 202 | size && m_size 203 | end 204 | 205 | ## 206 | # @private Sanitizes a name by removing special symbols and ensuring 207 | # it is alphanumeric (and hyphens, underscores). 208 | # 209 | # @param [String] name The name to be sanitized. 210 | # 211 | # @return [String] 212 | def sanitize_name name 213 | name = name.chomp 214 | name.gsub(/[^0-9a-z\-\_]/i, "") 215 | end 216 | 217 | ## 218 | # @private Outputs helpful information to the console indicating the 219 | # URL the gemserver is running at and how to use the gemserver. 220 | def display_next_steps 221 | puts "\nThe gemserver has been deployed! It is running on #{remote}" 222 | puts "\nTo see the status of the gemserver, visit: \n" \ 223 | " #{remote}/health" 224 | puts "\nTo see how to use your gemserver to push and download " \ 225 | "gems read https://github.com/GoogleCloudPlatform/google-cloud-" \ 226 | "gemserver/blob/master/docs/usage_example.md for some examples." 227 | puts "\nFor general information, visit https://github.com/" \ 228 | "GoogleCloudPlatform/google-cloud-gemserver/blob/master/README.md" 229 | end 230 | 231 | ## 232 | # @private Pings the gemserver until a timeout or the gemserver 233 | # replies with a 200 response code. 234 | # 235 | # @param [Integer] timeout The length of time the gemserver is 236 | # pinged. Optional. 237 | def wait_until_server_accessible timeout = 60 238 | puts "Waiting for the gemserver to be accessible..." 239 | start_time = Time.now 240 | loop do 241 | if Time.now - start_time > timeout 242 | fail "Could not establish a connection to the gemserver" 243 | else 244 | r = Request.new(nil, @config[:proj_id]).health 245 | break if r.code.to_i == 200 246 | end 247 | sleep 5 248 | end 249 | end 250 | 251 | ## 252 | # @private The URL of the gemserver. 253 | # 254 | # @return [String] 255 | def remote 256 | flag = "--project #{@config[:proj_id]}" 257 | descrip = YAML.load(run_cmd "gcloud app describe #{flag}") 258 | descrip["defaultHostname"] 259 | end 260 | 261 | ## 262 | # @private The Gemfile used by the gemserver on Google App Engine. 263 | # 264 | # @return [String] 265 | def gemfile_source 266 | <<~SOURCE 267 | source "https://rubygems.org" 268 | 269 | gem "google-cloud-gemserver", "#{Google::Cloud::Gemserver::VERSION}", path: "." 270 | gem "concurrent-ruby", require: "concurrent" 271 | gem "gemstash", git: "https://github.com/bundler/gemstash.git", ref: "a5a78e2" 272 | gem "mysql2", "~> 0.4" 273 | gem "filelock", "~> 1.1.1" 274 | gem "google-cloud-storage", "~> 1.1.0" 275 | gem "google-cloud-resource_manager", "~> 0.24" 276 | gem "activesupport", "~> 4.2" 277 | SOURCE 278 | end 279 | 280 | ## 281 | # @private Creates a Gemfile and Gemfile.lock for the gemserver that 282 | # runs on Google App Engine such that gemstash is not required 283 | # client side for the CLI. 284 | def gemfile 285 | File.open("#{Configuration::SERVER_PATH}/Gemfile", "w") do |f| 286 | f.write gemfile_source 287 | end 288 | 289 | require "bundler" 290 | Bundler.with_clean_env do 291 | run_cmd "cd #{Configuration::SERVER_PATH} && bundle lock" 292 | end 293 | end 294 | 295 | ## 296 | # @private Creates a temporary directory with the necessary files to 297 | # deploy the gemserver. 298 | def prepare_dir 299 | dir = Gem::Specification.find_by_name(Configuration::GEM_NAME).gem_dir 300 | cleanup if Dir.exist? Configuration::SERVER_PATH 301 | FileUtils.mkpath Configuration::SERVER_PATH 302 | FileUtils.cp_r "#{dir}/.", Configuration::SERVER_PATH 303 | FileUtils.cp @config.config_path, Configuration::SERVER_PATH 304 | FileUtils.cp @config.app_path, Configuration::SERVER_PATH 305 | gemfile 306 | end 307 | 308 | ## 309 | # @private Deletes the temporary directory containing the files used 310 | # to deploy the gemserver. 311 | def cleanup 312 | FileUtils.rm_rf Configuration::SERVER_PATH 313 | end 314 | 315 | ## 316 | # @private Runs a given command on the local machine. 317 | # 318 | # @param [String] args The command to be run. 319 | def run_cmd args 320 | `#{args}` 321 | end 322 | 323 | ## 324 | # @private Gets input from the user after displaying a message. 325 | # 326 | # @param [String] msg The message to be displayed. 327 | # 328 | # @return [String] 329 | def user_input msg 330 | puts msg 331 | STDIN.gets.chomp 332 | end 333 | end 334 | end 335 | end 336 | end 337 | end 338 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/configuration.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "fileutils" 16 | require "yaml" 17 | require "active_support/core_ext/hash/deep_merge" 18 | 19 | module Google 20 | module Cloud 21 | module Gemserver 22 | ## 23 | # 24 | # # Configuration 25 | # 26 | # Stores configurations for the gemserver and provides methods for 27 | # altering that configuration. 28 | # 29 | class Configuration 30 | ## 31 | # Default configuration settings for the production database. 32 | PROD_DB_DEFAULTS = { 33 | database: "mygems", 34 | username: "test", 35 | password: "test", 36 | host: "localhost", 37 | socket: "# this will be set automatically" 38 | }.freeze 39 | 40 | ## 41 | # Default configuration settings for the dev database. 42 | DEV_DB_DEFAULTS = { 43 | database: "mygems", 44 | username: "test", 45 | password: "test", 46 | host: "localhost", 47 | socket: "# this will need to be set manually" 48 | }.freeze 49 | 50 | ## 51 | # Default configuration settings for the test database. 52 | TEST_DB_DEFAULTS = { 53 | database: "testgems", 54 | username: "test", 55 | password: "test", 56 | host: "localhost", 57 | }.freeze 58 | 59 | ## 60 | # Beta setting used by Google App Engine to connect to the Cloud SQL 61 | # instance. 62 | BETA_SETTING_DEFAULTS = { 63 | "cloud_sql_instances" => "# automatically set" 64 | }.freeze 65 | 66 | ## 67 | # Setting used by Google App Engine to disable health checks for 68 | # faster deploys. 69 | HEALTH_CHECK_DEFAULT = { 70 | "enable_health_check" => false 71 | }.freeze 72 | 73 | ## 74 | # Setting used by Google App Engine to enable auto scaling. 75 | AUTO_SCALING_DEFAULT = { 76 | "min_num_instances" => 1, 77 | "max_num_instances" => 5 78 | }.freeze 79 | 80 | ## 81 | # Default configuration settings for the production gemserver. 82 | DEFAULT_CONFIG = { 83 | db_connection_options: PROD_DB_DEFAULTS, 84 | db_adapter: "cloud_sql", 85 | cache_type: "memory", 86 | protected_fetch: true, 87 | bind: "tcp://0.0.0.0:8080", 88 | :log_file => :stdout 89 | }.freeze 90 | 91 | ## 92 | # Default configuration settings for the development gemserver. 93 | DEFAULT_DEV_CONFIG = { 94 | db_connection_options: DEV_DB_DEFAULTS, 95 | db_adapter: "cloud_sql", 96 | cache_type: "memory", 97 | protected_fetch: true, 98 | bind: "tcp://0.0.0.0:8080", 99 | :log_file => :stdout 100 | }.freeze 101 | 102 | ## 103 | # Default configuration settings for the test gemserver. 104 | DEFAULT_TEST_CONFIG = { 105 | db_connection_options: TEST_DB_DEFAULTS, 106 | db_adapter: "sqlite3", 107 | cache_type: "memory", 108 | protected_fetch: true, 109 | bind: "tcp://0.0.0.0:8080", 110 | :log_file => :stdout 111 | }.freeze 112 | 113 | ## 114 | # Prefix for all general configuration setting fields in app.yaml. 115 | CONFIG_PREFIX = "gen".freeze 116 | 117 | ## 118 | # Prefix for all database configuration setting fields in app.yaml. 119 | CONFIG_DB_PREFIX = "db".freeze 120 | 121 | ## 122 | # Environment variables used by app.yaml for gemserver deployment. 123 | APP_ENGINE_ENV_VARS = { 124 | "GEMSERVER_ON_APPENGINE" => true, 125 | "production_db_database" => "mygems", 126 | "production_db_username" => "test", 127 | "production_db_password" => "test", 128 | "production_db_host" => "localhost", 129 | "production_db_socket" => "# this is set automatically", 130 | "production_db_adapter" => "cloud_sql", 131 | "dev_db_database" => "mygems", 132 | "dev_db_username" => "test", 133 | "dev_db_password" => "test", 134 | "dev_db_host" => "localhost", 135 | "dev_db_socket" => "# this must be set manually", 136 | "dev_db_adapter" => "cloud_sql", 137 | "test_db_database" => "mygems", 138 | "test_db_username" => "test", 139 | "test_db_password" => "test", 140 | "test_db_host" => "localhost", 141 | "test_db_adapter" => "sqlite3", 142 | "gen_cache_type" => "memory", 143 | "gen_protected_fetch" => true, 144 | "gen_bind" => "tcp://0.0.0.0:8080", 145 | "gen_log_file" => :stdout 146 | }.freeze 147 | 148 | ## 149 | # Default settings for gemserver deployment to Google App Engine via 150 | # app.yaml. 151 | APP_DEFAULTS = { 152 | "runtime" => "ruby", 153 | "env" => "flex", 154 | "entrypoint" => "./bin/google-cloud-gemserver start", 155 | "beta_settings" => BETA_SETTING_DEFAULTS, 156 | "health_check" => HEALTH_CHECK_DEFAULT, 157 | "automatic_scaling" => AUTO_SCALING_DEFAULT, 158 | "env_variables" => APP_ENGINE_ENV_VARS 159 | }.freeze 160 | 161 | ## 162 | # @private Temporary directory created to prepare a gemserver deploy 163 | # to a Google Cloud Platform project. 164 | SERVER_PATH = File.join("/tmp", "google-cloud-gemserver").freeze 165 | 166 | ## 167 | # @private Path to the configuration file on Google Cloud Storage that 168 | # was last used for a gemserver deploy. This path is checked by the 169 | # `config` command to display the last deployed gemserver's 170 | # configuration. 171 | GCS_PATH = "#{SERVER_PATH}/config.yml".freeze 172 | 173 | ## 174 | # @private The path to the app folder on a deployed gemserver. 175 | GAE_DIR = "/app".freeze 176 | 177 | ## 178 | # @private The path to the configuration file on a deployed gemserver. 179 | GAE_PATH = File.join(GAE_DIR, "config.yml").freeze 180 | 181 | ## 182 | # @private The path to the credentials file used by the `gem` command. 183 | CREDS_PATH = File.expand_path(File.join("~", ".gem", "credentials")) 184 | .freeze 185 | 186 | ## 187 | # @private Base directory containing configuration files. 188 | CONFIG_DIR = File.expand_path(File.join("~", ".google_cloud_gemserver")) 189 | 190 | ## 191 | # The name of the gem. 192 | GEM_NAME = "google-cloud-gemserver".freeze 193 | 194 | ## 195 | # @private The default name of the gemserver key. 196 | DEFAULT_KEY_NAME = "master-gemserver-key".freeze 197 | 198 | ## 199 | # @private The path gem data is stored 200 | GEMSTASH_DIR = "/root/.gemstash".freeze 201 | 202 | ## 203 | # The configuration used by the gemserver. 204 | # @return [Hash] 205 | attr_accessor :config 206 | 207 | ## 208 | # The configuration used by gcloud to deploy the gemserver to Google 209 | # App Engine. 210 | # @return [Hash] 211 | attr_accessor :app 212 | 213 | ## 214 | # Instantiate a new instance of Configuration 215 | def initialize 216 | @app = load_app 217 | @config = load_config 218 | end 219 | 220 | ## 221 | # Saves the configuration file used for a deployment. 222 | def save_to_cloud 223 | puts "Saving configuration" 224 | GCS.upload config_path, GCS_PATH 225 | end 226 | 227 | ## 228 | # Deletes the configuration file used for a deployment 229 | def delete_from_cloud 230 | GCS.delete_file GCS_PATH 231 | end 232 | 233 | ## 234 | # Updates the configuration file. 235 | # 236 | # @param [String] value New value of the key. 237 | # @param [String] key Name of the key that will be updated. 238 | # @param [String] sub_key Name of the sub key that will be updated. 239 | def update_config value, key, sub_key = nil 240 | if sub_key 241 | @config[key][sub_key] = value 242 | else 243 | @config[key] = value 244 | end 245 | write_config 246 | end 247 | # Updates the app configuration file. 248 | # 249 | # @param [String] value New value of the key. 250 | # @param [String] key Name of the key that will be updated. 251 | # @param [String] sub_key Name of the sub key that will be updated. 252 | def update_app value, key, sub_key = nil 253 | if sub_key 254 | @app[key][sub_key] = value 255 | else 256 | @app[key] = value 257 | end 258 | write_app 259 | end 260 | 261 | ## 262 | # Accesses a key in the Configuration object. 263 | # 264 | # @param [String] key Name of the key accessed. 265 | # 266 | # @return [String] 267 | def [] key 268 | @config[key] 269 | end 270 | 271 | ## 272 | # @private Generates a set of configuration files for the gemserver to 273 | # run and deploy to Google App Engine. 274 | def gen_config 275 | return if on_appengine 276 | FileUtils.mkpath config_dir unless Dir.exist? config_dir 277 | 278 | write_file "#{config_dir}/app.yaml", app_config, true 279 | write_file "#{config_dir}/config.yml", prod_config 280 | write_file "#{config_dir}/dev_config.yml", dev_config 281 | write_file "#{config_dir}/test_config.yml", test_config 282 | end 283 | 284 | ## 285 | # Fetches the path to the relevant configuration file based on the 286 | # environment (production, test, development). 287 | # 288 | # @return [String] 289 | def config_path 290 | "#{config_dir}/#{suffix}" 291 | end 292 | 293 | ## 294 | # Fetches the path to the relevant app configuration file. 295 | # 296 | # @return [String] 297 | def app_path 298 | "#{config_dir}/app.yaml" 299 | end 300 | 301 | ## 302 | # Displays the configuration used by the current gemserver 303 | def self.display_config 304 | unless deployed? 305 | puts "No configuration found. Was the gemserver deployed?" 306 | return 307 | end 308 | prepare GCS.get_file(GCS_PATH) 309 | puts "Gemserver is running with this configuration:" 310 | puts YAML.load_file(GCS_PATH).to_yaml 311 | cleanup 312 | end 313 | 314 | ## 315 | # Checks if the gemserver was deployed by the existence of the config 316 | # file used to deploy it on a specific path on Google Cloud Storage. 317 | # 318 | # @return [Boolean] 319 | def self.deployed? 320 | !GCS.get_file(GCS_PATH).nil? 321 | end 322 | 323 | private 324 | 325 | ## 326 | # @private Fetches the current environment. 327 | # 328 | # @return [String] 329 | def env 330 | ENV["APP_ENV"].nil? == true ? "production" : ENV["APP_ENV"] 331 | end 332 | 333 | ## 334 | # @private Determines which configuration file to read based on the 335 | # environment. 336 | # 337 | # @return [String] 338 | def suffix 339 | if env == "dev" 340 | "dev_config.yml" 341 | elsif env == "test" 342 | "test_config.yml" 343 | else 344 | "config.yml" 345 | end 346 | end 347 | 348 | ## 349 | # @private Writes a given file to a given path. 350 | # 351 | # @param [String] path The path to write the file. 352 | # 353 | # @param [String] content The content to be written to the file. 354 | # 355 | # @param [boolean] check_existence If true, the file is not overwritten 356 | # if it already exists. Optional. 357 | def write_file path, content, check_existence = false 358 | if check_existence 359 | return if File.exist? path 360 | end 361 | File.open(path, "w") do |f| 362 | f.write content 363 | end 364 | end 365 | 366 | ## 367 | # @private The default app.yaml configuration formatted in YAML. 368 | # 369 | # @return [String] 370 | def app_config 371 | APP_DEFAULTS.merge(load_app).to_yaml 372 | end 373 | 374 | ## 375 | # @private The default config.yml configuration formatted in YAML 376 | # used by the gemserver in the production environment. 377 | # 378 | # @return [String] 379 | def prod_config 380 | DEFAULT_CONFIG.deep_merge(extract_config("production")).to_yaml 381 | end 382 | 383 | ## 384 | # @private The default dev_config.yml configuration formatted in YAML 385 | # used by the gemserver in the dev environment. 386 | # 387 | # @return [String] 388 | def dev_config 389 | DEFAULT_DEV_CONFIG.deep_merge(extract_config("dev")).to_yaml 390 | end 391 | 392 | ## 393 | # @private The default test_config.yml configuration formatted in YAML 394 | # used by the gemserver in the test environment. 395 | # 396 | # @return [String] 397 | def test_config 398 | DEFAULT_TEST_CONFIG.deep_merge(extract_config("test")).to_yaml 399 | end 400 | 401 | ## 402 | # @private Extracts the gemserver configuration from the app.yaml 403 | # environment variables. 404 | # 405 | # @param [String] pre The prefix of the config fields to extract. 406 | # 407 | # @return [Hash] 408 | def extract_config pre = "production" 409 | adapter = pre + "_" + CONFIG_DB_PREFIX + "_adapter" 410 | db_config = @app["env_variables"].map do |k, v| 411 | # db_adapter is a special case b/c it has the 'db' in its name but is 412 | # not a db_connection_options field 413 | next unless k.include?(pre) && k != adapter 414 | [(k[pre.size + CONFIG_DB_PREFIX.size + 2..k.size - 1]).to_sym, v] 415 | end.compact.to_h 416 | config = @app["env_variables"].map do |k, v| 417 | next unless k.include? CONFIG_PREFIX 418 | [(k[CONFIG_PREFIX.size + 1..k.size - 1]).to_sym, v] 419 | end.compact.to_h 420 | { 421 | :db_connection_options => db_config, 422 | :db_adapter => @app["env_variables"][adapter] 423 | }.deep_merge config 424 | end 425 | 426 | ## 427 | # @private Loads a configuration file. 428 | # 429 | # @return [Hash] 430 | def load_config 431 | extract_config env 432 | end 433 | 434 | ## 435 | # @private Loads the app configuration file. 436 | # 437 | # @return [Hash] 438 | def load_app 439 | return APP_DEFAULTS unless File.exist? app_path 440 | YAML.load_file app_path 441 | end 442 | 443 | ## 444 | # @private Writes the current Configuration object in YAML format 445 | # to the relevant configuration file (based on environment) and 446 | # updates app.yaml accordingly. 447 | def write_config 448 | db_key = env + "_" + CONFIG_DB_PREFIX + "_" 449 | key = CONFIG_PREFIX + "_" 450 | db = @config[:db_connection_options] 451 | non_db = @config.reject { |k, v| k == :db_connection_options } 452 | formatted_db = db.map { |k, v| [db_key + k.to_s, v] }.to_h 453 | formatted_non_db = non_db.map { |k, v| [key + k.to_s, v] }.to_h 454 | @app["env_variables"] = @app["env_variables"].merge( 455 | formatted_db.merge(formatted_non_db) 456 | ) 457 | File.open(config_path, "w") { |f| YAML.dump @config, f } 458 | write_app 459 | end 460 | 461 | ## 462 | # @private Writes the current app configuration object in YAML format 463 | # to the app configuration file. 464 | def write_app 465 | File.open(app_path, "w") { |f| YAML.dump @app, f } 466 | end 467 | 468 | ## 469 | # @private Fetches the directory that contains the configuration files. 470 | # 471 | # @return [String] 472 | def config_dir 473 | return GAE_DIR if on_appengine 474 | dir = ENV["GEMSERVER_CONFIG_DIR"] 475 | dir.nil? == true ? CONFIG_DIR : dir 476 | end 477 | 478 | ## 479 | # @private Determines if the gemserver is running on Google App Engine. 480 | # 481 | # @return [boolean] 482 | def on_appengine 483 | !ENV["GEMSERVER_ON_APPENGINE"].nil? 484 | end 485 | 486 | ## 487 | # @private Creates a temporary directory to download the configuration 488 | # file used to deploy the gemserver. 489 | def self.prepare file 490 | FileUtils.mkpath SERVER_PATH 491 | file.download file.name 492 | end 493 | 494 | ## 495 | # @private Deletes a temporary directory. 496 | def self.cleanup 497 | FileUtils.rm_rf SERVER_PATH 498 | end 499 | 500 | private_class_method :prepare 501 | private_class_method :cleanup 502 | end 503 | end 504 | end 505 | end 506 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/gcs.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "google/cloud/storage" 16 | require "google/cloud/gemserver" 17 | require "fileutils" 18 | 19 | module Google 20 | module Cloud 21 | module Gemserver 22 | ## 23 | # # Google Cloud Storage 24 | # 25 | # Interacts with Google Cloud Storage by providing methods that upload 26 | # and download files to and from Google Cloud Storage. 27 | # 28 | module GCS 29 | ## 30 | # @private Fetches the project ID of the Google Cloud Platform project 31 | # the gemserver was deployed to. 32 | # 33 | # @return [String] 34 | def self.proj_id 35 | Google::Cloud::Gemserver::Configuration.new[:proj_id] 36 | end 37 | 38 | ## 39 | # @private Creates a Google::Cloud::Storage::Project object with the 40 | # current project ID. 41 | # 42 | # @return [Google::Cloud::Storage::Project] 43 | def self.cs 44 | return unless proj_id 45 | Google::Cloud::Storage.new project: proj_id, keyfile: ENV["GOOGLE_APPLICATION_CREDENTIALS"] 46 | end 47 | 48 | ## 49 | # @private Fetches the bucket used to store gem files for the gemserver. 50 | # If it does not exist a bucket is created. 51 | # 52 | # @return [Google::Cloud::Storage::Bucket] 53 | def self.bucket 54 | return unless proj_id 55 | bucket = cs.bucket proj_id 56 | bucket ? bucket : cs.create_bucket(proj_id) 57 | end 58 | 59 | ## 60 | # Retrieves a file from Google Cloud Storage from a project's 61 | # corresponding bucket. 62 | # 63 | # @param [String] file Name of the file to be retrieved. 64 | # 65 | # @return [Google::Cloud::Storage::File] 66 | def self.get_file file 67 | return unless proj_id 68 | bucket.file file 69 | end 70 | 71 | ## 72 | # Uploads a given file to a project's corresponding bucket on Google 73 | # Cloud Storage. A destination path of the file can be provided. 74 | # By default the path of the file is the same on Google Cloud Storage. 75 | # 76 | # @param [String] file Path to the file to be uploaded. 77 | # @param [String] dest Destination path of the file on Google Cloud 78 | # Storage. Optional. 79 | # 80 | # @return [Google::Cloud::Storage::File] 81 | def self.upload file, dest = nil 82 | return unless proj_id 83 | bucket.create_file file, dest 84 | end 85 | 86 | ## 87 | # Deletes a given file from Google Cloud Storage. 88 | # 89 | # @param [String] file Name of the file to be deleted. 90 | def self.delete_file file 91 | return unless proj_id 92 | get_file(file).delete 93 | end 94 | 95 | ## 96 | # @private Retrieves all files in the bucket corresponding to the 97 | # project the gemserver was deployed. If specified, only files with a 98 | # certain prefix will be retrieved. 99 | # 100 | # @param [String] prefix Prefix of the file name. Optional 101 | # 102 | # @return [Google::Cloud::Storage::File::List] 103 | def self.files prefix = nil 104 | return unless proj_id 105 | bucket.files prefix: prefix 106 | end 107 | 108 | ## 109 | # @private Checks if a file exists on both Google Cloud Storage and the 110 | # local file system. If the file is on Cloud Storage, but missing on 111 | # the file system it will be downloaded. 112 | # 113 | # @param [String] file_path File path of the file to be synced. 114 | # 115 | # @return [Boolean] 116 | def self.sync file_path 117 | return true unless proj_id 118 | on_cloud = on_gcs? file_path 119 | on_host = File.exist? file_path 120 | 121 | if on_cloud && !on_host 122 | copy_to_host file_path 123 | true 124 | elsif on_cloud && on_host 125 | true 126 | else 127 | false 128 | end 129 | end 130 | 131 | ## 132 | # @private Checks if a given file exists on Google Cloud Storage. 133 | # 134 | # @param [String] file_path Path of the file on Google Cloud Storage. 135 | # 136 | # @return [Boolean] 137 | def self.on_gcs? file_path 138 | return false unless proj_id 139 | get_file(file_path) != nil 140 | end 141 | 142 | ## 143 | # @private Downloads a given file from Google Cloud Storage. 144 | # 145 | # @param [String] path Path to the file. 146 | def self.copy_to_host path 147 | return unless proj_id 148 | file = get_file path 149 | folder = extract_dir path 150 | begin 151 | FileUtils.mkpath(folder) unless Dir.exist?(folder) 152 | file.download path 153 | rescue 154 | puts "Could not download #{file.name}." if file 155 | end 156 | end 157 | 158 | ## 159 | # @private Extracts the parent directory from a file path 160 | # 161 | # @param [String] path Path of the file. 162 | # 163 | # @return [String] 164 | def self.extract_dir path 165 | parts = path.split("/") 166 | parts.map { |x| x != parts.last ? x : nil }.join("/") 167 | end 168 | end 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /lib/google/cloud/gemserver/version.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | module Google 16 | module Cloud 17 | module Gemserver 18 | ## 19 | # The version of the google-cloud-gemserver gem. 20 | VERSION = "0.1.0".freeze 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/patched/configuration.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "gemstash" 16 | 17 | module PatchedConfiguration 18 | ## 19 | # Monkeypatch to support Cloud SQL by returning the necessary settings. 20 | def database_connection_config 21 | if self[:db_adapter] == "cloud_sql" 22 | { adapter: "mysql2" }.merge(self[:db_connection_options]) 23 | else 24 | super 25 | end 26 | end 27 | end 28 | 29 | Gemstash::Configuration.send :prepend, PatchedConfiguration 30 | -------------------------------------------------------------------------------- /lib/patched/dependencies.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "gemstash" 16 | require "google/cloud/gemserver" 17 | require "active_support/core_ext/module/aliasing" 18 | 19 | module PatchedDependencies 20 | ## 21 | # Monkeypatch to run a gem syncing service in the background after fetches. 22 | # 23 | # @param [Array] gems An array of gems to fetch. 24 | # 25 | # @return [Array] 26 | def fetch gems 27 | fetched = super gems 28 | Google::Cloud::Gemserver::Backend::StorageSync.run 29 | fetched 30 | end 31 | end 32 | 33 | Gemstash::Dependencies.send :prepend, PatchedDependencies 34 | -------------------------------------------------------------------------------- /lib/patched/env.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "gemstash" 16 | require "active_support/core_ext/file/atomic" 17 | require "active_support/core_ext/module/aliasing" 18 | 19 | module PatchedEnv 20 | ## 21 | # Monkey patch to support Cloud SQL as an adapter 22 | def db 23 | return @db if @db 24 | 25 | @db = if config[:db_adapter] == "cloud_sql" 26 | connection = Sequel.connect config.database_connection_config 27 | migrate_cloud_sql connection 28 | connection 29 | else 30 | super 31 | end 32 | end 33 | 34 | def migrate_cloud_sql database 35 | Sequel.extension :migration 36 | lib_dir = Gem::Specification.find_by_name("gemstash").lib_dirs_glob 37 | m_dir = "#{lib_dir}/gemstash/migrations" 38 | Sequel::Migrator.run database, m_dir, use_transactions: false 39 | end 40 | end 41 | 42 | Gemstash::Env.send :prepend, PatchedEnv 43 | -------------------------------------------------------------------------------- /lib/patched/gem_pusher.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "gemstash" 16 | require "google/cloud/gemserver" 17 | 18 | module PatchedGemPusher 19 | ## 20 | # Monkeypatch to run a gem syncing service in the background after pushing a 21 | # gem to the gemserver. 22 | def serve 23 | super 24 | Google::Cloud::Gemserver::Backend::StorageSync.run 25 | end 26 | end 27 | 28 | Gemstash::GemPusher.send :prepend, PatchedGemPusher 29 | -------------------------------------------------------------------------------- /lib/patched/gem_yanker.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "gemstash" 16 | require "google/cloud/gemserver" 17 | 18 | module PatchedGemYanker 19 | ## 20 | # Monkeypatch to run a gem syncing service in the background after yanking 21 | # a gem from the gemserver. 22 | def serve 23 | super 24 | Google::Cloud::Gemserver::Backend::StorageSync.run 25 | end 26 | end 27 | 28 | Gemstash::GemYanker.send :prepend, PatchedGemYanker 29 | -------------------------------------------------------------------------------- /lib/patched/storage.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "gemstash" 16 | require "google/cloud/gemserver" 17 | require "filelock" 18 | 19 | module PatchedResource 20 | ## 21 | # Monkeypatch to delete a file from both the local file system and Google 22 | # Cloud Storage. Done atomically to prevent circular file syncing where 23 | # files never get deleted. 24 | # 25 | # @param [String] key Name of the gem to delete. 26 | def delete key 27 | file = content_filename key 28 | return unless File.exist?(file) && File.exist?(properties_filename) 29 | Filelock file do 30 | super 31 | Google::Cloud::Gemserver::GCS.delete_file file 32 | Google::Cloud::Gemserver::GCS.delete_file properties_filename 33 | end 34 | end 35 | end 36 | 37 | Gemstash::Resource.send :prepend, PatchedResource 38 | -------------------------------------------------------------------------------- /lib/patched/web.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "gemstash" 16 | require "google/cloud/gemserver" 17 | 18 | ## 19 | # Monkeypatch gemstash to handle gemserver specific endpoints. 20 | Gemstash::Web.class_eval do 21 | ## 22 | # Displays statistics on the currently deployed gemserver such as private 23 | # gems, cached gems, gemserver creation time, etc. 24 | post "/api/v1/stats" do 25 | auth = Google::Cloud::Gemserver::Authentication.new 26 | if auth.validate_token request.env["HTTP_AUTHORIZATION"] 27 | content_type "application/json;charset=UTF-8" 28 | Google::Cloud::Gemserver::Backend::Stats.new.run 29 | else 30 | halt 401, "Unauthorized operation." 31 | end 32 | end 33 | 34 | ## 35 | # Creates a key used for installing or pushing gems to the gemserver 36 | # with given permissions. By default, a key with all permissions is created. 37 | post "/api/v1/key" do 38 | auth = Google::Cloud::Gemserver::Authentication.new 39 | if auth.validate_token request.env["HTTP_AUTHORIZATION"] 40 | key = Google::Cloud::Gemserver::Backend::Key.create_key params["permissions"] 41 | content_type "application/json;charset=UTF-8" 42 | "Generated key: #{key}" 43 | else 44 | halt 401, "Unauthorized operation." 45 | end 46 | end 47 | 48 | ## 49 | # Deletes a key. 50 | put "/api/v1/key" do 51 | auth = Google::Cloud::Gemserver::Authentication.new 52 | if auth.validate_token request.env["HTTP_AUTHORIZATION"] 53 | res = Google::Cloud::Gemserver::Backend::Key.delete_key params["key"] 54 | content_type "application/json;charset=UTF-8" 55 | if res 56 | "Deleted key #{params["key"]} successfully." 57 | else 58 | "Deleting key #{params["key"]} failed." 59 | end 60 | else 61 | halt 401, "Unauthorized operation." 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /secrets/app.yaml.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/google-cloud-gemserver/7a246a68405fb28056a75d76130adb8d35b5236d/secrets/app.yaml.enc -------------------------------------------------------------------------------- /secrets/service-account-key.json.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/google-cloud-gemserver/7a246a68405fb28056a75d76130adb8d35b5236d/secrets/service-account-key.json.enc -------------------------------------------------------------------------------- /test/google/cloud/gemserver/authentication_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | require "yaml" 17 | 18 | describe Google::Cloud::Gemserver::Authentication do 19 | describe ".new" do 20 | it "sets the project property" do 21 | assert GCG::Authentication.new.proj == GCG::Configuration.new[:proj_id] 22 | end 23 | end 24 | 25 | let(:owners) { ["serviceaccount:owner_a", "user:owner_b"] } 26 | let(:editors) { ["serviceaccount:editor_a", "user:editor_b"] } 27 | let(:token) { { "access_token": "test-token" } } 28 | let(:header) { "Bearer #{token[:access_token]}" } 29 | 30 | describe ".can_modify?" do 31 | it "returns true iff the logged in user is the project owner" do 32 | auth = GCG::Authentication.new 33 | 34 | auth.stub :owners, owners do 35 | auth.stub :editors, [] do 36 | auth.stub :curr_user, "owner_b" do 37 | assert auth.can_modify? 38 | end 39 | auth.stub :curr_user, "owner_a" do 40 | assert auth.can_modify? 41 | end 42 | auth.stub :curr_user, "invalid_user" do 43 | refute auth.can_modify? 44 | end 45 | end 46 | end 47 | end 48 | 49 | it "returns true if the logged in user is the project editor" do 50 | auth = GCG::Authentication.new 51 | 52 | auth.stub :editors, editors do 53 | auth.stub :owners, [] do 54 | auth.stub :curr_user, "editor_a" do 55 | assert auth.can_modify? 56 | end 57 | auth.stub :curr_user, "editor_b" do 58 | assert auth.can_modify? 59 | end 60 | auth.stub :curr_user, "invalid_user" do 61 | refute auth.can_modify? 62 | end 63 | end 64 | end 65 | end 66 | end 67 | 68 | describe ".access_token" do 69 | it "creates a token if the user is authorized with default credentials" do 70 | auth = GCG::Authentication.new 71 | 72 | mock = Minitest::Mock.new 73 | mock.expect :fetch_access_token!, token 74 | 75 | tmp = ENV["GOOGLE_APPLICATION_CREDENTIALS"] 76 | ENV["GOOGLE_APPLICATION_CREDENTIALS"] = nil 77 | 78 | auth.stub :can_modify?, true do 79 | Google::Auth.stub :get_application_default, mock do 80 | t = auth.access_token 81 | assert_equal t, token 82 | mock.verify 83 | end 84 | end 85 | 86 | ENV["GOOGLE_APPLICATION_CREDENTIALS"] = tmp 87 | end 88 | 89 | it "creates a token if the user is authorized with a service account" do 90 | auth = GCG::Authentication.new 91 | 92 | mock = Minitest::Mock.new 93 | mock.expect :fetch_access_token!, token 94 | 95 | tmp = ENV["GOOGLE_APPLICATION_CREDENTIALS"] 96 | ENV["GOOGLE_APPLICATION_CREDENTIALS"] = "test" 97 | 98 | auth.stub :can_modify?, true do 99 | Google::Auth.stub :get_application_default, mock do 100 | t = auth.access_token 101 | assert_equal t, token 102 | mock.verify 103 | end 104 | end 105 | 106 | ENV["GOOGLE_APPLICATION_CREDENTIALS"] = tmp 107 | end 108 | 109 | it "does nothing if the user is not authenticated" do 110 | auth = GCG::Authentication.new 111 | 112 | auth.stub :can_modify?, false do 113 | refute auth.access_token 114 | end 115 | end 116 | end 117 | 118 | describe "validate_token" do 119 | it "gets the latest app version" do 120 | auth = GCG::Authentication.new 121 | mock = Minitest::Mock.new 122 | mock.expect :call, nil, [String] 123 | 124 | res_mock = Minitest::Mock.new 125 | res_mock.expect :body, "{ \"name\": \"test\" }" 126 | 127 | auth.stub :send_req, res_mock do 128 | auth.stub :check_status, true do 129 | auth.stub :wait_for_op, nil do 130 | auth.stub :appengine_version, mock do 131 | auth.send :validate_token, header 132 | mock.verify 133 | end 134 | end 135 | end 136 | end 137 | end 138 | 139 | it "performs a redundant project update" do 140 | auth = GCG::Authentication.new 141 | path = "/v1/apps/#{auth.proj}/services/default?updateMask=split" 142 | params = { 143 | "split" => { 144 | "allocations" => { 145 | "123" => 1 146 | } 147 | } 148 | } 149 | 150 | mock = Minitest::Mock.new 151 | mock.expect :call, nil, [String, path, Net::HTTP::Patch, token[:access_token], params] 152 | 153 | auth.stub :send_req, mock do 154 | auth.stub :check_status, false do 155 | auth.stub :appengine_version, "123" do 156 | auth.send :validate_token, header 157 | assert_equal auth.appengine_version("abc"), params["split"]["allocations"].first[0] 158 | mock.verify 159 | end 160 | end 161 | end 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /test/google/cloud/gemserver/backend/key_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | require "gemstash" 17 | 18 | describe Google::Cloud::Gemserver::Backend::Key do 19 | describe "Key.create_key" do 20 | it "calls Gemstash::Backend.start" do 21 | mock = Minitest::Mock.new 22 | mock.expect :call, nil, [Array] 23 | Gemstash::CLI.stub :start, mock do 24 | GCG::Backend::Key.stub :parse_key, "" do 25 | GCG::Backend::Key.create_key 26 | mock.verify 27 | end 28 | end 29 | end 30 | end 31 | 32 | describe "Key.delete_key" do 33 | it "calls Gemstash::Backend.start" do 34 | mock = Minitest::Mock.new 35 | mock.expect :call, nil, [Array] 36 | Gemstash::CLI.stub :start, mock do 37 | GCG::Backend::Key.stub :parse_key, "" do 38 | GCG::Backend::Key.delete_key "" 39 | mock.verify 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/google/cloud/gemserver/backend/stats_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | 17 | describe Google::Cloud::Gemserver::Backend::Stats do 18 | describe "Stats.new" do 19 | it "must set the properties" do 20 | stats = GCG::Backend::Stats.new 21 | assert stats.proj.nil? || stats.proj.class == String 22 | end 23 | 24 | end 25 | 26 | describe ".run" do 27 | it "it calls log_app_description" do 28 | ENV["APP_ENV"] = "dev" 29 | stats = GCG::Backend::Stats.new 30 | mock = Minitest::Mock.new 31 | mock.expect :call, "", ["gcloud app describe --project #{stats.proj}"] 32 | stats.stub :run_cmd, mock do 33 | stats.log_app_description 34 | end 35 | mock.verify 36 | ENV["APP_ENV"] = "test" 37 | end 38 | 39 | it "calls log_uptime" do 40 | stats = GCG::Backend::Stats.new 41 | mock_uptime = Minitest::Mock.new 42 | mock_uptime.expect :call, nil 43 | 44 | stats.stub :project, mock_uptime do 45 | stats.send :log_uptime 46 | mock_uptime.verify 47 | end 48 | end 49 | 50 | it "calls log_private_gems" do 51 | stats = GCG::Backend::Stats.new 52 | mock = Minitest::Mock.new 53 | mock.expect :call, String 54 | stats.stub :db, [] do 55 | stats.stub :log_private_gems, mock do 56 | stats.send :log_private_gems 57 | mock.verify 58 | end 59 | end 60 | end 61 | 62 | it "calls log_cached_gems" do 63 | stats = GCG::Backend::Stats.new 64 | mock = Minitest::Mock.new 65 | mock.expect :call, String 66 | stats.stub :db, [] do 67 | stats.stub :log_cached_gems, mock do 68 | stats.send :log_cached_gems 69 | mock.verify 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/google/cloud/gemserver/cli/cloud_sql_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | require "google/apis/sqladmin_v1beta4" 17 | 18 | SQL = Google::Apis::SqladminV1beta4 19 | 20 | describe Google::Cloud::Gemserver::CLI::CloudSQL do 21 | describe "CloudSQL.new" do 22 | it "must set the properties" do 23 | sql = GCG::CLI::CloudSQL.new 24 | config = GCG::Configuration.new 25 | assert sql.inst.include? "instance" 26 | assert sql.proj_id == config[:proj_id] 27 | assert sql.db == config[:db_connection_options][:database] 28 | assert sql.user == config[:db_connection_options][:username] 29 | assert sql.pwd == config[:db_connection_options][:password] 30 | end 31 | 32 | it "calls SQLAdminService.new.insert_instance" do 33 | sql = GCG::CLI::CloudSQL.new 34 | mock_create_inst = Minitest::Mock.new 35 | mock_create_inst.expect :call, nil, [sql.proj_id, SQL::DatabaseInstance] 36 | mock_task = Minitest::Mock.new 37 | mock_task.expect :call, nil, [nil] 38 | 39 | sql.service.stub :insert_instance, mock_create_inst do 40 | sql.stub :update_root_user, nil do 41 | sql.stub :run_sql_task, mock_task do 42 | sql.send :create_instance 43 | mock_create_inst.verify 44 | end 45 | end 46 | end 47 | end 48 | 49 | it "calls SQLAdminService.new.insert_database" do 50 | sql = GCG::CLI::CloudSQL.new 51 | mock_create_db = Minitest::Mock.new 52 | mock_create_db.expect :call, nil, [sql.proj_id, String, SQL::Database] 53 | mock_task = Minitest::Mock.new 54 | mock_task.expect :call, nil, [nil] 55 | 56 | sql.service.stub :insert_database, mock_create_db do 57 | sql.stub :update_root_user, nil do 58 | sql.stub :run_sql_task, mock_task do 59 | sql.send :create_db 60 | mock_create_db.verify 61 | end 62 | end 63 | end 64 | end 65 | 66 | it "calls SQLAdminService.new.insert_user" do 67 | sql = GCG::CLI::CloudSQL.new 68 | mock_create_user = Minitest::Mock.new 69 | mock_create_user.expect :call, nil, [sql.proj_id, String, SQL::User] 70 | mock_task = Minitest::Mock.new 71 | mock_task.expect :call, nil, [nil] 72 | 73 | sql.service.stub :insert_user, mock_create_user do 74 | sql.stub :update_root_user, nil do 75 | sql.stub :run_sql_task, mock_task do 76 | sql.send :create_user 77 | mock_create_user.verify 78 | end 79 | end 80 | end 81 | end 82 | end 83 | 84 | describe ".del_instance" do 85 | it "calls SQLAdminService.new.delete_instance" do 86 | sql = GCG::CLI::CloudSQL.new 87 | mock_del_inst = Minitest::Mock.new 88 | mock_del_inst.expect :call, nil, [sql.proj_id, String] 89 | mock_task = Minitest::Mock.new 90 | mock_task.expect :call, nil, [nil] 91 | 92 | sql.service.stub :delete_instance, mock_del_inst do 93 | sql.stub :update_root_user, nil do 94 | sql.stub :run_sql_task, mock_task do 95 | sql.send :del_instance 96 | mock_del_inst.verify 97 | end 98 | end 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /test/google/cloud/gemserver/cli/project_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | 17 | describe Google::Cloud::Gemserver::CLI::Project do 18 | describe "Project.new" do 19 | it "must set project name" do 20 | project = GCG::CLI::Project.new "a-project" 21 | assert project.proj_name == "a-project" 22 | end 23 | 24 | end 25 | 26 | describe ".create" do 27 | it "calls ResourceManager" do 28 | project = GCG::CLI::Project.new "test" 29 | mock_project = Minitest::Mock.new 30 | mock_project.expect :call, nil 31 | 32 | project.stub :project, mock_project do 33 | project.stub :prompt_user, nil do 34 | project.stub :project_exists?, true do 35 | project.config.stub :update_config, nil do 36 | project.create 37 | mock_project.verify 38 | end 39 | end 40 | end 41 | end 42 | end 43 | 44 | it "creates an App Engine project if it does not exist" do 45 | project = GCG::CLI::Project.new "test" 46 | mock = Minitest::Mock.new 47 | mock.expect :call, nil, ["gcloud app create --project test"] 48 | 49 | project.stub :project, nil do 50 | project.stub :prompt_user, nil do 51 | project.config.stub :update_config, nil do 52 | project.stub :project_exists?, false do 53 | project.stub :system, mock do 54 | project.create 55 | mock.verify 56 | end 57 | end 58 | end 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/google/cloud/gemserver/cli/request_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | require "net/http" 17 | 18 | describe Google::Cloud::Gemserver::CLI::Request do 19 | 20 | let(:token) { { "access_token": "test-token" } } 21 | 22 | describe "Request.new" do 23 | it "creates an HTTP object for the gemserver" do 24 | req = GCG::CLI::Request.new "google.com" 25 | assert req.http.class == Net::HTTP 26 | end 27 | end 28 | 29 | describe "create_key" do 30 | it "calls send_req with the correct arguments" do 31 | req = GCG::CLI::Request.new "google.com" 32 | mock = Minitest::Mock.new 33 | mock.expect :call, nil, [Net::HTTP::Post, "/api/v1/key", {permissions: nil}] 34 | 35 | req.stub :send_req, mock do 36 | req.create_key 37 | mock.verify 38 | end 39 | end 40 | end 41 | 42 | describe "delete_key" do 43 | it "calls send_req with the correct arguments" do 44 | req = GCG::CLI::Request.new "google.com" 45 | mock = Minitest::Mock.new 46 | mock.expect :call, nil, [Net::HTTP::Put, "/api/v1/key", {key: "key"}] 47 | 48 | req.stub :send_req, mock do 49 | req.delete_key "key" 50 | mock.verify 51 | end 52 | end 53 | end 54 | 55 | describe ".stats" do 56 | it "calls send_req with the correct arguments" do 57 | req = GCG::CLI::Request.new "google.com" 58 | mock = Minitest::Mock.new 59 | mock.expect :call, nil, [Net::HTTP::Post, "/api/v1/stats"] 60 | 61 | 62 | req.stub :send_req, mock do 63 | req.stats 64 | mock.verify 65 | end 66 | end 67 | end 68 | 69 | describe ".send_req" do 70 | it "adds a token in request headers" do 71 | req = GCG::CLI::Request.new "google.com" 72 | 73 | mock = Minitest::Mock.new 74 | mock.expect :access_token, token 75 | 76 | http_mock = Minitest::Mock.new 77 | http_mock.expect :[]=, nil, [String, String] 78 | 79 | GCG::Authentication.stub :new, mock do 80 | req.http.stub :request, nil do 81 | Net::HTTP::Get.stub :new, http_mock do 82 | req.send :send_req, Net::HTTP::Get, "/search?query=hi" 83 | mock.verify 84 | end 85 | end 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /test/google/cloud/gemserver/cli/server_test.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | require "helper" 16 | require "gemstash" 17 | 18 | describe Google::Cloud::Gemserver::CLI::Server do 19 | describe "Server.new" do 20 | it "must set the properties" do 21 | server = GCG::CLI::Server.new 22 | assert server.config.class == GCG::Configuration.new.class 23 | end 24 | 25 | end 26 | 27 | describe ".start" do 28 | it "calls Gemstash::CLI.start" do 29 | server = GCG::CLI::Server.new 30 | mock_server = Minitest::Mock.new 31 | mock_server.expect :call, nil, [Array] 32 | 33 | Gemstash::CLI.stub :start, mock_server do 34 | Google::Cloud::Gemserver::Backend::StorageSync.stub :download_service, nil do 35 | server.start 36 | mock_server.verify 37 | end 38 | end 39 | end 40 | end 41 | 42 | describe ".deploy" do 43 | it "calls deploy_to_gae" do 44 | server = GCG::CLI::Server.new 45 | 46 | mock = Minitest::Mock.new 47 | mock.expect :call, nil 48 | 49 | ENV["APP_ENV"] = "production" 50 | server.stub :deploy_to_gae, mock do 51 | server.stub :setup_default_keys, nil do 52 | server.stub :display_next_steps, nil do 53 | server.deploy 54 | mock.verify 55 | end 56 | end 57 | end 58 | ENV["APP_ENV"] = "test" 59 | end 60 | 61 | it "sets up default keys" do 62 | server = GCG::CLI::Server.new 63 | 64 | mock = Minitest::Mock.new 65 | mock.expect :call, nil 66 | 67 | ENV["APP_ENV"] = "production" 68 | server.stub :deploy_to_gae, nil do 69 | server.stub :setup_default_keys, mock do 70 | server.stub :display_next_steps, nil do 71 | server.deploy 72 | mock.verify 73 | end 74 | end 75 | end 76 | ENV["APP_ENV"] = "test" 77 | end 78 | end 79 | 80 | describe ".deploy_to_gae" do 81 | it "calls prepare_dir" do 82 | ENV["APP_ENV"] = "production" 83 | server = GCG::CLI::Server.new 84 | mock = Minitest::Mock.new 85 | mock.expect :call, nil 86 | 87 | server.stub :system, true do 88 | server.config.stub :save_to_cloud, nil do 89 | server.stub :setup_default_keys, nil do 90 | server.stub :display_next_steps, nil do 91 | server.stub :prepare_dir, mock do 92 | server.stub :wait_until_server_accessible, nil do 93 | server.stub :remote, nil do 94 | server.send :deploy_to_gae 95 | mock.verify 96 | end 97 | end 98 | end 99 | end 100 | end 101 | end 102 | end 103 | 104 | ENV["APP_ENV"] = "test" 105 | end 106 | 107 | it "calls gcloud app deploy" do 108 | server = GCG::CLI::Server.new 109 | app_path = "#{GCG::Configuration::SERVER_PATH}/app.yaml" 110 | ENV["APP_ENV"] = "production" 111 | mock_server = Minitest::Mock.new 112 | mock_server.expect :call, true, ["gcloud app deploy #{app_path} -q --project #{server.config[:proj_id]}"] 113 | 114 | server.stub :system, mock_server do 115 | server.stub :prepare_dir, nil do 116 | server.config.stub :save_to_cloud, nil do 117 | server.stub :setup_default_keys, nil do 118 | server.stub :display_next_steps, nil do 119 | server.stub :wait_until_server_accessible, nil do 120 | server.stub :remote, nil do 121 | server.send :deploy_to_gae 122 | mock_server.verify 123 | end 124 | end 125 | end 126 | end 127 | end 128 | end 129 | end 130 | ENV["APP_ENV"] = "test" 131 | end 132 | 133 | it "waits for the gemserver to be accessible" do 134 | ENV["APP_ENV"] = "production" 135 | server = GCG::CLI::Server.new 136 | mock = Minitest::Mock.new 137 | mock.expect :call, nil 138 | 139 | server.stub :prepare_dir, nil do 140 | server.config.stub :save_to_cloud, nil do 141 | server.stub :system, true do 142 | server.stub :setup_default_keys, nil do 143 | server.stub :display_next_steps, nil do 144 | server.stub :wait_until_server_accessible, mock do 145 | server.stub :remote, nil do 146 | server.send :deploy_to_gae 147 | mock.verify 148 | end 149 | end 150 | end 151 | end 152 | end 153 | end 154 | end 155 | 156 | ENV["APP_ENV"] = "test" 157 | end 158 | 159 | it "saves the deploy config file to GCS" do 160 | ENV["APP_ENV"] = "production" 161 | server = GCG::CLI::Server.new 162 | mock = Minitest::Mock.new 163 | mock.expect :call, nil 164 | 165 | server.stub :prepare_dir, nil do 166 | server.stub :system, true do 167 | server.stub :setup_default_keys, nil do 168 | server.stub :display_next_steps, nil do 169 | server.stub :wait_until_server_accessible, nil do 170 | server.config.stub :save_to_cloud, mock do 171 | server.stub :remote, nil do 172 | server.send :deploy_to_gae 173 | mock.verify 174 | end 175 | end 176 | end 177 | end 178 | end 179 | end 180 | end 181 | 182 | ENV["APP_ENV"] = "test" 183 | end 184 | 185 | it "displays helpful tips after deploying" do 186 | ENV["APP_ENV"] = "production" 187 | server = GCG::CLI::Server.new 188 | mock = Minitest::Mock.new 189 | mock.expect :call, nil 190 | 191 | server.stub :prepare_dir, nil do 192 | server.stub :system, true do 193 | server.stub :setup_default_keys, nil do 194 | server.stub :display_next_steps, mock do 195 | server.stub :wait_until_server_accessible, nil do 196 | server.config.stub :save_to_cloud, nil do 197 | server.send :deploy_to_gae 198 | mock.verify 199 | end 200 | end 201 | end 202 | end 203 | end 204 | end 205 | 206 | ENV["APP_ENV"] = "test" 207 | end 208 | end 209 | 210 | describe ".update" do 211 | it "calls deploy" do 212 | server = GCG::CLI::Server.new 213 | mock_server = Minitest::Mock.new 214 | mock_server.expect :call, nil 215 | 216 | GCG::Configuration.stub :deployed?, true do 217 | server.stub :deploy_to_gae, mock_server do 218 | server.update 219 | mock_server.verify 220 | end 221 | end 222 | end 223 | end 224 | 225 | describe ".delete" do 226 | it "calls gcloud projects delete on a full delete" do 227 | server = GCG::CLI::Server.new 228 | 229 | mock_server = Minitest::Mock.new 230 | mock_server.expect :call, nil, ["gcloud projects delete bob"] 231 | 232 | GCG::Configuration.stub :deployed?, true do 233 | server.stub :user_input, "y" do 234 | server.stub :system, mock_server do 235 | server.delete "bob" 236 | mock_server.verify 237 | end 238 | end 239 | end 240 | end 241 | 242 | it "prompts the user to manually delete if not a full project deletion" do 243 | server = GCG::CLI::Server.new 244 | link = "https://console.cloud.google.com/appengine/settings?project=bob" 245 | 246 | GCG::Configuration.stub :deployed?, true do 247 | server.stub :user_input, "n" do 248 | server.config.stub :delete_from_cloud, nil do 249 | server.stub :del_gcs_files, nil do 250 | server.stub :system, true do 251 | out = capture_io { server.delete "bob" }[0] 252 | assert out.include? link 253 | end 254 | end 255 | end 256 | end 257 | end 258 | end 259 | 260 | it "deletes the Cloud SQL instance if not a full project deletion" do 261 | server = GCG::CLI::Server.new 262 | inst_name = "test" 263 | inst_connection = "/cloudsql/a:b:#{inst_name}" 264 | app = { 265 | "beta_settings" => { 266 | "cloud_sql_instances" => inst_connection 267 | } 268 | } 269 | params = "delete #{inst_name} --project bob" 270 | 271 | mock = Minitest::Mock.new 272 | mock.expect :call, true, ["gcloud beta sql instances #{params}"] 273 | 274 | GCG::Configuration.stub :deployed?, true do 275 | server.config.stub :app, app do 276 | server.config.stub :delete_from_cloud, nil do 277 | server.stub :user_input, "n" do 278 | server.stub :del_gcs_files, nil do 279 | server.stub :system, mock do 280 | server.delete "bob" 281 | mock.verify 282 | end 283 | end 284 | end 285 | end 286 | end 287 | end 288 | end 289 | 290 | it "deletes the config file from GCS if not full project deletion" do 291 | server = GCG::CLI::Server.new 292 | 293 | mock = Minitest::Mock.new 294 | mock.expect :call, nil 295 | 296 | GCG::Configuration.stub :deployed?, true do 297 | server.stub :user_input, "n" do 298 | server.stub :system, true do 299 | server.stub :del_gcs_files, nil do 300 | server.config.stub :delete_from_cloud, mock do 301 | server.delete "bob" 302 | mock.verify 303 | end 304 | end 305 | end 306 | end 307 | end 308 | end 309 | 310 | it "deletes gem data files on GCS if not full project deletion" do 311 | server = GCG::CLI::Server.new 312 | 313 | mock = Minitest::Mock.new 314 | mock.expect :call, nil 315 | 316 | GCG::Configuration.stub :deployed?, true do 317 | server.stub :user_input, "n" do 318 | server.stub :system, true do 319 | server.stub :del_gcs_files, mock do 320 | server.config.stub :delete_from_cloud, nil do 321 | server.delete "bob" 322 | mock.verify 323 | end 324 | end 325 | end 326 | end 327 | end 328 | end 329 | end 330 | end 331 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | gem "minitest" 16 | 17 | require "minitest/autorun" 18 | require "minitest/focus" 19 | require "minitest/rg" 20 | require "google/cloud/gemserver" 21 | 22 | GCG = Google::Cloud::Gemserver 23 | --------------------------------------------------------------------------------