kubes_google 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39787bd787626a2712c07ccfc6ec8599e267a1662dcc3a8e135237629ee473b6
4
- data.tar.gz: e7e205d13e3918c5c7fe530c4e0d524765de7747daac1e3149d0179baeafd4de
3
+ metadata.gz: c00e66f307251f8ea29d5ba9f624197a31f7c914615925816f5086b1948d6122
4
+ data.tar.gz: 75e4cfc3a285162e64ef6e0ae68b79e55f6491774cc18e3c510f7c83feed8d1f
5
5
  SHA512:
6
- metadata.gz: 6e6c8863d58694242b9273812dbc28e31743436cbd6f744f84422eac5ade05d6144ceba6e00fe662c8d39c6bea2aa64cfe1b992bb0cbc6a30fc2e370654a05cf
7
- data.tar.gz: f9ad89b3050a457106bad1bcb74e962472477f8c249dd893fb38ff521970780bbce4a00607f58cc309f9469ccb15e69e8df54daed1be1dd98c005e7949b67cd2
6
+ metadata.gz: 95d854c940d876be88544ce89e4eb67c0a85989b279bdfa42f3b7fb504f291650283c2b18f9deec03e5f35cb9d977dcd0e19497cb9a4da805d445c2a779ea88c
7
+ data.tar.gz: 2a7648e2f8f2aeb14dd3e8d9beb4fd920512269a8bf6dcd03ec8dce6f118731ccfafc60437b785382001691ea5d22516562338a6cfb01092f5479feb2d8ed3be
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ This project *loosely tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
+
6
+ ## [0.3.1] - 2020-11-11
7
+ - [#4](https://github.com/boltops-tools/kubes_google/pull/4) get_credentials hook
8
+
9
+ ## [0.3.0]
10
+ - #3 gke hook to whitelist ip
11
+
12
+ ## [0.2.0]
13
+ - #2 add google_secret helper and register plugin
14
+ - fix GOOGLE_PROJECT check
15
+
16
+ ## [0.1.2]
17
+ - #1 base64 option
18
+
19
+ ## [0.1.1]
20
+ - dont base64 secret values in data by default
21
+
22
+ ## [0.1.0]
23
+ - Initial release.
data/README.md CHANGED
@@ -8,119 +8,7 @@
8
8
 
9
9
  ## Usage
10
10
 
11
- The helpers include:
12
-
13
- * Secrets
14
- * Service Accounts
15
-
16
- ## Secrets
17
-
18
- Set up a [Kubes hook](https://kubes.guru/docs/config/hooks/kubes/).
19
-
20
- .kubes/config/hooks/kubes.rb
21
-
22
- ```ruby
23
- ENV['GCP_SECRET_PREFIX'] ||= 'projects/686010496118/secrets/demo-dev-'
24
- before("compile",
25
- execute: KubesGoogle::Secrets.new(upcase: true)
26
- )
27
- ```
28
-
29
- Then set the secrets in the YAML:
30
-
31
- .kubes/resources/shared/secret.yaml
32
-
33
- ```
34
- apiVersion: v1
35
- kind: Secret
36
- metadata:
37
- name: demo
38
- labels:
39
- app: demo
40
- data:
41
- <% KubesGoogle::Secrets.data.each do |k,v| -%>
42
- <%= k %>: <%= v %>
43
- <% end -%>
44
- ```
45
-
46
- This results in Google secrets with the prefix the `demo-dev-` being added to the Kubernetes secret data. The values are automatically base64 encoded.
47
-
48
- For example if you have these secret values:
49
-
50
- $ gcloud secrets versions access latest --secret demo-dev-db_user
51
- test1
52
- $ gcloud secrets versions access latest --secret demo-dev-db_pass
53
- test2
54
- $
55
-
56
- .kubes/output/shared/secret.yaml
57
-
58
- ```yaml
59
- metadata:
60
- namespace: demo
61
- name: demo-2a78a13682
62
- labels:
63
- app: demo
64
- apiVersion: v1
65
- kind: Secret
66
- data:
67
- db_pass: dGVzdDEK
68
- db_user: dGVzdDIK
69
- ```
70
-
71
- These environment variables can be set:
72
-
73
- Name | Description
74
- ---|---
75
- GCP_SECRET_PREFIX | Prefixed used to list and filter Google secrets. IE: `projects/686010496118/secrets/demo-dev-`.
76
- GOOGLE_PROJECT | Google project id.
77
-
78
- Secrets#initialize options:
79
-
80
- Variable | Description | Default
81
- ---|---|---
82
- upcase | Automatically upcase the Kubernetes secret data keys. | false
83
- prefix | Prefixed used to list and filter Google secrets. IE: `projects/686010496118/secrets/demo-dev-`. Can also be set with the `GCP_SECRET_PREFIX` env variable. The env variable takes the highest precedence. | nil
84
-
85
- Note, Kubernetes secrets are only base64 encoded. So users who have access to read Kubernetes secrets will be able to decode and get the value trivially. Depending on your security posture requirements, this may or may not suffice.
86
-
87
- ## Service Accounts
88
-
89
- This library can also be used to automatically create Google Service Accounts associated with the [GKE Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity).
90
-
91
- Here's a Kubes hook that creates a service account:
92
-
93
- .kubes/config/hooks/kubes.rb
94
-
95
- ```ruby
96
- service_account = KubesGoogle::ServiceAccount.new(
97
- app: "demo",
98
- namespace: "demo-dev", # defaults to APP-ENV when not set. IE: demo-dev
99
- roles: ["cloudsql.client", "secretmanager.viewer"], # defaults to empty when not set
100
- )
101
- before("apply",
102
- label: "create service account",
103
- execute: service_account,
104
- )
105
- ```
106
-
107
- The role permissions are currently always added to the existing permissions. So removing roles that were previously added does not remove them.
108
-
109
- ServiceAccount#initialize options:
110
-
111
- Variable | Description | Default
112
- ---|---|---
113
- app | The app name. It's used to conventionally set other variables. This is required. | nil
114
- gsa | The Google Service Account name. The conventional name is APP-ENV. IE: demo-dev. | APP-ENV
115
- ksa | The Kubernetes Service Account name. The conventional name is APP. IE: demo | APP
116
- namespace | The Kubernetes namespace. Defaults to the APP-ENV. IE: demo-dev. | APP-ENV
117
- roles | Google IAM roles to add. This adds permissions to the Google service account. | []
118
-
119
- Notes:
120
-
121
- * By default, `KubeGoogle.logger = Kubes.logger`. This means, you can set `logger.level = "debug"` in `.kubes/config.rb` to see more details.
122
- * The `gcloud` cli is used to create IAM roles. So `gcloud` is required.
123
- * Note: Would like to use the google sdk, but it wasn't obvious how to do so. PRs are welcomed.
11
+ For more detailed usage instructions refer to the [Kubes Helpers docs](https://kubes.guru/docs/helpers/google/).
124
12
 
125
13
  ## Contributing
126
14
 
@@ -23,7 +23,10 @@ Gem::Specification.new do |spec|
23
23
  spec.require_paths = ["lib"]
24
24
 
25
25
  spec.add_dependency "activesupport"
26
+ spec.add_dependency "google-cloud-container"
26
27
  spec.add_dependency "google-cloud-secret_manager"
27
28
  spec.add_dependency "memoist"
28
29
  spec.add_dependency "zeitwerk"
30
+
31
+ spec.add_development_dependency "kubes"
29
32
  end
@@ -0,0 +1,22 @@
1
+ gke = KubesGoogle::Gke.new(
2
+ cluster_name: KubesGoogle.config.gke.cluster_name,
3
+ google_region: KubesGoogle.config.gke.google_region,
4
+ google_project: KubesGoogle.config.gke.google_project,
5
+ enable_get_credentials: KubesGoogle.config.gke.enable_get_credentials,
6
+ whitelist_ip: KubesGoogle.config.gke.whitelist_ip,
7
+ )
8
+
9
+ before("apply",
10
+ label: "gke get-credentials hook",
11
+ execute: gke.method(:get_credentials).to_proc,
12
+ ) if gke.get_credentials_enabled?
13
+
14
+ before("apply",
15
+ label: "gke whitelist hook",
16
+ execute: gke.method(:allow).to_proc,
17
+ ) if gke.enabled?
18
+
19
+ after("apply",
20
+ label: "gke whitelist hook",
21
+ execute: gke.method(:deny).to_proc,
22
+ ) if gke.enabled?
@@ -16,5 +16,26 @@ module KubesGoogle
16
16
  @@logger = v
17
17
  end
18
18
 
19
+ # Friendlier method configure.
20
+ #
21
+ # .kubes/config/env/dev.rb
22
+ # .kubes/config/plugins/google.rb # also works
23
+ #
24
+ # Example:
25
+ #
26
+ # KubesGoogle.configure do |config|
27
+ # config.hooks.gke_whitelist = true
28
+ # end
29
+ #
30
+ def configure(&block)
31
+ Config.instance.configure(&block)
32
+ end
33
+
34
+ def config
35
+ Config.instance.config
36
+ end
37
+
19
38
  extend self
20
39
  end
40
+
41
+ Kubes::Plugin.register(KubesGoogle)
@@ -0,0 +1,23 @@
1
+ module KubesGoogle
2
+ class Config
3
+ include Singleton
4
+
5
+ def defaults
6
+ c = ActiveSupport::OrderedOptions.new
7
+ c.gke = ActiveSupport::OrderedOptions.new
8
+ c.gke.cluster_name = nil
9
+ c.gke.enable_hooks = nil # nil since need cluster_name also. setting to false will explicitly disable hooks
10
+ c.gke.whitelist_ip = nil # default will auto-detect IP
11
+ c
12
+ end
13
+
14
+ @@config = nil
15
+ def config
16
+ @@config ||= defaults
17
+ end
18
+
19
+ def configure
20
+ yield(config)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,120 @@
1
+ require 'open-uri'
2
+
3
+ module KubesGoogle
4
+ class Gke
5
+ extend Memoist
6
+ include Logging
7
+ include Services
8
+ include Util::Sh
9
+
10
+ def initialize(cluster_name:,
11
+ enable_get_credentials: false,
12
+ google_project: nil,
13
+ google_region: "us-central1",
14
+ whitelist_ip: nil)
15
+ @cluster_name = cluster_name
16
+ @enable_get_credentials = enable_get_credentials
17
+ @google_project = ENV['GOOGLE_PROJECT'] || google_project
18
+ @google_region = ENV['GOOGLE_REGION'] || google_region
19
+ @whitelist_ip = whitelist_ip
20
+ end
21
+
22
+ def allow
23
+ logger.debug "Updating cluster. Adding IP: #{ip}"
24
+ update_cluster(cidr_blocks(:with_whitelist))
25
+ end
26
+
27
+ def deny
28
+ logger.debug "Updating cluster. Removing IP: #{ip}"
29
+ update_cluster(cidr_blocks(:without_whitelist))
30
+ end
31
+
32
+ def get_credentials
33
+ return unless get_credentials_enabled?
34
+ sh "gcloud container clusters get-credentials --project=#{@google_project} --region=#{@google_region} #{@cluster_name}"
35
+ end
36
+
37
+ def full_name
38
+ "projects/#{@google_project}/locations/#{@google_region}/clusters/#{@cluster_name}"
39
+ end
40
+
41
+ def enabled?
42
+ enable = KubesGoogle.config.gke.enable_hooks
43
+ enable = enable.nil? ? true : enable
44
+ # gke = KubesGoogle::Gke.new(name: KubesGoogle.config.gke.cluster_name)
45
+ # so @name = KubesGoogle.config.gke.cluster_name
46
+ !!(enable && @cluster_name)
47
+ end
48
+
49
+ def get_credentials_enabled?
50
+ enable = KubesGoogle.config.gke.enable_get_credentials
51
+ enable = enable.nil? ? false : enable
52
+ !!(enable && full_name)
53
+ end
54
+
55
+ def update_cluster(cidr_blocks)
56
+ resp = cluster_manager.update_cluster(
57
+ name: full_name,
58
+ update: {
59
+ desired_master_authorized_networks_config: {
60
+ cidr_blocks: cidr_blocks,
61
+ enabled: true,
62
+ }
63
+ }
64
+ )
65
+ operation_name = resp.self_link.sub(/.*projects/,'projects')
66
+ wait_for(operation_name)
67
+ end
68
+
69
+ def wait_for(operation_name)
70
+ resp = cluster_manager.get_operation(name: operation_name)
71
+ until resp.status != :RUNNING do
72
+ sleep 5
73
+ resp = cluster_manager.get_operation(name: operation_name)
74
+ end
75
+ end
76
+
77
+ def cidr_blocks(type)
78
+ # so we dont keep adding duplicates
79
+ old = old_cidrs.reject do |x|
80
+ x[:display_name] == new_cidr[:display_name] &&
81
+ x[:cidr_block] == new_cidr[:cidr_block]
82
+ end
83
+ if type == :with_whitelist
84
+ old + [new_cidr]
85
+ else
86
+ old
87
+ end
88
+ end
89
+
90
+ def old_cidrs
91
+ resp = cluster_manager.get_cluster(name: full_name)
92
+ config = resp.master_authorized_networks_config.to_h
93
+ config[:cidr_blocks]
94
+ end
95
+ memoize :old_cidrs
96
+
97
+ def new_cidr
98
+ {
99
+ display_name: "added-by-kubes-google",
100
+ cidr_block: ip,
101
+ }
102
+ end
103
+ memoize :new_cidr
104
+
105
+ def ip
106
+ @whitelist_ip || current_ip
107
+ end
108
+
109
+ def current_ip
110
+ resp = URI.open("http://ifconfig.me")
111
+ ip = resp.read
112
+ "#{ip}/32"
113
+ rescue SocketError => e
114
+ logger.info "WARN: #{e.message}"
115
+ logger.info "Unable to detect current ip. Will use 0.0.0.0/0"
116
+ "0.0.0.0/0"
117
+ end
118
+ memoize :current_ip
119
+ end
120
+ end
@@ -0,0 +1,11 @@
1
+ module KubesGoogle
2
+ module Helpers
3
+ extend Memoist
4
+ include Services
5
+
6
+ def google_secret(name, options={})
7
+ fetcher = Secrets::Fetcher.new(options)
8
+ fetcher.fetch(name)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module KubesGoogle
2
+ class Hooks
3
+ def path
4
+ File.expand_path("../hooks", __dir__)
5
+ end
6
+ end
7
+ end
@@ -2,10 +2,10 @@ require "google-cloud-secret_manager"
2
2
 
3
3
  module KubesGoogle
4
4
  class Secrets
5
- def initialize(upcase: false, prefix: nil)
6
- @upcase = upcase
7
- @prefix = ENV['GCP_SECRET_PREFIX'] || prefix || raise("GOOGLE_PROJECT env variable is not set. It's required.")
8
- @project_id = ENV['GOOGLE_PROJECT']
5
+ def initialize(upcase: false, base64: false, prefix: nil)
6
+ @upcase, @base64 = upcase, base64
7
+ @prefix = ENV['GCP_SECRET_PREFIX'] || prefix
8
+ @project_id = ENV['GOOGLE_PROJECT'] || raise("GOOGLE_PROJECT env variable is not set. It's required.")
9
9
  # IE: prefix: projects/686010496118/secrets/demo-dev-
10
10
  end
11
11
 
@@ -13,7 +13,7 @@ module KubesGoogle
13
13
  client = Google::Cloud::SecretManager.secret_manager_service
14
14
 
15
15
  parent = "projects/#{@project_id}"
16
- resp = client.list_secrets(parent: parent, page_size: 1)
16
+ resp = client.list_secrets(parent: parent) # note: page_size doesnt seem to get respected
17
17
  resp.each do |secret|
18
18
  next unless secret.name.include?(@prefix)
19
19
  version = client.access_secret_version(name: "#{secret.name}/versions/latest")
@@ -22,10 +22,16 @@ module KubesGoogle
22
22
  key = secret.name.sub(@prefix,'')
23
23
  key = key.upcase if @upcase
24
24
  value = version.payload.data
25
- self.class.data[key] = Base64.encode64(value).strip
25
+ # strict_encode64 to avoid newlines https://stackoverflow.com/questions/2620975/strange-n-in-base64-encoded-string-in-ruby
26
+ value = Base64.strict_encode64(value).strip if @base64
27
+ self.class.data[key] = value
26
28
  end
27
29
  end
28
30
 
31
+ def data
32
+ self.class.data
33
+ end
34
+
29
35
  class_attribute :data
30
36
  self.data = {}
31
37
  end
@@ -0,0 +1,41 @@
1
+ class KubesGoogle::Secrets
2
+ class Fetcher
3
+ include KubesGoogle::Logging
4
+ include KubesGoogle::Services
5
+
6
+ def initialize(options={})
7
+ @options = options
8
+ @base64 = options[:base64].nil? ? true : options[:base64]
9
+ @project_id = ENV['GOOGLE_PROJECT'] || raise("GOOGLE_PROJECT env variable is not set. It's required.")
10
+ end
11
+
12
+ def fetch(short_name)
13
+ value = fetch_value(short_name)
14
+ value = Base64.strict_encode64(value).strip if @base64
15
+ value
16
+ end
17
+
18
+ def fetch_value(short_name)
19
+ name = "projects/#{project_number}/secrets/#{short_name}/versions/latest"
20
+ version = secret_manager_service.access_secret_version(name: name)
21
+ version.payload.data
22
+ rescue Google::Cloud::NotFoundError => e
23
+ logger.info "WARN: secret #{name} not found".color(:yellow)
24
+ logger.info e.message
25
+ "NOT FOUND #{name}" # simple string so Kubernetes YAML is valid
26
+ end
27
+
28
+ # TODO: Get the project from the list project api instead. Unsure where the docs are for this.
29
+ # If someone knows, let me know.
30
+ # Right now grabbing the first secret to then be able to get the google project number
31
+ @@project_number = nil
32
+ def project_number
33
+ return @@project_number if @@project_number
34
+
35
+ parent = "projects/#{@project_id}"
36
+ resp = secret_manager_service.list_secrets(parent: parent) # note: page_size doesnt seem to get respected
37
+ name = resp.first.name # IE: projects/686010496118/secrets/demo-dev-db_host
38
+ @@project_number = name.split('/')[1]
39
+ end
40
+ end
41
+ end
@@ -4,6 +4,7 @@ require "json"
4
4
  module KubesGoogle
5
5
  class ServiceAccount
6
6
  include Logging
7
+ include Util::Sh
7
8
 
8
9
  def initialize(app:, namespace:nil, roles: [], gsa: nil, ksa: nil)
9
10
  @app, @roles = app, roles
@@ -71,25 +72,5 @@ module KubesGoogle
71
72
  --member=serviceAccount:#{@service_account} \
72
73
  --role=#{role} > /dev/null".squish
73
74
  end
74
-
75
- private
76
- def sh(command)
77
- logger.debug "=> #{command}"
78
- success = system(command)
79
- unless success
80
- logger.info "ERROR: Running #{command}"
81
- end
82
- success
83
- end
84
-
85
- def capture(command)
86
- out = `#{command}`
87
- unless $?.exitstatus == 0
88
- logger.info "ERROR: Running #{command}"
89
- logger.info out
90
- exit 1
91
- end
92
- out
93
- end
94
75
  end
95
76
  end
@@ -0,0 +1,19 @@
1
+ require "google-cloud-secret_manager"
2
+ require "google/cloud/container"
3
+
4
+ module KubesGoogle
5
+ module Services
6
+ extend Memoist
7
+
8
+ def cluster_manager
9
+ Google::Cloud::Container.cluster_manager
10
+ end
11
+ memoize :cluster_manager
12
+
13
+ def secret_manager_service
14
+ Google::Cloud::SecretManager.secret_manager_service
15
+ end
16
+ memoize :secret_manager_service
17
+ end
18
+ end
19
+
@@ -0,0 +1,23 @@
1
+ module KubesGoogle::Util
2
+ module Sh
3
+ private
4
+ def sh(command)
5
+ logger.debug "=> #{command}"
6
+ success = system(command)
7
+ unless success
8
+ logger.info "WARN: Running #{command}"
9
+ end
10
+ success
11
+ end
12
+
13
+ def capture(command)
14
+ out = `#{command}`
15
+ unless $?.exitstatus == 0
16
+ logger.info "ERROR: Running #{command}"
17
+ logger.info out
18
+ exit 1
19
+ end
20
+ out
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module KubesGoogle
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kubes_google
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tung Nguyen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-10-13 00:00:00.000000000 Z
11
+ date: 2020-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: google-cloud-container
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: google-cloud-secret_manager
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +80,20 @@ dependencies:
66
80
  - - ">="
67
81
  - !ruby/object:Gem::Version
68
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: kubes
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
69
97
  description:
70
98
  email:
71
99
  - tung@boltops.com
@@ -75,16 +103,25 @@ extra_rdoc_files: []
75
103
  files:
76
104
  - ".gitignore"
77
105
  - ".rspec"
106
+ - CHANGELOG.md
78
107
  - Gemfile
79
108
  - LICENSE.txt
80
109
  - README.md
81
110
  - Rakefile
82
111
  - kubes_google.gemspec
112
+ - lib/hooks/kubes.rb
83
113
  - lib/kubes_google.rb
84
114
  - lib/kubes_google/autoloader.rb
115
+ - lib/kubes_google/config.rb
116
+ - lib/kubes_google/gke.rb
117
+ - lib/kubes_google/helpers.rb
118
+ - lib/kubes_google/hooks.rb
85
119
  - lib/kubes_google/logging.rb
86
120
  - lib/kubes_google/secrets.rb
121
+ - lib/kubes_google/secrets/fetcher.rb
87
122
  - lib/kubes_google/service_account.rb
123
+ - lib/kubes_google/services.rb
124
+ - lib/kubes_google/util/sh.rb
88
125
  - lib/kubes_google/version.rb
89
126
  homepage: https://github.com/boltops-tools/kubes_google
90
127
  licenses:
@@ -106,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
143
  - !ruby/object:Gem::Version
107
144
  version: '0'
108
145
  requirements: []
109
- rubygems_version: 3.1.2
146
+ rubygems_version: 3.1.4
110
147
  signing_key:
111
148
  specification_version: 4
112
149
  summary: Kubes Google Helpers Library