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.
@@ -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