seira 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b3217d5194a295fc619a76aeee458e668f8c6098
4
- data.tar.gz: 171b4d7b51e17fc15829e398a1236e11e2d6d556
3
+ metadata.gz: c5be00c536176932142946b6895318dc29d53f76
4
+ data.tar.gz: 2a902ea955ee77f72c174de8672d15def2eef544
5
5
  SHA512:
6
- metadata.gz: b8d3ad1639cdf424c7b60ea3000cdddf09baa02a90d716be94d2fab6530f27bcf60ca712aa6db22a1ea9e252336294f34ba89a4967ed8022b0b77849205fa554
7
- data.tar.gz: 36e28341f6a5b493374f926b36903fce10977e48ec19c43318bcf1ee7c56714eca63254603337cc19a4d4bcd40fa5d4cab6c634af82940cdb7870338e890135f
6
+ metadata.gz: fb4e57972ea5e115b7daa89105662789e74aa9c80bd51a36cd5454331bc88d873610fb532a3e1a528dc2bc7577ad5b47b95b0969006459b06145aa89e480e475
7
+ data.tar.gz: ecaa4df24618206589985f0e06641243173d0ccef6b11934651f928f10f686b0a3fa8ffad35dd42ff71f6d946c73fea07057c0c861b541b6e77586f4d665e31e
data/.gitignore CHANGED
@@ -10,3 +10,6 @@
10
10
 
11
11
  # rspec failure tracking
12
12
  .rspec_status
