hippo-cli 1.1.0 → 1.1.1
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/bin/hippo +4 -4
- data/cli/apply_config.rb +1 -0
- data/cli/apply_services.rb +1 -0
- data/cli/console.rb +2 -0
- data/cli/create.rb +83 -0
- data/cli/deploy.rb +2 -0
- data/cli/init.rb +1 -1
- data/cli/install.rb +3 -1
- data/cli/key.rb +34 -0
- data/cli/kubectl.rb +2 -0
- data/cli/logs.rb +56 -0
- data/cli/objects.rb +50 -0
- data/cli/package_install.rb +30 -0
- data/cli/package_list.rb +40 -0
- data/cli/package_notes.rb +23 -0
- data/cli/package_test.rb +21 -0
- data/cli/package_uninstall.rb +27 -0
- data/cli/package_upgrade.rb +30 -0
- data/cli/package_values.rb +22 -0
- data/cli/prepare.rb +18 -0
- data/cli/run.rb +37 -0
- data/cli/secrets.rb +24 -0
- data/cli/stages.rb +26 -0
- data/cli/status.rb +25 -0
- data/cli/vars.rb +20 -0
- data/cli/version.rb +8 -0
- data/lib/hippo.rb +12 -0
- data/lib/hippo/bootstrap_parser.rb +64 -0
- data/lib/hippo/cli.rb +47 -14
- data/lib/hippo/extensions.rb +9 -0
- data/lib/hippo/image.rb +35 -31
- data/lib/hippo/manifest.rb +17 -7
- data/lib/hippo/object_definition.rb +18 -5
- data/lib/hippo/package.rb +124 -0
- data/lib/hippo/repository_tag.rb +38 -0
- data/lib/hippo/secret_manager.rb +61 -14
- data/lib/hippo/stage.rb +60 -26
- data/lib/hippo/util.rb +34 -0
- data/lib/hippo/version.rb +1 -1
- data/template/Hippofile +10 -2
- metadata +44 -13
- metadata.gz.sig +0 -0
- data/cli/secrets_edit.rb +0 -34
- data/cli/secrets_key.rb +0 -20
- data/lib/hippo/secret.rb +0 -112
- data/template/config/env-vars.yaml +0 -7
- data/template/deployments/web.yaml +0 -29
- data/template/deployments/worker.yaml +0 -26
- data/template/jobs/deploy/db-migration.yaml +0 -22
- data/template/jobs/install/load-schema.yaml +0 -22
- data/template/services/main.ingress.yaml +0 -13
- data/template/services/web.svc.yaml +0 -11
- 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
|
16
|
-
@options['
|
16
|
+
def host
|
17
|
+
@options['host']
|
17
18
|
end
|
18
19
|
|
19
|
-
def
|
20
|
-
@options['
|
20
|
+
def image_name
|
21
|
+
@options['name']
|
21
22
|
end
|
22
23
|
|
23
|
-
def
|
24
|
-
|
25
|
-
'
|
26
|
-
|
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
|
31
|
-
|
36
|
+
def image_url
|
37
|
+
"#{host}/#{image_name}:#{tag}"
|
32
38
|
end
|
33
39
|
|
34
|
-
def
|
35
|
-
|
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
|
39
|
-
@
|
40
|
-
|
41
|
-
end
|
49
|
+
def can_check_for_existence?
|
50
|
+
@options['existenceCheck'].nil? ||
|
51
|
+
@options['existenceCheck'] == true
|
42
52
|
end
|
43
53
|
|
44
|
-
def
|
45
|
-
|
54
|
+
def exists?
|
55
|
+
return true unless can_check_for_existence?
|
46
56
|
|
47
|
-
|
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/#{
|
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 #{
|
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 #{
|
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
|
data/lib/hippo/manifest.rb
CHANGED
@@ -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
|
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
|
data/lib/hippo/secret_manager.rb
CHANGED
@@ -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
|
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' =>
|
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
|
-
|
87
|
+
Base64.encode64([
|
89
88
|
Base64.encode64(encrypted_value),
|
90
89
|
Base64.encode64(salt),
|
91
90
|
Base64.encode64(iv)
|
92
|
-
].join('---'))
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|