seira 0.4.0 → 0.4.1

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
  SHA1:
3
- metadata.gz: 6bcdddd7385af9bd0c795472c6f1fe911d9586b1
4
- data.tar.gz: ad65f785990edea62925605e04bba6b4d116cd93
3
+ metadata.gz: 95d1b86a64920650d43cdc3d5063fadc2f17be89
4
+ data.tar.gz: d40cbb781664aeef22dfcadd4bc048ad27a69414
5
5
  SHA512:
6
- metadata.gz: a7c87cba6fa27f9debf2f11b0db52ecab602465e92e77c1a9774f875152d79c1b11b996d05dfcfbc671731a5557b2fb975031dad69c08a5709892e1b79593eb0
7
- data.tar.gz: 5e8d13d180754d03d0abeb381047efae3ae65eb795bb6320f36fe3e5ce81915ac2ca530108571ae90451f1a1c0aa7dd6b760ea1b1e4ac35f9ac779acada61ae5
6
+ metadata.gz: 60c8eaaa70a5d8dad533e3bd1533f74bb7b59edbc7ce360ab68d43a4271d9eaf5c141e03ec2e7fbdb70ea917fcba5b8b32d5c45ab08108487fc84ce1ab2d34ef
7
+ data.tar.gz: aa114b74294c756197411515f4f94cd160187a00f9ef47c22f7d0fae181ff8089f2533266efe9aed62f989b5ff553c984300fa1e0af4d40ea3d0c2b68c577c94
data/lib/helpers.rb CHANGED
@@ -29,6 +29,12 @@ module Seira
29
29
  def get_secret(key:, context:)
30
30
  Secrets.new(app: context[:app], action: 'get', args: [], context: context).get(key)
31
31
  end
32
+
33
+ def shell_username
34
+ `whoami`
35
+ rescue
36
+ 'unknown'
37
+ end
32
38
  end
33
39
  end
34
40
  end
data/lib/seira/app.rb CHANGED
@@ -197,24 +197,20 @@ module Seira
197
197
  puts "Copying source yaml from #{source} to temp folder"
198
198
  FileUtils.mkdir_p destination # Create the nested directory
199
199
  FileUtils.rm_rf("#{destination}/.", secure: true) # Clean out old files from the tmp folder
200
- FileUtils.copy_entry source, destination
201
- # Anything in jobs directory is not intended to be applied when deploying
202
- # the app, but rather ran when needed as Job objects. Force to avoid exception if DNE.
203
- FileUtils.rm_rf("#{destination}/jobs/") if File.directory?("#{destination}/jobs/")
204
200
 
205
201
  # Iterate through each yaml file and find/replace and save
206
202
  puts "Iterating temp folder files find/replace revision information"
207
- Dir.foreach(destination) do |item|
203
+ Dir.foreach(source) do |item|
208
204
  next if item == '.' || item == '..'
209
205
 
210
206
  # If we have run into a directory item, skip it
211
- next if File.directory?("#{destination}/#{item}")
207
+ next if File.directory?("#{source}/#{item}")
212
208
 
213
209
  # Skip any manifest file that has "seira-skip.yaml" at the end. Common use case is for Job definitions
214
210
  # to be used in "seira staging <app> jobs run"
215
211
  next if item.end_with?("seira-skip.yaml")
216
212
 
217
- text = File.read("#{destination}/#{item}")
213
+ text = File.read("#{source}/#{item}")
218
214
 
219
215
  new_contents = text
220
216
  replacement_hash.each do |key, value|
data/lib/seira/db.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'securerandom'
2
+ require 'English'
2
3
 
3
4
  require_relative 'db/create'
4
5
 
@@ -6,7 +7,7 @@ module Seira
6
7
  class Db
7
8
  include Seira::Commands
8
9
 
9
- VALID_ACTIONS = %w[help create delete list restart connect ps kill analyze create-readonly-user].freeze
10
+ VALID_ACTIONS = %w[help create delete list restart connect ps kill analyze create-readonly-user psql table-sizes index-sizes vacuum unused-indexes unused-indices user-connections info].freeze
10
11
  SUMMARY = "Manage your Cloud SQL Postgres databases.".freeze
