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 +4 -4
- data/.gitignore +3 -0
- data/README.md +111 -2
- data/lib/seira.rb +4 -2
- data/lib/seira/app.rb +49 -7
- data/lib/seira/cluster.rb +1 -1
- data/lib/seira/db.rb +140 -0
- data/lib/seira/memcached.rb +39 -10
- data/lib/seira/pods.rb +103 -22
- data/lib/seira/random.rb +8 -1
- data/lib/seira/redis.rb +45 -11
- data/lib/seira/secrets.rb +26 -14
- data/lib/seira/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c5be00c536176932142946b6895318dc29d53f76
|
4
|
+
data.tar.gz: 2a902ea955ee77f72c174de8672d15def2eef544
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb4e57972ea5e115b7daa89105662789e74aa9c80bd51a36cd5454331bc88d873610fb532a3e1a528dc2bc7577ad5b47b95b0969006459b06145aa89e480e475
|
7
|
+
data.tar.gz: ecaa4df24618206589985f0e06641243173d0ccef6b11934651f928f10f686b0a3fa8ffad35dd42ff71f6d946c73fea07057c0c861b541b6e77586f4d665e31e
|
data/.gitignore
CHANGED
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
|
-
|
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/
|
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
|
data/lib/seira.rb
CHANGED
@@ -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
|
data/lib/seira/app.rb
CHANGED
@@ -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
|
data/lib/seira/cluster.rb
CHANGED
@@ -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]
|
data/lib/seira/db.rb
ADDED
@@ -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
|
data/lib/seira/memcached.rb
CHANGED
@@ -35,11 +35,7 @@ module Seira
|
|
35
35
|
private
|
36
36
|
|
37
37
|
def run_list
|
38
|
-
|
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
|
-
|
57
|
-
|
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 =
|
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 "
|
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
|
data/lib/seira/pods.rb
CHANGED
@@ -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, :
|
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
|
-
@
|
12
|
-
@
|
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
|
39
|
+
puts `kubectl get pods --namespace=#{app} -o wide`
|
37
40
|
end
|
38
41
|
|
39
42
|
def run_delete
|
40
|
-
puts `kubectl delete pod #{
|
43
|
+
puts `kubectl delete pod #{pod_name} --namespace=#{app}`
|
41
44
|
end
|
42
45
|
|
43
46
|
def run_logs
|
44
|
-
puts `kubectl logs #{
|
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 #{
|
51
|
+
puts `kubectl top pod #{pod_name} --namespace=#{app} --containers`
|
49
52
|
end
|
50
53
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
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
|
67
|
-
|
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
|
data/lib/seira/random.rb
CHANGED
@@ -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
|
data/lib/seira/redis.rb
CHANGED
@@ -35,11 +35,7 @@ module Seira
|
|
35
35
|
private
|
36
36
|
|
37
37
|
def run_list
|
38
|
-
|
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: '
|
51
|
+
size: '1Gi'
|
56
52
|
},
|
57
53
|
resources: {
|
58
54
|
requests: {
|
59
|
-
cpu: '
|
60
|
-
memory: '
|
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] = '
|
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] = '
|
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 =
|
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
|
data/lib/seira/secrets.rb
CHANGED
@@ -2,37 +2,34 @@ require 'json'
|
|
2
2
|
require 'base64'
|
3
3
|
|
4
4
|
# Example usages:
|
5
|
-
# seira staging specs secret set RAILS_ENV
|
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, :
|
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
|
-
|
26
|
+
validate_single_key
|
30
27
|
run_get
|
31
28
|
when 'set'
|
32
|
-
|
29
|
+
validate_keys_and_values
|
33
30
|
run_set
|
34
31
|
when 'unset'
|
35
|
-
|
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
|
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']
|
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
|
-
|
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
|
data/lib/seira/version.rb
CHANGED
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.
|
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
|
+
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
|