hippo-cli 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/hippo/cli.rb CHANGED
@@ -3,28 +3,32 @@
3
3
  require 'securerandom'
4
4
  require 'hippo/manifest'
5
5
  require 'hippo/deployment_monitor'
6
+ require 'hippo/working_directory'
6
7
 
7
8
  module Hippo
8
9
  class CLI
9
- attr_reader :manifest
10
+ attr_reader :wd
10
11
  attr_reader :stage
11
12
 
12
13
  # Initialize a new CLI instance
13
14
  #
14
- # @param manifest [Hippo::Manifest]
15
- # @param stage [Hippo::Stage]
15
+ # @param wd [Hippo::WorkingDirectory]
16
16
  # @return [Hippo::CLI]
17
- def initialize(manifest, stage)
18
- @manifest = manifest
17
+ def initialize(wd, stage)
18
+ @wd = wd
19
19
  @stage = stage
20
20
  end
21
21
 
22
+ def manifest
23
+ wd.manifest
24
+ end
25
+
22
26
  # Verify image existence
23
27
  #
24
28
  # @return [void]
25
29
  def verify_image_existence
26
30
  missing = 0
27
- @stage.images.each do |_, image|
31
+ stage.images.each do |_, image|
28
32
  if image.exists?
29
33
  puts "Image for #{image.name} exists at #{image.image_url}"
30
34
  else
@@ -43,7 +47,7 @@ module Hippo
43
47
  #
44
48
  # @return [void]
45
49
  def preflight
46
- if @stage.context.nil?
50
+ if stage.context.nil?
47
51
  puts "\e[33mStage does not specify a context. The current context specified"
48
52
  puts "by the kubectl config will be used (#{Hippo.current_kubectl_context}).\e[0m"
49
53
  puts
@@ -61,9 +65,9 @@ module Hippo
61
65
  {
62
66
  'kind' => 'Namespace',
63
67
  'apiVersion' => 'v1',
64
- 'metadata' => { 'name' => @stage.namespace, 'labels' => { 'name' => @stage.namespace } }
68
+ 'metadata' => { 'name' => stage.namespace, 'labels' => { 'name' => stage.namespace } }
65
69
  },
66
- @stage
70
+ stage
67
71
  )
68
72
  apply([od], 'namespace')
69
73
  end
@@ -72,19 +76,19 @@ module Hippo
72
76
  #
73
77
  # @return [void]
74
78
  def apply_config
75
- apply(@stage.configs, 'configuration')
79
+ apply(stage.configs, 'configuration')
76
80
  end
77
81
 
78
82
  # Install all packages
79
83
  #
80
84
  # @return [void]
81
85
  def install_all_packages
82
- if @stage.packages.empty?
86
+ if stage.packages.empty?
83
87
  puts 'There are no packages to install'
84
88
  return
85
89
  end
86
90
 
87
- @stage.packages.values.each do |package|
91
+ stage.packages.values.each do |package|
88
92
  if package.installed?
89
93
  puts "#{package.name} is already installed. Upgrading..."
90
94
  package.upgrade
@@ -94,14 +98,14 @@ module Hippo
94
98
  end
95
99
  end
96
100
 
97
- puts "Finished with #{@stage.packages.size} #{@stage.packages.size == 1 ? 'package' : 'packages'}"
101
+ puts "Finished with #{stage.packages.size} #{stage.packages.size == 1 ? 'package' : 'packages'}"
98
102
  end
99
103
 
100
104
  # Apply all services, ingresses and policies
101
105
  #
102
106
  # @return [void]
103
107
  def apply_services
104
- apply(@stage.services, 'service')
108
+ apply(stage.services, 'service')
105
109
  end
106
110
 
107
111
  # Run all deploy jobs
@@ -123,7 +127,7 @@ module Hippo
123
127
  # @return [void]
124
128
  def deploy
125
129
  deployment_id = SecureRandom.hex(6)
126
- deployments = @stage.deployments
130
+ deployments = stage.deployments
127
131
  if deployments.empty?
128
132
  puts 'There are no deployment objects defined'
129
133
  return true
@@ -138,7 +142,7 @@ module Hippo
138
142
  apply(deployments, 'deployment')
139
143
  puts 'Waiting for all deployments to roll out...'
140
144
 
141
- monitor = DeploymentMonitor.new(@stage, deployment_id)
145
+ monitor = DeploymentMonitor.new(stage, deployment_id)
142
146
  monitor.on_success do |poll|
143
147
  if poll.replica_sets.size == 1
144
148
  puts "\e[32mDeployment rolled out successfully\e[0m"
