keel 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/.gitignore +9 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +104 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/keel.gemspec +28 -0
- data/lib/generators/keel/config_generator.rb +13 -0
- data/lib/generators/keel/controller_generator.rb +59 -0
- data/lib/generators/keel/service_generator.rb +55 -0
- data/lib/generators/keel/templates/gc-controller.yml.erb +58 -0
- data/lib/generators/keel/templates/gc-service.yml.erb +15 -0
- data/lib/generators/keel/templates/gcloud.yml +19 -0
- data/lib/keel/gcloud/auth.rb +32 -0
- data/lib/keel/gcloud/cli.rb +22 -0
- data/lib/keel/gcloud/component.rb +24 -0
- data/lib/keel/gcloud/config.rb +109 -0
- data/lib/keel/gcloud/kubernetes/namespace.rb +62 -0
- data/lib/keel/gcloud/kubernetes/pod.rb +96 -0
- data/lib/keel/gcloud/kubernetes/replication_controller.rb +117 -0
- data/lib/keel/gcloud/kubernetes.rb +3 -0
- data/lib/keel/gcloud/notifier/base.rb +39 -0
- data/lib/keel/gcloud/notifier/new_relic.rb +17 -0
- data/lib/keel/gcloud/notifier.rb +2 -0
- data/lib/keel/gcloud/prompter.rb +88 -0
- data/lib/keel/gcloud.rb +7 -0
- data/lib/keel/railtie.rb +7 -0
- data/lib/keel/version.rb +3 -0
- data/lib/keel.rb +7 -0
- data/lib/tasks/keel.rake +157 -0
- metadata +147 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Keel::GCloud
|
4
|
+
#
|
5
|
+
# Config file parser.
|
6
|
+
# Takes the YAML file and creates an object with attributes matching
|
7
|
+
# the supplied configurations.
|
8
|
+
#
|
9
|
+
# Also serves as a wrapper for the GCloud configurations API.
|
10
|
+
#
|
11
|
+
class Config
|
12
|
+
attr_accessor :cli,
|
13
|
+
:config,
|
14
|
+
:app_name,
|
15
|
+
:compute_region,
|
16
|
+
:compute_zone,
|
17
|
+
:container_app_image_path,
|
18
|
+
:container_cloud_sql_image_path,
|
19
|
+
:container_cluster,
|
20
|
+
:cloud_sql_instance,
|
21
|
+
:env_variables,
|
22
|
+
:project_id
|
23
|
+
|
24
|
+
GCLOUD_OPTIONS_LIST = [:compute_zone, :container_cluster, :project_id]
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@cli = Cli.new
|
28
|
+
@config = YAML.load_file(Rails.root.join('config', 'gcloud.yml'))
|
29
|
+
|
30
|
+
@app_name = @config[:app][:name]
|
31
|
+
@compute_region = @config[:compute][:region]
|
32
|
+
@compute_zone = @config[:compute][:zone]
|
33
|
+
@container_app_image_path = @config[:container][:app_image_path]
|
34
|
+
@container_cloud_sql_image_path = @config[:container][:cloud_sql_image_path]
|
35
|
+
@container_cluster = @config[:container][:cluster]
|
36
|
+
@cloud_sql_instance = @config[:cloud_sql_instance]
|
37
|
+
@env_variables = @config[:env_variables]
|
38
|
+
@project_id = @config[:project_id]
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Lists the GCloud configurations.
|
43
|
+
#
|
44
|
+
# @return [Array] of configurations
|
45
|
+
#
|
46
|
+
def self.list
|
47
|
+
Cli.new.execute 'gcloud config list'
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Checks if the gcloud excutable is installed.
|
52
|
+
#
|
53
|
+
# @return [Boolean]
|
54
|
+
#
|
55
|
+
def executable_installed?
|
56
|
+
@cli.system 'which gcloud'
|
57
|
+
$?.success?
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# (see #executable_installed?)
|
62
|
+
#
|
63
|
+
def executable_missing?
|
64
|
+
!executable_installed?
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Gets the application specific configurations for GCloud by checking
|
69
|
+
# if any ENV variables are set first, otherwise returning the ones
|
70
|
+
# from the config file.
|
71
|
+
# This allows the developer to override a config param locally without
|
72
|
+
# changing the YAML file.
|
73
|
+
#
|
74
|
+
# @return [Hash] of config name/value
|
75
|
+
#
|
76
|
+
def app_config
|
77
|
+
values = {}
|
78
|
+
|
79
|
+
GCLOUD_OPTIONS_LIST.each do |option|
|
80
|
+
values[option] = ENV[option.to_s.upcase] || self.send(option)
|
81
|
+
end
|
82
|
+
|
83
|
+
return values
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Checks if the user's system is configured properly or if it's missing
|
88
|
+
# any configurations.
|
89
|
+
#
|
90
|
+
# @return [Boolean]
|
91
|
+
#
|
92
|
+
def system_configured?
|
93
|
+
system_configs = self.class.list
|
94
|
+
desired_configs = self.app_config
|
95
|
+
|
96
|
+
desired_configs.values.all? { |config| system_configs.include? config }
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Sets the appropriate GCloud configurations properties for the system
|
101
|
+
# if they are not already set.
|
102
|
+
#
|
103
|
+
def set_properties
|
104
|
+
@cli.system "gcloud config set compute/zone #{self.compute_zone}"
|
105
|
+
@cli.system "gcloud config set container/cluster #{self.container_cluster}"
|
106
|
+
@cli.system "gcloud config set project #{self.project_id}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Keel::GCloud
|
4
|
+
module Kubernetes
|
5
|
+
#
|
6
|
+
# A class to represent a Kubernetes Namespace.
|
7
|
+
# It is a simplified view of what Kubernetes returns with only
|
8
|
+
# the necessary information required to perform the operations needed.
|
9
|
+
#
|
10
|
+
class Namespace
|
11
|
+
attr_accessor :cli, :name, :status, :uid
|
12
|
+
|
13
|
+
def initialize **params
|
14
|
+
@name = params[:name]
|
15
|
+
@status = params[:status]
|
16
|
+
@uid = params[:uid]
|
17
|
+
|
18
|
+
@cli = Cli.new
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Parses the returned YAML into objects of the Namespace class.
|
23
|
+
#
|
24
|
+
# @param yaml [Hash] the parsed result of the API call
|
25
|
+
# @return [Array<Namespace>] an array of Namespace objects
|
26
|
+
#
|
27
|
+
def self.from_yaml yaml
|
28
|
+
yaml['items'].map do |item|
|
29
|
+
params = {
|
30
|
+
name: item['metadata']['name'],
|
31
|
+
status: item['status']['phase'],
|
32
|
+
uid: item['metadata']['uid'],
|
33
|
+
}
|
34
|
+
|
35
|
+
self.new params
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Fetches all the namespaces from Kubernetes.
|
41
|
+
#
|
42
|
+
# @return [Hash] the parsed result of the API call
|
43
|
+
#
|
44
|
+
def self.fetch_all
|
45
|
+
command = 'kubectl get namespaces -o yaml'
|
46
|
+
namespaces_yaml = YAML.load Cli.new.execute(command)
|
47
|
+
return false unless namespaces_yaml
|
48
|
+
|
49
|
+
self.from_yaml namespaces_yaml
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Checks if the namespace is active by comparing the status attribute.
|
54
|
+
#
|
55
|
+
# @return [Boolean]
|
56
|
+
#
|
57
|
+
def active?
|
58
|
+
'Active' == self.status
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Keel::GCloud
|
4
|
+
module Kubernetes
|
5
|
+
#
|
6
|
+
# A class to represent a Kubernetes Pod.
|
7
|
+
# It is a simplified view of what Kubernetes returns with only
|
8
|
+
# the necessary information required to perform the operations needed.
|
9
|
+
#
|
10
|
+
class Pod
|
11
|
+
attr_accessor :cli, :app, :name, :namespace, :status, :uid
|
12
|
+
|
13
|
+
def initialize **params
|
14
|
+
@app = params[:app]
|
15
|
+
@name = params[:name]
|
16
|
+
@namespace = params[:namespace]
|
17
|
+
@status = params[:status]
|
18
|
+
@uid = params[:uid]
|
19
|
+
|
20
|
+
@cli = Cli.new
|
21
|
+
@prompter = Prompter.new
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Parses the returned YAML into objects of the Pod class.
|
26
|
+
#
|
27
|
+
# @param yaml [Hash] the parsed result of the API call
|
28
|
+
# @return [Array<Pod>] an array of Pod objects
|
29
|
+
#
|
30
|
+
def self.from_yaml yaml
|
31
|
+
yaml['items'].map do |item|
|
32
|
+
params = {
|
33
|
+
app: item['metadata']['labels']['app'],
|
34
|
+
name: item['metadata']['name'],
|
35
|
+
namespace: item['metadata']['namespace'],
|
36
|
+
status: item['status']['phase'],
|
37
|
+
uid: item['metadata']['uid'],
|
38
|
+
}
|
39
|
+
|
40
|
+
self.new params
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Fetches all the pods from Kubernetes.
|
46
|
+
#
|
47
|
+
# @param env [String] the namespace/environment for which to fetch the pods
|
48
|
+
# @param app [String] the app for which to fetch the pods
|
49
|
+
# @return [Hash] the parsed result of the API call
|
50
|
+
#
|
51
|
+
def self.fetch_all env, app
|
52
|
+
command = "kubectl get po --namespace=#{env} -l app=#{app} -o yaml"
|
53
|
+
rcs_yaml = YAML.load Cli.new.execute(command)
|
54
|
+
return false unless rcs_yaml
|
55
|
+
|
56
|
+
self.from_yaml rcs_yaml
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Checks if the namespace is running by comparing the status attribute.
|
61
|
+
#
|
62
|
+
# @return [Boolean]
|
63
|
+
#
|
64
|
+
def running?
|
65
|
+
'Running' == self.status
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Deletes the pod.
|
70
|
+
#
|
71
|
+
# @return [Boolean] whether the call succeeded or not
|
72
|
+
#
|
73
|
+
def delete
|
74
|
+
@cli.system "kubectl delete po #{self.name} --namespace=#{self.namespace}"
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Fetches the logs for the pod.
|
79
|
+
# If the param +tail+ is set to true, it tails the logs.
|
80
|
+
#
|
81
|
+
# @param tail [Boolean, nil] flag whether to tail the logs or not
|
82
|
+
# @return [Boolean] whether the call succeeded or not
|
83
|
+
#
|
84
|
+
def logs tail=nil
|
85
|
+
f = tail ? '-f ' : ''
|
86
|
+
|
87
|
+
if tail
|
88
|
+
@prompter.print 'Fetching logs...'
|
89
|
+
@prompter.print 'Use Ctrl-C to stop'
|
90
|
+
end
|
91
|
+
|
92
|
+
@cli.system "kubectl logs #{f}#{self.name} --namespace=#{self.namespace} -c=#{self.app}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Keel::GCloud
|
4
|
+
module Kubernetes
|
5
|
+
#
|
6
|
+
# A class to represent a Kubernetes ReplicationController.
|
7
|
+
# It is a simplified view of what Kubernetes returns with only
|
8
|
+
# the necessary information required to perform the operations needed.
|
9
|
+
#
|
10
|
+
class ReplicationController
|
11
|
+
attr_accessor :containers, :name, :namespace, :original, :uid
|
12
|
+
|
13
|
+
def initialize **params
|
14
|
+
@containers = params[:containers]
|
15
|
+
@name = params[:name]
|
16
|
+
@namespace = params[:namespace]
|
17
|
+
@uid = params[:uid]
|
18
|
+
|
19
|
+
@original = params[:original]
|
20
|
+
@original['metadata'].delete 'creationTimestamp'
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Parses the returned YAML into objects of the ReplicationController class.
|
25
|
+
#
|
26
|
+
# @param yaml [Hash] the parsed result of the API call
|
27
|
+
# @return [Array<ReplicationController>] an array of ReplicationController objects
|
28
|
+
#
|
29
|
+
def self.from_yaml yaml
|
30
|
+
yaml['items'].map do |item|
|
31
|
+
params = {
|
32
|
+
containers: item['spec']['template']['spec']['containers'],
|
33
|
+
name: item['metadata']['name'],
|
34
|
+
namespace: item['metadata']['namespace'],
|
35
|
+
original: item,
|
36
|
+
uid: item['metadata']['uid'],
|
37
|
+
}
|
38
|
+
|
39
|
+
self.new params
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Fetches all the controllers from Kubernetes.
|
45
|
+
#
|
46
|
+
# @param env [String] the namespace/environment for which to fetch the controllers
|
47
|
+
# @param app [String] the app for which to fetch the controllers
|
48
|
+
# @return [Hash] the parsed result of the API call
|
49
|
+
#
|
50
|
+
def self.fetch_all env, app
|
51
|
+
command = "kubectl get rc --namespace=#{env} -l app=#{app} -o yaml"
|
52
|
+
rcs_yaml = YAML.load Cli.new.execute(command)
|
53
|
+
return false unless rcs_yaml
|
54
|
+
|
55
|
+
self.from_yaml rcs_yaml
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Replaces the controller's specifications with a new one.
|
60
|
+
#
|
61
|
+
# @param file [File] the new specifications file
|
62
|
+
# @return [Boolean] whether the call succeeded or not
|
63
|
+
#
|
64
|
+
def self.replace file
|
65
|
+
Cli.new.system "kubectl replace -f #{file}"
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Get the YAML representation of the controller.
|
70
|
+
#
|
71
|
+
# @return [String] the YAML format
|
72
|
+
#
|
73
|
+
def to_yaml
|
74
|
+
self.original.to_yaml
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Writes the current specifications to a file.
|
79
|
+
#
|
80
|
+
# @param filename [String] the name of the file to write to
|
81
|
+
# @return [Boolean] result of the operation
|
82
|
+
#
|
83
|
+
def to_file filename
|
84
|
+
File.open(filename, 'w') do |io|
|
85
|
+
io.write self.to_yaml
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# Increments the number of replicas.
|
91
|
+
#
|
92
|
+
def increment_replica_count
|
93
|
+
self.original['spec']['replicas'] += 1
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
# Decrements the number of replicas.
|
98
|
+
#
|
99
|
+
def decrement_replica_count
|
100
|
+
self.original['spec']['replicas'] -= 1
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# Updates the specifications of a controller on Kubernetes
|
105
|
+
# with the latest specs.
|
106
|
+
#
|
107
|
+
# (see #to_file)
|
108
|
+
# (see #replace)
|
109
|
+
#
|
110
|
+
def update
|
111
|
+
tmp_file = Rails.root.join('tmp', 'deployment-rc.yml')
|
112
|
+
self.to_file tmp_file
|
113
|
+
self.class.replace tmp_file
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class NotImplemented < StandardError; end
|
2
|
+
|
3
|
+
module Keel::GCloud
|
4
|
+
module Notifier
|
5
|
+
#
|
6
|
+
# Base class to be inherited for notifiers that are used to send any
|
7
|
+
# notifications when a deployment is complete.
|
8
|
+
#
|
9
|
+
class Base
|
10
|
+
attr_accessor :cli, :env, :sha, :user
|
11
|
+
|
12
|
+
def initialize env:, sha:
|
13
|
+
@env = env
|
14
|
+
@sha = sha
|
15
|
+
|
16
|
+
@cli = Cli.new
|
17
|
+
|
18
|
+
set_user
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Determines the user id to be sent with the nofications
|
23
|
+
# based on the ENV variable if set, otherwise on the system user.
|
24
|
+
#
|
25
|
+
def set_user
|
26
|
+
unless ENV['DEPLOY_USERNAME'].nil? || ENV['DEPLOY_USERNAME'] == ''
|
27
|
+
return @user = ENV['DEPLOY_USERNAME']
|
28
|
+
end
|
29
|
+
|
30
|
+
whoami = @cli.execute 'whoami'
|
31
|
+
@user = whoami.chomp
|
32
|
+
end
|
33
|
+
|
34
|
+
def notify # :nodoc:
|
35
|
+
raise NotImplemented
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Keel::GCloud
|
2
|
+
module Notifier
|
3
|
+
#
|
4
|
+
# Notifier for NewRelic that send a deployment notification.
|
5
|
+
#
|
6
|
+
class NewRelic < Base
|
7
|
+
#
|
8
|
+
# Sends a notification to NewRelic of a new deployment with the
|
9
|
+
# appropriate attributes.
|
10
|
+
#
|
11
|
+
def notify
|
12
|
+
env = @env == 'production' ? 'production' : 'staging'
|
13
|
+
@cli.system "newrelic deployments -e #{env} -r #{@sha} -u #{@user}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'inquirer'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
module Keel::GCloud
|
5
|
+
#
|
6
|
+
# A helper to output to the command line and prompt the user for input.
|
7
|
+
#
|
8
|
+
class Prompter
|
9
|
+
#
|
10
|
+
# Prints the message with coloring based on the level param.
|
11
|
+
#
|
12
|
+
# @param message [String] the message to print
|
13
|
+
# @param level [String, nil] the level that determines the color
|
14
|
+
#
|
15
|
+
def print message, level=nil
|
16
|
+
case level
|
17
|
+
when :success
|
18
|
+
puts message.green
|
19
|
+
when :error
|
20
|
+
puts message.red
|
21
|
+
when :info
|
22
|
+
puts message.blue
|
23
|
+
else
|
24
|
+
puts message
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Prompts the user to select the namespace from a list.
|
30
|
+
# If a default is provided it returns that instead.
|
31
|
+
#
|
32
|
+
# @param namespaces [Array<Namespace>] the array of namespaces to choose from
|
33
|
+
# @param default [String, nil] the default choice
|
34
|
+
#
|
35
|
+
def prompt_for_namespace namespaces, default=nil
|
36
|
+
return default unless default.blank?
|
37
|
+
|
38
|
+
options = namespaces.map { |namespace| namespace.name }
|
39
|
+
index = Ask.list 'Please choose an environment (destination)', options
|
40
|
+
options[index]
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Prompts the user to provide a SHA.
|
45
|
+
# If a default is provided it returns that instead.
|
46
|
+
#
|
47
|
+
# @param default [String, nil] the default choice
|
48
|
+
#
|
49
|
+
def prompt_for_sha default=nil
|
50
|
+
return default unless default.blank?
|
51
|
+
|
52
|
+
# Get current git SHA
|
53
|
+
current_sha = `git rev-parse --short HEAD`.lines.first.split(' ')[0]
|
54
|
+
Ask.input 'Git SHA', default: current_sha
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Prompts the user to provide a datbase URL.
|
59
|
+
# If a default is provided it returns that instead.
|
60
|
+
#
|
61
|
+
# @param default [String, nil] the default choice
|
62
|
+
#
|
63
|
+
def prompt_for_database_url default=nil
|
64
|
+
return default unless default.blank?
|
65
|
+
|
66
|
+
Ask.input 'Database URL'
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Prompts the user to provide a secret key.
|
71
|
+
# If a default is provided it returns that instead.
|
72
|
+
#
|
73
|
+
# @param default [String, nil] the default choice
|
74
|
+
#
|
75
|
+
def prompt_for_secret_key default=nil
|
76
|
+
return default unless default.blank?
|
77
|
+
|
78
|
+
Ask.input 'Secret key'
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Prompts the user to choose if they want to tail the logs or not.
|
83
|
+
#
|
84
|
+
def prompt_for_tailing_logs
|
85
|
+
Ask.confirm "To tail or not to tail? (ie: -f, --follow[=false]: Specify if the logs should be streamed.)", default: true
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/keel/gcloud.rb
ADDED
data/lib/keel/railtie.rb
ADDED
data/lib/keel/version.rb
ADDED