seira 0.1.0
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 +7 -0
 - data/.default-rubocop.yml +1614 -0
 - data/.gitignore +12 -0
 - data/.hound.yml +3 -0
 - data/.rspec +2 -0
 - data/.rubocop.yml +109 -0
 - data/.ruby-version +1 -0
 - data/.travis.yml +5 -0
 - data/Gemfile +4 -0
 - data/LICENSE +21 -0
 - data/README.md +41 -0
 - data/Rakefile +6 -0
 - data/bin/console +14 -0
 - data/bin/seira +5 -0
 - data/bin/setup +8 -0
 - data/lib/seira.rb +125 -0
 - data/lib/seira/app.rb +130 -0
 - data/lib/seira/cluster.rb +79 -0
 - data/lib/seira/memcached.rb +108 -0
 - data/lib/seira/pods.rb +70 -0
 - data/lib/seira/proxy.rb +13 -0
 - data/lib/seira/random.rb +404 -0
 - data/lib/seira/redis.rb +123 -0
 - data/lib/seira/secrets.rb +148 -0
 - data/lib/seira/settings.rb +59 -0
 - data/lib/seira/setup.rb +99 -0
 - data/lib/seira/version.rb +3 -0
 - data/seira.gemspec +29 -0
 - metadata +142 -0
 
    
        data/lib/seira/redis.rb
    ADDED
    
    | 
         @@ -0,0 +1,123 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'json'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'base64'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module Seira
         
     | 
| 
      
 5 
     | 
    
         
            +
              class Redis
         
     | 
| 
      
 6 
     | 
    
         
            +
                VALID_ACTIONS = %w[list status credentials create delete].freeze
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                attr_reader :app, :action, :args, :context
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                def initialize(app:, action:, args:, context:)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @app = app
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @action = action
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @args = args
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @context = context
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                # TODO: logs, upgrades?, backups, restores, CLI connection
         
     | 
| 
      
 18 
     | 
    
         
            +
                def run
         
     | 
| 
      
 19 
     | 
    
         
            +
                  case action
         
     | 
| 
      
 20 
     | 
    
         
            +
                  when 'list'
         
     | 
| 
      
 21 
     | 
    
         
            +
                    run_list
         
     | 
| 
      
 22 
     | 
    
         
            +
                  when 'status'
         
     | 
| 
      
 23 
     | 
    
         
            +
                    run_status
         
     | 
| 
      
 24 
     | 
    
         
            +
                  when 'credentials'
         
     | 
| 
      
 25 
     | 
    
         
            +
                    run_credentials
         
     | 
| 
      
 26 
     | 
    
         
            +
                  when 'create'
         
     | 
| 
      
 27 
     | 
    
         
            +
                    run_create
         
     | 
| 
      
 28 
     | 
    
         
            +
                  when 'delete'
         
     | 
| 
      
 29 
     | 
    
         
            +
                    run_delete
         
     | 
| 
      
 30 
     | 
    
         
            +
                  else
         
     | 
| 
      
 31 
     | 
    
         
            +
                    fail "Unknown command encountered"
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                private
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                def run_list
         
     | 
| 
      
 38 
     | 
    
         
            +
                  list = `helm list`.split("\n")
         
     | 
| 
      
 39 
     | 
    
         
            +
                  filtered_list = list.select { |item| item.start_with?("#{app}-redis") }
         
     | 
| 
      
 40 
     | 
    
         
            +
                  filtered_list.each do |item|
         
     | 
| 
      
 41 
     | 
    
         
            +
                    puts item
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                def run_status
         
     | 
| 
      
 46 
     | 
    
         
            +
                  puts `helm status #{app}-redis-#{args[0]}`
         
     | 
| 
      
 47 
     | 
    
         
            +
                end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                def run_create
         
     | 
| 
      
 50 
     | 
    
         
            +
                  # Fairly beefy default compute because it's cheap and the longer we can defer upgrading the
         
     | 
| 
      
 51 
     | 
    
         
            +
                  # better. Go even higher for production apps.
         
     | 
| 
      
 52 
     | 
    
         
            +
                  # TODO: Enable metrics
         
     | 