@@ -158,8 +162,8 @@ module Hippo
158
162
  poll.pending.each do |rs|
159
163
  puts
160
164
  name = rs.name.split('-').first
161
- puts " hippo #{@stage.name} kubectl -- describe deployment \e[35m#{name}\e[0m"
162
- puts " hippo #{@stage.name} kubectl -- logs deployment/\e[35m#{name}\e[0m --all-containers"
165
+ puts " hippo #{stage.name} kubectl -- describe deployment \e[35m#{name}\e[0m"
166
+ puts " hippo #{stage.name} kubectl -- logs deployment/\e[35m#{name}\e[0m --all-containers"
163
167
  end
164
168
  puts
165
169
  end
@@ -174,25 +178,25 @@ module Hippo
174
178
  puts "No #{type} objects found to apply"
175
179
  else
176
180
  puts "Applying #{objects.size} #{type} #{objects.size == 1 ? 'object' : 'objects'}"
177
- @stage.apply(objects)
181
+ stage.apply(objects)
178
182
  end
179
183
  end
180
184
 
181
185
  def run_jobs(type)
182
186
  puts "Running #{type} jobs"
183
- jobs = @stage.jobs(type)
187
+ jobs = stage.jobs(type)
184
188
  if jobs.empty?
185
189
  puts "There are no #{type} jobs to run"
186
190
  return true
187
191
  end
188
192
 
189
193
  jobs.each do |job|
190
- @stage.delete('job', job.name)
194
+ stage.delete('job', job.name)
191
195
  end
192
196
 
193
197
  applied_jobs = apply(jobs, 'deploy job')
194
198
 
195
- timeout, jobs = @stage.wait_for_jobs(applied_jobs.keys)
199
+ timeout, jobs = stage.wait_for_jobs(applied_jobs.keys)
196
200
  success_jobs = []
197
201
  failed_jobs = []
198
202
  jobs.each do |job|
@@ -221,22 +225,22 @@ module Hippo
221
225
  else
222
226
  '❌'
223
227
  end
224
- puts " #{icon} hippo #{@stage.name} kubectl -- logs job/#{job.name}"
228
+ puts " #{icon} hippo #{stage.name} kubectl -- logs job/#{job.name}"
225
229
  end
226
230
  puts
227
231
  result
228
232
  end
229
233
 
230
234
  class << self
231
- def setup(context)
232
- manifest = Hippo::Manifest.load_from_file(context.options[:hippofile] || './Hippofile')
235
+ def setup(_context)
236
+ wd = Hippo::WorkingDirectory.new
233
237
 
234
- stage = manifest.stages[CURRENT_STAGE]
238
+ stage = wd.stages[CURRENT_STAGE]
235
239
  if stage.nil?
236
240
  raise Error, "Invalid stage name `#{CURRENT_STAGE}`. Check this has been defined in in your stages directory with a matching name?"
237
241
  end
238
242
 
239
- new(manifest, stage)
243
+ new(wd, stage)
240
244
  end
241
245
  end
242
246
  end
data/lib/hippo/image.rb CHANGED
@@ -56,6 +56,7 @@ module Hippo
56
56
  end
57
57
 
58
58
  def exists?
59
+ return true unless tag.is_a?(RepositoryTag)
59
60
  return true if host.nil?
60
61
  return true unless can_check_for_existence?
61
62
 
@@ -51,6 +51,18 @@ module Hippo
51
51
  end
52
52
  end
53
53
 
54
+ def readme
55
+ return @readme if instance_variable_defined?('@readme')
56
+
57
+ path = File.join(@root, 'readme.txt')
58
+ unless File.file?(path)
59
+ @readme = nil
60
+ return
61
+ end
62
+
63
+ @readme = File.read(path)
64
+ end
65
+
54
66
  def template_vars
