seira 0.4.0 → 0.4.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
  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