seira 0.3.3 → 0.3.6
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 +4 -4
- data/.default-rubocop.yml +216 -61
- data/.rubocop.yml +4 -1
- data/lib/helpers.rb +17 -1
- data/lib/seira.rb +8 -1
- data/lib/seira/app.rb +8 -6
- data/lib/seira/cluster.rb +12 -7
- data/lib/seira/commands.rb +22 -0
- data/lib/seira/commands/gcloud.rb +43 -0
- data/lib/seira/commands/kubectl.rb +34 -0
- data/lib/seira/db.rb +83 -10
- data/lib/seira/db/create.rb +10 -9
- data/lib/seira/jobs.rb +9 -6
- data/lib/seira/node_pools.rb +10 -8
- data/lib/seira/pods.rb +6 -4
- data/lib/seira/proxy.rb +3 -1
- data/lib/seira/secrets.rb +6 -4
- data/lib/seira/settings.rb +5 -1
- data/lib/seira/version.rb +1 -1
- data/seira.gemspec +1 -1
- metadata +7 -4
data/.rubocop.yml
CHANGED
@@ -53,7 +53,10 @@ Style/SignalException:
|
|
53
53
|
Style/StringLiterals:
|
54
54
|
Enabled: false
|
55
55
|
|
56
|
-
Style/
|
56
|
+
Style/TrailingCommaInArrayLiteral:
|
57
|
+
Enabled: false
|
58
|
+
|
59
|
+
Style/TrailingCommaInHashLiteral:
|
57
60
|
Enabled: false
|
58
61
|
|
59
62
|
Style/TrailingCommaInArguments:
|
data/lib/helpers.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
module Seira
|
2
2
|
class Helpers
|
3
|
+
include Seira::Commands
|
4
|
+
|
3
5
|
class << self
|
4
6
|
def rails_env(context:)
|
5
7
|
if context[:cluster] == 'internal'
|
@@ -11,7 +13,21 @@ module Seira
|
|
11
13
|
|
12
14
|
def fetch_pods(filters:, app:)
|
13
15
|
filter_string = { app: app }.merge(filters).map { |k, v| "#{k}=#{v}" }.join(',')
|
14
|
-
|
16
|
+
output = Seira::Commands.kubectl("get pods -o json --selector=#{filter_string}", context: { app: app }, return_output: true)
|
17
|
+
JSON.parse(output)['items']
|
18
|
+
end
|
19
|
+
|
20
|
+
def log_link(context:, app:, query:)
|
21
|
+
link = context[:settings].log_link_format
|
22
|
+
return nil if link.nil?
|
23
|
+
link.gsub! 'APP', app
|
24
|
+
link.gsub! 'CLUSTER', context[:cluster]
|
25
|
+
link.gsub! 'QUERY', query
|
26
|
+
link
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_secret(app:, key:, context: {})
|
30
|
+
Secrets.new(app: app, action: 'get', args: [], context: context).get(key)
|
15
31
|
end
|
16
32
|
end
|
17
33
|
end
|
data/lib/seira.rb
CHANGED
@@ -3,6 +3,8 @@ require 'highline/import'
|
|
3
3
|
require 'colorize'
|
4
4
|
require 'tmpdir'
|
5
5
|
|
6
|
+
require 'seira/commands'
|
7
|
+
|
6
8
|
require "seira/version"
|
7
9
|
require 'helpers'
|
8
10
|
require 'seira/app'
|
@@ -23,6 +25,8 @@ require 'seira/node_pools'
|
|
23
25
|
# work for the command to a class in lib/seira folder.
|
24
26
|
module Seira
|
25
27
|
class Runner
|
28
|
+
include Seira::Commands
|
29
|
+
|
26
30
|
CATEGORIES = {
|
27
31
|
'secrets' => Seira::Secrets,
|
28
32
|
'pods' => Seira::Pods,
|
@@ -128,7 +132,10 @@ module Seira
|
|
128
132
|
cluster: cluster,
|
129
133
|
project: project,
|
130
134
|
settings: settings,
|
131
|
-
default_zone: settings.default_zone
|
135
|
+
default_zone: settings.default_zone,
|
136
|
+
app: app,
|
137
|
+
action: action,
|
138
|
+
args: args
|
132
139
|
}
|
133
140
|
end
|
134
141
|
|
data/lib/seira/app.rb
CHANGED
@@ -6,6 +6,8 @@ require 'fileutils'
|
|
6
6
|
# seira staging specs app bootstrap
|
7
7
|
module Seira
|
8
8
|
class App
|
9
|
+
include Seira::Commands
|
10
|
+
|
9
11
|
VALID_ACTIONS = %w[help bootstrap apply restart scale revision].freeze
|
10
12
|
SUMMARY = "Bootstrap, scale, configure, restart, your apps.".freeze
|
11
13
|
|
@@ -53,7 +55,7 @@ module Seira
|
|
53
55
|
|
54
56
|
def ask_cluster_for_current_revision
|
55
57
|
tier = context[:settings].config_for_app(app)['golden_tier'] || 'web'
|
56
|
-
current_image =
|
58
|
+
current_image = kubectl("get deployment -l app=#{app},tier=#{tier} -o=jsonpath='{$.items[:1].spec.template.spec.containers[:1].image}'", context: context, return_output: true).strip.chomp
|
57
59
|
current_revision = current_image.split(':').last
|
58
60
|
current_revision
|
59
61
|
end
|
@@ -63,7 +65,8 @@ module Seira
|
|
63
65
|
def run_bootstrap
|
64
66
|
# TODO: Verify that 00-namespace exists
|
65
67
|
# TODO: Do conformance test on the yaml files before running anything, including that 00-namespace.yaml exists and has right name
|
66
|
-
|
68
|
+
# Create namespace before anything else
|
69
|
+
kubectl("apply -f kubernetes/#{context[:cluster]}/#{app}/00-namespace.yaml", context: context)
|
67
70
|
bootstrap_main_secret
|
68
71
|
bootstrap_cloudsql_secret
|
69
72
|
bootstrap_gcr_secret
|
@@ -113,8 +116,7 @@ module Seira
|
|
113
116
|
replacement_hash: replacement_hash
|
114
117
|
)
|
115
118
|
|
116
|
-
|
117
|
-
system("kubectl apply -f #{destination}")
|
119
|
+
kubectl("apply -f #{destination}", context: context)
|
118
120
|
|
119
121
|
unless async
|
120
122
|
puts "Monitoring rollout status..."
|
@@ -144,7 +146,7 @@ module Seira
|
|
144
146
|
end
|
145
147
|
replicas = config['spec']['replicas'] if replicas == 'default'
|
146
148
|
puts "scaling #{tier} to #{replicas}"
|
147
|
-
|
149
|
+
kubectl("scale --replicas=#{replicas} deployments/#{config['metadata']['name']}", context: context)
|
148
150
|
end
|
149
151
|
end
|
150
152
|
|
@@ -159,7 +161,7 @@ module Seira
|
|
159
161
|
# 'internal' is a unique cluster/project "cluster". It always means production in terms of rails app.
|
160
162
|
rails_env = Helpers.rails_env(context: context)
|
161
163
|
|
162
|
-
|
164
|
+
kubectl("create secret generic #{main_secret_name} --from-literal=RAILS_ENV=#{rails_env} --from-literal=RACK_ENV=#{rails_env}", context: context)
|
163
165
|
end
|
164
166
|
|
165
167
|
# We use a secret in our container to use a service account to connect to our cloudsql databases. The secret in 'default'
|
data/lib/seira/cluster.rb
CHANGED
@@ -5,6 +5,8 @@ require 'fileutils'
|
|
5
5
|
# Example usages:
|
6
6
|
module Seira
|
7
7
|
class Cluster
|
8
|
+
include Seira::Commands
|
9
|
+
|
8
10
|
VALID_ACTIONS = %w[help bootstrap upgrade-master].freeze
|
9
11
|
SUMMARY = "For managing whole clusters.".freeze
|
10
12
|
|
@@ -49,12 +51,16 @@ module Seira
|
|
49
51
|
end
|
50
52
|
|
51
53
|
def self.current_cluster
|
52
|
-
|
54
|
+
Seira::Commands.kubectl("config current-context", context: :none, return_output: true).chomp.strip
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.current_project
|
58
|
+
Seira::Commands.gcloud("config get-value project", context: :none, return_output: true).chomp.strip
|
53
59
|
end
|
54
60
|
|
55
61
|
def current
|
56
|
-
puts
|
57
|
-
puts
|
62
|
+
puts current_project
|
63
|
+
puts current_cluster
|
58
64
|
end
|
59
65
|
|
60
66
|
private
|
@@ -76,7 +82,6 @@ module Seira
|
|
76
82
|
exit(1)
|
77
83
|
end
|
78
84
|
|
79
|
-
# puts `kubectl create secret generic gcr-secret --namespace default --from-file=.dockercfg=#{dockercfg_location}`
|
80
85
|
puts `kubectl create secret docker-registry gcr-secret --docker-username=_json_key --docker-password="$(cat #{dockercfg_location})" --docker-server=https://gcr.io --docker-email=doesnotmatter@example.com`
|
81
86
|
puts `kubectl create secret generic cloudsql-credentials --namespace default --from-file=credentials.json=#{cloudsql_credentials_location}`
|
82
87
|
end
|
@@ -92,7 +97,7 @@ module Seira
|
|
92
97
|
end
|
93
98
|
|
94
99
|
# Ensure the specified version is supported by GKE
|
95
|
-
server_config =
|
100
|
+
server_config = gcloud("container get-server-config", format: :json, context: context)
|
96
101
|
valid_versions = server_config['validMasterVersions']
|
97
102
|
unless valid_versions.include? new_version
|
98
103
|
puts "Version #{new_version} is unsupported. Supported versions are:"
|
@@ -100,7 +105,7 @@ module Seira
|
|
100
105
|
exit(1)
|
101
106
|
end
|
102
107
|
|
103
|
-
cluster_config = JSON.parse(
|
108
|
+
cluster_config = JSON.parse(gcloud("container clusters describe #{cluster}", format: :json, context: context))
|
104
109
|
|
105
110
|
# Update the master node first
|
106
111
|
exit(1) unless Highline.agree("Are you sure you want to upgrade cluster #{cluster} master to version #{new_version}? Services should continue to run fine, but the cluster control plane will be offline.")
|
@@ -109,7 +114,7 @@ module Seira
|
|
109
114
|
if cluster_config['currentMasterVersion'] == new_version
|
110
115
|
# Master has already been updated; this step is not needed
|
111
116
|
puts 'Already up to date!'
|
112
|
-
elsif
|
117
|
+
elsif gcloud("container clusters upgrade #{cluster} --cluster-version=#{new_version} --master", format: :boolean, context: context)
|
113
118
|
puts 'Master updated successfully!'
|
114
119
|
else
|
115
120
|
puts 'Failed to update master.'
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'seira/commands/kubectl'
|
2
|
+
require 'seira/commands/gcloud'
|
3
|
+
|
4
|
+
module Seira
|
5
|
+
module Commands
|
6
|
+
def kubectl(command, context:, clean_output: false, return_output: false)
|
7
|
+
Seira::Commands.kubectl(command, context: context, clean_output: clean_output, return_output: return_output)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.kubectl(command, context:, clean_output: false, return_output: false)
|
11
|
+
Kubectl.new(command, context: context).invoke(clean_output: clean_output, return_output: return_output)
|
12
|
+
end
|
13
|
+
|
14
|
+
def gcloud(command, context:, clean_output: false, format: :boolean)
|
15
|
+
Seira::Commands.gcloud(command, context: context, clean_output: clean_output, format: format)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.gcloud(command, context:, clean_output: false, format: :boolean)
|
19
|
+
Gcloud.new(command, context: context, clean_output: clean_output, format: format).invoke
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Seira
|
2
|
+
module Commands
|
3
|
+
class Gcloud
|
4
|
+
attr_reader :context, :command, :format, :clean_output
|
5
|
+
|
6
|
+
def initialize(command, context:, clean_output:, format:)
|
7
|
+
@command = command
|
8
|
+
@context = context
|
9
|
+
@format = format
|
10
|
+
@clean_output = clean_output
|
11
|
+
end
|
12
|
+
|
13
|
+
def invoke
|
14
|
+
puts "Calling: #{calculated_command.green}" unless clean_output
|
15
|
+
|
16
|
+
if format == :boolean
|
17
|
+
system(calculated_command)
|
18
|
+
elsif format == :json
|
19
|
+
`#{calculated_command}`
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def calculated_command
|
26
|
+
@_calculated_command ||= begin
|
27
|
+
rv =
|
28
|
+
if format == :json
|
29
|
+
"gcloud #{command} --format=json"
|
30
|
+
else
|
31
|
+
"gcloud #{command}"
|
32
|
+
end
|
33
|
+
|
34
|
+
unless context.nil?
|
35
|
+
rv = "#{rv} --project=#{context[:project]}"
|
36
|
+
end
|
37
|
+
|
38
|
+
rv
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Seira
|
2
|
+
module Commands
|
3
|
+
class Kubectl
|
4
|
+
attr_reader :context, :command
|
5
|
+
|
6
|
+
def initialize(command, context:)
|
7
|
+
@command = command
|
8
|
+
@context = context
|
9
|
+
end
|
10
|
+
|
11
|
+
def invoke(clean_output: false, return_output: false)
|
12
|
+
puts "Calling: #{calculated_command.green}" unless clean_output
|
13
|
+
|
14
|
+
if return_output
|
15
|
+
`#{calculated_command}`
|
16
|
+
else
|
17
|
+
system(calculated_command)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def calculated_command
|
24
|
+
@_calculated_command ||= begin
|
25
|
+
if context == :none
|
26
|
+
"kubectl #{command}"
|
27
|
+
else
|
28
|
+
"kubectl #{command} --namespace=#{context[:app]}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/seira/db.rb
CHANGED
@@ -4,7 +4,9 @@ require_relative 'db/create'
|
|
4
4
|
|
5
5
|
module Seira
|
6
6
|
class Db
|
7
|
-
|
7
|
+
include Seira::Commands
|
8
|
+
|
9
|
+
VALID_ACTIONS = %w[help create delete list restart connect ps kill analyze create-readonly-user].freeze
|
8
10
|
SUMMARY = "Manage your Cloud SQL Postgres databases.".freeze
|
9
11
|
|
10
12
|
attr_reader :app, :action, :args, :context
|
@@ -36,6 +38,8 @@ module Seira
|
|
36
38
|
run_kill
|
37
39
|
when 'analyze'
|
38
40
|
run_analyze
|
41
|
+
when 'create-readonly-user'
|
42
|
+
run_create_readonly_user
|
39
43
|
else
|
40
44
|
fail "Unknown command encountered"
|
41
45
|
end
|
@@ -43,7 +47,7 @@ module Seira
|
|
43
47
|
|
44
48
|
# NOTE: Relies on the pgbouncer instance being named based on the db name, as is done in create command
|
45
49
|
def primary_instance
|
46
|
-
database_url =
|
50
|
+
database_url = Helpers.get_secret(app: app, key: 'DATABASE_URL')
|
47
51
|
return nil unless database_url
|
48
52
|
|
49
53
|
primary_uri = URI.parse(database_url)
|
@@ -66,6 +70,7 @@ module Seira
|
|
66
70
|
puts "ps: List running queries"
|
67
71
|
puts "kill: Kill a query"
|
68
72
|
puts "analyze: Display database performance information"
|
73
|
+
puts "create-readonly-user: Create a database user named by --username=<name> with only SELECT access privileges"
|
69
74
|
end
|
70
75
|
|
71
76
|
def run_create
|
@@ -74,7 +79,7 @@ module Seira
|
|
74
79
|
|
75
80
|
def run_delete
|
76
81
|
name = "#{app}-#{args[0]}"
|
77
|
-
if
|
82
|
+
if gcloud("sql instances delete #{name}", context: context, format: :boolean)
|
78
83
|
puts "Successfully deleted sql instance #{name}"
|
79
84
|
|
80
85
|
# TODO: Automate the below
|
@@ -90,7 +95,7 @@ module Seira
|
|
90
95
|
|
91
96
|
def run_restart
|
92
97
|
name = "#{app}-#{args[0]}"
|
93
|
-
if
|
98
|
+
if gcloud("sql instances restart #{name}", context: context, format: :boolean)
|
94
99
|
puts "Successfully restarted sql instance #{name}"
|
95
100
|
else
|
96
101
|
puts "Failed to restart sql instance #{name}"
|
@@ -100,7 +105,7 @@ module Seira
|
|
100
105
|
def run_connect
|
101
106
|
name = args[0] || primary_instance
|
102
107
|
puts "Connecting to #{name}..."
|
103
|
-
root_password =
|
108
|
+
root_password = Helpers.get_secret(app: app, key: "#{name.tr('-', '_').upcase}_ROOT_PASSWORD") || "Not found in secrets"
|
104
109
|
puts "Your root password for 'postgres' user is: #{root_password}"
|
105
110
|
system("gcloud sql connect #{name}")
|
106
111
|
end
|
@@ -176,20 +181,88 @@ module Seira
|
|
176
181
|
)
|
177
182
|
end
|
178
183
|
|
179
|
-
|
184
|
+
# Example: seira staging app-name db create-readonly-user --username=readonlyuser
|
185
|
+
def run_create_readonly_user
|
186
|
+
instance_name = primary_instance # Always make user changes to primary instance, and they will propogate to replicas
|
187
|
+
user_name = nil
|
188
|
+
|
189
|
+
args.each do |arg|
|
190
|
+
if arg.start_with? '--username='
|
191
|
+
user_name = arg.split('=')[1]
|
192
|
+
else
|
193
|
+
puts "Warning: Unrecognized argument '#{arg}'"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
if user_name.nil? || user_name.strip.chomp == ''
|
198
|
+
puts "Please specify the name of the read-only user to create, such as --username=testuser"
|
199
|
+
exit(1)
|
200
|
+
end
|
201
|
+
|
202
|
+
# Require that the name be alpha only for simplicity and strict but basic validation
|
203
|
+
if user_name.match(/\A[a-zA-Z]*\z/).nil?
|
204
|
+
puts "Username must be characters only"
|
205
|
+
exit(1)
|
206
|
+
end
|
207
|
+
|
208
|
+
valid_instance_names = existing_instances(remove_app_prefix: false).join(', ')
|
209
|
+
if instance_name.nil? || instance_name.strip.chomp == '' || !valid_instance_names.include?(instance_name)
|
210
|
+
puts "Could not find a valid instance name - does the DATABASE_URL have a value? Must be one of: #{valid_instance_names}"
|
211
|
+
exit(1)
|
212
|
+
end
|
213
|
+
|
214
|
+
password = SecureRandom.urlsafe_base64(32)
|
215
|
+
if gcloud("sql users create #{user_name} '' --instance=#{instance_name} --password=#{password}", context: context, format: :boolean)
|
216
|
+
puts "Created user '#{user_name}' with password #{password}"
|
217
|
+
else
|
218
|
+
puts "Failed to create user '#{user_name}'"
|
219
|
+
exit(1)
|
220
|
+
end
|
221
|
+
|
222
|
+
puts 'Setting permissions...'
|
223
|
+
admin_commands =
|
224
|
+
<<~SQL
|
225
|
+
REVOKE cloudsqlsuperuser FROM #{user_name};
|
226
|
+
ALTER ROLE #{user_name} NOCREATEDB NOCREATEROLE;
|
227
|
+
SQL
|
228
|
+
database_commands =
|
229
|
+
<<~SQL
|
230
|
+
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM #{user_name};
|
231
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO #{user_name};
|
232
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA "public" GRANT SELECT ON TABLES TO #{user_name};
|
233
|
+
SQL
|
234
|
+
execute_db_command(admin_commands, as_admin: true)
|
235
|
+
execute_db_command(database_commands)
|
236
|
+
end
|
237
|
+
|
238
|
+
def execute_db_command(sql_command, as_admin: false)
|
180
239
|
# TODO(josh): move pgbouncer naming logic here and in Create to a common location
|
181
|
-
|
240
|
+
instance_name = primary_instance
|
241
|
+
tier = instance_name.gsub("#{app}-", '')
|
182
242
|
matching_pods = Helpers.fetch_pods(app: app, filters: { tier: tier })
|
183
243
|
if matching_pods.empty?
|
184
244
|
puts 'Could not find pgbouncer pod to connect to'
|
185
245
|
exit 1
|
186
246
|
end
|
187
247
|
pod_name = matching_pods.first['metadata']['name']
|
188
|
-
|
248
|
+
psql_command =
|
249
|
+
if as_admin
|
250
|
+
root_password = Helpers.get_secret(app: app, key: "#{instance_name.tr('-', '_').upcase}_ROOT_PASSWORD")
|
251
|
+
"psql postgres://postgres:#{root_password}@127.0.0.1:5432"
|
252
|
+
else
|
253
|
+
'psql'
|
254
|
+
end
|
255
|
+
exit 1 unless system("kubectl exec #{pod_name} --namespace #{app} -- #{psql_command} -c \"#{sql_command}\"")
|
189
256
|
end
|
190
257
|
|
191
|
-
def existing_instances
|
192
|
-
`gcloud sql instances list --uri`.split("\n").map { |uri| uri.split('/').last }.select { |name| name.start_with? "#{app}-" }
|
258
|
+
def existing_instances(remove_app_prefix: true)
|
259
|
+
plain_list = `gcloud sql instances list --uri`.split("\n").map { |uri| uri.split('/').last }.select { |name| name.start_with? "#{app}-" }
|
260
|
+
|
261
|
+
if remove_app_prefix
|
262
|
+
plain_list.map { |name| name.gsub(/^#{app}-/, '') }
|
263
|
+
else
|
264
|
+
plain_list
|
265
|
+
end
|
193
266
|
end
|
194
267
|
end
|
195
268
|
end
|