hippo-cli 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/bin/hippo +4 -4
  5. data/cli/apply_config.rb +1 -0
  6. data/cli/apply_services.rb +1 -0
  7. data/cli/console.rb +2 -0
  8. data/cli/create.rb +83 -0
  9. data/cli/deploy.rb +2 -0
  10. data/cli/init.rb +1 -1
  11. data/cli/install.rb +3 -1
  12. data/cli/key.rb +34 -0
  13. data/cli/kubectl.rb +2 -0
  14. data/cli/logs.rb +56 -0
  15. data/cli/objects.rb +50 -0
  16. data/cli/package_install.rb +30 -0
  17. data/cli/package_list.rb +40 -0
  18. data/cli/package_notes.rb +23 -0
  19. data/cli/package_test.rb +21 -0
  20. data/cli/package_uninstall.rb +27 -0
  21. data/cli/package_upgrade.rb +30 -0
  22. data/cli/package_values.rb +22 -0
  23. data/cli/prepare.rb +18 -0
  24. data/cli/run.rb +37 -0
  25. data/cli/secrets.rb +24 -0
  26. data/cli/stages.rb +26 -0
  27. data/cli/status.rb +25 -0
  28. data/cli/vars.rb +20 -0
  29. data/cli/version.rb +8 -0
  30. data/lib/hippo.rb +12 -0
  31. data/lib/hippo/bootstrap_parser.rb +64 -0
  32. data/lib/hippo/cli.rb +47 -14
  33. data/lib/hippo/extensions.rb +9 -0
  34. data/lib/hippo/image.rb +35 -31
  35. data/lib/hippo/manifest.rb +17 -7
  36. data/lib/hippo/object_definition.rb +18 -5
  37. data/lib/hippo/package.rb +124 -0
  38. data/lib/hippo/repository_tag.rb +38 -0
  39. data/lib/hippo/secret_manager.rb +61 -14
  40. data/lib/hippo/stage.rb +60 -26
  41. data/lib/hippo/util.rb +34 -0
  42. data/lib/hippo/version.rb +1 -1
  43. data/template/Hippofile +10 -2
  44. metadata +44 -13
  45. metadata.gz.sig +0 -0
  46. data/cli/secrets_edit.rb +0 -34
  47. data/cli/secrets_key.rb +0 -20
  48. data/lib/hippo/secret.rb +0 -112
  49. data/template/config/env-vars.yaml +0 -7
  50. data/template/deployments/web.yaml +0 -29
  51. data/template/deployments/worker.yaml +0 -26
  52. data/template/jobs/deploy/db-migration.yaml +0 -22
  53. data/template/jobs/install/load-schema.yaml +0 -22
  54. data/template/services/main.ingress.yaml +0 -13
  55. data/template/services/web.svc.yaml +0 -11
  56. data/template/stages/production.yaml +0 -7
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hash
4
+ def deep_merge(second)
5
+ merger = proc { |_, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : Array === v1 && Array === v2 ? v1 | v2 : [:undefined, nil, :nil].include?(v2) ? v1 : v2 }
6
+ merge(second.to_h, &merger)
7
+ end
8
+ end
9
+ # From https://stackoverflow.com/questions/9381553/ruby-merge-nested-hash
data/lib/hippo/image.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'git'
4
4
  require 'net/http'
5
+ require 'hippo/repository_tag'
5
6
 
6
7
  module Hippo
7
8
  class Image
@@ -12,63 +13,66 @@ module Hippo
12
13
 
13
14
  attr_reader :name
14
15
 
15
- def url
16
- @options['url']
16
+ def host
17
+ @options['host']
17
18
  end
18
19
 
19
- def repository
20
- @options['repository']
20
+ def image_name
21
+ @options['name']
21
22
  end
22
23
 
23
- def template_vars
24
- {
25
- 'url' => url,
26
- 'repository' => repository
27
- }
24
+ def tag
25
+ @tag ||= begin
26
+ if @options['tag'].is_a?(Hash) && repo = @options['tag']['fromRepository']
27
+ RepositoryTag.new(repo)
28
+ elsif @options['tag'].nil?
29
+ 'latest'
30
+ else
31
+ @options['tag'].to_s
32
+ end
33
+ end
28
34
  end
29
35
 
30
- def commit_ref_for_branch(branch)
31
- remote_refs.dig('branches', branch, :sha)
36
+ def image_url
37
+ "#{host}/#{image_name}:#{tag}"
32
38
  end
33
39
 