| 
      
 53 
     | 
    
         
            +
                  values = {
         
     | 
| 
      
 54 
     | 
    
         
            +
                    persistence: {
         
     | 
| 
      
 55 
     | 
    
         
            +
                      size: '32Gi'
         
     | 
| 
      
 56 
     | 
    
         
            +
                    },
         
     | 
| 
      
 57 
     | 
    
         
            +
                    resources: {
         
     | 
| 
      
 58 
     | 
    
         
            +
                      requests: {
         
     | 
| 
      
 59 
     | 
    
         
            +
                        cpu: '2', # roughly 2 vCPU in both AWS and GCP terms
         
     | 
| 
      
 60 
     | 
    
         
            +
                        memory: '8Gi' # redis is in-memory - give it a lot
         
     | 
| 
      
 61 
     | 
    
         
            +
                      }
         
     | 
| 
      
 62 
     | 
    
         
            +
                    }
         
     | 
| 
      
 63 
     | 
    
         
            +
                  }
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                  args.each do |arg|
         
     | 
| 
      
 66 
     | 
    
         
            +
                    puts "Applying arg #{arg} to values"
         
     | 
| 
      
 67 
     | 
    
         
            +
                    if arg.start_with?('--memory=')
         
     | 
| 
      
 68 
     | 
    
         
            +
                      values[:resources][:requests][:memory] = arg.split('=')[1]
         
     | 
| 
      
 69 
     | 
    
         
            +
                    elsif arg.start_with?('--volume=')
         
     | 
| 
      
 70 
     | 
    
         
            +
                      values[:persistence][:volume] = arg.split('=')[1]
         
     | 
| 
      
 71 
     | 
    
         
            +
                    elsif arg.start_with?('--cpu=')
         
     | 
| 
      
 72 
     | 
    
         
            +
                      values[:resources][:requests][:cpu] = arg.split('=')[1]
         
     | 
| 
      
 73 
     | 
    
         
            +
                    elsif arg.start_with?('--size=')
         
     | 
| 
      
 74 
     | 
    
         
            +
                      size = arg.split('=')[1]
         
     | 
| 
      
 75 
     | 
    
         
            +
                      case size
         
     | 
| 
      
 76 
     | 
    
         
            +
                      when '1'
         
     | 
| 
      
 77 
     | 
    
         
            +
                        values[:resources][:requests][:memory] = '100Mi' # 100mb
         
     | 
| 
      
 78 
     | 
    
         
            +
                        values[:persistence][:size] = '5Gi'
         
     | 
| 
      
 79 
     | 
    
         
            +
                        values[:resources][:requests][:cpu] = '100m' # .1 cpu
         
     | 
| 
      
 80 
     | 
    
         
            +
                      else
         
     | 
| 
      
 81 
     | 
    
         
            +
                        fail "There is no size option '#{size}'"
         
     | 
| 
      
 82 
     | 
    
         
            +
                      end
         
     | 
| 
      
 83 
     | 
    
         
            +
                    end
         
     | 
| 
      
 84 
     | 
    
         
            +
                  end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                  file_name = write_config(values)
         
     | 
| 
      
 87 
     | 
    
         
            +
                  unique_name = "#{Seira::Random.color}-#{Seira::Random.animal}"
         
     | 
| 
      
 88 
     | 
    
         
            +
                  name = "#{app}-#{unique_name}"
         
     | 
| 
      
 89 
     | 
    
         
            +
                  puts `helm install --namespace #{app} --name #{name} --wait -f #{file_name} stable/redis`
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                  File.delete(file_name)
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                  puts "To get status: 'seira #{context[:cluster]} #{app} redis status #{unique_name}'"
         
     | 
| 
      
 94 
     | 
    
         
            +
                  puts "To get credentials for storing in app secrets: 'seira #{context[:cluster]} #{app} redis credentials #{unique_name}'"
         
     | 
| 
      
 95 
     | 
    
         
            +
                  puts "Service URI for this Redis instance: 'redis://:<password goes here>@#{name}-redis:6379/0'."
         
     | 
| 
      
 96 
     | 
    
         
            +
                end
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                def run_delete
         
     | 
| 
      
 99 
     | 
    
         
            +
                  to_delete = "#{app}-#{args[0]}"
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                  exit(1) unless HighLine.agree("Are you sure you want to delete #{to_delete}? If any apps are using this redis instance, they will break.")
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
                  if system("helm delete #{to_delete}")
         
     | 
| 
      
 104 
     | 
    
         
            +
                    puts "Successfully deleted #{to_delete}. Mistake and seeing errors now? You can rollback easily. Below is last 5 revisions of the now deleted resource."
         
     | 
| 
      
 105 
     | 
    
         
            +
                    history = `helm history --max 5 #{to_delete}`
         
     | 
