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
         
     |