13
+
14
+ # don't commit app-specific config
15
+ .seira.yml
data/README.md CHANGED
@@ -1,7 +1,16 @@
1
1
  # Seira
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/seira.svg)](https://badge.fury.io/rb/seira)
4
+ [![Build Status](https://travis-ci.org/joinhandshake/seira.svg?branch=master)](https://travis-ci.org/joinhandshake/seira)
5
+
2
6
  An opinionated library for building applications on Kubernetes.
3
7
 
8
+ This library builds a framework for doing deployments, secrets management, managing memcached, managing redis, managing and accessing pods, bootstraping new apps and clusters, and more. It makes decisions about how to run the apps and cluster to make managing the cluster easier.
9
+
10
+ The vision for Seira is to produce a CLI and set of guidelines that makes deploying apps on Kubernetes as easy as Heroku.
11
+
4
12
  ## What does the name mean?
13
+
5
14
  Following Kubernetes naming pattern, Seira (Seirá) is greek for "order" or "the state of being well arranged".
6
15
 
7
16
  ## Installation
@@ -20,9 +29,95 @@ Or install it yourself as:
20
29
 
21
30
  $ gem install seira
22
31
 
32
+ The `gem install seira` option may be preferred for shorter typing, or generating a binstub is also an option.
33
+
23
34
  ## Usage
24
35
 
25
- TODO: Write usage instructions here
36
+ This library only currently works with `gcloud` and `kubectl`, meaning Google Container Engine and Kubernetes.
37
+
38
+ All commands follow a pattern:
39
+
40
+ `seira <cluster> <app> <category> <param1> <param2> <..>`
41
+
42
+ The only exception is commands on the cluster itself, which does not take an `<app>`. By making sure to take in a cluster as the first parameter to every command, the intent is to reduce mistake commands on the wrong cluster.
43
+
44
+ ### Configuration File
45
+
46
+ A configuration file is expected at `.seira.yml` relative path. Below is an example file.
47
+
48
+ ```
49
+ seira:
50
+ organization_id: "11111"
51
+ default_zone: "us-central1-a"
52
+ clusters:
53
+ internal:
54
+ project: org-internal
55
+ cluster: gke_org-internal_us-central1-a_internal
56
+ aliases:
57
+ - "i"
58
+ staging:
59
+ project: org-staging
60
+ cluster: gke_org-staging_us-central1-a_staging
61
+ aliases:
62
+ - "s"
63
+ demo:
64
+ project: org-demo
65
+ cluster: gke_org-demo_us-central1-a_demo
66
+ aliases:
67
+ - "d"
68
+ production:
69
+ project: org-production
70
+ cluster: gke_org-production_us-central1-a_production
71
+ aliases:
72
+ - "p"
73
+ valid_apps:
74
+ - app1
75
+ - app2
76
+ ```
77
+
78
+ This specification is read in and used to determine what `gcloud` context to use and what `kubectl` cluster to use when operating commands. For example, `seira internal` will connect to `org-internal` gcloud configuration and `gke_org-internal_us-central1-a_internal` kubectl cluster. For shorthand, `seira i` shorthand is specified as an alias.
79
+
80
+ ### Manifest Files
81
+
82
+ Seira expects your Kubernetes manifests to exist in the "kubernetes/<cluster>/<app>" directory. When a deploy is run on `foo` app in `staging` cluster, it looks to `kubernetes/staging/foo` directory for the manifest files.
83
+
84
+ ### Assumptions
85
+
86
+ - Each app has all its objects contained in a namespace, named after the app
87
+ - Each app has one or more deployments, and a deployment and all pods created by that deployment have a `tier` label matching the name of the deployment
88
+ - If using a SQL database (currently only postgresql is supported), pgbouncer is used for connection pooling, and the app uses the secret `DATABASE_URL` to connect and authenticate to the database
89
+
90
+ ### Initial Setup
91
+
92
+ In order to use Seira, an initial setup is needed. Use the `seira setup` command to set up each of your clusters in your configuration file.
93
+
94
+ ## Example functionality
95
+
96
+ ### Running Proxy UI
97
+
98
+ Easily run a proxy UI (`kubectl proxy`) by using `seira staging proxy` shorthand.
99
+
100
+ ### Applying New Manifest Files
101
+
102
+ By using `seira staging app-name app apply`, Seira will find/replace the string "REVISION" in your manifests with the value in the `REVISION` environment variable and apply the new configs to the cluster. If `REVISION` is nil, it will ask to use the tag currently being used by the current `web` deployment.
103
+
104
+ ### Setting Secrets
105
+
106
+ All secrets are stored in `appname-secrets` Secret object. They are expected to be used via `envFrom` in manifest files.
107
+
108
+ `seira staging app-name secrets list`
109
+
110
+ `seira staging app-name secrets set KEY=value`
111
+
112
+ `seira staging app-name secrets get KEY`
113
+
114
+ ### Pods
115
+
116
+ Pods can be listed and also exec'd into.
117
+
118
+ `seira staging app-name pods list`
119
+
120
+ `seira staging app-name pods run`
26
121
 
27
122
  ## Development
28
123
 
@@ -32,7 +127,21 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
127
 
33
128
  ## Contributing
34
129
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/sgringwe/seira.
130
+ Bug reports and pull requests are welcome on GitHub at https://github.com/joinhandshake/seira.
131
+
132
+ ## Roadmap
133
+
134
+ Future roadmap has plans for:
135
+
136
+ - Create CLI help commands and improve general CLI usability
137
+ - Cloud SQL Postgres Management (credentials, processes, diagnostics, psql, etc)
138
+ - Smooth application restarts, including such as after changing secrets
139
+ - Better redis and memcached production-level management
140
+ - More functionality for managing pods
141
+ - Maintenance page mode
142
+ - Quickly scaling up/down deployments
143
+ - SSL Certificate inspection and observability through kube-lego
144
+ - More seamless `seira setup` script
36
145
 
37
146
 
38
147
  ## License
@@ -8,6 +8,7 @@ require 'seira/memcached'
8
8
  require 'seira/pods'
9
9
  require 'seira/proxy'
10
10
  require 'seira/random'
11
+ require 'seira/db'
11
12
  require 'seira/redis'
12
13
  require 'seira/secrets'
13
14
  require 'seira/settings'
@@ -20,6 +21,7 @@ module Seira
20
21
  CATEGORIES = {
21
22
  'secrets' => Seira::Secrets,
22
23
  'pods' => Seira::Pods,
24
+ 'db' => Seira::Db,
23
25
  'redis' => Seira::Redis,
24
26
  'memcached' => Seira::Memcached,
25
27
  'app' => Seira::App,
@@ -98,7 +100,7 @@ module Seira
98
100
  end
99
101
 
100
102
  def base_validations
101
- # The first arg must always be the cluster. This ensures commands are not run by
103
+ # The first arg must always be the cluster. This ensures commands are not run by
102
104
  # accident on the wrong kubernetes cluster or gcloud project.
103
105
  exit(1) unless Seira::Cluster.new(action: nil, args: nil, context: nil, settings: settings).switch(target_cluster: cluster, verbose: false)
104
106
  exit(0) if simple_cluster_change?
@@ -122,4 +124,4 @@ module Seira
122
124
  app.nil? && category.nil? # Special case where user is simply changing environments
123
125
  end
124
126
  end
125
- end
127
+ end
@@ -6,7 +6,7 @@ require 'fileutils'
6
6
  # seira staging specs app bootstrap
7
7
  module Seira
8
8
  class App
9
- VALID_ACTIONS = %w[bootstrap apply upgrade restart].freeze
9
+ VALID_ACTIONS = %w[bootstrap apply upgrade restart scale].freeze
10
10
 
11
11
  attr_reader :app, :action, :args, :context
12
12
 
@@ -27,6 +27,8 @@ module Seira
27
27
  run_upgrade
28
28
  when 'restart'
29
29
  run_restart
30
+ when 'scale'
31
+ run_scale
30
32
  else
31
33
  fail "Unknown command encountered"
32
34
  end
@@ -57,7 +59,7 @@ module Seira
57
59
  exit(1) unless HighLine.agree("No REVISION specified. Use current deployment revision '#{current_revision}'?")
58
60
  revision = current_revision
59
61
  end
60
-
62
+
61
63
  replacement_hash = { 'REVISION' => revision }
62
64
 
63
65
  replacement_hash.each do |k, v|
@@ -76,12 +78,36 @@ module Seira
76
78
  system("kubectl apply -f #{destination}")
77
79
  end
78
80
 
81
+ def run_scale
82
+ scales = key_value_map.dup
83
+ configs = load_configs
84
+
85
+ if scales.key? 'all'
86
+ configs.each do |config|
87
+ next unless config['kind'] == 'Deployment'
88
+ scales[config['metadata']['labels']['tier']] ||= scales['all']
89
+ end
90
+ scales.delete 'all'
91
+ end
92
+
93
+ scales.each do |tier, replicas|
94
+ config = configs.find { |c| c['kind'] == 'Deployment' && c['metadata']['labels']['tier'] == tier }
95
+ if config.nil?
96
+ puts "Warning: could not find config for tier #{tier}"
97
+ next
98
+ end
99
+ replicas = config['spec']['replicas'] if replicas == 'default'
100
+ puts "scaling #{tier} to #{replicas}"
101
+ system("kubectl scale --namespace=#{app} --replicas=#{replicas} deployments/#{config['metadata']['name']}")
102
+ end
103
+ end
104
+
79
105
  def bootstrap_main_secret
80
106
  puts "Creating main secret and namespace..."
81
107
  main_secret_name = Seira::Secrets.new(app: app, action: action, args: args, context: context).main_secret_name
82
108
 
83
109
  # 'internal' is a unique cluster/project "cluster". It always means production in terms of rails app.
84
- rails_env =
110
+ rails_env =
85
111
  if context[:cluster] == 'internal'
86
112
  'production'
87
113
  else
@@ -109,22 +135,38 @@ module Seira
109
135
  puts "Copying source yaml from #{source} to #{destination}"
110
136
  FileUtils::mkdir_p destination # Create the nested directory
111
137
  FileUtils.copy_entry source, destination
112
-
138
+
113
139
  # Iterate through each yaml file and find/replace and save
114
140
  puts "Iterating #{destination} files find/replace revision information"
115
141
  Dir.foreach(destination) do |item|
116
142
  next if item == '.' || item == '..'
117
-
143
+
118
144
  text = File.read("#{destination}/#{item}")
119
-
145
+
120
146
  new_contents = text
121
147
  replacement_hash.each do |key, value|
122
148
  new_contents.gsub!(key, value)
123
149
  end
124
-
150
+
125
151
  # To write changes to the file, use:
126
152
  File.open("#{destination}/#{item}", 'w') { |file| file.write(new_contents) }
127
153
  end
128
154
  end
155
+
156
+ # TODO(josh): factor out and share this method with similar commands (e.g. `secrets set`)
157
+ def key_value_map
158
+ args.map do |arg|
159
+ equals_index = arg.index('=')
160
+ [arg[0..equals_index - 1], arg[equals_index + 1..-1]]
161
+ end.to_h
162
+ end
163
+
164
+ def load_configs
165
+ directory = "kubernetes/#{context[:cluster]}/#{app}/"
166
+ Dir.new(directory).flat_map do |filename|
167
+ next if ['.', '..'].include? filename
168
+ YAML.load_stream(File.read(File.join(directory, filename)))
169
+ end.compact
170
+ end
129
171
  end
130
172
  end
@@ -55,7 +55,7 @@ module Seira
55
55
  private
56
56
 
57
57
  # Intended for use when spinning up a whole new cluster. It stores two main secrets
58
- # in the default space that are intended to be copied into individual namespaces when
58
+ # in the default space that are intended to be copied into individual namespaces when
59
59
  # new apps are built.
60
60
  def run_bootstrap
61
61
  dockercfg_location = args[0]
@@ -0,0 +1,140 @@
1
+ require 'securerandom'
2
+
3
+ module Seira
4
+ class Db
5
+ VALID_ACTIONS = %w[create delete list].freeze
6
+
7
+ attr_reader :app, :action, :args, :context
8
+
9
+ def initialize(app:, action:, args:, context:)
10
+ @app = app
11
+ @action = action
12
+ @args = args
13
+ @context = context
14
+ end
15
+
16
+ def run
17
+ case action
18
+ when 'create'
19
+ run_create
20
+ when 'delete'
21
+ run_delete
22
+ when 'list'
23
+ run_list
24
+ else
25
+ fail "Unknown command encountered"
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def run_create
32
+ # We allow overriding the version, so you could specify a mysql version but much of the
33
+ # below assumes postgres for now
34
+ version = 'POSTGRES_9_6'
35
+ cpu = 1 # Number of CPUs
36
+ memory = 4 # GB
37
+ storage = 10 # GB
38
+ set_as_primary = false
39
+
40
+ name = "#{app}-#{Seira::Random.unique_name(existing_instances)}"
41
+
42
+ create_command = "gcloud sql instances create #{name}"
43
+
44
+ args.each do |arg|
45
+ if arg.start_with? '--version='
46
+ version = arg.split('=')[1]
47
+ elsif arg.start_with? '--cpu='
48
+ cpu = arg.split('=')[1]
49
+ elsif arg.start_with? '--memory='
50
+ memory = arg.split('=')[1]
51
+ elsif arg.start_with? '--storage='
52
+ storage = arg.split('=')[1]
53
+ elsif arg.start_with? '--set-as-primary='
54
+ set_as_primary = %w[true yes t y].include?(arg.split('=')[1])
55
+ elsif /^--[\w\-]+=.+$/.match? arg
56
+ create_command += " #{arg}"
57
+ else
58
+ puts "Warning: Unrecognized argument '#{arg}'"
59
+ end
60
+ end
61
+
62
+ create_command += " --database-version=#{version}"
63
+ create_command += " --cpu=#{cpu}"
64
+ create_command += " --memory=#{memory}"
65
+ create_command += " --storage-size=#{storage}"
66
+
67
+ # Create the sql instance with the specified/default parameters
68
+ if system(create_command)
69
+ puts "Successfully created sql instance #{name}"
70
+ else
71
+ puts "Failed to create sql instance"
72
+ exit(1)
73
+ end
74
+
75
+ # Set the root user's password to something secure
76
+ root_password = SecureRandom.urlsafe_base64(32)
77
+ if system("gcloud sql users set-password postgres '' --instance=#{name} --password=#{root_password}")
78
+ puts "Set root password to #{root_password}"
79
+ else
80
+ puts "Failed to set root password"
81
+ exit(1)
82
+ end
83
+
84
+ # Create proxyuser with secure password
85
+ proxyuser_password = SecureRandom.urlsafe_base64(32)
86
+ if system("gcloud sql users create proxyuser '' --instance=#{name} --password=#{proxyuser_password}")
87
+ puts "Created proxyuser with password #{proxyuser_password}"
88
+ else
89
+ puts "Failed to create proxyuser"
90
+ exit(1)
91
+ end
92
+
93
+ # Connect to the instance and remove some of the default group memberships and permissions
94
+ # from proxyuser, leaving it with only what it needs to be able to do
95
+ expect_script = <<~BASH
96
+ set timeout 90
97
+ spawn gcloud sql connect #{name}
98
+ expect "Password for user postgres:"
99
+ send "#{root_password}\\r"
100
+ expect "postgres=>"
101
+ send "REVOKE cloudsqlsuperuser FROM proxyuser;\\r"
102
+ expect "postgres=>"
103
+ send "ALTER ROLE proxyuser NOCREATEDB NOCREATEROLE;\\r"
104
+ expect "postgres=>"
105
+ BASH
106
+ if system("expect <<EOF\n#{expect_script}EOF")
107
+ puts "Successfully removed unnecessary permissions from proxyuser"
108
+ else
109
+ puts "Failed to remove unnecessary permissions from proxyuser"
110
+ exit(1)
111
+ end
112
+
113
+ # If setting as primary, update relevant secrets
114
+ if set_as_primary
115
+ Secrets.new(app: app, action: 'create-pgbouncer-secret', args: ['proxyuser', proxyuser_password], context: context).run
116
+ Secrets.new(app: app, action: 'set', args: ["DATABASE_URL=postgres://proxyuser:#{proxyuser_password}@#{app}-pgbouncer-service:6432"], context: context).run
117
+ end
118
+ # Regardless of primary or not, store a URL for this db matching its unique name
119
+ env_name = name.tr('-', '_').upcase
120
+ Secrets.new(app: app, action: 'set', args: ["#{env_name}_DB_URL=postgres://proxyuser:#{proxyuser_password}@#{app}-pgbouncer-service:6432", "#{env_name}_ROOT_PASSWORD=#{root_password}"], context: context).run
121
+ end
122
+
123
+ def run_delete
124
+ name = "#{app}-#{args[0]}"
125
+ if system("gcloud sql instances delete #{name}")
126
+ puts "Successfully deleted sql instance #{name}"
127
+ else
128
+ puts "Failed to delete sql instance #{name}"
129
+ end
130
+ end
131
+
132
+ def run_list
133
+ puts existing_instances
134
+ end
135
+
136
+ def existing_instances
137
+ `gcloud sql instances list --uri`.split("\n").map { |uri| uri.split('/').last }.select { |name| name.start_with? "#{app}-" }.map { |name| name.gsub(/^#{app}-/, '') }
138
+ end
139
+ end
140
+ end
@@ -35,11 +35,7 @@ module Seira
35
35
  private
36
36
 
37
37
  def run_list
38
- list = `helm list`.split("\n")
39
- filtered_list = list.select { |item| item.start_with?("#{app}-memcached") }
40
- filtered_list.each do |item|
41
- puts item
42
- end
38
+ puts existing_instances
43
39
  end
44
40
 
45
41
  def run_status
@@ -53,8 +49,8 @@ module Seira
53
49
  values = {
54
50
  resources: {
55
51
  requests: {
56
- cpu: '1', # roughly 1 vCPU in both AWS and GCP terms
57
- memory: '5Gi' # memcached is in-memory - give it a lot
52
+ memory: '500Mi',
53
+ cpu: '50m'
58
54
  }
59
55
  }
60
56
  }
@@ -65,19 +61,48 @@ module Seira
65
61
  values[:resources][:requests][:memory] = arg.split('=')[1]
66
62
  elsif arg.start_with?('--cpu=')
67
63
  values[:resources][:requests][:cpu] = arg.split('=')[1]
64
+ elsif arg.start_with?('--size=')
65
+ size = arg.split('=')[1]
66
+ case size
67
+ when '1'
68
+ values[:resources][:requests][:memory] = '100Mi'
69
+ values[:resources][:requests][:cpu] = '50m'
70
+ when '2'
71
+ values[:resources][:requests][:memory] = '250Mi'
72
+ values[:resources][:requests][:cpu] = '100m'
73
+ when '3'
74
+ values[:resources][:requests][:memory] = '500Mi'
75
+ values[:resources][:requests][:cpu] = '200m'
76
+ when '4'
77
+ values[:resources][:requests][:memory] = '1Gi'
78
+ values[:resources][:requests][:cpu] = '500m'
79
+ when '5'
80
+ values[:resources][:requests][:memory] = '2Gi'
81
+ values[:resources][:requests][:cpu] = '500m'
82
+ when '6'
83
+ values[:resources][:requests][:memory] = '5Gi'
84
+ values[:resources][:requests][:cpu] = '1'
85
+ when '7'
86
+ values[:resources][:requests][:memory] = '10Gi'
87
+ values[:resources][:requests][:cpu] = '2'
88
+ when '8'
89
+ values[:resources][:requests][:memory] = '50Gi'
90
+ values[:resources][:requests][:cpu] = '4'
91
+ else
92
+ fail "There is no size option '#{size}'"
93
+ end
68
94
  end
69
95
  end
70
96
 
71
97
  file_name = write_config(values)
72
- unique_name = "#{Seira::Random.color}-#{Seira::Random.animal}"
98
+ unique_name = Seira::Random.unique_name(existing_instances)
73
99
  name = "#{app}-memcached-#{unique_name}"
74
100
  puts `helm install --namespace #{app} --name #{name} --wait -f #{file_name} stable/memcached`
75
101
 
76
102
  File.delete(file_name)
77
103
 
78
104
  puts "To get status: 'seira #{context[:cluster]} #{app} memcached status #{unique_name}'"
79
- puts "To get credentials for storing in app secrets: 'siera #{context[:cluster]} #{app} memcached credentials #{unique_name}'"
80
- puts "Service URI for this memcached instance: 'memcached://#{name}:11211'."
105
+ puts "Service URI for this memcached instance: 'memcached://#{name}-memcached:11211'."
81
106
  end
82
107
 
83
108
  def run_delete
@@ -104,5 +129,9 @@ module Seira
104
129
  end
105
130
  file_name
106
131
  end
132
+
133
+ def existing_instances
134
+ `helm list`.split("\n").select { |item| item.start_with?("#{app}-memcached") }.map { |name| name.gsub(/^#{app}-memcached-/, '') }
135
+ end
107
136
  end
108
137
  end
@@ -1,19 +1,20 @@
1
+ require 'json'
2
+
1
3
  module Seira
2
4
  class Pods
3
- VALID_ACTIONS = %w[list delete logs top run].freeze
5
+ VALID_ACTIONS = %w[list delete logs top run connect].freeze
4
6
 
5
- attr_reader :app, :action, :key, :value, :context
7
+ attr_reader :app, :action, :args, :pod_name, :context
6
8
 
7
9
  def initialize(app:, action:, args:, context:)
8
10
  @app = app
9
11
  @action = action
10
12
  @context = context
11
- @key = args[0]
12
- @value = args[1]
13
+ @args = args
14
+ @pod_name = args[0]
13
15
  end
14
16
 
15
17
  def run
16
- # TODO: Some options: 'top', 'kill', 'delete', 'logs'
17
18
  case action
18
19
  when 'list'
19
20
  run_list
@@ -23,6 +24,8 @@ module Seira
23
24
  run_logs
24
25
  when 'top'
25
26
  run_top
27
+ when 'connect'
28
+ run_connect
26
29
  when 'run'
27
30
  run_run
28
31
  else
@@ -33,38 +36,116 @@ module Seira
33
36
  private
34
37
 
35
38
  def run_list
36
- puts list_pods
39
+ puts `kubectl get pods --namespace=#{app} -o wide`
37
40
  end
38
41
 
39
42
  def run_delete
40
- puts `kubectl delete pod #{@key} --namespace=#{@app}`
43
+ puts `kubectl delete pod #{pod_name} --namespace=#{app}`
41
44
  end
42
45
 
43
46
  def run_logs
44
- puts `kubectl logs #{@key} --namespace=#{@app} -c #{@app}`
47
+ puts `kubectl logs #{pod_name} --namespace=#{app} -c #{app}`
45
48
  end
46
49
 
47
50
  def run_top
48
- puts `kubectl top pod #{@key} --namespace=#{@app} --containers`
51
+ puts `kubectl top pod #{pod_name} --namespace=#{app} --containers`
49
52
  end
50
53
 
51
- def run_run
52
- pod_list = list_pods.split("\n")
53
- target_pod_type = "#{@app}-web"
54
- target_pod_options = pod_list.select { |pod| pod.include?(target_pod_type) }
55
-
56
- if target_pod_options.count > 0
57
- target_pod = target_pod_options[0]
58
- pod_name = target_pod.split(" ")[0]
59
- puts pod_name
60
- system("kubectl exec -ti #{pod_name} --namespace=#{@app} -- bash")
54
+ def run_connect
55
+ # If a pod name is specified, connect to that pod; otherwise pick a random web pod
56
+ target_pod_name = pod_name || fetch_pods(app: app, tier: 'web').sample&.dig('metadata', 'name')
57
+
58
+ if target_pod_name
59
+ connect_to_pod(target_pod_name)
61
60
  else
62
- puts "Could not find web with name #{target_pod_type} to attach to"
61
+ puts "Could not find web pod to connect to"
62
+ end
63
+ end
64
+
65
+ def run_run
66
+ # Set defaults
67
+ tier = 'web'
68
+ clear_commands = false
69
+
70
+ # Loop through args and process any that aren't just the command to run
71
+ loop do
72
+ arg = args.first
73
+ if arg.nil?
74
+ puts 'Please specify a command to run'
75
+ exit(1)
76
+ end
77
+ break unless arg.start_with? '--'
78
+ if arg.start_with? '--tier='
79
+ tier = arg.split('=')[1]
80
+ elsif arg.start_with? '--clear-commands='
81
+ clear_commands = %w[true yes t y].include? arg.split('=')[1]
82
+ else
83
+ puts "Warning: Unrecognized argument #{arg}"
84
+ end
85
+ args.shift
86
+ end
87
+
88
+ # Any remaining args are the command to run
89
+ command = args.join(' ')
90
+
91
+ # Find a 'template' pod from the proper tier
92
+ template_pod = fetch_pods(app: app, tier: tier).first
93
+ if template_pod.nil?
94
+ puts "Unable to find #{tier} tier pod to copy config from"
95
+ exit(1)
63
96
  end
97
+
98
+ # Use that template pod's configuration to create a new temporary pod
99
+ temp_name = "#{app}-temp-#{Random.unique_name}"
100
+ spec = template_pod['spec']
101
+ temp_pod = {
102
+ apiVersion: template_pod['apiVersion'],
103
+ kind: 'Pod',
104
+ spec: spec,
105
+ metadata: {
106
+ name: temp_name
107
+ }
108
+ }
109
+ spec['restartPolicy'] = 'Never'
110
+ if clear_commands
111
+ spec['containers'].each do |container|
112
+ container['command'] = ['bash', '-c', 'tail -f /dev/null']
113
+ end
114
+ end
115
+
116
+ puts "Creating temporary pod #{temp_name}"
117
+ unless system("kubectl --namespace=#{app} create -f - <<JSON\n#{temp_pod.to_json}\nJSON")
118
+ puts 'Failed to create pod'
119
+ exit(1)
120
+ end
121
+
122
+ # Check pod status until it's ready to connect to
123
+ print 'Waiting for pod to start...'
124
+ loop do
125
+ pod = JSON.parse(`kubectl --namespace=#{app} get pods/#{temp_name} -o json`)
126
+ break if pod['status']['phase'] == 'Running'
127
+ print '.'
128
+ sleep 1
129
+ end
130
+ print "\n"
131
+
132
+ # Connect to the pod, running the specified command
133
+ connect_to_pod(temp_name, command)
134
+
135
+ # Clean up
136
+ unless system("kubectl --namespace=#{app} delete pod #{temp_name}")
137
+ puts "Warning: failed to clean up pod #{temp_name}"
138
+ end
139
+ end
140
+
141
+ def fetch_pods(filters)
142
+ filter_string = filters.map { |k, v| "#{k}=#{v}" }.join(',')
143
+ JSON.parse(`kubectl get pods --namespace=#{app} -o json --selector=#{filter_string}`)['items']
64
144
  end
65
145
 
66
- def list_pods
67
- `kubectl get pods --namespace=#{@app} -o wide`
146
+ def connect_to_pod(name, command = 'bash')
147
+ puts "Connecting to #{name}..."
148
+ system("kubectl exec -ti #{name} --namespace=#{app} -- #{command}")
68
149
  end
69
150
  end
70
151
  end
@@ -1,6 +1,13 @@
1
1
  # For random colors for resource installations via helm
2
2
  module Seira
3
3
  class Random
4
+ def self.unique_name(existing = [])
5
+ loop do
6
+ name = "#{color}-#{animal}"
7
+ return name unless existing.include? name
8
+ end
9
+ end
10
+
4
11
  def self.color
5
12
  %w[
6
13
  red
@@ -49,7 +56,6 @@ module Seira
49
56
  antelope
50
57
  ape
51
58
  armadillo
52
- ass
53
59
  avocet
54
60
  axolotl
55
61
  baboon
@@ -374,6 +380,7 @@ module Seira
374
380
  uakari
375
381
  uguisu
376
382
  umbrellabird
383
+ unicorn
377
384
  viper
378
385
  vulture
379
386
  wallaby
@@ -35,11 +35,7 @@ module Seira
35
35
  private
36
36
 
37
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
38
+ puts existing_instances
43
39
  end
44
40
 
45
41
  def run_status
@@ -52,16 +48,18 @@ module Seira
52
48
  # TODO: Enable metrics
53
49
  values = {
54
50
  persistence: {
55
- size: '32Gi'
51
+ size: '1Gi'
56
52
  },
57
53
  resources: {
58
54
  requests: {
59
- cpu: '2', # roughly 2 vCPU in both AWS and GCP terms
60
- memory: '8Gi' # redis is in-memory - give it a lot
55
+ cpu: '50m',
56
+ memory: '50Mi'
61
57
  }
62
58
  }
63
59
  }
64
60
 
61
+ # Redis is single-threaded, thus only one CPU is needed? Primary/replica
62
+ # is needed for further throughput
65
63
  args.each do |arg|
66
64
  puts "Applying arg #{arg} to values"
67
65
  if arg.start_with?('--memory=')
@@ -74,9 +72,41 @@ module Seira
74
72
  size = arg.split('=')[1]
75
73
  case size
76
74
  when '1'
77
- values[:resources][:requests][:memory] = '100Mi' # 100mb
75
+ values[:resources][:requests][:memory] = '50Mi'
76
+ values[:persistence][:size] = '1Gi'
77
+ values[:resources][:requests][:cpu] = '50m'
78
+ when '2'
79
+ values[:resources][:requests][:memory] = '100Mi'
80
+ values[:persistence][:size] = '1Gi'
81
+ values[:resources][:requests][:cpu] = '100m'
82
+ when '3'
83
+ values[:resources][:requests][:memory] = '250Mi'
84
+ values[:persistence][:size] = '1Gi'
85
+ values[:resources][:requests][:cpu] = '200m'
86
+ when '4'
87
+ values[:resources][:requests][:memory] = '500Mi'
88
+ values[:persistence][:size] = '1Gi'
89
+ values[:resources][:requests][:cpu] = '500m'
90
+ when '5'
91
+ values[:resources][:requests][:memory] = '1Gi'
92
+ values[:persistence][:size] = '5Gi'
93
+ values[:resources][:requests][:cpu] = '1'
94
+ when '6'
95
+ values[:resources][:requests][:memory] = '2Gi'
96
+ values[:persistence][:size] = '5Gi'
97
+ values[:resources][:requests][:cpu] = '1'
98
+ when '7'
99
+ values[:resources][:requests][:memory] = '5Gi'
78
100
  values[:persistence][:size] = '5Gi'
79
- values[:resources][:requests][:cpu] = '100m' # .1 cpu
101
+ values[:resources][:requests][:cpu] = '1'
102
+ when '8'
103
+ values[:resources][:requests][:memory] = '10Gi'
104
+ values[:persistence][:size] = '20Gi'
105
+ values[:resources][:requests][:cpu] = '1'
106
+ when '9'
107
+ values[:resources][:requests][:memory] = '20Gi'
108
+ values[:persistence][:size] = '40Gi'
109
+ values[:resources][:requests][:cpu] = '1'
80
110
  else
81
111
  fail "There is no size option '#{size}'"
82
112
  end
@@ -84,7 +114,7 @@ module Seira
84
114
  end
85
115
 
86
116
  file_name = write_config(values)
87
- unique_name = "#{Seira::Random.color}-#{Seira::Random.animal}"
117
+ unique_name = Seira::Random.unique_name(existing_instances)
88
118
  name = "#{app}-#{unique_name}"
89
119
  puts `helm install --namespace #{app} --name #{name} --wait -f #{file_name} stable/redis`
90
120
 
@@ -119,5 +149,9 @@ module Seira
119
149
  end
120
150
  file_name
121
151
  end
152
+
153
+ def existing_instances
154
+ list = `helm list`.split("\n").select { |item| item.start_with?("#{app}-redis") }.map { |name| name.gsub(/^#{app}-redis-/, '') }
155
+ end
122
156
  end
123
157
  end
@@ -2,37 +2,34 @@ require 'json'
2
2
  require 'base64'
3
3
 
4
4
  # Example usages:
5
- # seira staging specs secret set RAILS_ENV staging
5
+ # seira staging specs secret set RAILS_ENV=staging
6
6
  # seira demo tracking secret unset DISABLE_SOME_FEATURE
7
7
  # seira staging importer secret list
8
- # TODO: Multiple secrets in one command
9
8
  # TODO: Can we avoid writing to disk completely and instead pipe in raw json?
10
9
  module Seira
11
10
  class Secrets
12
11
  VALID_ACTIONS = %w[get set unset list list-decoded create-pgbouncer-secret].freeze
13
12
  PGBOUNCER_SECRETS_NAME = 'pgbouncer-secrets'.freeze
14
13
 
15
- attr_reader :app, :action, :key, :value, :args, :context
14
+ attr_reader :app, :action, :args, :context
16
15
 
17
16
  def initialize(app:, action:, args:, context:)
18
17
  @app = app
19
18
  @action = action
20
19
  @args = args
21
- @key = args[0]
22
- @value = args[1]
23
20
  @context = context
24
21
  end
25
22
 
26
23
  def run
27
24
  case action
28
25
  when 'get'
29
- perform_key_validation
26
+ validate_single_key
30
27
  run_get
31
28
  when 'set'
32
- perform_key_validation
29
+ validate_keys_and_values
33
30
  run_set
34
31
  when 'unset'
35
- perform_key_validation
32
+ validate_single_key
36
33
  run_unset
37
34
  when 'list'
38
35
  run_list
@@ -40,8 +37,6 @@ module Seira
40
37
  run_list_decoded
41
38
  when 'create-pgbouncer-secret'
42
39
  run_create_pgbouncer_secret
43
- when 'bootstrap-cluster'
44
- run_bootstrap_cluster
45
40
  else
46
41
  fail "Unknown command encountered"
47
42
  end
@@ -69,22 +64,28 @@ module Seira
69
64
 
70
65
  private
71
66
 
72
- def perform_key_validation
67
+ def validate_single_key
73
68
  if key.nil? || key.strip == ""
74
69
  puts "Please specify a key in all caps and with underscores"
75
70
  exit(1)
76
71
  end
77
72
  end
78
73
 
74
+ def validate_keys_and_values
75
+ if args.empty? || !args.all? { |arg| /^[^=]+=.+$/ =~ arg }
76
+ puts "Please list keys and values to set like KEY_ONE=value_one KEY_TWO=value_two"
77
+ exit(1)
78
+ end
79
+ end
80
+
79
81
  def run_get
80
82
  secrets = fetch_current_secrets
81
83
  puts "#{key}: #{Base64.decode64(secrets['data'][key])}"
82
84
  end
83
85
 
84
86
  def run_set
85
- fail "Please specify a value as the third argument" if value.nil? || value.strip == ""
86
87
  secrets = fetch_current_secrets
87
- secrets['data'][key] = Base64.encode64(value)
88
+ secrets['data'].merge!(key_value_map.transform_values { |value| Base64.strict_encode64(value) })
88
89
  write_secrets(secrets: secrets)
89
90
  end
90
91
 
@@ -113,7 +114,7 @@ module Seira
113
114
  def run_create_pgbouncer_secret
114
115
  db_user = args[0]
115
116
  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
+ write_secrets(secrets: { DB_USER: db_user, DB_PASSWORD: db_password }, secret_name: PGBOUNCER_SECRETS_NAME)
117
118
  end
118
119
 
119
120
  # In the normal case the secret we are updating is just main_secret_name,
@@ -144,5 +145,16 @@ module Seira
144
145
  fail "Unexpected Kind" unless json['kind'] == 'Secret'
145
146
  json
146
147
  end
148
+
149
+ def key
150
+ args[0]
151
+ end
152
+
153
+ def key_value_map
154
+ args.map do |arg|
155
+ equals_index = arg.index('=')
156
+ [arg[0..equals_index - 1], arg[equals_index + 1..-1]]
157
+ end.to_h
158
+ end
147
159
  end
148
160
  end
@@ -1,3 +1,3 @@
1
1
  module Seira
2
- VERSION = "0.1.1".freeze
2
+ VERSION = "0.1.2".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: seira
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Ringwelski
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-11-27 00:00:00.000000000 Z
11
+ date: 2017-12-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: highline
@@ -105,6 +105,7 @@ files:
105
105
  - lib/seira.rb
106
106
  - lib/seira/app.rb
107
107
  - lib/seira/cluster.rb
108
+ - lib/seira/db.rb
108
109
  - lib/seira/memcached.rb
109
110
  - lib/seira/pods.rb
110
111
  - lib/seira/proxy.rb