hippo-cli 1.1.1 → 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)