| 
      
 106 
     | 
    
         
            +
                    puts history
         
     | 
| 
      
 107 
     | 
    
         
            +
                    last_revision = history.split("\n").last.split(" ").map(&:strip)[0]
         
     | 
| 
      
 108 
     | 
    
         
            +
                    puts "helm rollback #{to_delete} #{last_revision}"
         
     | 
| 
      
 109 
     | 
    
         
            +
                    puts "Docs: https://github.com/kubernetes/helm/blob/master/docs/helm/helm_rollback.md"
         
     | 
| 
      
 110 
     | 
    
         
            +
                  else
         
     | 
| 
      
 111 
     | 
    
         
            +
                    puts "Delete failed"
         
     | 
| 
      
 112 
     | 
    
         
            +
                  end
         
     | 
| 
      
 113 
     | 
    
         
            +
                end
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                def write_config(values)
         
     | 
| 
      
 116 
     | 
    
         
            +
                  file_name = "tmp/temp-redis-config-#{Seira::Cluster.current_cluster}-#{app}.json"
         
     | 
| 
      
 117 
     | 
    
         
            +
                  File.open(file_name, "wb") do |f|
         
     | 
| 
      
 118 
     | 
    
         
            +
                    f.write(values.to_json)
         
     | 
| 
      
 119 
     | 
    
         
            +
                  end
         
     | 
| 
      
 120 
     | 
    
         
            +
                  file_name
         
     | 
| 
      
 121 
     | 
    
         
            +
                end
         
     | 
| 
      
 122 
     | 
    
         
            +
              end
         
     | 
| 
      
 123 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,148 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'json'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'base64'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            # Example usages:
         
     | 
| 
      
 5 
     | 
    
         
            +
            # seira staging specs secret set RAILS_ENV staging
         
     | 
| 
      
 6 
     | 
    
         
            +
            # seira demo tracking secret unset DISABLE_SOME_FEATURE
         
     | 
| 
      
 7 
     | 
    
         
            +
            # seira staging importer secret list
         
     | 
| 
      
 8 
     | 
    
         
            +
            # TODO: Multiple secrets in one command
         
     | 
| 
      
 9 
     | 
    
         
            +
            # TODO: Can we avoid writing to disk completely and instead pipe in raw json?
         
     | 
| 
      
 10 
     | 
    
         
            +
            module Seira
         
     | 
| 
      
 11 
     | 
    
         
            +
              class Secrets
         
     | 
| 
      
 12 
     | 
    
         
            +
                VALID_ACTIONS = %w[get set unset list list-decoded create-pgbouncer-secret].freeze
         
     | 
| 
      
 13 
     | 
    
         
            +
                PGBOUNCER_SECRETS_NAME = 'pgbouncer-secrets'.freeze
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                attr_reader :app, :action, :key, :value, :args, :context
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                def initialize(app:, action:, args:, context:)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  @app = app
         
     | 
| 
      
 19 
     | 
    
         
            +
                  @action = action
         
     | 
| 
      
 20 
     | 
    
         
            +
                  @args = args
         
     | 
| 
      
 21 
     | 
    
         
            +
                  @key = args[0]
         
     | 
| 
      
 22 
     | 
    
         
            +
                  @value = args[1]
         
     | 
| 
      
 23 
     | 
    
         
            +
                  @context = context
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                def run
         
     | 
| 
      
 27 
     | 
    
         
            +
                  case action
         
     | 
| 
      
 28 
     | 
    
         
            +
                  when 'get'
         
     | 
| 
      
 29 
     | 
    
         
            +
                    perform_key_validation
         
     | 
| 
      
 30 
     | 
    
         
            +
                    run_get
         
     | 
| 
      
 31 
     | 
    
         
            +
                  when 'set'
         
     | 
| 
      
 32 
     | 
    
         
            +
                    perform_key_validation
         
     | 
| 
      
 33 
     | 
    
         
            +
                    run_set
         
     | 
| 
      
 34 
     | 
    
         
            +
                  when 'unset'
         
     | 
| 
      
 35 
     | 
    
         
            +
                    perform_key_validation
         
     | 
| 
      
 36 
     | 
    
         
            +
                    run_unset
         
     | 
| 
      
 37 
     | 
    
         
            +
                  when 'list'
         
     | 
| 
      
 38 
     | 
    
         
            +
                    run_list
         
     | 
