hippo-cli 1.1.1 → 1.2.3

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.
@@ -3,10 +3,6 @@
3
3
  command :status do
4
4
  desc 'Show current status of the namespace'
5
5
 
6
- option '-h', '--hippofile [RECIPE]', 'The path to the Hippofile (defaults: ./Hippofile)' do |value, options|
7
- options[:hippofile] = value.to_s
8
- end
9
-
10
6
  option '--full', 'Include all relevant objects in namespace' do |_value, options|
11
7
  options[:full] = true
12
8
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ command :tidy do
4
+ desc 'Remove live objects that are not referenced by the manifest'
5
+ action do |context|
6
+ require 'hippo/cli'
7
+ cli = Hippo::CLI.setup(context)
8
+ cli.preflight
9
+
10
+ objects_to_prune = cli.stage.live_objects(pruneable_only: true)
11
+ if objects_to_prune.empty?
12
+ puts 'There are no objects to tidy'
13
+ exit 0
14
+ end
15
+
16
+ puts "Found #{objects_to_prune.size} object(s) to tidy"
17
+ puts
18
+ objects_to_prune.each do |obj|
19
+ $stdout.print ' ' + obj[:live].kind.ljust(25)
20
+ $stdout.print obj[:live].name
21
+ puts
22
+ end
23
+ puts
24
+
25
+ require 'hippo/util'
26
+ unless Hippo::Util.confirm('Do you wish to continue?')
27
+ puts 'No problem, not removing anything right now'
28
+ exit 0
29
+ end
30
+
31
+ cli.stage.delete_pruneable_objects
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ command :update do
4
+ desc 'Get the latest updates from the remote manifest'
5
+ action do
6
+ require 'hippo/working_directory'
7
+ wd = Hippo::WorkingDirectory.new
8
+
9
+ unless wd.can_update?
10
+ puts "No need to update #{wd.source_type} manifests"
11
+ exit 0
12
+ end
13
+
14
+ if wd.last_updated_at
15
+ puts "Last updated: #{wd.last_updated_at}"
16
+ else
17
+ puts 'Does not exist yet. Downloading for the first time.'
18
+ end
19
+
20
+ wd.update_from_remote(verbose: true)
21
+ end
22
+ end
@@ -3,10 +3,6 @@
3
3
  command :vars do
4
4
  desc 'Show all variables available for use in this stage'
5
5
 
6
- option '-h', '--hippofile [RECIPE]', 'The path to the Hippofile (defaults: ./Hippofile)' do |value, options|
7
- options[:hippofile] = value.to_s
8
- end
9
-
10
6
  action do |context|
11
7
  require 'hippo/cli'
12
8
  cli = Hippo::CLI.setup(context)
@@ -35,4 +35,11 @@ module Hippo
35
35
 
36
36
  stdout.strip
37
37
  end
38
+
39
+ # Path to store temp files
40
+ #
41
+ # @return [String]
42
+ def self.tmp_root
43
+ File.join(ENV['HOME'], '.hippo')
44
+ end
38
45
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'securerandom'
4
4
  require 'secure_random_string'
5
+ require 'openssl'
5
6
 
6
7
  module Hippo
7
8
  class BootstrapParser
@@ -54,6 +55,27 @@ module Hippo
54
55
  SecureRandom.hex(value['size'] ? value['size'].to_i : 16)
55
56
  when 'random'
56
57
  Base64.encode64(SecureRandom.random_bytes(value['size'] ? value['size'].to_i : 16)).strip
58
+ when 'rsa'
59
+ OpenSSL::PKey::RSA.new(value['size'] ? value['size'].to_i : 2048).to_s
60
+ when 'certificate'
61
+ key = OpenSSL::PKey::RSA.new(value['key_size'] ? value['key_size'].to_i : 2048)
62
+
63
+ cert = OpenSSL::X509::Certificate.new
64
+ cert.subject = cert.issuer = OpenSSL::X509::Name.new(
65
+ [
66
+ ['C', value['country'] || 'GB'],
67
+ ['O', value['organization'] || 'Default'],
68
+ ['OU', value['organization_unit'] || 'Default'],
69
+ ['CN', value['common_name'] || 'default']
70
+ ]
71
+ )
72
+ cert.not_before = Time.now
73
+ cert.not_after = Time.now + (60 * 60 * 24 * (value['days'] ? value['days'].to_i : 730))
74
+ cert.public_key = key.public_key
75
+ cert.serial = 0x0
76
+ cert.version = 2
77
+ cert.sign key, OpenSSL::Digest::SHA256.new
78
+ { 'certificate' => cert.to_s, 'key' => key.to_s }
57
79
  when nil