11
12
 
12
13
  attr_reader :app, :action, :args, :context
@@ -40,6 +41,20 @@ module Seira
40
41
  run_analyze
41
42
  when 'create-readonly-user'
42
43
  run_create_readonly_user
44
+ when 'psql'
45
+ run_psql
46
+ when 'table-sizes'
47
+ run_table_sizes
48
+ when 'index-sizes'
49
+ run_index_sizes
50
+ when 'vacuum'
51
+ run_vacuum
52
+ when 'unused-indexes', 'unused-indices'
53
+ run_unused_indexes
54
+ when 'user-connections'
55
+ run_user_connections
56
+ when 'info'
57
+ run_info
43
58
  else
44
59
  fail "Unknown command encountered"
45
60
  end
@@ -62,15 +77,24 @@ module Seira
62
77
  def run_help
63
78
  puts SUMMARY
64
79
  puts "\n"
65
- puts "create: Create a new postgres instance in cloud sql. Supports creating replicas and other numerous flags."
66
- puts "delete: Delete a postgres instance from cloud sql. Use with caution, and remove all kubernetes configs first."
67
- puts "list: List all postgres instances."
68
- puts "restart: Fully restart the database."
69
- puts "connect: Open a psql command prompt. You will be shown the password needed before the prompt opens."
70
- puts "ps: List running queries"
71
- puts "kill: Kill a query"
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"
80
+ puts <<~HELPTEXT
81
+ analyze: Display database performance information
82
+ connect: Open a psql command prompt via gcloud connect. You will be shown the password needed before the prompt opens.
83
+ create: Create a new postgres instance in cloud sql. Supports creating replicas and other numerous flags.
84
+ create-readonly-user: Create a database user named by --username=<name> with only SELECT access privileges
85
+ delete: Delete a postgres instance from cloud sql. Use with caution, and remove all kubernetes configs first.
86
+ index-sizes: List sizes of all indexes in the database
87
+ info: Summarize all database instances for the app
88
+ kill: Kill a query
89
+ list: List all postgres instances.
90
+ ps: List running queries
91
+ psql: Open a psql prompt via kubectl exec into a pgbouncer pod.
92
+ restart: Fully restart the database.
93
+ table-sizes: List sizes of all tables in the database
94
+ unused-indexes: Show indexes with zero or low usage
95
+ user-connections: List number of connections per user
96
+ vacuum: Run a VACUUM ANALYZE
97
+ HELPTEXT
74
98
  end
75
99
 
76
100
  def run_create
@@ -120,21 +144,22 @@ module Seira
120
144
  end
121
145
  end
122
146
 