| 
      
 39 
     | 
    
         
            +
                  when 'list-decoded'
         
     | 
| 
      
 40 
     | 
    
         
            +
                    run_list_decoded
         
     | 
| 
      
 41 
     | 
    
         
            +
                  when 'create-pgbouncer-secret'
         
     | 
| 
      
 42 
     | 
    
         
            +
                    run_create_pgbouncer_secret
         
     | 
| 
      
 43 
     | 
    
         
            +
                  when 'bootstrap-cluster'
         
     | 
| 
      
 44 
     | 
    
         
            +
                    run_bootstrap_cluster
         
     | 
| 
      
 45 
     | 
    
         
            +
                  else
         
     | 
| 
      
 46 
     | 
    
         
            +
                    fail "Unknown command encountered"
         
     | 
| 
      
 47 
     | 
    
         
            +
                  end
         
     | 
| 
      
 48 
     | 
    
         
            +
                end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                def copy_secret_across_namespace(key:, to:, from:)
         
     | 
| 
      
 51 
     | 
    
         
            +
                  puts "Copying the #{key} secret from namespace #{from} to #{to}."
         
     | 
| 
      
 52 
     | 
    
         
            +
                  json_string = `kubectl get secret #{key} --namespace #{from} -o json`
         
     | 
| 
      
 53 
     | 
    
         
            +
                  secrets = JSON.parse(json_string)
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                  # At this point we would preferably simply do a write_secrets call, but the metadata is highly coupled to old
         
     | 
| 
      
 56 
     | 
    
         
            +
                  # namespace so we need to clear out the old metadata
         
     | 
| 
      
 57 
     | 
    
         
            +
                  new_secrets = Marshal.load(Marshal.dump(secrets))
         
     | 
| 
      
 58 
     | 
    
         
            +
                  new_secrets.delete('metadata')
         
     | 
| 
      
 59 
     | 
    
         
            +
                  new_secrets['metadata'] = {
         
     | 
| 
      
 60 
     | 
    
         
            +
                    'name' => key,
         
     | 
| 
      
 61 
     | 
    
         
            +
                    'namespace' => to
         
     | 
| 
      
 62 
     | 
    
         
            +
                  }
         
     | 
| 
      
 63 
     | 
    
         
            +
                  write_secrets(secrets: new_secrets, secret_name: key)
         
     | 
| 
      
 64 
     | 
    
         
            +
                end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                def main_secret_name
         
     | 
| 
      
 67 
     | 
    
         
            +
                  "#{app}-secrets"
         
     | 
| 
      
 68 
     | 
    
         
            +
                end
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                private
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                def perform_key_validation
         
     | 
| 
      
 73 
     | 
    
         
            +
                  if key.nil? || key.strip == ""
         
     | 
| 
      
 74 
     | 
    
         
            +
                    puts "Please specify a key in all caps and with underscores"
         
     | 
| 
      
 75 
     | 
    
         
            +
                    exit(1)
         
     | 
| 
      
 76 
     | 
    
         
            +
                  end
         
     | 
| 
      
 77 
     | 
    
         
            +
                end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                def run_get
         
     | 
| 
      
 80 
     | 
    
         
            +
                  secrets = fetch_current_secrets
         
     | 
| 
      
 81 
     | 
    
         
            +
                  puts "#{key}: #{Base64.decode64(secrets['data'][key])}"
         
     | 
| 
      
 82 
     | 
    
         
            +
                end
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
                def run_set
         
     | 
| 
      
 85 
     | 
    
         
            +
                  fail "Please specify a value as the third argument" if value.nil? || value.strip == ""
         
     | 
| 
      
 86 
     | 
    
         
            +
                  secrets = fetch_current_secrets
         
     | 
| 
      
 87 
     | 
    
         
            +
                  secrets['data'][key] = Base64.encode64(value)
         
     | 
| 
      
 88 
     | 
    
         
            +
                  write_secrets(secrets: secrets)
         
     | 
| 
      
 89 
     | 
    
         
            +
                end
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
                def run_unset
         
     | 
| 
      
 92 
     | 
    
         
            +
                  secrets = fetch_current_secrets
         
     | 
| 
      
 93 
     | 
    
         
            +
                  secrets['data'].delete(key)
         
     | 
| 
      
 94 
     | 
    
         
            +
                  write_secrets(secrets: secrets)
         
     | 
| 
      
 95 
     | 
    
         
            +
                end
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                def run_list
         
     | 
