keel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ require_relative 'kubernetes/namespace'
2
+ require_relative 'kubernetes/replication_controller'
3
+ require_relative 'kubernetes/pod'
@@ -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,2 @@
1
+ require_relative 'notifier/base'
2
+ require_relative 'notifier/new_relic'
@@ -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
@@ -0,0 +1,7 @@
1
+ require_relative 'gcloud/auth'
2
+ require_relative 'gcloud/cli'
3
+ require_relative 'gcloud/component'
4
+ require_relative 'gcloud/config'
5
+ require_relative 'gcloud/kubernetes'
6
+ require_relative 'gcloud/notifier'
7
+ require_relative 'gcloud/prompter'
@@ -0,0 +1,7 @@
1
+ module Keel
2
+ class Railtie < Rails::Railtie # :nodoc: all
3
+ rake_tasks do
4
+ load 'tasks/keel.rake'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module Keel
2
+ VERSION = "0.1.0" # :nodoc:
3
+ end
data/lib/keel.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'keel/version'
2
+ require 'keel/railtie' if defined?(Rails)
3
+ require 'keel/gcloud'
4
+
5
+ # {file:README.md Usage#usage}
6
+ module Keel
7
+ end