hippo-cli 1.0.1 → 1.1.0
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/cli/apply_config.rb +4 -5
- data/cli/apply_services.rb +3 -3
- data/cli/console.rb +6 -6
- data/cli/deploy.rb +11 -22
- data/cli/install.rb +22 -23
- data/cli/kubectl.rb +3 -3
- data/cli/secrets_edit.rb +34 -0
- data/cli/secrets_key.rb +20 -0
- data/lib/hippo.rb +19 -0
- data/lib/hippo/cli.rb +210 -0
- data/lib/hippo/deployment_monitor.rb +82 -0
- data/lib/hippo/error.rb +0 -12
- data/lib/hippo/image.rb +74 -0
- data/lib/hippo/manifest.rb +81 -0
- data/lib/hippo/object_definition.rb +63 -0
- data/lib/hippo/secret.rb +61 -114
- data/lib/hippo/secret_manager.rb +43 -32
- data/lib/hippo/stage.rb +157 -7
- data/lib/hippo/util.rb +56 -33
- data/lib/hippo/version.rb +1 -1
- data/template/Hippofile +12 -13
- data/template/config/{production/env-vars.yaml → env-vars.yaml} +0 -0
- data/template/jobs/{upgrade → deploy}/db-migration.yaml +0 -0
- data/template/stages/production.yaml +1 -0
- metadata +31 -16
- metadata.gz.sig +0 -0
- data/cli/build.rb +0 -16
- data/cli/edit-secret.rb +0 -45
- data/cli/objects.rb +0 -34
- data/cli/publish.rb +0 -17
- data/cli/secrets.rb +0 -23
- data/cli/status.rb +0 -15
- data/lib/hippo/build_spec.rb +0 -32
- data/lib/hippo/cli_steps.rb +0 -315
- data/lib/hippo/kubernetes.rb +0 -200
- data/lib/hippo/recipe.rb +0 -126
- data/lib/hippo/repository.rb +0 -122
- data/lib/hippo/yaml_part.rb +0 -47
data/lib/hippo/secret_manager.rb
CHANGED
@@ -1,41 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'encryptor'
|
4
|
+
require 'openssl'
|
3
5
|
require 'base64'
|
4
6
|
require 'hippo/secret'
|
5
7
|
|
6
8
|
module Hippo
|
7
9
|
class SecretManager
|
8
|
-
attr_reader :recipe
|
9
10
|
attr_reader :stage
|
10
|
-
|
11
|
+
|
12
|
+
def initialize(stage)
|
13
|
+
@stage = stage
|
14
|
+
end
|
11
15
|
|
12
16
|
CIPHER = OpenSSL::Cipher.new('aes-256-gcm')
|
13
17
|
|
14
|
-
def
|
15
|
-
@
|
16
|
-
@stage = stage
|
18
|
+
def root
|
19
|
+
File.join(@stage.manifest.root, 'secrets', @stage.name)
|
17
20
|
end
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
def create_key
|
23
|
-
if key_available?
|
24
|
-
raise Hippo::Error, 'A key already exists on Kubernetes. Remove this first.'
|
25
|
-
end
|
22
|
+
def secret(name)
|
23
|
+
Secret.new(self, name)
|
24
|
+
end
|
26
25
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
'apiVersion' => 'v1',
|
32
|
-
'kind' => 'Secret',
|
33
|
-
'type' => 'hippo.adam.ac/secret-encryption-key',
|
34
|
-
'metadata' => { 'name' => 'hippo-secret-key', 'namespace' => @stage.namespace },
|
35
|
-
'data' => { 'key' => Base64.encode64(secret_key64).gsub("\n", '').strip }
|
36
|
-
}
|
37
|
-
@recipe.kubernetes.apply_with_kubectl(@stage, object.to_yaml)
|
38
|
-
@key = secret_key
|
26
|
+
def secrets
|
27
|
+
Dir[File.join(root, '*.{yml,yaml}')].map do |path|
|
28
|
+
secret(path.split('/').last.sub(/\.ya?ml\z/, ''))
|
29
|
+
end
|
39
30
|
end
|
40
31
|
|
41
32
|
# Download the current key from the Kubernetes API and set it as the
|
@@ -45,7 +36,7 @@ module Hippo
|
|
45
36
|
def download_key
|
46
37
|
return if @key
|
47
38
|
|
48
|
-
value = @
|
39
|
+
value = @stage.get('secret', 'hippo-secret-key').first
|
49
40
|
return if value.nil?
|
50
41
|
return if value.dig('data', 'key').nil?
|
51
42
|
|
@@ -54,22 +45,42 @@ module Hippo
|
|
54
45
|
raise unless e.message =~ /not found/
|
55
46
|
end
|
56
47
|
|
48
|
+
# Is there a key availale in this manager?
|
49
|
+
#
|
50
|
+
# @return [Boolean]
|
57
51
|
def key_available?
|
58
52
|
download_key
|
59
53
|
!@key.nil?
|
60
54
|
end
|
61
55
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
secret(path.split('/').last.sub(/\.ya?ml\z/, ''))
|
56
|
+
# Generate and publish a new secret key to the Kubernetes API.
|
57
|
+
#
|
58
|
+
# @return [void]
|
59
|
+
def create_key
|
60
|
+
if key_available?
|
61
|
+
raise Hippo::Error, 'A key already exists on Kubernetes. Remove this first.'
|
69
62
|
end
|
63
|
+
|
64
|
+
CIPHER.encrypt
|
65
|
+
secret_key = CIPHER.random_key
|
66
|
+
secret_key64 = Base64.encode64(secret_key).gsub("\n", '').strip
|
67
|
+
od = ObjectDefinition.new({
|
68
|
+
'apiVersion' => 'v1',
|
69
|
+
'kind' => 'Secret',
|
70
|
+
'type' => 'hippo.adam.ac/secret-encryption-key',
|
71
|
+
'metadata' => { 'name' => 'hippo-secret-key' },
|
72
|
+
'data' => { 'key' => Base64.encode64(secret_key64).gsub("\n", '').strip }
|
73
|
+
}, @stage)
|
74
|
+
@stage.apply(od)
|
75
|
+
@key = secret_key
|
70
76
|
end
|
71
77
|
|
78
|
+
# Encrypt a given value?
|
72
79
|
def encrypt(value)
|
80
|
+
unless key_available?
|
81
|
+
raise Error, 'Cannot encrypt values because there is no key'
|
82
|
+
end
|
83
|
+
|
73
84
|
CIPHER.encrypt
|
74
85
|
iv = CIPHER.random_iv
|
75
86
|
salt = SecureRandom.random_bytes(16)
|
data/lib/hippo/stage.rb
CHANGED
@@ -1,8 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'liquid'
|
4
|
+
require 'open3'
|
5
|
+
require 'hippo/secret_manager'
|
6
|
+
|
3
7
|
module Hippo
|
4
8
|
class Stage
|
5
|
-
|
9
|
+
attr_reader :manifest
|
10
|
+
|
11
|
+
def initialize(manifest, options)
|
12
|
+
@manifest = manifest
|
6
13
|
@options = options
|
7
14
|
end
|
8
15
|
|
@@ -22,24 +29,167 @@ module Hippo
|
|
22
29
|
@options['context']
|
23
30
|
end
|
24
31
|
|
32
|
+
def vars
|
33
|
+
@options['vars']
|
34
|
+
end
|
35
|
+
|
36
|
+
# These are the vars to represent this
|
25
37
|
def template_vars
|
26
38
|
{
|
27
39
|
'name' => name,
|
28
40
|
'branch' => branch,
|
29
41
|
'namespace' => namespace,
|
30
|
-
'
|
42
|
+
'context' => context,
|
43
|
+
'images' => @manifest.images.values.each_with_object({}) { |image, hash| hash[image.name] = image.image_path_for_branch(branch) },
|
44
|
+
'vars' => vars
|
31
45
|
}
|
32
46
|
end
|
33
47
|
|
34
|
-
|
35
|
-
|
48
|
+
# Return a new decorator object that can be passed to objects that
|
49
|
+
# would like to decorator things.
|
50
|
+
def decorator
|
51
|
+
proc do |data|
|
52
|
+
template = Liquid::Template.parse(data)
|
53
|
+
template.render(
|
54
|
+
'stage' => template_vars,
|
55
|
+
'manifest' => @manifest.template_vars
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def objects(path)
|
61
|
+
@manifest.objects(path, decorator: decorator)
|
62
|
+
end
|
63
|
+
|
64
|
+
def secret_manager
|
65
|
+
@secret_manager ||= SecretManager.new(self)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return an array of all deployments for this stage
|
69
|
+
#
|
70
|
+
# @return [Hash<String,Hippo::ObjectDefinition>]
|
71
|
+
def deployments
|
72
|
+
Util.create_object_definitions(objects('deployments'), self, required_kinds: ['Deployment'])
|
73
|
+
end
|
74
|
+
|
75
|
+
# Return an array of all services/ingresses for this stage
|
76
|
+
#
|
77
|
+
# @return [Hash<String,Hippo::ObjectDefinition>]
|
78
|
+
def services
|
79
|
+
Util.create_object_definitions(objects('services'), self, required_kinds: %w[Service Ingress NetworkPolicy])
|
80
|
+
end
|
81
|
+
|
82
|
+
# Return an array of all configuration objects
|
83
|
+
#
|
84
|
+
# @return [Hash<String,Hippo::ObjectDefinition>]
|
85
|
+
def configs
|
86
|
+
Util.create_object_definitions(objects('config'), self)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Return an array of all job objects
|
90
|
+
#
|
91
|
+
# @return [Hash<String,Hippo::ObjectDefinition>]
|
92
|
+
def jobs(type)
|
93
|
+
Util.create_object_definitions(objects("jobs/#{type}"), self)
|
36
94
|
end
|
37
95
|
|
38
|
-
|
96
|
+
# Return a kubectl command ready for use within this stage's
|
97
|
+
# namespace and context
|
98
|
+
#
|
99
|
+
# @return [Array<String>]
|
100
|
+
def kubectl(*commands)
|
101
|
+
prefix = ['kubectl']
|
102
|
+
prefix += ['--context', context] if context
|
103
|
+
prefix += ['-n', namespace]
|
104
|
+
prefix + commands
|
105
|
+
end
|
106
|
+
|
107
|
+
# Apply a series of objecst with
|
108
|
+
#
|
109
|
+
# @param objects [Array<Hippo::ObjectDefinition>]
|
110
|
+
# @return [Hash]
|
111
|
+
def apply(objects)
|
112
|
+
yaml_to_apply = objects.map(&:yaml).join("\n")
|
113
|
+
|
39
114
|
command = ['kubectl']
|
40
115
|
command += ['--context', context] if context
|
41
|
-
command += ['-
|
42
|
-
command
|
116
|
+
command += ['apply', '-f', '-']
|
117
|
+
Open3.popen3(command.join(' ')) do |stdin, stdout, stderr, wt|
|
118
|
+
stdin.puts yaml_to_apply
|
119
|
+
stdin.close
|
120
|
+
|
121
|
+
stdout = stdout.read.strip
|
122
|
+
stderr = stderr.read.strip
|
123
|
+
|
124
|
+
if wt.value.success?
|
125
|
+
stdout.split("\n").each_with_object({}) do |line, hash|
|
126
|
+
next unless line =~ %r{\A([\w\/\-\.]+) (\w+)\z}
|
127
|
+
|
128
|
+
object = Regexp.last_match(1)
|
129
|
+
status = Regexp.last_match(2)
|
130
|
+
hash[object] = status
|
131
|
+
|
132
|
+
status = "\e[32m#{status}\e[0m" unless status == 'unchanged'
|
133
|
+
puts "\e[37m====> #{object} #{status}\e[0m"
|
134
|
+
end
|
135
|
+
else
|
136
|
+
raise Error, "[kubectl] #{stderr}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Get some data from the kubernetes API
|
142
|
+
#
|
143
|
+
# @param names [Array<String>]
|
144
|
+
# @return [Array<Hippo::ObjectDefinition>]
|
145
|
+
def get(*names)
|
146
|
+
command = kubectl('get', '-o', 'yaml', *names)
|
147
|
+
Open3.popen3(*command) do |_, stdout, stderr, wt|
|
148
|
+
raise Error, "[kutectl] #{stderr.read}" unless wt.value.success?
|
149
|
+
|
150
|
+
yaml = YAML.safe_load(stdout.read, permitted_classes: [Time])
|
151
|
+
yaml = yaml['items'] || [yaml]
|
152
|
+
yaml.map { |y| ObjectDefinition.new(y, self, clean: true) }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Delete an object from the kubernetes API
|
157
|
+
#
|
158
|
+
# @param names [Array<String>]
|
159
|
+
# @return [Boolean]
|
160
|
+
def delete(*names)
|
161
|
+
command = kubectl('delete', *names)
|
162
|
+
Open3.popen3(*command) do |_, stdout, stderr, wt|
|
163
|
+
if wt.value.success?
|
164
|
+
stdout.read.split("\n").each do |line|
|
165
|
+
puts "\e[37m====> #{line}\e[0m"
|
166
|
+
end
|
167
|
+
true
|
168
|
+
else
|
169
|
+
stderr = stderr.read
|
170
|
+
if stderr =~ /\" not found$/
|
171
|
+
false
|
172
|
+
else
|
173
|
+
raise Error, "[kutectl] #{stderr}"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Wait for the named jobs to complete
|
180
|
+
def wait_for_jobs(names, times = 120)
|
181
|
+
jobs = nil
|
182
|
+
times.times do
|
183
|
+
jobs = get(*names)
|
184
|
+
|
185
|
+
if jobs.all? { |j| j['status']['active'].nil? }
|
186
|
+
return [false, jobs]
|
187
|
+
else
|
188
|
+
sleep 2
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
[true, jobs]
|
43
193
|
end
|
44
194
|
end
|
45
195
|
end
|
data/lib/hippo/util.rb
CHANGED
@@ -1,49 +1,72 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'yaml'
|
3
4
|
require 'hippo/error'
|
4
|
-
require 'hippo/
|
5
|
+
require 'hippo/object_definition'
|
5
6
|
|
6
7
|
module Hippo
|
7
8
|
module Util
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
class << self
|
10
|
+
def load_yaml_from_file(path, decorator: nil)
|
11
|
+
raise Error, "No file found at #{path} to load" unless File.file?(path)
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
file = File.read(path)
|
14
|
+
load_yaml_from_data(file, path: path, decorator: decorator)
|
15
|
+
end
|
16
|
+
|
17
|
+
def load_yaml_from_data(data, path: nil, decorator: nil)
|
18
|
+
data = decorator.call(data) if decorator
|
15
19
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
+
parts = data.split(/^\-\-\-\s*$/)
|
21
|
+
parts.each_with_index.each_with_object([]) do |(p, i), array|
|
22
|
+
begin
|
23
|
+
yaml = YAML.safe_load(p)
|
24
|
+
next unless yaml.is_a?(Hash)
|
25
|
+
|
26
|
+
array << yaml
|
27
|
+
rescue Psych::SyntaxError => e
|
28
|
+
raise Error, e.message.sub('(<unknown>): ', "(#{path}[#{i}]): ")
|
29
|
+
end
|
30
|
+
end
|
20
31
|
end
|
21
|
-
end
|
22
32
|
|
23
|
-
|
24
|
-
|
25
|
-
|
33
|
+
def create_object_definitions(hash, stage, required_kinds: nil, clean: false)
|
34
|
+
index = 0
|
35
|
+
hash.each_with_object([]) do |(path, objects), array|
|
36
|
+
objects.each_with_index do |object, inner_index|
|
37
|
+
od = ObjectDefinition.new(object, stage, clean: clean)
|
26
38
|
|
27
|
-
|
28
|
-
|
29
|
-
|
39
|
+
if od.name.nil?
|
40
|
+
raise Error, "All object defintions must have a name. Missing metadata.name for object in #{path} at index #{inner_index}"
|
41
|
+
end
|
30
42
|
|
31
|
-
|
32
|
-
|
33
|
-
|
43
|
+
if od.kind.nil?
|
44
|
+
raise Error, "All object definitions must have a kind defined. Check #{path} at index #{inner_index}"
|
45
|
+
end
|
46
|
+
|
47
|
+
if required_kinds && !required_kinds.include?(od.kind)
|
48
|
+
raise Error, "Kind '#{od.kind}' cannot be defined in #{path} at index #{inner_index}. Only kinds #{required_kinds} are permitted."
|
49
|
+
end
|
50
|
+
|
51
|
+
array << od
|
52
|
+
index += 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
34
56
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
57
|
+
def open_in_editor(name, contents)
|
58
|
+
tmp_root = File.join(ENV['HOME'], '.hippo')
|
59
|
+
FileUtils.mkdir_p(tmp_root)
|
60
|
+
begin
|
61
|
+
tmpfile = Tempfile.new([name, '.yaml'], tmp_root)
|
62
|
+
tmpfile.write(contents)
|
63
|
+
tmpfile.close
|
64
|
+
system("#{ENV['EDITOR']} #{tmpfile.path}")
|
65
|
+
tmpfile.open
|
66
|
+
tmpfile.read
|
67
|
+
ensure
|
68
|
+
tmpfile.unlink
|
69
|
+
end
|
47
70
|
end
|
48
71
|
end
|
49
72
|
end
|
data/lib/hippo/version.rb
CHANGED
data/template/Hippofile
CHANGED
@@ -3,17 +3,16 @@
|
|
3
3
|
# In here you configure how you wish to build, publish and
|
4
4
|
# deploy your application using Docker & Kubernetes.
|
5
5
|
|
6
|
-
|
7
|
-
repository:
|
8
|
-
url: git@github.com:username/repo
|
6
|
+
name: myapp
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
8
|
+
images:
|
9
|
+
main:
|
10
|
+
repository: git@github.com:myorg/myapp
|
11
|
+
url: myorg/myapp
|
12
|
+
|
13
|
+
# If you wish, you can define a console command that allows you to easil
|
14
|
+
# open a console using `hippo [stage] console`
|
15
|
+
#
|
16
|
+
# console:
|
17
|
+
# deployment: worker
|
18
|
+
# command: bundle exec rails console
|
File without changes
|