| 
      
 98 
     | 
    
         
            +
                  secrets = fetch_current_secrets
         
     | 
| 
      
 99 
     | 
    
         
            +
                  puts "Base64 encoded keys for #{app}:"
         
     | 
| 
      
 100 
     | 
    
         
            +
                  secrets['data'].each do |k, v|
         
     | 
| 
      
 101 
     | 
    
         
            +
                    puts "#{k}: #{v}"
         
     | 
| 
      
 102 
     | 
    
         
            +
                  end
         
     | 
| 
      
 103 
     | 
    
         
            +
                end
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
                def run_list_decoded
         
     | 
| 
      
 106 
     | 
    
         
            +
                  secrets = fetch_current_secrets
         
     | 
| 
      
 107 
     | 
    
         
            +
                  puts "Decoded (raw) keys for #{app}:"
         
     | 
| 
      
 108 
     | 
    
         
            +
                  secrets['data'].each do |k, v|
         
     | 
| 
      
 109 
     | 
    
         
            +
                    puts "#{k}: #{Base64.decode64(v)}"
         
     | 
| 
      
 110 
     | 
    
         
            +
                  end
         
     | 
| 
      
 111 
     | 
    
         
            +
                end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                def run_create_pgbouncer_secret
         
     | 
| 
      
 114 
     | 
    
         
            +
                  db_user = args[0]
         
     | 
| 
      
 115 
     | 
    
         
            +
                  db_password = args[1]
         
     | 
| 
      
 116 
     | 
    
         
            +
                  puts `kubectl create secret generic #{PGBOUNCER_SECRETS_NAME} --namespace #{app} --from-literal=DB_USER=#{db_user} --from-literal=DB_PASSWORD=#{db_password}`
         
     | 
| 
      
 117 
     | 
    
         
            +
                end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                # In the normal case the secret we are updating is just main_secret_name,
         
     | 
| 
      
 120 
     | 
    
         
            +
                # but in special cases we may be doing an operation on a different secret
         
     | 
| 
      
 121 
     | 
    
         
            +
                def write_secrets(secrets:, secret_name: main_secret_name)
         
     | 
| 
      
 122 
     | 
    
         
            +
                  file_name = "tmp/temp-secrets-#{Seira::Cluster.current_cluster}-#{secret_name}.json"
         
     | 
| 
      
 123 
     | 
    
         
            +
                  File.open(file_name, "wb") do |f|
         
     | 
| 
      
 124 
     | 
    
         
            +
                    f.write(secrets.to_json)
         
     | 
| 
      
 125 
     | 
    
         
            +
                  end
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                  # The command we use depends on if it already exists or not
         
     | 
| 
      
 128 
     | 
    
         
            +
                  secret_exists = system("kubectl get secret #{secret_name} --namespace #{app} > /dev/null")
         
     | 
| 
      
 129 
     | 
    
         
            +
                  command = secret_exists ? "replace" : "create"
         
     | 
| 
      
 130 
     | 
    
         
            +
             
     | 
| 
      
 131 
     | 
    
         
            +
                  if system("kubectl #{command} --namespace #{app} -f #{file_name}")
         
     | 
| 
      
 132 
     | 
    
         
            +
                    puts "Successfully created/replaced #{secret_name} secret #{key} in cluster #{Seira::Cluster.current_cluster}"
         
     | 
| 
      
 133 
     | 
    
         
            +
                  else
         
     | 
| 
      
 134 
     | 
    
         
            +
                    puts "Failed to update secret"
         
     | 
| 
      
 135 
     | 
    
         
            +
                  end
         
     | 
| 
      
 136 
     | 
    
         
            +
             
     | 
| 
      
 137 
     | 
    
         
            +
                  File.delete(file_name)
         
     | 
| 
      
 138 
     | 
    
         
            +
                end
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
                # Returns the still-base64encoded secrets hashmap
         
     | 
| 
      
 141 
     | 
    
         
            +
                def fetch_current_secrets
         
     | 
| 
      
 142 
     | 
    
         
            +
                  json_string = `kubectl get secret #{main_secret_name} --namespace #{app} -o json`
         
     | 
| 
      
 143 
     | 
    
         
            +
                  json = JSON.parse(json_string)
         
     | 
| 
      
 144 
     | 
    
         
            +
                  fail "Unexpected Kind" unless json['kind'] == 'Secret'
         
     | 
| 
      
 145 
     | 
    
         
            +
                  json
         
     | 
