seira 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
@@ -0,0 +1,3 @@
1
+ ruby:
2
+ enabled: true
3
+ config_file: .rubocop.yml
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -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
@@ -0,0 +1 @@
1
+ 2.4.1
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.1
5
+ before_install: gem install bundler -v 1.14.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in seira.gemspec
4
+ gemspec
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.
@@ -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
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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__)
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'seira'
4
+
5
+ Seira::Runner.new.run
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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