34
- def image_path_for_branch(branch)
35
- "#{url}:#{commit_ref_for_branch(branch)}"
40
+ def template_vars
41
+ @template_vars ||= {
42
+ 'host' => host,
43
+ 'name' => image_name,
44
+ 'tag' => tag.to_s,
45
+ 'url' => image_url
46
+ }
36
47
  end
37
48
 
38
- def remote_refs
39
- @remote_refs ||= begin
40
- Git.ls_remote(repository)
41
- end
49
+ def can_check_for_existence?
50
+ @options['existenceCheck'].nil? ||
51
+ @options['existenceCheck'] == true
42
52
  end
43
53
 
44
- def exists_for_commit?(commit)
45
- credentials = Hippo.config.dig('docker', 'credentials', registry_host)
54
+ def exists?
55
+ return true unless can_check_for_existence?
46
56
 
47
- http = Net::HTTP.new(registry_host, 443)
57
+ credentials = Hippo.config.dig('docker', 'credentials', host)
58
+ http = Net::HTTP.new(host, 443)
48
59
  http.use_ssl = true
49
- request = Net::HTTP::Head.new("/v2/#{registry_image_name}/manifests/#{commit}")
60
+ request = Net::HTTP::Head.new("/v2/#{image_name}/manifests/#{tag}")
50
61
  if credentials
51
62
  request.basic_auth(credentials['username'], credentials['password'])
52
63
  end
53
64
  response = http.request(request)
65
+
54
66
  case response
55
67
  when Net::HTTPOK
56
68
  true
57
69
  when Net::HTTPUnauthorized
58
- raise Error, "Could not authenticate to #{registry_host} to verify image existence"
70
+ raise Error, "Could not authenticate to #{host} to verify image existence"
59
71
  when Net::HTTPNotFound
60
72
  false
61
73
  else
62
- raise Error, "Got #{response.code} status when verifying imag existence with #{registry_host}"
74
+ raise Error, "Got #{response.code} status when verifying imag existence with #{host}"
63
75
  end
64
76
  end
65
-
66
- def registry_host
67
- url.split('/').first
68
- end
69
-
70
- def registry_image_name
71
- url.split('/', 2).last
72
- end
73
77
  end
74
78
  end
@@ -36,21 +36,31 @@ module Hippo
36
36
  @options['console']
37
37
  end
38
38
 
39
+ def config
40
+ @options['config'] || {}
41
+ end
42
+
43
+ def bootstrap
44
+ @bootstrap ||= begin
45
+ bootstrap_file = File.join(@root, 'bootstrap.yaml')
46
+ if File.file?(bootstrap_file)
47
+ YAML.load_file(bootstrap_file)
48
+ else
49
+ {}
50
+ end
51
+ end
52
+ end
53
+
39
54
  def template_vars
40
55
  {
41
- 'name' => name,
42
- 'images' => images.each_with_object({}) { |(name, image), hash| hash[name.to_s] = image.template_vars }
56
+ 'name' => name
43
57
  }
44
58
  end
45
59
 
46
60
  def images
47
61
  return {} unless @options['images'].is_a?(Hash)
48
62
 
49
- @images ||= begin
50
- @options['images'].each_with_object({}) do |(key, value), hash|
51
- hash[key] = Image.new(key, value)
52
- end
53
- end
63
+ @options['images']
54
64
  end
55
65
 
56
66
  # Load all stages that are available in the manifest
@@ -7,11 +7,6 @@ module Hippo
7
7
  def initialize(object, stage, clean: false)
8
8
  @object = object
9
9
  @stage = stage
10
-
11
- unless clean
12
- insert_namespace!
13
- insert_default_labels!
14
- end
15
10
  end
16
11
 
17
12
  def [](name)
@@ -38,6 +33,24 @@ module Hippo
38
33
  @object.to_yaml
39
34
  end
40
35
 
36
+ def yaml_to_apply
37
+ object = ObjectDefinition.new(@object.dup, @stage)
38
+ object.insert_namespace!
39
+ object.insert_default_labels!
40
+ object.base64_encode_data! if kind == 'Secret'
41
+ object.yaml
42
+ end
43
+
44
+ def base64_encode_data!(object = @object['data'])
45
+ object.each do |key, value|
46
+ object[key] = if value.is_a?(Hash)
47
+ base64_encode_data!(value)
48
+ else
49
+ Base64.encode64(value.to_s).gsub(/\n/, '').strip
50
+ end
51
+ end
52
+ end
53
+
41
54
  def insert_namespace!