| 
      
 146 
     | 
    
         
            +
                end
         
     | 
| 
      
 147 
     | 
    
         
            +
              end
         
     | 
| 
      
 148 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,59 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'json'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'yaml'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module Seira
         
     | 
| 
      
 5 
     | 
    
         
            +
              class Settings
         
     | 
| 
      
 6 
     | 
    
         
            +
                DEFAULT_CONFIG_PATH = '.seira.yml'.freeze
         
     | 
| 
      
 7 
     | 
    
         
            +
                
         
     | 
| 
      
 8 
     | 
    
         
            +
                attr_reader :config_path
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                def initialize(config_path: DEFAULT_CONFIG_PATH)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @config_path = config_path
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                def settings
         
     | 
| 
      
 15 
     | 
    
         
            +
                  return @_settings if defined?(@_settings)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  @_settings = parse_settings
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                def organization_id
         
     | 
| 
      
 20 
     | 
    
         
            +
                  settings['seira']['organization_id']
         
     | 
| 
      
 21 
     | 
    
         
            +
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                def default_zone
         
     | 
| 
      
 24 
     | 
    
         
            +
                  settings['seira']['default_zone']
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                def valid_apps
         
     | 
| 
      
 28 
     | 
    
         
            +
                  settings['seira']['valid_apps']
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                def valid_cluster_names
         
     | 
| 
      
 32 
     | 
    
         
            +
                  settings['seira']['clusters'].keys
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                def clusters
         
     | 
| 
      
 36 
     | 
    
         
            +
                  settings['seira']['clusters']
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                def full_cluster_name_for_shorthand(shorthand)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  return shorthand if valid_cluster_names.include?(shorthand)
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                  # Try iterating through each cluster to find the relevant alias
         
     | 
| 
      
 43 
     | 
    
         
            +
                  clusters.each do |cluster_name, cluster_metadata|
         
     | 
| 
      
 44 
     | 
    
         
            +
                    next if cluster_metadata['aliases'].empty?
         
     | 
| 
      
 45 
     | 
    
         
            +
                    return cluster_name if cluster_metadata['aliases'].include?(shorthand)
         
     | 
| 
      
 46 
     | 
    
         
            +
                  end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  nil
         
     | 
| 
      
 49 
     | 
    
         
            +
                end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                private
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                def parse_settings
         
     | 
| 
      
 54 
     | 
    
         
            +
                  raw_settings = YAML.load_file(config_path)
         
     | 
| 
      
 55 
     | 
    
         
            +
                  puts raw_settings.inspect
         
     | 
| 
      
 56 
     | 
    
         
            +
                  raw_settings
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
              end
         
     | 
| 
      
 59 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/seira/setup.rb
    ADDED
    
    | 
         @@ -0,0 +1,99 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'net/http'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'json'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module Seira
         
     | 
| 
      
 5 
     | 
    
         
            +
              class Setup
         
     | 
| 
      
 6 
     | 
    
         
            +
                attr_reader :arg, :settings
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                def initialize(arg:, settings:)
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @arg = arg
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @settings = settings
         
     | 
| 
      
 11 
     | 
    
         
            +
                end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                # This script should be all that's needed to fully set up gcloud and kubectl cli, fully configured,
         
     | 
| 
      
 14 
     | 
    
         
            +
                # on a development machine.
         
     | 
| 
      
 15 
     | 
    
         
            +
                def run
         
     | 
| 
      
 16 
     | 
    
         
            +
                  ensure_software_installed
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  if arg == 'all'
         
     | 
| 
      
 19 
     | 
    
         
            +
                    puts "We will now set up gcloud and kubectl for each project. We use a distinct GCP Project for each environment: #{ENVIRONMENTS.join(', ')}"
         
     | 
| 
      
 20 
     | 
    
         
            +
                    settings.valid_cluster_names.each do |cluster|
         
     | 
| 
      
 21 
     | 
    
         
            +
                      setup_cluster(cluster)
         
     | 
| 
      
 22 
     | 
    
         
            +
                    end
         
     | 
| 
      
 23 
     | 
    
         
            +
                  elsif settings.valid_cluster_names.include?(arg)
         
     | 
| 
      
 24 
     | 
    
         
            +
                    puts "We will now set up gcloud and kubectl for #{arg}"
         
     | 
| 
      
 25 
     | 
    
         
            +
                    setup_cluster(arg)
         
     | 
| 
      
 26 
     | 
    
         
            +
                  else
         
     | 