123
- execute_db_command(<<~SQL
124
- SELECT
125
- pid,
126
- state,
127
- application_name AS source,
128
- age(now(),query_start) AS running_for,
129
- query_start,
130
- wait_event IS NOT NULL AS waiting,
131
- query
132
- FROM pg_stat_activity
133
- WHERE
134
- query <> '<insufficient privilege>'
135
- #{verbose ? '' : "AND state <> 'idle'"}
136
- AND pid <> pg_backend_pid()
137
- ORDER BY query_start DESC
147
+ execute_db_command(
148
+ <<~SQL
149
+ SELECT
150
+ pid,
151
+ state,
152
+ application_name AS source,
153
+ age(now(),query_start) AS running_for,
154
+ query_start,
155
+ wait_event IS NOT NULL AS waiting,
156
+ query
157
+ FROM pg_stat_activity
158
+ WHERE
159
+ query <> '<insufficient privilege>'
160
+ #{verbose ? '' : "AND state <> 'idle'"}
161
+ AND pid <> pg_backend_pid()
162
+ ORDER BY query_start DESC
138
163
  SQL
139
164
  )
140
165
  end
@@ -235,7 +260,148 @@ module Seira
235
260
  execute_db_command(database_commands)
236
261
  end
237
262
 
238
- def execute_db_command(sql_command, as_admin: false)
263
+ def run_psql
264
+ execute_db_command(nil, interactive: true)
265
+ end
266
+
267
+ def run_table_sizes
268
+ # https://wiki.postgresql.org/wiki/Disk_Usage
269
+ execute_db_command(
270
+ <<~SQL
271
+ SELECT table_name
272
+ , row_estimate
273
+ , pg_size_pretty(table_bytes) AS table
274
+ , pg_size_pretty(index_bytes) AS index
275
+ , pg_size_pretty(toast_bytes) AS toast
276
+ , pg_size_pretty(total_bytes) AS total
277
+ FROM (
278
+ SELECT *, total_bytes-index_bytes-COALESCE(toast_bytes,0) AS table_bytes FROM (
279
+ SELECT relname AS table_name
280
+ , c.reltuples AS row_estimate
281
+ , pg_total_relation_size(c.oid) AS total_bytes
282
+ , pg_indexes_size(c.oid) AS index_bytes
283
+ , pg_total_relation_size(reltoastrelid) AS toast_bytes
284
+ FROM pg_class c
285
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
286
+ WHERE relkind = 'r'
287
+ AND n.nspname = 'public'
288
+ ) a
289
+ ) a
290
+ ORDER BY total_bytes DESC;
291
+ SQL
292
+ )
293
+ end
294
+
295
+ def run_index_sizes
296
+ # https://wiki.postgresql.org/wiki/Disk_Usage
297
+ execute_db_command(
298
+ <<~SQL
299
+ SELECT relname AS index
300
+ , c.reltuples AS row_estimate
301
+ , pg_size_pretty(pg_relation_size(c.oid)) AS "size"
302
+ FROM pg_class c
303
+ LEFT JOIN pg_namespace n ON (n.oid = c.relnamespace)
304
+ WHERE relkind = 'i'
305
+ AND n.nspname = 'public'
306
+ ORDER BY pg_relation_size(c.oid) DESC;
307
+ SQL
308
+ )
309
+ end
310
+
311
+ def run_vacuum
312
+ execute_db_command(
313
+ <<~SQL
314
+ VACUUM VERBOSE ANALYZE;
315
+ SQL
316
+ )
317
+ end
318
+
319
+ def run_unused_indexes
320
+ # https://github.com/heroku/heroku-pg-extras/blob/master/commands/unused_indexes.js
321
+ execute_db_command(
322
+ <<~SQL
323
+ SELECT
324
+ schemaname || '.' || relname AS table,
325
+ indexrelname AS index,
326
+ pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
327
+ idx_scan as index_scans
328
+ FROM pg_stat_user_indexes ui
329
+ JOIN pg_index i ON ui.indexrelid = i.indexrelid
330
+ WHERE NOT indisunique AND idx_scan < 50 AND pg_relation_size(relid) > 5 * 8192
331
+ ORDER BY pg_relation_size(i.indexrelid) / nullif(idx_scan, 0) DESC NULLS FIRST,
332
+ pg_relation_size(i.indexrelid) DESC;
333
+ SQL
334
+ )
335
+ end
336
+
337
+ def run_user_connections
338
+ execute_db_command(
339
+ <<~SQL
340
+ SELECT usename AS user, count(pid) FROM pg_stat_activity GROUP BY usename;
341
+ SQL
342
+ )
343
+ end
344
+
345
+ def run_info
346
+ instances = JSON.parse(gcloud("sql instances list --filter='name~\\A#{app}-'", context: context, format: :json))
347
+ instances.each do |instance|
348
+ db_info_command =
349
+ <<~SQL
350
+ COPY (SELECT pg_size_pretty(sum(pg_database_size(datname))) FROM pg_database) TO stdout;
351
+ COPY (SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE') TO stdout;
352
+ COPY (SELECT count(*) FROM pg_stat_activity) TO stdout;
353
+ SQL
354
+ db_info = execute_db_command(db_info_command, print: false)
355
+ data_size, table_count, connection_count = db_info.split("\n")
356
+ instance['data_size'] = data_size
357
+ instance['table_count'] = table_count
358
+ instance['connection_count'] = connection_count
359
+ end
360
+ instances.each do |instance|
361
+ # https://cloud.google.com/sql/faq
362
+ disk_size = instance['settings']['dataDiskSizeGb'].to_f
363
+ connection_limit =
364
+ if disk_size <= 0.6
365
+ 25
366
+ elsif disk_size <= 3.75
367
+ 50
368
+ elsif disk_size <= 6
369
+ 100
370
+ elsif disk_size <= 7.5
371
+ 150
372
+ elsif disk_size <= 15
373
+ 200
374
+ elsif disk_size <= 30
375
+ 250
376
+ elsif disk_size <= 60
377
+ 300
378
+ elsif disk_size <= 120
379
+ 400
380
+ else
381
+ 500
382
+ end
383
+
384
+ backup_info = instance['settings']['backupConfiguration']['enabled'] == 'true' ? instance['settings']['backupConfiguration']['startTime'] : 'false'
385
+
386
+ puts "\n"
387
+ puts instance['name'].bold
388
+ puts <<~INFOTEXT
389
+ State: #{instance['state']}
390
+ Tables: #{instance['table_count']}
391
+ Disk Size: #{disk_size} GB
392
+ Data Size: #{instance['data_size']}
393
+ Auto Resize: #{instance['settings']['storageAutoResize']}
394
+ Disk Type: #{instance['settings']['dataDiskType']}
395
+ Tier: #{instance['settings']['tier']}
396
+ Availability: #{instance['settings']['availabilityType']}
397
+ Version: #{instance['databaseVersion']}
398
+ Backups: #{backup_info}
399
+ Connections: #{instance['connection_count']}/#{connection_limit}(?)
400
+ INFOTEXT
401
+ end
402
+ end
403
+
404
+ def execute_db_command(sql_command, as_admin: false, interactive: false, print: true)
239
405
  # TODO(josh): move pgbouncer naming logic here and in Create to a common location
240
406
  instance_name = primary_instance
241
407
  tier = instance_name.gsub("#{app}-", '')
@@ -252,7 +418,19 @@ module Seira
252
418
  else
253
419
  'psql'
254
420
  end
255
- exit 1 unless system("kubectl exec #{pod_name} --namespace #{app} -- #{psql_command} -c \"#{sql_command}\"")
421
+ system_command = "kubectl exec #{pod_name} --namespace #{app}"
422
+ system_command += ' -ti' if interactive
423
+ system_command += " -- #{psql_command}"
424
+ system_command += " -c \"#{sql_command}\"" unless sql_command.nil?
425
+ if interactive
426
+ exit(1) unless system(system_command)
427
+ else
428
+ output = `#{system_command}`
429
+ success = $CHILD_STATUS.success?
430
+ puts output if print || !success
431
+ exit(1) unless success
432
+ output
433
+ end
256
434
  end
257
435
 
258
436
  def existing_instances(remove_app_prefix: true)
data/lib/seira/jobs.rb CHANGED
@@ -84,7 +84,7 @@ module Seira
84
84
  replacement_hash = {
85
85
  'UNIQUE_NAME' => unique_name,
86
86
  'REVISION' => revision,
87
- 'COMMAND' => command.split(' ').map { |part| "\"#{part}\"" }.join(", "),
87
+ 'COMMAND' => %("sh", "-c", "#{command}"),
88
88
  'CPU_REQUEST' => '200m',
89
89
  'CPU_LIMIT' => '500m',
90
90
  'MEMORY_REQUEST' => '500Mi',
data/lib/seira/pods.rb CHANGED
@@ -105,7 +105,10 @@ module Seira
105
105
  kind: 'Pod',
106
106
  spec: spec,
107
107
  metadata: {
108
- name: temp_name
108
+ name: temp_name,
109
+ annotations: {
110
+ owner: Helpers.shell_username
111
+ }
109
112
  }
110
113
  }
111
114
  # Don't restart the pod when it dies
data/lib/seira/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Seira
2
- VERSION = "0.4.0".freeze
2
+ VERSION = "0.4.1".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: seira
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Ringwelski
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-13 00:00:00.000000000 Z
11
+ date: 2018-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: highline