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.
- 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
|