| 
      
 27 
     | 
    
         
            +
                    puts "Please specify a valid cluster name or 'all'."
         
     | 
| 
      
 28 
     | 
    
         
            +
                    exit(1)
         
     | 
| 
      
 29 
     | 
    
         
            +
                  end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                  puts "You have now configured all of your configurations. Please note that 'gcloud' and 'kubectl' are two separate command line tools."
         
     | 
| 
      
 32 
     | 
    
         
            +
                  puts "gcloud: For manipulating GCP entities such as sql databases and kubernetes clusters themselves"
         
     | 
| 
      
 33 
     | 
    
         
            +
                  puts "kubectl: For working within a kubernetes cluster, such as listing pods and deployment statuses"
         
     | 
| 
      
 34 
     | 
    
         
            +
                  puts "Always remember to update both by using 'seira <cluster>', such as 'seira staging'."
         
     | 
| 
      
 35 
     | 
    
         
            +
                  puts "Except for special circumstances, you should be able to always use 'seira' tool and avoid `gcloud` and `kubectl` directly."
         
     | 
| 
      
 36 
     | 
    
         
            +
                  puts "All set!"
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                private
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                def setup_cluster(cluster_name)
         
     | 
| 
      
 42 
     | 
    
         
            +
                  cluster_metadata = settings.clusters[cluster_name]
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  if system("gcloud config configurations describe #{cluster_name}")
         
     | 
| 
      
 45 
     | 
    
         
            +
                    puts "Configuration already exists for #{cluster_name}..."
         
     | 
| 
      
 46 
     | 
    
         
            +
                  else
         
     | 
| 
      
 47 
     | 
    
         
            +
                    puts "Creating configuration for this cluster and activating it..."
         
     | 
| 
      
 48 
     | 
    
         
            +
                    system("gcloud config configurations create #{cluster_name}")
         
     | 
| 
      
 49 
     | 
    
         
            +
                  end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                  system("gcloud config configurations activate #{cluster_name}")
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                  # TODO: Is this possible to automate?
         
     | 
| 
      
 54 
     | 
    
         
            +
                  # system("gcloud iam service-accounts create #{iam_user} --display-name=#{iam_user}")
         
     | 
| 
      
 55 
     | 
    
         
            +
                  # puts "Created service account:"
         
     | 
| 
      
 56 
     | 
    
         
            +
                  # system("gcloud iam service-accounts describe #{iam_user}@#{cluster_metadata['project']}.iam.gserviceaccount.com")
         
     | 
| 
      
 57 
     | 
    
         
            +
                  puts "First,"
         
     | 
| 
      
 58 
     | 
    
         
            +
                  puts "First, set up a service account in the #{cluster_metadata['project']} project and download the credentials for it. You may do so by accessing the below link. Save the file in a safe location."
         
     | 
| 
      
 59 
     | 
    
         
            +
                  puts "https://console.cloud.google.com/iam-admin/serviceaccounts/project?project=#{cluster_metadata['project']}&organizationId=#{settings.organization_id}"
         
     | 
| 
      
 60 
     | 
    
         
            +
                  puts "Then, set up an IAM user that it will inherit the permissions for."
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                  puts "Please enter the path of your JSON key:"
         
     | 
| 
      
 63 
     | 
    
         
            +
                  filename = STDIN.gets
         
     | 
| 
      
 64 
     | 
    
         
            +
                  puts "Activating service account..."
         
     | 
| 
      
 65 
     | 
    
         
            +
                  system("gcloud auth activate-service-account --key-file #{filename}")
         
     | 
| 
      
 66 
     | 
    
         
            +
                  system("gcloud config set project #{cluster_metadata['project']}")
         
     | 
| 
      
 67 
     | 
    
         
            +
                  system("gcloud config set compute/zone #{settings.default_zone}")
         
     | 
| 
      
 68 
     | 
    
         
            +
                  puts "Your new gcloud setup for #{cluster_name}:"
         
     | 
| 
      
 69 
     | 
    
         
            +
                  system("gcloud config configurations describe #{cluster_name}")
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                  puts "Configuring kubectl for interactions with this project's kubernetes cluster"
         
     | 
| 
      
 72 
     | 
    
         
            +
                  system("gcloud container clusters get-credentials #{cluster_name} --zone #{settings.default_zone} --project #{cluster_metadata['project']}")
         
     | 
| 
      
 73 
     | 
    
         
            +
                  puts "Your kubectl is set up with:"
         
     | 
