kubes_google 0.1.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b3c67a729485761593635bc0393fbe23f1edc9b68210d843fdf2d01bf64c34c
4
- data.tar.gz: 7790b2ba42f85e001874dddb8aa2e250c3c1a3f4e6a402f1b7ae2529b5579588
3
+ metadata.gz: e286468a570668c5d92665f0966165c18f987de7bc09a27c0527d4e732ba3cc0
4
+ data.tar.gz: 5f36b3d707942e78160a677dcb3dd9b936bba513d43b61d20176486cacf201ba
5
5
  SHA512:
6
- metadata.gz: c230c68192f0605e1a7285c0842757b3ac1a54a4b33c782a208436ffa6e12fa838c3b9258081a07b25a4108b3266de30b8c1fda066d5805c935b7c0b52e7183d
7
- data.tar.gz: '09bf54cf2eaaa4b99ec4f28c8095d924ab70a0025d37e53b8fc6d413f93a7e295de172c0ee43609488383ce65be9f2e8f1dcbacb12b4bb0716489065cd072140'
6
+ metadata.gz: 0c86e64af5fd59083820f5a34ae59ad3ee323ae038e5f99ecb63900ffe41701c5ae5e1b661117f210153eacc151cc01e44681e63292e9b059802ae1916aa9dcb
7
+ data.tar.gz: 572b87da4fc774078994cce80eea09a4a3ca74d445f605fb286958c1ecf0eb0ea4210d7ed30b713d160c46f06acecc6e757652d79e7e9d00d4d58afd00291922
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  This project *loosely tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
5
 
