hippo-cli 1.1.2 → 1.2.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.
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