diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..81e0069 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..428c102 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,89 @@ +name: Release Gem + +on: workflow_dispatch + +jobs: + release: + runs-on: ubuntu-latest + if: github.repository == 'puppetlabs/vmpooler-dns-gcp' + steps: + - uses: actions/checkout@v3 + + - name: Get Current Version + uses: actions/github-script@v6 + id: cv + with: + script: | + const { data: response } = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + }) + console.log(`The latest release is ${response.tag_name}`) + return response.tag_name + result-encoding: string + + - name: Get Next Version + id: nv + run: | + version=$(grep VERSION lib/vmpooler-dns-gcp/version.rb |rev |cut -d "'" -f2 |rev) + echo "version=$version" >> $GITHUB_OUTPUT + echo "Found version $version from lib/vmpooler-dns-gcp/version.rb" + + - name: Generate Changelog + uses: docker://githubchangeloggenerator/github-changelog-generator:1.16.2 + with: + args: >- + --future-release ${{ steps.nv.outputs.version }} + env: + CHANGELOG_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Validate Changelog + run : | + set -e + if [[ -n $(git status --porcelain) ]]; then + echo "Here is the current git status:" + git status + echo + echo "The following changes were detected:" + git --no-pager diff + echo "Uncommitted PRs found in the changelog. Please submit a release prep PR of changes after running `./update-changelog`" + exit 1 + fi + + - name: Generate Release Notes + uses: docker://githubchangeloggenerator/github-changelog-generator:1.16.2 + with: + args: >- + --since-tag ${{ steps.cv.outputs.result }} + --future-release ${{ steps.nv.outputs.version }} + --output release-notes.md + env: + CHANGELOG_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Tag Release + uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.nv.outputs.version }} + token: ${{ secrets.GITHUB_TOKEN }} + bodyfile: release-notes.md + draft: false + prerelease: false + + # This step should closely match what is used in `docker/Dockerfile` in vmpooler-deployment + - name: Install Ruby jruby-9.4.1.0 + uses: ruby/setup-ruby@v1 + with: + ruby-version: 'jruby-9.4.1.0' + + - name: Build gem + run: gem build *.gemspec + + - name: Publish gem + run: | + mkdir -p $HOME/.gem + touch $HOME/.gem/credentials + chmod 0600 $HOME/.gem/credentials + printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials + gem push *.gem + env: + GEM_HOST_API_KEY: '${{ secrets.RUBYGEMS_AUTH_TOKEN }}' diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..666c602 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,39 @@ +name: Security +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + scan: + name: Mend Scanning + runs-on: ubuntu-latest + steps: + - name: checkout repo content + uses: actions/checkout@v3 + with: + fetch-depth: 1 + - name: setup ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + # setup a package lock if one doesn't exist, otherwise do nothing + - name: check lock + run: '[ -f "Gemfile.lock" ] && echo "package lock file exists, skipping" || bundle lock' + # install java + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '17' + # download mend + - name: download_mend + run: curl -o wss-unified-agent.jar https://unified-agent.s3.amazonaws.com/wss-unified-agent.jar + - name: run mend + run: java -jar wss-unified-agent.jar + env: + WS_APIKEY: ${{ secrets.MEND_API_KEY }} + WS_WSS_URL: https://saas-eu.whitesourcesoftware.com/agent + WS_USERKEY: ${{ secrets.MEND_TOKEN }} + WS_PRODUCTNAME: RE + WS_PROJECTNAME: ${{ github.event.repository.name }} diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..eac55b3 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,46 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake +# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby + +name: Testing + +on: + pull_request: + branches: + - main + +jobs: + rubocop: + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: + - 'jruby-9.4.1.0' + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run Rubocop + run: bundle exec rake rubocop + + spec_tests: + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: + - 'jruby-9.4.1.0' + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run spec tests + run: bundle exec rake test diff --git a/.github_changelog_generator b/.github_changelog_generator new file mode 100644 index 0000000..411780b --- /dev/null +++ b/.github_changelog_generator @@ -0,0 +1,3 @@ +project=vmpooler-dns-gcp +user=puppetlabs +exclude_labels=maintenance diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9106b2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ diff --git a/.jrubyrc b/.jrubyrc new file mode 100644 index 0000000..e140581 --- /dev/null +++ b/.jrubyrc @@ -0,0 +1,2 @@ +# for simplecov to work in jruby, without this we are getting errors when debugging spec tests +debug.fullTrace=true diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..850e19f --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,53 @@ +AllCops: + Include: + - 'lib/**/*.rb' + Exclude: + - 'scripts/**/*' + - 'spec/**/*' + - 'vendor/**/*' + - Gemfile + - Rakefile + +# These short variable names make sense as exceptions to the rule, but generally I think short variable names do hurt readability +Naming/MethodParameterName: + AllowedNames: + - vm + - dc + - s + - x + - f + +#new cops: +Lint/DuplicateRegexpCharacterClassElement: # (new in 1.1) + Enabled: true +Lint/EmptyBlock: # (new in 1.1) + Enabled: true +Lint/ToEnumArguments: # (new in 1.1) + Enabled: true +Lint/UnmodifiedReduceAccumulator: # (new in 1.1) + Enabled: true +Style/ArgumentsForwarding: # (new in 1.1) + Enabled: false +Style/DocumentDynamicEvalDefinition: # (new in 1.1) + Enabled: true +Style/SwapValues: # (new in 1.1) + Enabled: false + +#disabled + +Metrics/AbcSize: + Enabled: false +Metrics/ClassLength: + Enabled: false +Metrics/CyclomaticComplexity: + Enabled: false +Metrics/MethodLength: + Enabled: false +Metrics/PerceivedComplexity: + Enabled: false +Metrics/ParameterLists: + Enabled: false +Layout/LineLength: + Enabled: false +Metrics/BlockLength: + Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3b42514 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..122d6b5 --- /dev/null +++ b/Gemfile @@ -0,0 +1,13 @@ +source ENV['GEM_SOURCE'] || 'https://rubygems.org' + +gemspec + +# Evaluate Gemfile.local if it exists +if File.exists? "#{__FILE__}.local" + instance_eval(File.read("#{__FILE__}.local")) +end + +# Evaluate ~/.gemfile if it exists +if File.exists?(File.join(Dir.home, '.gemfile')) + instance_eval(File.read(File.join(Dir.home, '.gemfile'))) +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..c1dd62e --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,213 @@ +PATH + remote: . + specs: + vmpooler-dns-gcp (0.1.0) + google-cloud-dns (~> 0.35.1) + googleauth (>= 0.16.2, < 1.3.0) + vmpooler (~> 3.0) + +GEM + remote: https://rubygems.org/ + specs: + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + ast (2.4.2) + bindata (2.4.14) + concurrent-ruby (1.2.0) + connection_pool (2.3.0) + declarative (0.0.20) + deep_merge (1.2.2) + diff-lcs (1.5.0) + docile (1.4.0) + faraday (2.7.4) + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-net_http (3.0.2) + google-apis-core (0.10.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-dns_v1 (0.29.0) + google-apis-core (>= 0.9.1, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-dns (0.35.1) + google-apis-dns_v1 (~> 0.1) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + zonefile (~> 1.04) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + googleauth (1.2.0) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + httpclient (2.8.3) + jwt (2.6.0) + memoist (0.16.2) + mini_mime (1.1.2) + mock_redis (0.36.0) + ruby2_keywords + multi_json (1.15.0) + mustermann (2.0.2) + ruby2_keywords (~> 0.0.1) + net-ldap (0.17.1) + nio4r (2.5.8) + nio4r (2.5.8-java) + opentelemetry-api (1.1.0) + opentelemetry-common (0.19.6) + opentelemetry-api (~> 1.0) + opentelemetry-exporter-jaeger (0.20.1) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.19.2) + opentelemetry-sdk (~> 1.0) + thrift + opentelemetry-instrumentation-base (0.19.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-concurrent_ruby (0.19.2) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.19.0) + opentelemetry-instrumentation-http_client (0.19.4) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.19.3) + opentelemetry-instrumentation-base (~> 0.19.0) + opentelemetry-instrumentation-redis (0.21.3) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.19.3) + opentelemetry-instrumentation-base (~> 0.19.0) + opentelemetry-instrumentation-sinatra (0.19.3) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.19.3) + opentelemetry-instrumentation-base (~> 0.19.0) + opentelemetry-registry (0.2.0) + opentelemetry-api (~> 1.1) + opentelemetry-resource_detectors (0.19.1) + google-cloud-env + opentelemetry-sdk + opentelemetry-sdk (1.2.0) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.19.3) + opentelemetry-registry (~> 0.2) + opentelemetry-semantic_conventions + opentelemetry-semantic_conventions (1.8.0) + opentelemetry-api (~> 1.0) + os (1.1.4) + parallel (1.22.1) + parser (3.2.0.0) + ast (~> 2.4.1) + pickup (0.0.11) + prometheus-client (2.1.0) + public_suffix (5.0.1) + puma (5.6.5) + nio4r (~> 2.0) + puma (5.6.5-java) + nio4r (~> 2.0) + rack (2.2.6.4) + rack-protection (2.2.4) + rack + rainbow (3.1.1) + rake (13.0.6) + redis (4.8.0) + regexp_parser (2.6.2) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.5) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.0) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.4) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-support (3.12.0) + rubocop (1.1.0) + parallel (~> 1.10) + parser (>= 2.7.1.5) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8) + rexml + rubocop-ast (>= 1.0.1) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 2.0) + rubocop-ast (1.24.1) + parser (>= 3.1.1.0) + ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) + signet (0.17.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.4) + sinatra (2.2.4) + mustermann (~> 2.0) + rack (~> 2.2) + rack-protection (= 2.2.4) + tilt (~> 2.0) + spicy-proton (2.1.15) + bindata (~> 2.3) + statsd-ruby (1.5.0) + thrift (0.17.0) + tilt (2.0.11) + trailblazer-option (0.1.2) + uber (0.1.0) + unicode-display_width (1.8.0) + vmpooler (3.0.0) + concurrent-ruby (~> 1.1) + connection_pool (~> 2.2) + deep_merge (~> 1.2) + net-ldap (~> 0.16) + opentelemetry-exporter-jaeger (= 0.20.1) + opentelemetry-instrumentation-concurrent_ruby (= 0.19.2) + opentelemetry-instrumentation-http_client (= 0.19.4) + opentelemetry-instrumentation-redis (= 0.21.3) + opentelemetry-instrumentation-sinatra (= 0.19.3) + opentelemetry-resource_detectors (= 0.19.1) + opentelemetry-sdk (~> 1.0, >= 1.0.2) + pickup (~> 0.0.11) + prometheus-client (~> 2.0) + puma (~> 5.0, >= 5.0.4) + rack (~> 2.2) + rake (~> 13.0) + redis (~> 4.1) + sinatra (~> 2.0) + spicy-proton (~> 2.1) + statsd-ruby (~> 1.4) + webrick (1.8.1) + zonefile (1.06) + +PLATFORMS + universal-java-11 + +DEPENDENCIES + mock_redis (>= 0.17.0) + rspec (>= 3.2) + rubocop (~> 1.1.0) + simplecov (>= 0.11.2) + vmpooler-dns-gcp! + +BUNDLED WITH + 2.4.10 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 42e2aef..0f25d9e 100644 --- a/README.md +++ b/README.md @@ -1 +1,49 @@ -# vmpooler-dns-google-clouddns \ No newline at end of file +# vmpooler-dns-gcp + +- [vmpooler-dns-gcp](#vmpooler-dns-gcp) + - [Requirements](#requirements) + - [Usage](#usage) + - [Update the Gemfile Lock](#update-the-gemfile-lock) + - [Releasing](#releasing) + - [License](#license) + +## Requirements + +1. A Google Cloud Project with the [Cloud DNS](https://cloud.google.com/dns/) enabled. +2. A custom IAM role with the permissions listed in `util/vmpooler-dns-gcp-role.yaml`. +3. A service account assigned to the custom role above. +4. A service account key, using the account above, exported as `GOOGLE_APPLICATION_CREDENTIALS` where VMPooler is run. + +## Usage + +Example dns config setup: + +```yaml +:dns_configs: + :example: + dns_class: gcp + project: vmpooler-example + domain: vmpooler.example.com + zone_name: vmpooler-example-com +``` + +Examples of deploying VMPooler with dns configs can be found in the [puppetlabs/vmpooler-deployment](https://github.com/puppetlabs/vmpooler-deployment) repository. + +## Update the Gemfile Lock + +To update the `Gemfile.lock` run `./update-gemfile-lock`. + +Verify, and update if needed, that the docker tag in the script and GitHub action workflows matches what is used in the [vmpooler-deployment Dockerfile](https://github.com/puppetlabs/vmpooler-deployment/blob/main/docker/Dockerfile). + +## Releasing + +Follow these steps to publish a new GitHub release, and build and push the gem to . + +1. Bump the "VERSION" in `lib/vmpooler-dns-gcp/version.rb` appropriately based on changes in `CHANGELOG.md` since the last release. +2. Run `./release-prep` to update `Gemfile.lock` and `CHANGELOG.md`. +3. Commit and push changes to a new branch, then open a pull request against `main` and be sure to add the "maintenance" label. +4. After the pull request is approved and merged, then navigate to Actions --> Release Gem --> run workflow --> Branch: main --> Run workflow. + +## License + +vmpooler-dns-gcp is distributed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). See the [LICENSE](LICENSE) file for more details. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..76d6a80 --- /dev/null +++ b/Rakefile @@ -0,0 +1,25 @@ +require 'rspec/core/rake_task' + +rubocop_available = Gem::Specification::find_all_by_name('rubocop').any? +require 'rubocop/rake_task' if rubocop_available + +desc 'Run rspec tests with coloring.' +RSpec::Core::RakeTask.new(:test) do |t| + t.rspec_opts = %w[--color --format documentation] + t.pattern = 'spec/' +end + +desc 'Run rspec tests and save JUnit output to results.xml.' +RSpec::Core::RakeTask.new(:junit) do |t| + t.rspec_opts = %w[-r yarjuf -f JUnit -o results.xml] + t.pattern = 'spec/' +end + +if rubocop_available + desc 'Run RuboCop' + RuboCop::RakeTask.new(:rubocop) do |task| + task.options << '--display-cop-names' + end +end + +task :default => [:test] diff --git a/install-gemfile-lock b/install-gemfile-lock new file mode 100755 index 0000000..07cbd49 --- /dev/null +++ b/install-gemfile-lock @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# The container tag should closely match what is used in `docker/Dockerfile` in vmpooler-deployment +docker run -it --rm \ + -v $(pwd):/app \ + jruby:9.4.1.0-jdk11 \ + /bin/bash -c 'apt-get update -qq && apt-get install -y --no-install-recommends make git netbase && cd /app && gem install bundler && bundle install --jobs 3; echo "LOCK_FILE_UPDATE_EXIT_CODE=$?"' diff --git a/lib/vmpooler-dns-gcp/version.rb b/lib/vmpooler-dns-gcp/version.rb new file mode 100644 index 0000000..c38e817 --- /dev/null +++ b/lib/vmpooler-dns-gcp/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module VmpoolerDnsGcp + VERSION = '0.1.0' +end diff --git a/lib/vmpooler/dns/gcp.rb b/lib/vmpooler/dns/gcp.rb new file mode 100644 index 0000000..6ee2ec1 --- /dev/null +++ b/lib/vmpooler/dns/gcp.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require 'googleauth' +require 'google/cloud/dns' +require 'vmpooler/dns/base' + +module Vmpooler + class PoolManager + class Dns + # This class represent a DNS plugin to CRUD resources in Google Cloud DNS. + class Gcp < Vmpooler::PoolManager::Dns::Base + # The connection_pool method is normally used only for testing + attr_reader :connection_pool + + def initialize(config, logger, metrics, redis_connection_pool, name, options) + super(config, logger, metrics, redis_connection_pool, name, options) + + task_limit = global_config[:config].nil? || global_config[:config]['task_limit'].nil? ? 10 : global_config[:config]['task_limit'].to_i + + default_connpool_size = [provided_pools.count, task_limit, 2].max + connpool_timeout = 60 + logger.log('d', "[#{name}] ConnPool - Creating a connection pool of size #{default_connpool_size} with timeout #{connpool_timeout}") + @connection_pool = Vmpooler::PoolManager::GenericConnectionPool.new( + metrics: metrics, + connpool_type: 'dns_connection_pool', + connpool_provider: name, + size: default_connpool_size, + timeout: connpool_timeout + ) do + logger.log('d', "[#{name}] Connection Pool - Creating a connection object") + # Need to wrap the GCP connection object in another object. The generic connection pooler will preserve + # the object reference for the connection, which means it cannot "reconnect" by creating an entirely new connection + # object. Instead by wrapping it in a Hash, the Hash object reference itself never changes but the content of the + # Hash can change, and is preserved across invocations. + new_conn = connect_to_gcp + { connection: new_conn } + end + end + + def name + 'gcp' + end + + # main configuration options + def project + dns_config['project'] + end + + def zone_name + dns_config['zone_name'] + end + + def create_or_replace_record(hostname) + retries = 0 + ip = get_ip(hostname) + if ip.nil? + debug_logger("An IP Address was not recorded for #{hostname}") + else + begin + change = connection.zone(zone_name).add(hostname, 'A', 60, ip) + debug_logger("#{change.id} - #{change.started_at} - #{change.status} DNS address added") if change + rescue Google::Cloud::AlreadyExistsError => _e + # the error is Google::Cloud::AlreadyExistsError: alreadyExists: The resource 'entity.change.additions[0]' named 'instance-8.test.vmpooler.net. (A)' already exists + # the error is Google::Cloud::AlreadyExistsError: alreadyExists: The resource 'entity.change.additions[0]' named 'instance-8.test.vmpooler.net. (A)' already exists + change = connection.zone(zone_name).replace(hostname, 'A', 60, ip) + debug_logger("#{change.id} - #{change.started_at} - #{change.status} DNS address previously existed and was replaced") if change + rescue Google::Cloud::FailedPreconditionError => e + debug_logger("DNS create failed, retrying error: #{e}") + sleep 5 + retry if (retries += 1) < 30 + end + end + end + + def delete_record(hostname) + retries = 0 + begin + connection.zone(zone_name).remove(hostname, 'A') + rescue Google::Cloud::FailedPreconditionError => e + # this error was experienced intermittently, will retry to see if it can complete successfully + # the error is Google::Cloud::FailedPreconditionError: conditionNotMet: Precondition not met for 'entity.change.deletions[1]' + debug_logger("GCP DNS delete_record failed, retrying error: #{e}") + sleep 5 + retry if (retries += 1) < 30 + end + end + + def connection + @connection_pool.with_metrics do |pool_object| + return ensured_gcp_connection(pool_object) + end + end + + def ensured_gcp_connection(connection_pool_object) + connection_pool_object[:connection] = connect_to_gcp unless gcp_connection_ok?(connection_pool_object[:connection]) + connection_pool_object[:connection] + end + + def gcp_connection_ok?(connection) + _result = connection.id + true + rescue StandardError + false + end + + def connect_to_gcp + max_tries = global_config[:config]['max_tries'] || 3 + retry_factor = global_config[:config]['retry_factor'] || 10 + try = 1 + begin + Google::Cloud::Dns.configure do |config| + config.project_id = project + end + + dns = Google::Cloud::Dns.new + + metrics.increment('connect.open') + dns + rescue StandardError => e + metrics.increment('connect.fail') + raise e if try >= max_tries + + sleep(try * retry_factor) + try += 1 + retry + end + end + + # used in local dev environment, set DEBUG_FLAG=true + # this way the upstream vmpooler manager does not get polluted with logs + def debug_logger(message, send_to_upstream: false) + # the default logger is simple and does not enforce debug levels (the first argument) + puts message if ENV['DEBUG_FLAG'] + logger.log('[g]', message) if send_to_upstream + end + end + end + end +end diff --git a/release-prep b/release-prep new file mode 100755 index 0000000..31d7f07 --- /dev/null +++ b/release-prep @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# The container tag should closely match what is used in `docker/Dockerfile` in vmpooler-deployment +# +# Update Gemfile.lock +docker run -it --rm \ + -v $(pwd):/app \ + jruby:9.4.1.0-jdk11 \ + /bin/bash -c 'apt-get update -qq && apt-get install -y --no-install-recommends git make netbase && cd /app && gem install bundler && bundle install --jobs 3; echo "LOCK_FILE_UPDATE_EXIT_CODE=$?"' + +# Update Changelog +docker run -it --rm -e CHANGELOG_GITHUB_TOKEN -v $(pwd):/usr/local/src/your-app \ + githubchangeloggenerator/github-changelog-generator:1.16.2 \ + github_changelog_generator --future-release $(grep VERSION lib/vmpooler-dns-gcp/version.rb |rev |cut -d "'" -f2 |rev) + diff --git a/spec/gcp_helper.rb b/spec/gcp_helper.rb new file mode 100644 index 0000000..65add92 --- /dev/null +++ b/spec/gcp_helper.rb @@ -0,0 +1,26 @@ +MockDnsZone = Struct.new( + # https://github.com/googleapis/google-cloud-ruby/blob/main/google-cloud-dns/lib/google/cloud/dns/zone.rb + :service, :gapi, :id, :name, :dns, :description, :name_servers, :name_server_set, :created_at +) do + def add(name, type, ttl, data) + change = MockDnsChange.new + change.additions(name, type, ttl, data) + # return name + end +end + +# -------------------- +# Main GoogleCloudDnsProject Object +# -------------------- +MockGoogleCloudDnsProjectConnection = Struct.new( + # https://cloud.google.com/ruby/docs/reference/google-cloud-dns/latest/Google-Cloud#Google__Cloud_dns_instance_ + :scope, :retries, :timeout +) do + def zone + MockDnsZone.new + end +end + +def mock_Google_Cloud_Dns_Project_Connection(options = {}) + MockGoogleCloudDnsProjectConnection.new() +end diff --git a/spec/helpers.rb b/spec/helpers.rb new file mode 100644 index 0000000..478a869 --- /dev/null +++ b/spec/helpers.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'mock_redis' + +def redis + @redis ||= MockRedis.new + @redis +end + +# Mock an object which represents a Logger. This stops the proliferation +# of allow(logger).to .... expectations in tests. +class MockLogger + def log(_level, string); end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..1891dfa --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'simplecov' +SimpleCov.start do + add_filter '/spec/' +end +require 'helpers' +require 'rspec' +require 'vmpooler' +require 'redis' +require 'vmpooler/metrics' +require 'gcp_helper' + +def project_root_dir + File.dirname(File.dirname(__FILE__)) +end + +def fixtures_dir + File.join(project_root_dir, 'spec', 'fixtures') +end diff --git a/spec/unit/dns/gcp_spec.rb b/spec/unit/dns/gcp_spec.rb new file mode 100644 index 0000000..2c16907 --- /dev/null +++ b/spec/unit/dns/gcp_spec.rb @@ -0,0 +1,294 @@ +require 'spec_helper' +require 'mock_redis' +require 'vmpooler/dns/gcp' + +describe 'Vmpooler::PoolManager::Dns::Gcp' do + let(:logger) { MockLogger.new } + let(:metrics) { Vmpooler::Metrics::DummyStatsd.new } + let(:poolname) { 'debian-9' } + let(:options) { { 'param' => 'value' } } + let(:name) { 'gcp' } + let(:zone_name) { 'vmpooler-example-com' } + let(:config) { YAML.load(<<~EOT + --- + :config: + max_tries: 3 + retry_factor: 10 + :dns_configs: + :#{name}: + dns_class: gcp + project: vmpooler-example + domain: vmpooler.example.com + zone_name: vmpooler-example-com + dns_zone_resource_name: vmpooler-example-com + :providers: + :dummy: + filename: '/tmp/dummy-backing.yaml' + :pools: + - name: '#{poolname}' + alias: [ 'mockpool' ] + template: 'Templates/debian-9-x86_64' + size: 5 + timeout: 10 + ready_ttl: 1440 + provider: 'dummy' + dns_config: '#{name}' +EOT + ) + } + + let(:vmname) { 'spicy-proton' } + let(:connection_options) {{}} + let(:connection) { mock_Google_Cloud_Dns_Project_Connection(connection_options) } + let(:redis_connection_pool) do + Vmpooler::PoolManager::GenericConnectionPool.new( + metrics: metrics, + connpool_type: 'redis_connection_pool', + connpool_provider: 'testprovider', + size: 1, + timeout: 5 + ) { MockRedis.new } + end + + subject { Vmpooler::PoolManager::Dns::Gcp.new(config, logger, metrics, redis_connection_pool, name, options) } + + describe '#name' do + it 'should be gcp' do + expect(subject.name).to eq('gcp') + end + end + + describe '#project' do + it 'should be the project specified in the dns config' do + expect(subject.project).to eq('vmpooler-example') + end + end + + describe '#zone_name' do + it 'should be the zone_name specified in the dns config' do + expect(subject.zone_name).to eq('vmpooler-example-com') + end + end + + describe '#create_or_replace_record' do + let(:hostname) { 'spicy-proton' } + let(:zone) { MockDnsZone.new } + let(:ip) { '169.254.255.255' } + + context 'when adding a record' do + before(:each) do + allow(Google::Cloud::Dns).to receive(:configure) + allow(Google::Cloud::Dns).to receive(:new).and_return(connection) + allow(connection).to receive(:zone).and_return(zone) + allow(subject).to receive(:get_ip).and_return(ip) + end + + it 'should attempt to add a record' do + expect(zone).to receive(:add).with(hostname, 'A', 60, ip) + result = subject.create_or_replace_record(hostname) + end + end + + context 'when record already exists' do + before(:each) do + allow(Google::Cloud::Dns).to receive(:configure) + allow(Google::Cloud::Dns).to receive(:new).and_return(connection) + allow(connection).to receive(:zone).and_return(zone) + allow(subject).to receive(:get_ip).and_return(ip) + end + + it 'should attempt to replace a record' do + allow(zone).to receive(:add).with(:hostname, 'A', 60, ip).and_raise(Google::Cloud::AlreadyExistsError,'MockError') + expect(zone).to receive(:replace).with(:hostname, 'A', 60, ip) + allow(subject).to receive(:get_ip).and_return(ip) + result = subject.create_or_replace_record(:hostname) + end + end + + context 'when add record fails' do + before(:each) do + allow(Google::Cloud::Dns).to receive(:configure) + allow(Google::Cloud::Dns).to receive(:new).and_return(connection) + allow(connection).to receive(:zone).and_return(zone) + allow(subject).to receive(:get_ip).and_return(ip) + end + + it 'should retry' do + allow(zone).to receive(:add).with(:hostname, 'A', 60, ip).and_raise(Google::Cloud::FailedPreconditionError,'MockError') + expect(zone).to receive(:add).with(:hostname, 'A', 60, ip).exactly(30).times + allow(subject).to receive(:sleep) + result = subject.create_or_replace_record(:hostname) + end + end + + context 'when IP does not exist' do + let(:ip) { nil } + + before(:each) do + allow(Google::Cloud::Dns).to receive(:configure) + allow(Google::Cloud::Dns).to receive(:new).and_return(connection) + allow(connection).to receive(:zone).and_return(zone) + allow(subject).to receive(:get_ip).and_return(ip) + end + + it 'should not attempt to add a record' do + allow(zone).to receive(:add).with(:hostname, 'A', 60, ip).and_raise(Google::Cloud::AlreadyExistsError,'MockError') + expect(zone).to_not have_received(:add) + allow(subject).to receive(:get_ip).and_return(ip) + result = subject.create_or_replace_record(:hostname) + end + end + end + + describe "#delete_record" do + let(:hostname) { 'spicy-proton' } + let(:zone) { MockDnsZone.new } + + context 'when removing a record' do + before(:each) do + allow(Google::Cloud::Dns).to receive(:configure) + allow(Google::Cloud::Dns).to receive(:new).and_return(connection) + allow(connection).to receive(:zone).and_return(zone) + end + + it 'should attempt to remove a record' do + expect(zone).to receive(:remove).with(:hostname, 'A') + result = subject.delete_record(:hostname) + end + end + + context 'when removing a record fails' do + before(:each) do + allow(Google::Cloud::Dns).to receive(:configure) + allow(Google::Cloud::Dns).to receive(:new).and_return(connection) + allow(connection).to receive(:zone).and_return(zone) + end + + it 'should retry' do + allow(zone).to receive(:remove).with(:hostname, 'A').and_raise(Google::Cloud::FailedPreconditionError,'MockError') + expect(zone).to receive(:remove).with(:hostname, 'A').exactly(30).times + allow(subject).to receive(:sleep) + result = subject.delete_record(:hostname) + end + end + end + + describe '#ensured_gcp_connection' do + let(:connection1) { mock_Google_Cloud_Dns_Project_Connection(connection_options) } + let(:connection2) { mock_Google_Cloud_Dns_Project_Connection(connection_options) } + + before(:each) do + allow(subject).to receive(:connect_to_gcp).and_return(connection1) + end + + it 'should return the same connection object when calling the pool multiple times' do + subject.connection_pool.with_metrics do |pool_object| + expect(pool_object[:connection]).to be(connection1) + end + subject.connection_pool.with_metrics do |pool_object| + expect(pool_object[:connection]).to be(connection1) + end + subject.connection_pool.with_metrics do |pool_object| + expect(pool_object[:connection]).to be(connection1) + end + end + + context 'when the connection breaks' do + before(:each) do + # Emulate the connection state being good, then bad, then good again + expect(subject).to receive(:gcp_connection_ok?).and_return(true, false, true) + expect(subject).to receive(:connect_to_gcp).and_return(connection1, connection2) + end + + it 'should restore the connection' do + subject.connection_pool.with_metrics do |pool_object| + # This line needs to be added to all instances of the connection_pool allocation + connection = subject.ensured_gcp_connection(pool_object) + + expect(connection).to be(connection1) + end + + subject.connection_pool.with_metrics do |pool_object| + connection = subject.ensured_gcp_connection(pool_object) + # The second connection would have failed. This test ensures that a + # new connection object was created. + expect(connection).to be(connection2) + end + + subject.connection_pool.with_metrics do |pool_object| + connection = subject.ensured_gcp_connection(pool_object) + expect(connection).to be(connection2) + end + end + end + end + + describe '#connect_to_gcp' do + before(:each) do + allow(Google::Cloud::Dns).to receive(:configure) + allow(Google::Cloud::Dns).to receive(:new).and_return(connection) + end + + context 'successful connection' do + it 'should return the connection object' do + result = subject.connect_to_gcp + + expect(result).to be(connection) + end + + it 'should increment the connect.open counter' do + expect(metrics).to receive(:increment).with('connect.open') + subject.connect_to_gcp + end + end + + context 'connection is initially unsuccessful' do + before(:each) do + # Simulate a failure and then success + allow(Google::Cloud::Dns).to receive(:configure) + expect(Google::Cloud::Dns).to receive(:new).and_raise(RuntimeError,'MockError') + allow(subject).to receive(:sleep) + end + + it 'should return the connection object' do + result = subject.connect_to_gcp + + expect(result).to be(connection) + end + + it 'should increment the connect.fail and then connect.open counter' do + expect(metrics).to receive(:increment).with('connect.fail').exactly(1).times + expect(metrics).to receive(:increment).with('connect.open').exactly(1).times + subject.connect_to_gcp + end + end + + context 'connection is always unsuccessful' do + before(:each) do + allow(Google::Cloud::Dns).to receive(:configure) + allow(Google::Cloud::Dns).to receive(:new).exactly(3).times.and_raise(RuntimeError,'MockError') + allow(subject).to receive(:sleep) + end + + it 'should retry the connection attempt config.max_tries times' do + expect(Google::Cloud::Dns).to receive(:new).exactly(config[:config]['max_tries']).times + + begin + # Swallow any errors + subject.connect_to_gcp + rescue + end + end + + it 'should increment the connect.fail counter config.max_tries times' do + expect(metrics).to receive(:increment).with('connect.fail').exactly(config[:config]['max_tries']).times + + begin + # Swallow any errors + subject.connect_to_gcp + rescue + end + end + end + end +end diff --git a/update-gemfile-lock b/update-gemfile-lock new file mode 100755 index 0000000..778bde9 --- /dev/null +++ b/update-gemfile-lock @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# The container tag should closely match what is used in `docker/Dockerfile` in vmpooler-deployment +docker run -it --rm \ + -v $(pwd):/app \ + jruby:9.4.1.0-jdk11 \ + /bin/bash -c 'apt-get update -qq && apt-get install -y --no-install-recommends make git netbase && cd /app && gem install bundler && bundle install --jobs 3 && bundle update; echo "LOCK_FILE_UPDATE_EXIT_CODE=$?"' diff --git a/util/vmpooler-dns-gcp-role.yaml b/util/vmpooler-dns-gcp-role.yaml new file mode 100644 index 0000000..bff6b98 --- /dev/null +++ b/util/vmpooler-dns-gcp-role.yaml @@ -0,0 +1,14 @@ +title: VMPooler DNS GCP +description: VMPooler DNS GCP Plugin +stage: GA +includedPermissions: +- dns.changes.create +- dns.changes.get +- dns.changes.list +- dns.managedZones.get +- dns.managedZones.list +- dns.resourceRecordSets.create +- dns.resourceRecordSets.update +- dns.resourceRecordSets.delete +- dns.resourceRecordSets.get +- dns.resourceRecordSets.list diff --git a/vmpooler-dns-gcp.gemspec b/vmpooler-dns-gcp.gemspec new file mode 100644 index 0000000..a05a249 --- /dev/null +++ b/vmpooler-dns-gcp.gemspec @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'vmpooler-dns-gcp/version' + +Gem::Specification.new do |spec| + spec.name = "vmpooler-dns-gcp" + spec.version = VmpoolerDnsGcp::VERSION + spec.authors = ["Puppet by Perforce"] + + spec.summary = "Google Cloud DNS for VMPooler" + spec.homepage = "https://github.com/puppetlabs/vmpooler-dns-gcp" + spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0') + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = spec.homepage + spec.metadata["changelog_uri"] = "https://github.com/puppetlabs/vmpooler-dns-gcp/blob/main/CHANGELOG.md" + + # Specify which files should be added to the gem when it is released. + spec.files = Dir[ "lib/**/*" ] + spec.require_paths = ["lib"] + spec.add_dependency "googleauth", ">= 0.16.2", "< 1.3.0" + spec.add_dependency "google-cloud-dns", "~> 0.35.1" + spec.add_dependency 'vmpooler', '~> 3.0' + + # Testing dependencies + spec.add_development_dependency 'mock_redis', '>= 0.17.0' + spec.add_development_dependency 'rspec', '>= 3.2' + spec.add_development_dependency 'rubocop', '~> 1.1.0' + spec.add_development_dependency 'simplecov', '>= 0.11.2' +end