6
+ ## [0.3.2] - 2020-11-11
7
+ - [#5](https://github.com/boltops-tools/kubes_google/pull/5) config.base64 option
8
+
9
+ ## [0.3.1] - 2020-11-11
10
+ - [#4](https://github.com/boltops-tools/kubes_google/pull/4) get_credentials hook
11
+
12
+ ## [0.3.0]
13
+ - #3 gke hook to whitelist ip
14
+
15
+ ## [0.2.0]
16
+ - #2 add google_secret helper and register plugin
17
+ - fix GOOGLE_PROJECT check
18
+
19
+ ## [0.1.2]
20
+ - #1 base64 option
21
+
6
22
  ## [0.1.1]
7
23
  - dont base64 secret values in data by default
8
24
 
data/README.md CHANGED
@@ -8,118 +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
- before("compile",
24
- execute: KubesGoogle::Secrets.new(upcase: true, prefix: 'projects/686010496118/secrets/demo-dev-')
25
- )
26
- ```
27
-
28
- Then set the secrets in the YAML:
29
-
30
- .kubes/resources/shared/secret.yaml
31
-
32
- ```
33
- apiVersion: v1
34
- kind: Secret
35
- metadata:
36
- name: demo
37
- labels:
38
- app: demo
39
- data:
40
- <% KubesGoogle::Secrets.data.each do |k,v| -%>
41
- <%= k %>: <%= Base64.encode64(v).strip %>
42
- <% end -%>
43
- ```
44
-
45
- This results in Google secrets with the prefix the `demo-dev-` being added to the Kubernetes secret data. The values are automatically base64 encoded.
46
-
47
- For example if you have these secret values:
48
-
49
- $ gcloud secrets versions access latest --secret demo-dev-db_user
50
- test1
51
- $ gcloud secrets versions access latest --secret demo-dev-db_pass
52
- test2
53
- $
54
-
55
- .kubes/output/shared/secret.yaml
56
-
57
- ```yaml
58
- metadata:
59
- namespace: demo
60
- name: demo-2a78a13682
61
- labels:
62
- app: demo
63
- apiVersion: v1
64
- kind: Secret
65
- data:
66
- db_pass: dGVzdDEK
67
- db_user: dGVzdDIK
68
- ```
69
-
70
- These environment variables can be set:
71
-
72
- Name | Description
73
- ---|---
74
- GCP_SECRET_PREFIX | Prefixed used to list and filter Google secrets. IE: `projects/686010496118/secrets/demo-dev-`.
75
- GOOGLE_PROJECT | Google project id.
76
-
77
- Secrets#initialize options:
78
-
79
- Variable | Description | Default
80
- ---|---|---
81
- upcase | Automatically upcase the Kubernetes secret data keys. | false
82
- 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
83
-
84
- 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.
85
-
86
- ## Service Accounts
87
-
88
- 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).
89
-
90
- Here's a Kubes hook that creates a service account:
91
-
92
- .kubes/config/hooks/kubes.rb
93
-
94
- ```ruby
95
- service_account = KubesGoogle::ServiceAccount.new(
96
- app: "demo",
97
- namespace: "demo-#{Kubes.env}", # defaults to APP-ENV when not set. IE: demo-dev
98
- roles: ["cloudsql.client", "secretmanager.viewer"], # defaults to empty when not set
99
- )
100
- before("apply",
101
- label: "create service account",
102
- execute: service_account,
103
- )
104
- ```
105
-
106
- The role permissions are currently always added to the existing permissions. So removing roles that were previously added does not remove them.
107
-
108
- ServiceAccount#initialize options:
109
-
110
- Variable | Description | Default
111
- ---|---|---
112
- app | The app name. It's used to conventionally set other variables. This is required. | nil
113
- gsa | The Google Service Account name. The conventional name is APP-ENV. IE: demo-dev. | APP-ENV
114
- ksa | The Kubernetes Service Account name. The conventional name is APP. IE: demo | APP
115
- namespace | The Kubernetes namespace. Defaults to the APP-ENV. IE: demo-dev. | APP-ENV
116
- roles | Google IAM roles to add. This adds permissions to the Google service account. | []
117
-
118
- Notes:
119
-
120
- * By default, `KubeGoogle.logger = Kubes.logger`. This means, you can set `logger.level = "debug"` in `.kubes/config.rb` to see more details.
121
- * The `gcloud` cli is used to create IAM roles. So `gcloud` is required.
122
- * 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/).
123
12
 
124
13
  ## Contributing
125
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,27 @@
1
+ module KubesGoogle
2
+ class Config
3
+ include Singleton
4
+
5
+ def defaults
6
+ c = ActiveSupport::OrderedOptions.new
7
+ c.base64_secrets = true
8
+ c.gke = ActiveSupport::OrderedOptions.new
9
+ c.gke.cluster_name = nil
10
+ c.gke.enable_get_credentials = nil
11
+ c.gke.enable_hooks = nil # nil since need cluster_name also. setting to false will explicitly disable hooks
12
+ c.gke.google_project = nil
13
+ c.gke.google_region = nil
14
+ c.gke.whitelist_ip = nil # default will auto-detect IP
15
+ c
16
+ end
17
+
18
+ @@config = nil
19
+ def config
20
+ @@config ||= defaults
21
+ end
22
+
23
+ def configure
24
+ yield(config)
25
+ end
26
+ end
27
+ 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
+ # 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
25
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,45 @@
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]
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 base64?
19
+ @base64.nil? ? KubesGoogle.config.base64_secrets : @base64
20
+ end
21
+
22
+ def fetch_value(short_name)
23
+ name = "projects/#{project_number}/secrets/#{short_name}/versions/latest"
24
+ version = secret_manager_service.access_secret_version(name: name)
25
+ version.payload.data
26
+ rescue Google::Cloud::NotFoundError => e
27
+ logger.info "WARN: secret #{name} not found".color(:yellow)
28
+ logger.info e.message
29
+ "NOT FOUND #{name}" # simple string so Kubernetes YAML is valid
30
+ end
31
+
32
+ # TODO: Get the project from the list project api instead. Unsure where the docs are for this.
33
+ # If someone knows, let me know.
34
+ # Right now grabbing the first secret to then be able to get the google project number
35
+ @@project_number = nil
36
+ def project_number
37
+ return @@project_number if @@project_number
38
+
39
+ parent = "projects/#{@project_id}"
40
+ resp = secret_manager_service.list_secrets(parent: parent) # note: page_size doesnt seem to get respected
41
+ name = resp.first.name # IE: projects/686010496118/secrets/demo-dev-db_host
42
+ @@project_number = name.split('/')[1]
43
+ end
44
+ end
45
+ 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 "WARN: 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.1"
2
+ VERSION = "0.3.2"
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.1
4
+ version: 0.3.2
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
@@ -81,11 +109,19 @@ files:
81
109
  - README.md
82
110
  - Rakefile
83
111
  - kubes_google.gemspec
112
+ - lib/hooks/kubes.rb
84
113
  - lib/kubes_google.rb
85
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
86
119
  - lib/kubes_google/logging.rb
87
120
  - lib/kubes_google/secrets.rb
121
+ - lib/kubes_google/secrets/fetcher.rb
88
122
  - lib/kubes_google/service_account.rb
123
+ - lib/kubes_google/services.rb
124
+ - lib/kubes_google/util/sh.rb
89
125
  - lib/kubes_google/version.rb
90
126
  homepage: https://github.com/boltops-tools/kubes_google
91
127
  licenses:
@@ -107,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
143
  - !ruby/object:Gem::Version
108
144
  version: '0'
109
145
  requirements: []
110
- rubygems_version: 3.1.2
146
+ rubygems_version: 3.1.4
111
147
  signing_key:
112
148
  specification_version: 4
113
149
  summary: Kubes Google Helpers Library