55
67
  {
56
68
  'name' => name
@@ -63,29 +75,13 @@ module Hippo
63
75
  @options['images']
64
76
  end
65
77
 
66
- # Load all stages that are available in the manifest
67
- #
68
- # @return [Hash<Symbol, Hippo::Stage>]
69
- def stages
70
- objects('stages').each_with_object({}) do |(_, objects), hash|
71
- objects.each do |obj|
72
- stage = Stage.new(self, obj)
73
- hash[stage.name] = stage
74
- end
75
- end
76
- end
77
-
78
78
  # Load all YAML objects at a given path and return them.
79
79
  #
80
80
  # @param path [String]
81
81
  # @param decorator [Proc] an optional parser to run across the raw YAML file
82
82
  # @return [Array<Hash>]
83
83
  def objects(path, decorator: nil)
84
- files = Dir[File.join(@root, path, '*.{yaml,yml}')]
85
- files.each_with_object({}) do |path, objects|
86
- file = Util.load_yaml_from_file(path, decorator: decorator)
87
- objects[path.sub(%r{\A#{@root}/}, '')] = file
88
- end
84
+ Util.load_objects_from_path(File.join(@root, path, '*.{yaml,yml}'), decorator: decorator)
89
85
  end
90
86
  end
91
87
  end
@@ -30,8 +30,9 @@ module Hippo
30
30
  return nil if @options['url'].nil?
31
31
 
32
32
  @remote_refs ||= begin
33
- puts "Getting remote refs from #{@options['url']}"
34
- Git.ls_remote(@options['url'])
33
+ Util.action "Getting remote refs from #{@options['url']}..." do
34
+ Git.ls_remote(@options['url'])
35
+ end
35
36
  end
36
37
  end
37
38
  end
@@ -15,17 +15,7 @@ module Hippo
15
15
  CIPHER = OpenSSL::Cipher.new('aes-256-gcm')
16
16
 
17
17
  def path
18
- File.join(@stage.manifest.root, 'secrets', @stage.name + '.yaml')
19
- end
20
-
21
- def secret(name)
22
- Secret.new(self, name)
23
- end
24
-
25
- def secrets
26
- Dir[File.join(root, '*.{yml,yaml}')].map do |path|
27
- secret(path.split('/').last.sub(/\.ya?ml\z/, ''))
28
- end
18
+ File.join(@stage.config_root, 'secrets.yaml')
29
19
  end
30
20
 
31
21
  # Download the current key from the Kubernetes API and set it as the
@@ -35,13 +25,24 @@ module Hippo
35
25
  def download_key
36
26
  return if @key
37
27
 
38
- value = @stage.get('secret', 'hippo-secret-key').first
39
- return if value.nil?
40
- return if value.dig('data', 'key').nil?
41
-
42
- @key = Base64.decode64(Base64.decode64(value['data']['key']))
43
- rescue Hippo::Error => e
44
- raise unless e.message =~ /not found/
28
+ Util.action 'Downloading secret encryiption key...' do |state|
29
+ begin
30
+ value = @stage.get('secret', 'hippo-secret-key').first
31
+
32
+ if value.nil? || value.dig('data', 'key').nil?
33
+ state.call('not found')
34
+ return
35
+ end
36
+
37
+ @key = Base64.decode64(Base64.decode64(value['data']['key']))
38
+ rescue Hippo::Error => e
39
+ if e.message =~ /not found/
40
+ state.call('not found')
41
+ else
42
+ raise
43
+ end
44
+ end
45
+ end
45
46
  end
46
47
 
47
48
  # Is there a key availale in this manager?
@@ -118,7 +119,7 @@ module Hippo
118
119
 
119
120
  return if exists?
120
121
 
121
- yaml = { 'example' => 'This is an example secret!' }.to_yaml
122
+ yaml = { 'example' => 'This is an example secret2!' }.to_yaml
122
123
  FileUtils.mkdir_p(File.dirname(path))
123
124
  File.open(path, 'w') { |f| f.write(encrypt(yaml)) }
124
125
  end
data/lib/hippo/stage.rb CHANGED
@@ -8,13 +8,19 @@ require 'hippo/liquid_filters'
8
8
 
9
9
  module Hippo
10
10
  class Stage
11
- attr_reader :manifest
11
+ attr_reader :wd
12
+ attr_reader :config_root
12
13
 
13
- def initialize(manifest, options)
14
- @manifest = manifest
14
+ def initialize(wd, config_root, options)
15
+ @wd = wd
16
+ @config_root = config_root
15
17
  @options = options
16
18
  end
17
19
 
20
+ def manifest
21
+ wd.manifest
22
+ end
23
+
18
24
  def name
19
25
  @options['name']
20
26
  end
@@ -40,7 +46,7 @@ module Hippo
40
46
  end
41
47
 
42
48
  def images
43
- @images ||= @manifest.images.deep_merge(@options['images'] || {}).each_with_object({}) do |(key, image), hash|
49
+ @images ||= manifest.images.deep_merge(@options['images'] || {}).each_with_object({}) do |(key, image), hash|
44
50
  hash[key] = Image.new(key, image)
45
51
  end
46
52
  end
@@ -49,14 +55,14 @@ module Hippo
49
55
  def template_vars
50
56
  @template_vars ||= begin
51
57
  {
52
- 'manifest' => @manifest.template_vars,
58
+ 'manifest' => manifest.template_vars,
53
59
  'stage-name' => name,
54
60
  'branch' => branch,
55
61
  'image-tag' => image_tag,
56
62
  'namespace' => namespace,
57
63
  'context' => context,
58
64
  'images' => images.values.each_with_object({}) { |image, hash| hash[image.name] = image.template_vars },
59
- 'config' => @manifest.config.deep_merge(config),
65
+ 'config' => manifest.config.deep_merge(config),
60
66
  'secrets' => secret_manager.all
61
67
  }
62
68
  end
@@ -75,8 +81,44 @@ module Hippo
75
81
  end
76
82
  end
77
83
 
84
+ def readme
85
+ return unless manifest.readme
86
+
87
+ decorator.call(manifest.readme)
88
+ end
89
+
90
+ # Return an array of objects that currently exist on the kubernetesa
91
+ # API.
92
+ #
93
+ # @return [Array<Hash>]
94
+ def live_objects(pruneable_only: false)
95
+ los = get(all_objects.keys.join(','), '--selector', 'app.kubernetes.io/managed-by=hippo')
96
+ los.each_with_object([]) do |live_obj, array|
97
+ local = all_objects.dig(live_obj.kind, live_obj.name)
98
+ pruneable = local.nil? && (live_obj.kind != 'Secret' && live_obj.name != 'hippo-secret-key')
99
+
100
+ next if pruneable_only && !pruneable
101
+
102
+ array << {
103
+ live: live_obj,
104
+ local: local,
105
+ pruneable: pruneable
106
+ }
107
+ end
108
+ end
109
+
110
+ # Remove any objects which are prunable
111
+ #
112
+ # @return [void]
113
+ def delete_pruneable_objects
114
+ live_objects(pruneable_only: true).each do |object|
115
+ object = object[:live]
116
+ delete(object.kind, object.name)
117
+ end
118
+ end
119
+
78
120
  def objects(path)
79
- @manifest.objects(path, decorator: decorator)
121
+ manifest.objects(path, decorator: decorator)
80
122
  end
81
123
 
82
124
  def secret_manager
@@ -112,6 +154,19 @@ module Hippo
112
154
  @jobs[type] ||= Util.create_object_definitions(objects("jobs/#{type}"), self)
113
155
  end
114
156
 
157
+ # Return an array of all objects that should be managed by Hippo
158
+ #
159
+ # @return [Hash]
160
+ def all_objects
161
+ @all_objects ||= begin
162
+ all = (deployments | services | configs | jobs('install') | jobs('deploy'))
163
+ all.each_with_object({}) do |object, hash|
164
+ hash[object.kind] ||= {}
165
+ hash[object.kind][object.name] = object
166
+ end
167
+ end
168
+ end
169
+
115
170
  # Return a hash of all packages available in the stage
116
171
  #
117
172
  # @return [Hash<String, Hippo::Package>]
@@ -145,33 +200,17 @@ module Hippo
145
200
  # @param objects [Array<Hippo::ObjectDefinition>]
146
201
  # @return [Hash]
147
202
  def apply(objects)
148
- yaml_to_apply = objects.map(&:yaml_to_apply).join("\n")
149
-
150
203
  command = ['kubectl']
151
204
  command += ['--context', context] if context
152
205
  command += ['apply', '-f', '-']
153
- Open3.popen3(command.join(' ')) do |stdin, stdout, stderr, wt|
154
- stdin.puts yaml_to_apply
155
- stdin.close
156
206
 
157
- stdout = stdout.read.strip
158
- stderr = stderr.read.strip
207
+ yaml_to_apply = objects.map(&:yaml_to_apply).join("\n")
159
208
 
160
- if wt.value.success?
161
- stdout.split("\n").each_with_object({}) do |line, hash|
162
- next unless line =~ %r{\A([\w\/\-\.]+) (\w+)\z}
209
+ stdout, stderr, status = Open3.capture3(command.join(' '), stdin_data: yaml_to_apply + "\n")
163
210
 
164
- object = Regexp.last_match(1)
165
- status = Regexp.last_match(2)
166
- hash[object] = status
211
+ raise Error, "[kubectl] #{stderr}" unless status.success?
167
212
 
168
- status = "\e[32m#{status}\e[0m" unless status == 'unchanged'
169
- puts "\e[37m====> #{object} #{status}\e[0m"
170
- end
171
- else
172
- raise Error, "[kubectl] #{stderr}"
173
- end
174
- end
213
+ Util.parse_kubectl_apply_lines(stdout)
175
214
  end
176
215
 
177
216
  # Get some data from the kubernetes API
@@ -194,19 +233,16 @@ module Hippo
194
233
  # @return [Boolean]
195
234
  def delete(*names)
196
235
  command = kubectl('delete', *names)
197
- Open3.popen3(*command) do |_, stdout, stderr, wt|
198
- if wt.value.success?
199
- stdout.read.split("\n").each do |line|
200
- puts "\e[37m====> #{line}\e[0m"
201
- end
202
- true
236
+ stdout, stderr, status = Open3.capture3(*command)
237
+ if status.success?
238
+ Util.parse_kubectl_apply_lines(stdout)
239
+ true
240
+ else
241
+ stderr = stderr.read
242
+ if stderr =~ /\" not found$/
243
+ false
203
244
  else
204
- stderr = stderr.read
205
- if stderr =~ /\" not found$/
206
- false
207
- else
208
- raise Error, "[kutectl] #{stderr}"
209
- end
245
+ raise Error, "[kutectl] #{stderr}"
210
246
  end
211
247
  end
212
248
  end
data/lib/hippo/util.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
+ require 'open3'
4
5
  require 'hippo/error'
5
6
  require 'hippo/object_definition'
6
7
 
@@ -30,6 +31,14 @@ module Hippo
30
31
  end
31
32
  end
32
33
 
34
+ def load_objects_from_path(path, decorator: nil)
35
+ files = Dir[path]
36
+ files.each_with_object({}) do |path, objects|
37
+ file = load_yaml_from_file(path, decorator: decorator)
38
+ objects[path] = file
39
+ end
40
+ end
41
+
33
42
  def create_object_definitions(hash, stage, required_kinds: nil, clean: false)
34
43
  index = 0
35
44
  hash.each_with_object([]) do |(path, objects), array|
@@ -55,13 +64,17 @@ module Hippo
55
64
  end
56
65
 
57
66
  def open_in_editor(name, contents)
67
+ if ENV['EDITOR'].nil?
68
+ raise Error, 'No EDITOR environment variable has been defined'
69
+ end
70
+
58
71
  tmp_root = File.join(ENV['HOME'], '.hippo')
59
72
  FileUtils.mkdir_p(tmp_root)
60
73
  begin
61
74
  tmpfile = Tempfile.new([name, '.yaml'], tmp_root)
62
75
  tmpfile.write(contents)
63
76
  tmpfile.close
64
- system("#{ENV['EDITOR']} #{tmpfile.path}")
77
+ Kernel.system("#{ENV['EDITOR']} #{tmpfile.path}")
65
78
  tmpfile.open
66
79
  tmpfile.read
67
80
  ensure
@@ -102,6 +115,53 @@ module Hippo
102
115
  response = response.to_s.strip
103
116
  response.empty? ? default : response
104
117
  end
118
+
119
+ def system(command, stdin_data: nil)
120
+ stdout, stderr, status = Open3.capture3(command, stdin_data: stdin_data)
121
+ unless status.success?
122
+ raise Error, "Command failed to execute: #{stderr}"
123
+ end
124
+
125
+ stdout
126
+ end
127
+
128
+ def action(message)
129
+ $stdout.print message
130
+ complete_state = 'done'
131
+ color = '32'
132
+ passed_proc = proc do |value|
133
+ complete_state = value
134
+ color = '33'
135
+ end
136
+ return_value = yield(passed_proc)
137
+ puts " \e[#{color}m#{complete_state}\e[0m"
138
+ return_value
139
+ rescue StandardError => e
140
+ puts " \e[31merror\e[0m"
141
+ raise
142
+ end
143
+ end
144
+
145
+ def self.parse_kubectl_apply_lines(stdout)
146
+ stdout.split("\n").each_with_object({}) do |line, hash|
147
+ if line =~ %r{\A([\w\/\-\.]+) (\w+)\z}
148
+ object = Regexp.last_match(1)
149
+ status = Regexp.last_match(2)
150
+ elsif line =~ %r{\A[\w\.\/\-]+ \"([\w\-]+)\" (\w+)\z}
151
+ object = Regexp.last_match(1)
152
+ status = Regexp.last_match(2)
153
+ else
154
+ next
155
+ end
156
+ hash[object] = status
157
+
158
+ color = '32'
159
+ color = '31' if status == 'deleted'
160
+ color = '33' if status == 'configured'
161
+
162
+ status = "\e[#{color}m#{status}\e[0m" unless status == 'unchanged'
163
+ puts "\e[37m====> #{object} #{status}\e[0m"
164
+ end
105
165
  end
106
166
  end
107
167
  end
data/lib/hippo/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hippo
4
- VERSION = '1.1.2'
4
+ VERSION = '1.2.0'
5
5
  end