hippo-cli 1.0.1 → 1.1.0
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/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
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hippo
|
4
|
+
class DeploymentMonitor
|
5
|
+
def initialize(stage, deployment_id, sleep: 4, count: 15)
|
6
|
+
@stage = stage
|
7
|
+
@deployment_id = deployment_id
|
8
|
+
@sleep = sleep
|
9
|
+
@count = count
|
10
|
+
end
|
11
|
+
|
12
|
+
def on_wait(&block)
|
13
|
+
@on_wait = block
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_failure(&block)
|
17
|
+
@on_failure = block
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_success(&block)
|
21
|
+
@on_success = block
|
22
|
+
end
|
23
|
+
|
24
|
+
def wait
|
25
|
+
count = 0
|
26
|
+
loop do
|
27
|
+
sleep @sleep
|
28
|
+
poll = Poll.new(@stage, @deployment_id)
|
29
|
+
if poll.pending.empty?
|
30
|
+
@on_success&.call(poll)
|
31
|
+
return true
|
32
|
+
else
|
33
|
+
if count >= @count
|
34
|
+
@on_failure&.call(poll)
|
35
|
+
return false
|
36
|
+
else
|
37
|
+
count += 1
|
38
|
+
@on_wait&.call(poll)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
class Poll
|
47
|
+
def initialize(stage, deployment_id)
|
48
|
+
@stage = stage
|
49
|
+
@deployment_id = deployment_id
|
50
|
+
|
51
|
+
@replica_sets = @stage.get(
|
52
|
+
'rs',
|
53
|
+
'--selector',
|
54
|
+
'hippo.adam.ac/deployID=' + @deployment_id
|
55
|
+
)
|
56
|
+
|
57
|
+
@pending = @replica_sets.reject do |deploy|
|
58
|
+
deploy['status']['availableReplicas'] == deploy['status']['replicas']
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
attr_reader :pending
|
63
|
+
attr_reader :replica_sets
|
64
|
+
|
65
|
+
def pending_names
|
66
|
+
make_names(@pending)
|
67
|
+
end
|
68
|
+
|
69
|
+
def names
|
70
|
+
make_names(@replica_sets)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def make_names(array)
|
76
|
+
array.map do |d|
|
77
|
+
d.name.split('-').first
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/hippo/error.rb
CHANGED
@@ -3,16 +3,4 @@
|
|
3
3
|
module Hippo
|
4
4
|
class Error < StandardError
|
5
5
|
end
|
6
|
-
|
7
|
-
class RepositoryAlreadyClonedError < Error
|
8
|
-
end
|
9
|
-
|
10
|
-
class RepositoryCloneError < Error
|
11
|
-
end
|
12
|
-
|
13
|
-
class RepositoryFetchError < Error
|
14
|
-
end
|
15
|
-
|
16
|
-
class RepositoryCheckoutError < Error
|
17
|
-
end
|
18
6
|
end
|
data/lib/hippo/image.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'git'
|
4
|
+
require 'net/http'
|
5
|
+
|
6
|
+
module Hippo
|
7
|
+
class Image
|
8
|
+
def initialize(name, options)
|
9
|
+
@name = name
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :name
|
14
|
+
|
15
|
+
def url
|
16
|
+
@options['url']
|
17
|
+
end
|
18
|
+
|
19
|
+
def repository
|
20
|
+
@options['repository']
|
21
|
+
end
|
22
|
+
|
23
|
+
def template_vars
|
24
|
+
{
|
25
|
+
'url' => url,
|
26
|
+
'repository' => repository
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def commit_ref_for_branch(branch)
|
31
|
+
remote_refs.dig('branches', branch, :sha)
|
32
|
+
end
|
33
|
+
|
34
|
+
def image_path_for_branch(branch)
|
35
|
+
"#{url}:#{commit_ref_for_branch(branch)}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def remote_refs
|
39
|
+
@remote_refs ||= begin
|
40
|
+
Git.ls_remote(repository)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def exists_for_commit?(commit)
|
45
|
+
credentials = Hippo.config.dig('docker', 'credentials', registry_host)
|
46
|
+
|
47
|
+
http = Net::HTTP.new(registry_host, 443)
|
48
|
+
http.use_ssl = true
|
49
|
+
request = Net::HTTP::Head.new("/v2/#{registry_image_name}/manifests/#{commit}")
|
50
|
+
if credentials
|
51
|
+
request.basic_auth(credentials['username'], credentials['password'])
|
52
|
+
end
|
53
|
+
response = http.request(request)
|
54
|
+
case response
|
55
|
+
when Net::HTTPOK
|
56
|
+
true
|
57
|
+
when Net::HTTPUnauthorized
|
58
|
+
raise Error, "Could not authenticate to #{registry_host} to verify image existence"
|
59
|
+
when Net::HTTPNotFound
|
60
|
+
false
|
61
|
+
else
|
62
|
+
raise Error, "Got #{response.code} status when verifying imag existence with #{registry_host}"
|
63
|
+
end
|
64
|
+
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
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hippo/util'
|
4
|
+
require 'hippo/stage'
|
5
|
+
require 'hippo/image'
|
6
|
+
|
7
|
+
module Hippo
|
8
|
+
class Manifest
|
9
|
+
# Load a new manifest from a given Hippofile.
|
10
|
+
#
|
11
|
+
# @param path [String]
|
12
|
+
# @return [Hippo::Manifest]
|
13
|
+
class << self
|
14
|
+
def load_from_file(path)
|
15
|
+
unless File.file?(path)
|
16
|
+
raise Error, "Hippofile file not found at #{path}"
|
17
|
+
end
|
18
|
+
|
19
|
+
root = File.dirname(path)
|
20
|
+
new(Util.load_yaml_from_file(path).first, root)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :root
|
25
|
+
|
26
|
+
def initialize(options, root)
|
27
|
+
@options = options
|
28
|
+
@root = File.expand_path(root)
|
29
|
+
end
|
30
|
+
|
31
|
+
def name
|
32
|
+
@options['name'] || 'app'
|
33
|
+
end
|
34
|
+
|
35
|
+
def console
|
36
|
+
@options['console']
|
37
|
+
end
|
38
|
+
|
39
|
+
def template_vars
|
40
|
+
{
|
41
|
+
'name' => name,
|
42
|
+
'images' => images.each_with_object({}) { |(name, image), hash| hash[name.to_s] = image.template_vars }
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def images
|
47
|
+
return {} unless @options['images'].is_a?(Hash)
|
48
|
+
|
49
|
+
@images ||= begin
|
50
|
+
@options['images'].each_with_object({}) do |(key, value), hash|
|
51
|
+
hash[key] = Image.new(key, value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Load all stages that are available in the manifest
|
57
|
+
#
|
58
|
+
# @return [Hash<Symbol, Hippo::Stage>]
|
59
|
+
def stages
|
60
|
+
objects('stages').each_with_object({}) do |(_, objects), hash|
|
61
|
+
objects.each do |obj|
|
62
|
+
stage = Stage.new(self, obj)
|
63
|
+
hash[stage.name] = stage
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Load all YAML objects at a given path and return them.
|
69
|
+
#
|
70
|
+
# @param path [String]
|
71
|
+
# @param decorator [Proc] an optional parser to run across the raw YAML file
|
72
|
+
# @return [Array<Hash>]
|
73
|
+
def objects(path, decorator: nil)
|
74
|
+
files = Dir[File.join(@root, path, '*.{yaml,yml}')]
|
75
|
+
files.each_with_object({}) do |path, objects|
|
76
|
+
file = Util.load_yaml_from_file(path, decorator: decorator)
|
77
|
+
objects[path.sub(%r{\A#{@root}/}, '')] = file
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Hippo
|
6
|
+
class ObjectDefinition
|
7
|
+
def initialize(object, stage, clean: false)
|
8
|
+
@object = object
|
9
|
+
@stage = stage
|
10
|
+
|
11
|
+
unless clean
|
12
|
+
insert_namespace!
|
13
|
+
insert_default_labels!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](name)
|
18
|
+
@object[name]
|
19
|
+
end
|
20
|
+
|
21
|
+
def dig(*args)
|
22
|
+
@object.dig(*args)
|
23
|
+
end
|
24
|
+
|
25
|
+
def name
|
26
|
+
metadata['name']
|
27
|
+
end
|
28
|
+
|
29
|
+
def metadata
|
30
|
+
@object['metadata'] ||= {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def kind
|
34
|
+
@object['kind']
|
35
|
+
end
|
36
|
+
|
37
|
+
def yaml
|
38
|
+
@object.to_yaml
|
39
|
+
end
|
40
|
+
|
41
|
+
def insert_namespace!
|
42
|
+
metadata['namespace'] = @stage.namespace
|
43
|
+
end
|
44
|
+
|
45
|
+
def insert_default_labels!
|
46
|
+
metadata['labels'] ||= {}
|
47
|
+
metadata['labels']['app.kubernetes.io/name'] = @stage.manifest.name
|
48
|
+
metadata['labels']['app.kubernetes.io/instance'] = @stage.name
|
49
|
+
metadata['labels']['app.kubernetes.io/managed-by'] = 'hippo'
|
50
|
+
end
|
51
|
+
|
52
|
+
def insert_deployment_id!(deployment_id)
|
53
|
+
metadata['labels'] ||= {}
|
54
|
+
metadata['labels']['hippo.adam.ac/deployID'] = deployment_id
|
55
|
+
|
56
|
+
# For deployments, insert the ID on the template too for deployments.
|
57
|
+
if kind == 'Deployment' && pod_metadata = @object.dig('spec', 'template', 'metadata')
|
58
|
+
pod_metadata['labels'] ||= {}
|
59
|
+
pod_metadata['labels']['hippo.adam.ac/deployID'] = deployment_id
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/hippo/secret.rb
CHANGED
@@ -1,165 +1,112 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'securerandom'
|
4
|
-
require 'openssl'
|
5
|
-
require 'encryptor'
|
6
|
-
require 'hippo/util'
|
7
|
-
|
8
3
|
module Hippo
|
9
4
|
class Secret
|
10
|
-
include Hippo::Util
|
11
|
-
|
12
5
|
HEADER = [
|
13
6
|
'# This file is encrypted and managed by Hippo.',
|
14
|
-
'# Use `hippo
|
7
|
+
'# Use `hippo [stage] secrets:edit [name]` to make changes to it.',
|
15
8
|
'#',
|
16
9
|
'# Note: this cannot be applied directly to your Kubernetes server because',
|
17
10
|
'# HippoEncryptedSecret is not a valid object. It will be automatically ',
|
18
11
|
'# converted to a Secret when it is applied by Hippo.'
|
19
12
|
].join("\n")
|
20
13
|
|
14
|
+
EDIT_HEADER = [
|
15
|
+
'# This file has been unencrypted for you to edit it.',
|
16
|
+
'# Make your changes and close your edit to re-encrypt and save the file.',
|
17
|
+
'# You can change the apiVersion or add any additional metadata.',
|
18
|
+
'#',
|
19
|
+
'# You should not change the kind of document, it should be HippoEncryptedSecret.'
|
20
|
+
].join("\n")
|
21
|
+
|
21
22
|
def initialize(manager, name)
|
22
23
|
@manager = manager
|
23
24
|
@name = name
|
24
25
|
end
|
25
26
|
|
26
|
-
# Return the path
|
27
|
+
# Return the path where this secret is stored
|
27
28
|
#
|
28
29
|
# @return [String]
|
29
30
|
def path
|
30
|
-
File.join(@manager.
|
31
|
+
File.join(@manager.root, "#{@name}.yaml")
|
31
32
|
end
|
32
33
|
|
33
|
-
# Does
|
34
|
+
# Does this secret exist yet?
|
34
35
|
#
|
35
36
|
# @return [Boolean]
|
36
37
|
def exists?
|
37
38
|
File.file?(path)
|
38
39
|
end
|
39
40
|
|
40
|
-
#
|
41
|
+
# Create a new empty secret file on the file system
|
41
42
|
#
|
42
|
-
# @return [
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
43
|
+
# @return [void]
|
44
|
+
def create
|
45
|
+
return if exists?
|
46
|
+
|
47
|
+
od = ObjectDefinition.new(
|
48
|
+
{
|
49
|
+
'kind' => 'HippoEncryptedSecret',
|
50
|
+
'apiVersion' => 'v1',
|
51
|
+
'metadata' => {
|
52
|
+
'name' => @name
|
53
|
+
},
|
54
|
+
'data' => {
|
55
|
+
'example-value' => @manager.encrypt('This is an example encrypted value!')
|
56
|
+
}
|
57
|
+
},
|
58
|
+
@manager.stage,
|
59
|
+
clean: true
|
60
|
+
)
|
61
|
+
File.open(path, 'w') { |f| f.write(HEADER + "\n" + od.yaml) }
|
54
62
|
end
|
55
63
|
|
56
|
-
#
|
64
|
+
# Read the value from the file and decrypt all values that are present
|
57
65
|
#
|
58
66
|
# @return [String]
|
59
|
-
def
|
60
|
-
decrypted_parts.map { |p| p.hash.to_yaml } .join("---\n")
|
61
|
-
end
|
62
|
-
|
63
|
-
# Edit a secret file by opening an editor and allow changes to be made.
|
64
|
-
# When the editor completes, finish by writing the file back to the disk.
|
65
|
-
#
|
66
|
-
# @return [void]
|
67
|
-
def edit
|
67
|
+
def editable_yaml
|
68
68
|
return unless exists?
|
69
69
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
next if part.nil? || part.empty?
|
75
|
-
|
76
|
-
hash[part.dig('metadata', 'name')] = part['data']
|
77
|
-
end
|
78
|
-
original_part_data = original_decrypted_part_data.each_with_object({}) do |part, hash|
|
79
|
-
name = part.dig('metadata', 'name')
|
80
|
-
enc = original_encrypted_part_data[name]
|
81
|
-
next if enc.nil?
|
82
|
-
|
83
|
-
hash[part.dig('metadata', 'name')] = part['data'].each_with_object({}) do |(key, value), hash2|
|
84
|
-
hash2[key] = [value, enc[key]]
|
70
|
+
objects = Util.load_yaml_from_file(path)
|
71
|
+
objects.each do |hash|
|
72
|
+
hash['data'].each do |key, value|
|
73
|
+
hash['data'][key] = @manager.decrypt(value)
|
85
74
|
end
|
86
75
|
end
|
87
76
|
|
88
|
-
|
89
|
-
saved_contents = open_in_editor("secret-#{@name}", to_editable_yaml)
|
90
|
-
|
91
|
-
# This saved contents should now be validated to ensure it is valid
|
92
|
-
# YAML and, if so, it should be encrypted and then saved into the
|
93
|
-
# secret file as needed.
|
94
|
-
begin
|
95
|
-
yaml_parts = load_yaml_from_data(saved_contents)
|
96
|
-
parts = parse_parts(yaml_parts, :encrypt, original_part_data)
|
97
|
-
write(parts)
|
98
|
-
rescue StandardError => e
|
99
|
-
raise
|
100
|
-
puts "An error occurred parsing your file: #{e.message}"
|
101
|
-
saved_contents = open_in_editor("secret-#{@name}", saved_contents)
|
102
|
-
retry
|
103
|
-
end
|
104
|
-
|
105
|
-
puts "#{@name} secret has been editted"
|
77
|
+
objects.map(&:to_yaml).join("\n---\n")
|
106
78
|
end
|
107
79
|
|
108
|
-
#
|
80
|
+
# Edit this secret
|
109
81
|
#
|
110
82
|
# @return [void]
|
111
|
-
def
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
'data' => {
|
119
|
-
'example' => @manager.encrypt('This is an example secret!')
|
120
|
-
}
|
121
|
-
}
|
122
|
-
write([template])
|
123
|
-
end
|
124
|
-
|
125
|
-
private
|
126
|
-
|
127
|
-
def decrypted_parts
|
128
|
-
return unless exists?
|
129
|
-
|
130
|
-
yaml_parts = load_yaml_from_path(path)
|
131
|
-
parse_parts(yaml_parts, :decrypt)
|
132
|
-
end
|
133
|
-
|
134
|
-
def parse_parts(yaml_parts, method, skips = {})
|
135
|
-
yaml_parts.each_with_object([]) do |part, array|
|
136
|
-
next if part.hash.nil? || part.hash.empty?
|
137
|
-
|
138
|
-
part['data'].each do |key, value|
|
139
|
-
skip = skips[part.dig('metadata', 'name')]
|
140
|
-
part['data'][key] = if skip && skip[key] && skip[key][0] == value
|
141
|
-
skip[key][1]
|
142
|
-
else
|
143
|
-
@manager.public_send(method, value.to_s)
|
144
|
-
end
|
83
|
+
def edit
|
84
|
+
contents = Util.open_in_editor("secret-#{@name}", EDIT_HEADER + "\n" + editable_yaml)
|
85
|
+
yamls = Util.load_yaml_from_data(contents)
|
86
|
+
ods = Util.create_object_definitions({ 'secret' => yamls }, @manager.stage, required_kinds: ['HippoEncryptedSecret'], clean: true)
|
87
|
+
ods.each do |od|
|
88
|
+
od['data'].each do |key, value|
|
89
|
+
od['data'][key] = @manager.encrypt(value)
|
145
90
|
end
|
146
|
-
|
147
|
-
array << part
|
148
91
|
end
|
92
|
+
File.open(path, 'w') { |f| f.write(HEADER + "\n" + ods.map(&:yaml).join("\n---\n")) }
|
93
|
+
rescue StandardError => e
|
94
|
+
puts "Failed to edit secret (#{e.message})"
|
95
|
+
retry
|
149
96
|
end
|
150
97
|
|
151
|
-
#
|
152
|
-
# explanatory header.
|
98
|
+
# Return this secret as it can be exported to kubernetes
|
153
99
|
#
|
154
|
-
# @return [
|
155
|
-
def
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
100
|
+
# @return [Array<ObjectDefinition>]
|
101
|
+
def applyable_yaml
|
102
|
+
objects = Util.load_yaml_from_file(path)
|
103
|
+
objects = objects.each do |hash|
|
104
|
+
hash['kind'] = 'Secret'
|
105
|
+
hash['data'].each do |key, value|
|
106
|
+
hash['data'][key] = Base64.encode64(@manager.decrypt(value)).gsub("\n", '')
|
107
|
+
end
|
162
108
|
end
|
109
|
+
Util.create_object_definitions({ 'secret' => objects }, @manager.stage)
|
163
110
|
end
|
164
111
|
end
|
165
112
|
end
|