| 
      
 74 
     | 
    
         
            +
                  system("kubectl config current-context")
         
     | 
| 
      
 75 
     | 
    
         
            +
                end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                def ensure_software_installed
         
     | 
| 
      
 78 
     | 
    
         
            +
                  puts "Making sure gcloud is installed..."
         
     | 
| 
      
 79 
     | 
    
         
            +
                  unless system('gcloud --version &> /dev/null')
         
     | 
| 
      
 80 
     | 
    
         
            +
                    puts "Installing gcloud..."
         
     | 
| 
      
 81 
     | 
    
         
            +
                    system('curl https://sdk.cloud.google.com | bash')
         
     | 
| 
      
 82 
     | 
    
         
            +
                    system('exec -l $SHELL')
         
     | 
| 
      
 83 
     | 
    
         
            +
                    system('gcloud init')
         
     | 
| 
      
 84 
     | 
    
         
            +
                  end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                  puts "Making sure kubectl is installed..."
         
     | 
| 
      
 87 
     | 
    
         
            +
                  unless system('kubectl version &> /dev/null')
         
     | 
| 
      
 88 
     | 
    
         
            +
                    puts "Installing kubectl..."
         
     | 
| 
      
 89 
     | 
    
         
            +
                    system('gcloud components install kubectl')
         
     | 
| 
      
 90 
     | 
    
         
            +
                  end
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                  puts "Making sure kubernetes-helm is installed..."
         
     | 
| 
      
 93 
     | 
    
         
            +
                  unless system('helm version &> /dev/null')
         
     | 
| 
      
 94 
     | 
    
         
            +
                    puts "Installing helm..."
         
     | 
| 
      
 95 
     | 
    
         
            +
                    system('brew install kubernetes-helm')
         
     | 
| 
      
 96 
     | 
    
         
            +
                  end
         
     | 
| 
      
 97 
     | 
    
         
            +
                end
         
     | 
| 
      
 98 
     | 
    
         
            +
              end
         
     | 
| 
      
 99 
     | 
    
         
            +
            end
         
     | 
    
        data/seira.gemspec
    ADDED
    
    | 
         @@ -0,0 +1,29 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # coding: utf-8
         
     | 
| 
      
 2 
     | 
    
         
            +
            lib = File.expand_path('../lib', __FILE__)
         
     | 
| 
      
 3 
     | 
    
         
            +
            $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'seira/version'
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            Gem::Specification.new do |spec|
         
     | 
| 
      
 7 
     | 
    
         
            +
              spec.name          = "seira"
         
     | 
| 
      
 8 
     | 
    
         
            +
              spec.version       = Seira::VERSION
         
     | 
| 
      
 9 
     | 
    
         
            +
              spec.authors       = ["Scott Ringwelski"]
         
     | 
| 
      
 10 
     | 
    
         
            +
              spec.email         = ["scott@joinhandshake.com"]
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
              spec.summary       = %q{An opinionated library for building applications on Kubernetes.}
         
     | 
| 
      
 13 
     | 
    
         
            +
              spec.description   = %q{An opinionated library for building applications on Kubernetes.}
         
     | 
| 
      
 14 
     | 
    
         
            +
              spec.homepage      = "https://github.com/joinhandshake/seira"
         
     | 
| 
      
 15 
     | 
    
         
            +
              spec.license       = "MIT"
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
              spec.files         = `git ls-files -z`.split("\x0").reject do |f|
         
     | 
| 
      
 18 
     | 
    
         
            +
                f.match(%r{^(test|spec|features)/})
         
     | 
| 
      
 19 
     | 
    
         
            +
              end
         
     | 
| 
      
 20 
     | 
    
         
            +
              spec.executables   = ['seira']
         
     | 
| 
      
 21 
     | 
    
         
            +
              spec.require_paths = ["lib"]
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
              spec.add_runtime_dependency "highline"
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
              spec.add_development_dependency "bundler", "~> 1.14"
         
     | 
| 
      
 26 
     | 
    
         
            +
              spec.add_development_dependency "rake", "~> 10.0"
         
     | 
| 
      
 27 
     | 
    
         
            +
              spec.add_development_dependency "rspec", "~> 3.0"
         
     | 
| 
      
 28 
     | 
    
         
            +
              spec.add_development_dependency "rubocop", "0.51.0"
         
     | 
| 
      
 29 
     | 
    
         
            +
            end
         
     |