58
80
  raise Error, "A 'type' must be provided for each generated item"
59
81
  else
@@ -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
@@ -34,7 +34,11 @@ module Hippo
34
34
  end
35
35
 
36
36
  def image_url
37
- "#{host}/#{image_name}:#{tag}"
37
+ if host
38
+ "#{host}/#{image_name}:#{tag}"
39
+ else
40
+ "#{image_name}:#{tag}"
41
+ end
38
42
  end
39
43
 
40
44
  def template_vars
@@ -52,6 +56,8 @@ module Hippo
52
56
  end
53
57
 
54
58
  def exists?
59
+ return true unless tag.is_a?(RepositoryTag)
60
+ return true if host.nil?
55
61
  return true unless can_check_for_existence?
56
62
 
57
63
  credentials = Hippo.config.dig('docker', 'credentials', host)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hippo
4
+ module LiquidFilters
5
+ def indent(text, depth = 2)
6
+ text.split("\n").map.each_with_index do |p, i|
7
+ i == 0 ? p : ' ' * depth + p
8
+ end.join("\n")
9
+ end
10
+
11
+ def multiline(text)
12
+ text.inspect
13
+ end
14
+ end
15
+ end
@@ -36,6 +36,10 @@ module Hippo
36
36
  @options['console']
37
37
  end
38
38
 
39
+ def commands
40
+ @options['commands'] || {}
41
+ end
42
+
39
43
  def config
40
44
  @options['config'] || {}
41
45
  end
@@ -51,6 +55,18 @@ module Hippo
51
55
  end
52
56
  end
53
57
 
58
+ def readme
59
+ return @readme if instance_variable_defined?('@readme')
60
+
61
+ path = File.join(@root, 'readme.txt')
62
+ unless File.file?(path)
63
+ @readme = nil
64
+ return
65
+ end
66
+
67
+ @readme = File.read(path)
68
+ end
69
+
54
70
  def template_vars
55
71
  {
56
72
  'name' => name
@@ -63,29 +79,13 @@ module Hippo
63
79
  @options['images']
64
80
  end
65
81
 
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
82
  # Load all YAML objects at a given path and return them.
79
83
  #
80
84
  # @param path [String]
81
85
  # @param decorator [Proc] an optional parser to run across the raw YAML file
82
86
  # @return [Array<Hash>]
83
87
  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
88
+ Util.load_objects_from_path(File.join(@root, path, '*.{yaml,yml}'), decorator: decorator)
89
89
  end
90
90
  end
91
91
  end
@@ -30,7 +30,7 @@ module Hippo
30
30
  end
31
31
 
32
32
  def yaml
33
- @object.to_yaml
33
+ @object.to_yaml(line_width: -1)
34
34
  end
35
35
 
36
36
  def yaml_to_apply
@@ -93,7 +93,7 @@ module Hippo
93
93
  end
94
94
 
95
95
  def run_install_command(verb, *additional)
96
- run(install_command(verb, *additional), stdin: final_values.to_yaml)
96
+ run(install_command(verb, *additional), stdin: final_values.to_yaml(line_width: -1))
97
97
  true
98
98
  end
99
99
 
@@ -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
@@ -130,9 +131,13 @@ module Hippo
130
131
  raise Error, 'Cannot create edit file because no key is available for decryption'
131
132
  end
132
133
 
133
- contents = decrypt(File.read(path))
134
- contents = Util.open_in_editor('secret', contents)
135
- write_file(contents)
134
+ old_contents = decrypt(File.read(path))
135
+ new_contents = Util.open_in_editor('secret', old_contents)
136
+ if old_contents != new_contents
137
+ write_file(new_contents)
138
+ else
139
+ puts 'No changes detected. Not re-encrypting secret file.'
140
+ end
136
141
  end
137
142
 
138
143
  def write_file(contents)