42
55
  metadata['namespace'] = @stage.namespace
43
56
  end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hippo/cli'
4
+ require 'hippo/extensions'
5
+
6
+ module Hippo
7
+ class Package
8
+ def initialize(options, stage)
9
+ @options = options
10
+ @stage = stage
11
+ end
12
+
13
+ # Return the name of the package (i.e. the release name)
14
+ # and how this package will be referred.
15
+ #
16
+ # @return [String]
17
+ def name
18
+ @options['name']
19
+ end
20
+
21
+ # Return the name of the package to be installed.
22
+ # Including the registry.
23
+ #
24
+ # @return [String]
25
+ def package
26
+ @options['package']
27
+ end
28
+
29
+ # return values defined in the package's manifest file
30
+ #
31
+ # @return [Hash]
32
+ def values
33
+ @options['values']
34
+ end
35
+
36
+ # Compile a set of final values which should be used when
37
+ # upgrading and installing this package.
38
+ #
39
+ # @return [Hash]
40
+ def final_values
41
+ overrides = @stage.overridden_package_values[name]
42
+ values.deep_merge(overrides)
43
+ end
44
+
45
+ # Install this package
46
+ #
47
+ # @return [void]
48
+ def install
49
+ run_install_command('install')
50
+ end
51
+
52
+ # Upgrade this package
53
+ #
54
+ # @return [void]
55
+ def upgrade
56
+ run_install_command('upgrade', '--history-max', @options['max-revisions'] ? @options['max-revisions'].to_i.to_s : '5')
57
+ end
58
+
59
+ # Uninstall this packgae
60
+ #
61
+ # @return [void]
62
+ def uninstall
63
+ run(helm('uninstall', name))
64
+ end
65
+
66
+ # Is this release currently installed for the stage?
67
+ #
68
+ # @return [Boolean]
69
+ def installed?
70
+ secrets = @stage.get('secrets').map(&:name)
71
+ secrets.any? { |s| s.match(/\Ash\.helm\.release\.v\d+\.#{Regexp.escape(name)}\./) }
72
+ end
73
+
74
+ # Return the notes for this package
75
+ #
76
+ # @return [String]
77
+ def notes
78
+ run(helm('get', 'notes', name))
79
+ end
80
+
81
+ def helm(*commands)
82
+ command = ['helm']
83
+ command += ['--kube-context', @stage.context] if @stage.context
84
+ command += ['-n', @stage.namespace]
85
+ command += commands
86
+ command
87
+ end
88
+
89
+ private
90
+
91
+ def install_command(verb, *additional)
92
+ helm(verb, name, package, '-f', '-', *additional)
93
+ end
94
+
95
+ def run_install_command(verb, *additional)
96
+ run(install_command(verb, *additional), stdin: final_values.to_yaml)
97
+ true
98
+ end
99
+
100
+ def run(command, stdin: nil)
101
+ stdout, stderr, status = Open3.capture3(*command, stdin_data: stdin)
102
+ raise Error, "[helm] #{stderr}" unless status.success?
103
+
104
+ stdout
105
+ end
106
+
107
+ class << self
108
+ def setup_from_cli_context(context)
109
+ cli = Hippo::CLI.setup(context)
110
+ package_name = context.options[:package]
111
+ if package_name.nil? || package_name.empty?
112
+ raise Error, 'A package name must be provided in -p or --package'
113
+ end
114
+
115
+ package = cli.stage.packages[package_name]
116
+ if package.nil?
117
+ raise Error, "No package named '#{package_name}' has been defined"
118
+ end
119
+
120
+ [package, cli]
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hippo
4
+ class RepositoryTag
5
+ def initialize(options)
6
+ @options = options
7
+ end
8
+
9
+ def branch
10
+ @options['branch'] || 'master'
11
+ end
12
+
13
+ def tag
14
+ @tag ||= commit_ref_for_branch(branch)
15
+ end
16
+
17
+ def to_s
18
+ tag
19
+ end
20
+
21
+ private
22
+
23
+ def commit_ref_for_branch(branch)
24
+ return nil if remote_refs.nil?
25
+
26
+ remote_refs.dig('branches', branch, :sha)
27
+ end
28
+
29
+ def remote_refs
30
+ return nil if @options['url'].nil?
31
+
32
+ @remote_refs ||= begin
33
+ puts "Getting remote refs from #{@options['url']}"
34
+ Git.ls_remote(@options['url'])
35
+ end
36
+ end
37
+ end
38
+ end
@@ -3,7 +3,6 @@
3
3
  require 'encryptor'
4
4
  require 'openssl'
5
5
  require 'base64'
6
- require 'hippo/secret'
7
6
 
8
7
  module Hippo
9
8
  class SecretManager
@@ -15,8 +14,8 @@ module Hippo
15
14
 
16
15
  CIPHER = OpenSSL::Cipher.new('aes-256-gcm')
17
16
 
18
- def root
19
- File.join(@stage.manifest.root, 'secrets', @stage.name)
17
+ def path
18
+ File.join(@stage.manifest.root, 'secrets', @stage.name + '.yaml')
20
19
  end
21
20
 
22
21
  def secret(name)
@@ -69,9 +68,9 @@ module Hippo
69
68
  'kind' => 'Secret',
70
69
  'type' => 'hippo.adam.ac/secret-encryption-key',
71
70
  'metadata' => { 'name' => 'hippo-secret-key' },
72
- 'data' => { 'key' => Base64.encode64(secret_key64).gsub("\n", '').strip }
71
+ 'data' => { 'key' => secret_key64 }
73
72
  }, @stage)
