keel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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