seira 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/.gitignore
ADDED
data/.hound.yml
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
AllCops:
|
2
|
+
Exclude:
|
3
|
+
- bin/**/*
|
4
|
+
|
5
|
+
TargetRubyVersion: 2.4.1
|
6
|
+
|
7
|
+
Rails:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
Style/CollectionMethods:
|
11
|
+
Enabled: true
|
12
|
+
PreferredMethods:
|
13
|
+
collect: 'map'
|
14
|
+
collect!: 'map!'
|
15
|
+
inject: 'reduce'
|
16
|
+
|
17
|
+
Style/FrozenStringLiteralComment:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Layout/AccessModifierIndentation:
|
21
|
+
Enabled: false
|
22
|
+
|
23
|
+
Style/BlockDelimiters:
|
24
|
+
Enabled: false
|
25
|
+
|
26
|
+
Style/BracesAroundHashParameters:
|
27
|
+
Enabled: false
|
28
|
+
|
29
|
+
Style/Documentation:
|
30
|
+
Enabled: false # Was annoying, not needed
|
31
|
+
|
32
|
+
Layout/DotPosition:
|
33
|
+
EnforcedStyle: trailing
|
34
|
+
|
35
|
+
Naming/FileName:
|
36
|
+
Enabled: false # is really annoying and not good
|
37
|
+
|
38
|
+
Style/GuardClause:
|
39
|
+
Enabled: false
|
40
|
+
|
41
|
+
Style/Not:
|
42
|
+
Enabled: false
|
43
|
+
|
44
|
+
Style/NumericPredicate:
|
45
|
+
Enabled: false
|
46
|
+
|
47
|
+
Style/RedundantSelf:
|
48
|
+
Enabled: false
|
49
|
+
|
50
|
+
Style/SignalException:
|
51
|
+
Enabled: false
|
52
|
+
|
53
|
+
Style/StringLiterals:
|
54
|
+
Enabled: false
|
55
|
+
|
56
|
+
Style/TrailingCommaInLiteral:
|
57
|
+
Enabled: false
|
58
|
+
|
59
|
+
Style/TrailingCommaInArguments:
|
60
|
+
Enabled: false
|
61
|
+
|
62
|
+
Style/SymbolArray:
|
63
|
+
Enabled: false
|
64
|
+
|
65
|
+
Metrics/AbcSize:
|
66
|
+
Enabled: false
|
67
|
+
|
68
|
+
Metrics/BlockLength:
|
69
|
+
Exclude:
|
70
|
+
- 'spec/**/*'
|
71
|
+
|
72
|
+
Metrics/ClassLength:
|
73
|
+
Enabled: false
|
74
|
+
|
75
|
+
Metrics/CyclomaticComplexity:
|
76
|
+
Enabled: false
|
77
|
+
|
78
|
+
Metrics/LineLength:
|
79
|
+
Enabled: false
|
80
|
+
|
81
|
+
Metrics/MethodLength:
|
82
|
+
Enabled: false
|
83
|
+
|
84
|
+
Metrics/PerceivedComplexity:
|
85
|
+
Enabled: false
|
86
|
+
|
87
|
+
Lint/UselessAssignment:
|
88
|
+
Enabled: false
|
89
|
+
|
90
|
+
Rails/Delegate:
|
91
|
+
Enabled: false
|
92
|
+
|
93
|
+
Rails/HasAndBelongsToMany:
|
94
|
+
Enabled: false
|
95
|
+
|
96
|
+
Rails/Output:
|
97
|
+
Enabled: false # we use puts for things like migrations
|
98
|
+
|
99
|
+
Rails/Blank:
|
100
|
+
Enabled: false
|
101
|
+
|
102
|
+
# We copy the 'default.yml' file provided by rubocop gem repo into the config/rubocop.yml file
|
103
|
+
# in order to make upgrades easy (just drop the new default in). This ensures we get the lastest
|
104
|
+
# config file when we upgrade with all new cops.
|
105
|
+
# Any non-default configuration should go into this file so that when we upgrade the dropped in
|
106
|
+
# new default file does not override any of our configurations.
|
107
|
+
# NOTE: Inorder to prioritize this file's declaration, we must inherit at bottom of file
|
108
|
+
inherit_from:
|
109
|
+
- .default-rubocop.yml
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.1
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Handshake
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# Seira
|
2
|
+
An opinionated library for building applications on Kubernetes.
|
3
|
+
|
4
|
+
## What does the name mean?
|
5
|
+
Following Kubernetes naming pattern, Seira (Seirá) is greek for "order" or "the state of being well arranged".
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'seira'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install seira
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/sgringwe/seira.
|
36
|
+
|
37
|
+
|
38
|
+
## License
|
39
|
+
|
40
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
41
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "seira"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/seira
ADDED
data/bin/setup
ADDED
data/lib/seira.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'highline/import'
|
3
|
+
|
4
|
+
require "seira/version"
|
5
|
+
require 'seira/app'
|
6
|
+
require 'seira/cluster'
|
7
|
+
require 'seira/memcached'
|
8
|
+
require 'seira/pods'
|
9
|
+
require 'seira/proxy'
|
10
|
+
require 'seira/random'
|
11
|
+
require 'seira/redis'
|
12
|
+
require 'seira/secrets'
|
13
|
+
require 'seira/settings'
|
14
|
+
require 'seira/setup'
|
15
|
+
|
16
|
+
# A base runner class that does base checks and then delegates the actual
|
17
|
+
# work for the command to a class in lib/seira folder.
|
18
|
+
module Seira
|
19
|
+
class Runner
|
20
|
+
CATEGORIES = {
|
21
|
+
'secrets' => Seira::Secrets,
|
22
|
+
'pods' => Seira::Pods,
|
23
|
+
'redis' => Seira::Redis,
|
24
|
+
'memcached' => Seira::Memcached,
|
25
|
+
'app' => Seira::App,
|
26
|
+
'cluster' => Seira::Cluster,
|
27
|
+
'proxy' => Seira::Proxy,
|
28
|
+
'setup' => Seira::Setup
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
attr_reader :cluster, :app, :category, :action, :args
|
32
|
+
attr_reader :settings
|
33
|
+
|
34
|
+
# Pop from beginning repeatedly for the first 4 main args, and then take the remaining back to original order for
|
35
|
+
# the remaining args
|
36
|
+
def initialize
|
37
|
+
@settings = Seira::Settings.new
|
38
|
+
|
39
|
+
reversed_args = ARGV.reverse.map(&:chomp)
|
40
|
+
|
41
|
+
# The cluster and proxy command are not specific to any app, so that
|
42
|
+
# arg is not in the ARGV array and should be skipped over
|
43
|
+
if ARGV[1] == 'cluster'
|
44
|
+
cluster = reversed_args.pop
|
45
|
+
@category = reversed_args.pop
|
46
|
+
@action = reversed_args.pop
|
47
|
+
@args = reversed_args.reverse
|
48
|
+
elsif ARGV[1] == 'proxy'
|
49
|
+
cluster = reversed_args.pop
|
50
|
+
@category = reversed_args.pop
|
51
|
+
elsif ARGV[0] == 'setup'
|
52
|
+
@category = reversed_args.pop
|
53
|
+
cluster = reversed_args.pop
|
54
|
+
else
|
55
|
+
cluster = reversed_args.pop
|
56
|
+
@app = reversed_args.pop
|
57
|
+
@category = reversed_args.pop
|
58
|
+
@action = reversed_args.pop
|
59
|
+
@args = reversed_args.reverse
|
60
|
+
end
|
61
|
+
|
62
|
+
@cluster = @settings.full_cluster_name_for_shorthand(cluster)
|
63
|
+
end
|
64
|
+
|
65
|
+
def run
|
66
|
+
if category == 'setup'
|
67
|
+
Seira::Setup.new(arg: cluster, settings: settings).run
|
68
|
+
exit(0)
|
69
|
+
end
|
70
|
+
|
71
|
+
base_validations
|
72
|
+
|
73
|
+
command_class = CATEGORIES[category]
|
74
|
+
|
75
|
+
unless command_class
|
76
|
+
puts "Unknown command specified. Usage: 'seira <cluster> <app> <category> <action> <args..>'."
|
77
|
+
puts "Valid categories are: #{CATEGORIES.keys.join(', ')}"
|
78
|
+
exit(1)
|
79
|
+
end
|
80
|
+
|
81
|
+
if category == 'cluster'
|
82
|
+
perform_action_validation(klass: command_class, action: action)
|
83
|
+
command_class.new(action: action, args: args, context: passed_context, settings: settings).run
|
84
|
+
elsif category == 'proxy'
|
85
|
+
command_class.new.run
|
86
|
+
else
|
87
|
+
perform_action_validation(klass: command_class, action: action)
|
88
|
+
command_class.new(app: app, action: action, args: args, context: passed_context).run
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def passed_context
|
95
|
+
{
|
96
|
+
cluster: cluster
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
def base_validations
|
101
|
+
# The first arg must always be the cluster. This ensures commands are not run by
|
102
|
+
# accident on the wrong kubernetes cluster or gcloud project.
|
103
|
+
exit(1) unless Seira::Cluster.new(action: nil, args: nil, context: nil, settings: settings).switch(target_cluster: cluster, verbose: false)
|
104
|
+
exit(0) if simple_cluster_change?
|
105
|
+
end
|
106
|
+
|
107
|
+
def perform_action_validation(klass:, action:)
|
108
|
+
return true if simple_cluster_change?
|
109
|
+
|
110
|
+
unless klass == Seira::Cluster || settings.valid_apps.include?(app)
|
111
|
+
puts "Invalid app name specified"
|
112
|
+
exit(1)
|
113
|
+
end
|
114
|
+
|
115
|
+
unless klass::VALID_ACTIONS.include?(action)
|
116
|
+
puts "Invalid action specified. Valid actions are: #{klass::VALID_ACTIONS.join(', ')}"
|
117
|
+
exit(1)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def simple_cluster_change?
|
122
|
+
app.nil? && category.nil? # Special case where user is simply changing environments
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/lib/seira/app.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'base64'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
# Example usages:
|
6
|
+
# seira staging specs app bootstrap
|
7
|
+
module Seira
|
8
|
+
class App
|
9
|
+
VALID_ACTIONS = %w[bootstrap apply upgrade restart].freeze
|
10
|
+
|
11
|
+
attr_reader :app, :action, :args, :context
|
12
|
+
|
13
|
+
def initialize(app:, action:, args:, context:)
|
14
|
+
@app = app
|
15
|
+
@action = action
|
16
|
+
@args = args
|
17
|
+
@context = context
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
case action
|
22
|
+
when 'bootstrap'
|
23
|
+
run_bootstrap
|
24
|
+
when 'apply'
|
25
|
+
run_apply
|
26
|
+
when 'upgrade'
|
27
|
+
run_upgrade
|
28
|
+
when 'restart'
|
29
|
+
run_restart
|
30
|
+
else
|
31
|
+
fail "Unknown command encountered"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_restart
|
36
|
+
# TODO
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def run_bootstrap
|
42
|
+
bootstrap_main_secret
|
43
|
+
bootstrap_cloudsql_secret
|
44
|
+
bootstrap_gcr_secret
|
45
|
+
|
46
|
+
puts "Successfully installed"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Kube vanilla based upgrade
|
50
|
+
def run_apply
|
51
|
+
destination = "tmp/#{context[:cluster]}/#{app}"
|
52
|
+
revision = ENV['REVISION']
|
53
|
+
|
54
|
+
if revision.nil?
|
55
|
+
current_image = `kubectl get deployment --namespace=#{app} -l app=#{app},tier=web -o=jsonpath='{$.items[:1].spec.template.spec.containers[:1].image}'`.strip.chomp
|
56
|
+
current_revision = current_image.split(':').last
|
57
|
+
exit(1) unless HighLine.agree("No REVISION specified. Use current deployment revision '#{current_revision}'?")
|
58
|
+
revision = current_revision
|
59
|
+
end
|
60
|
+
|
61
|
+
replacement_hash = { 'REVISION' => revision }
|
62
|
+
|
63
|
+
replacement_hash.each do |k, v|
|
64
|
+
next unless v.nil? || v == ''
|
65
|
+
puts "Found nil or blank value for replacement hash key #{k}. Aborting!"
|
66
|
+
exit(1)
|
67
|
+
end
|
68
|
+
|
69
|
+
find_and_replace_revision(
|
70
|
+
source: "kubernetes/#{context[:cluster]}/#{app}",
|
71
|
+
destination: destination,
|
72
|
+
replacement_hash: replacement_hash
|
73
|
+
)
|
74
|
+
|
75
|
+
puts "Running 'kubectl apply -f #{destination}'"
|
76
|
+
system("kubectl apply -f #{destination}")
|
77
|
+
end
|
78
|
+
|
79
|
+
def bootstrap_main_secret
|
80
|
+
puts "Creating main secret and namespace..."
|
81
|
+
main_secret_name = Seira::Secrets.new(app: app, action: action, args: args, context: context).main_secret_name
|
82
|
+
|
83
|
+
# 'internal' is a unique cluster/project "cluster". It always means production in terms of rails app.
|
84
|
+
rails_env =
|
85
|
+
if context[:cluster] == 'internal'
|
86
|
+
'production'
|
87
|
+
else
|
88
|
+
context[:cluster]
|
89
|
+
end
|
90
|
+
|
91
|
+
puts `kubectl create secret generic #{main_secret_name} --namespace #{app} --from-literal=RAILS_ENV=#{rails_env} --from-literal=RACK_ENV=#{rails_env}`
|
92
|
+
end
|
93
|
+
|
94
|
+
# We use a secret in our container to use a service account to connect to our cloudsql databases. The secret in 'default'
|
95
|
+
# namespace can't be used in this namespace, so copy it over to our namespace.
|
96
|
+
def bootstrap_gcr_secret
|
97
|
+
secrets = Seira::Secrets.new(app: app, action: action, args: args, context: context)
|
98
|
+
secrets.copy_secret_across_namespace(key: 'gcr-secret', from: 'default', to: app)
|
99
|
+
end
|
100
|
+
|
101
|
+
# We use a secret in our container to use a service account to connect to our docker registry. The secret in 'default'
|
102
|
+
# namespace can't be used in this namespace, so copy it over to our namespace.
|
103
|
+
def bootstrap_cloudsql_secret
|
104
|
+
secrets = Seira::Secrets.new(app: app, action: action, args: args, context: context)
|
105
|
+
secrets.copy_secret_across_namespace(key: 'cloudsql-credentials', from: 'default', to: app)
|
106
|
+
end
|
107
|
+
|
108
|
+
def find_and_replace_revision(source:, destination:, replacement_hash:)
|
109
|
+
puts "Copying source yaml from #{source} to #{destination}"
|
110
|
+
FileUtils::mkdir_p destination # Create the nested directory
|
111
|
+
FileUtils.copy_entry source, destination
|
112
|
+
|
113
|
+
# Iterate through each yaml file and find/replace and save
|
114
|
+
puts "Iterating #{destination} files find/replace revision information"
|
115
|
+
Dir.foreach(destination) do |item|
|
116
|
+
next if item == '.' || item == '..'
|
117
|
+
|
118
|
+
text = File.read("#{destination}/#{item}")
|
119
|
+
|
120
|
+
new_contents = text
|
121
|
+
replacement_hash.each do |key, value|
|
122
|
+
new_contents.gsub!(key, value)
|
123
|
+
end
|
124
|
+
|
125
|
+
# To write changes to the file, use:
|
126
|
+
File.open("#{destination}/#{item}", 'w') { |file| file.write(new_contents) }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|