74
- @stage.apply(od)
73
+ @stage.apply([od])
75
74
  @key = secret_key
76
75
  end
77
76
 
@@ -85,11 +84,11 @@ module Hippo
85
84
  iv = CIPHER.random_iv
86
85
  salt = SecureRandom.random_bytes(16)
87
86
  encrypted_value = Encryptor.encrypt(value: value.to_s, key: @key, iv: iv, salt: salt)
88
- 'encrypted:' + Base64.encode64([
87
+ Base64.encode64([
89
88
  Base64.encode64(encrypted_value),
90
89
  Base64.encode64(salt),
91
90
  Base64.encode64(iv)
92
- ].join('---')).gsub("\n", '')
91
+ ].join('---'))
93
92
  end
94
93
 
95
94
  # Decrypt the given value value and return it
@@ -97,14 +96,62 @@ module Hippo
97
96
  # @param value [String]
98
97
  # @return [String]
99
98
  def decrypt(value)
100
- value = value.to_s
101
- if value =~ /\Aencrypted:(.*)/
102
- value = Base64.decode64(Regexp.last_match(1))
103
- encrypted_value, salt, iv = value.split('---', 3).map { |s| Base64.decode64(s) }
104
- Encryptor.decrypt(value: encrypted_value, key: @key, iv: iv, salt: salt).to_s
105
- else
106
- value
99
+ value = Base64.decode64(value.to_s)
100
+ encrypted_value, salt, iv = value.split('---', 3).map { |s| Base64.decode64(s) }
101
+ Encryptor.decrypt(value: encrypted_value, key: @key, iv: iv, salt: salt).to_s
102
+ end
103
+
104
+ # Does a secrets file exist for this application.
105
+ #
106
+ # @return [Boolean]
107
+ def exists?
108
+ File.file?(path)
109
+ end
110
+
111
+ # Create an empty encrypted example secret file
112
+ #
113
+ # @return [void]
114
+ def create
115
+ unless key_available?
116
+ raise Error, 'Cannot create secret file because no key is available for encryption'
117
+ end
118
+
119
+ return if exists?
120
+
121
+ yaml = { 'example' => 'This is an example secret!' }.to_yaml
122
+ FileUtils.mkdir_p(File.dirname(path))
123
+ File.open(path, 'w') { |f| f.write(encrypt(yaml)) }
124
+ end
125
+
126
+ def edit
127
+ create unless exists?
128
+
129
+ unless key_available?
130
+ raise Error, 'Cannot create edit file because no key is available for decryption'
131
+ end
132
+
133
+ contents = decrypt(File.read(path))
134
+ contents = Util.open_in_editor('secret', contents)
135
+ write_file(contents)
136
+ end
137
+
138
+ def write_file(contents)
139
+ FileUtils.mkdir_p(File.dirname(path))
140
+ File.open(path, 'w') { |f| f.write(encrypt(contents)) }
141
+ end
142
+
143
+ def all
144
+ @all ||= begin
145
+ return {} unless exists?
146
+
147
+ unless key_available?
148
+ raise Error, 'No encryption key is available to decrypt secrets'
149
+ end
150
+
151
+ YAML.safe_load(decrypt(File.read(path)))
107
152
  end
153
+ rescue Psych::SyntaxError => e
154
+ raise Error, "Could not parse secrets file: #{e.message}"
108
155
  end
109
156
  end
110
157
  end