hippo-cli 1.1.1 → 1.2.3
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
- data/bin/hippo +1 -1
- data/cli/apply_config.rb +0 -4
- data/cli/apply_services.rb +0 -4
- data/cli/create.rb +9 -13
- data/cli/deploy.rb +0 -4
- data/cli/exec.rb +23 -0
- data/cli/install.rb +0 -4
- data/cli/key.rb +1 -4
- data/cli/kubectl.rb +0 -4
- data/cli/logs.rb +0 -4
- data/cli/objects.rb +0 -4
- data/cli/package_install.rb +0 -4
- data/cli/package_list.rb +0 -4
- data/cli/package_notes.rb +0 -4
- data/cli/package_test.rb +0 -4
- data/cli/package_uninstall.rb +0 -4
- data/cli/package_upgrade.rb +0 -4
- data/cli/package_values.rb +1 -5
- data/cli/prepare.rb +0 -4
- data/cli/readme.rb +18 -0
- data/cli/run.rb +1 -5
- data/cli/secrets.rb +0 -4
- data/cli/setup.rb +71 -0
- data/cli/stages.rb +4 -7
- data/cli/status.rb +0 -4
- data/cli/tidy.rb +33 -0
- data/cli/update.rb +22 -0
- data/cli/vars.rb +0 -4
- data/lib/hippo.rb +7 -0
- data/lib/hippo/bootstrap_parser.rb +22 -0
- data/lib/hippo/cli.rb +31 -27
- data/lib/hippo/image.rb +7 -1
- data/lib/hippo/liquid_filters.rb +15 -0
- data/lib/hippo/manifest.rb +17 -17
- data/lib/hippo/object_definition.rb +1 -1
- data/lib/hippo/package.rb +1 -1
- data/lib/hippo/repository_tag.rb +3 -2
- data/lib/hippo/secret_manager.rb +27 -22
- data/lib/hippo/stage.rb +86 -40
- data/lib/hippo/util.rb +61 -1
- data/lib/hippo/version.rb +1 -1
- data/lib/hippo/working_directory.rb +180 -0
- data/template/Hippofile +1 -2
- metadata +10 -26
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/cli/console.rb +0 -33
- metadata.gz.sig +0 -0
data/lib/hippo/stage.rb
CHANGED
@@ -4,16 +4,23 @@ require 'liquid'
|
|
4
4
|
require 'open3'
|
5
5
|
require 'hippo/secret_manager'
|
6
6
|
require 'hippo/package'
|
7
|
+
require 'hippo/liquid_filters'
|
7
8
|
|
8
9
|
module Hippo
|
9
10
|
class Stage
|
10
|
-
attr_reader :
|
11
|
+
attr_reader :wd
|
12
|
+
attr_reader :config_root
|
11
13
|
|
12
|
-
def initialize(
|
13
|
-
@
|
14
|
+
def initialize(wd, config_root, options)
|
15
|
+
@wd = wd
|
16
|
+
@config_root = config_root
|
14
17
|
@options = options
|
15
18
|
end
|
16
19
|
|
20
|
+
def manifest
|
21
|
+
wd.manifest
|
22
|
+
end
|
23
|
+
|
17
24
|
def name
|
18
25
|
@options['name']
|
19
26
|
end
|
@@ -38,8 +45,18 @@ module Hippo
|
|
38
45
|
@options['config']
|
39
46
|
end
|
40
47
|
|
48
|
+
def command(name)
|
49
|
+
base = manifest.commands[name]
|
50
|
+
return nil if base.nil?
|
51
|
+
|
52
|
+
{
|
53
|
+
target: base['target'],
|
54
|
+
command: decorator.call(base['command'])
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
41
58
|
def images
|
42
|
-
@images ||=
|
59
|
+
@images ||= manifest.images.deep_merge(@options['images'] || {}).each_with_object({}) do |(key, image), hash|
|
43
60
|
hash[key] = Image.new(key, image)
|
44
61
|
end
|
45
62
|
end
|
@@ -48,14 +65,14 @@ module Hippo
|
|
48
65
|
def template_vars
|
49
66
|
@template_vars ||= begin
|
50
67
|
{
|
51
|
-
'manifest' =>
|
68
|
+
'manifest' => manifest.template_vars,
|
52
69
|
'stage-name' => name,
|
53
70
|
'branch' => branch,
|
54
71
|
'image-tag' => image_tag,
|
55
72
|
'namespace' => namespace,
|
56
73
|
'context' => context,
|
57
74
|
'images' => images.values.each_with_object({}) { |image, hash| hash[image.name] = image.template_vars },
|
58
|
-
'config' =>
|
75
|
+
'config' => manifest.config.deep_merge(config),
|
59
76
|
'secrets' => secret_manager.all
|
60
77
|
}
|
61
78
|
end
|
@@ -67,15 +84,51 @@ module Hippo
|
|
67
84
|
proc do |data|
|
68
85
|
begin
|
69
86
|
template = Liquid::Template.parse(data)
|
70
|
-
template.render(template_vars)
|
87
|
+
template.render(template_vars, filters: [LiquidFilters])
|
71
88
|
rescue Liquid::SyntaxError => e
|
72
89
|
raise Error, "Template error: #{e.message}"
|
73
90
|
end
|
74
91
|
end
|
75
92
|
end
|
76
93
|
|
94
|
+
def readme
|
95
|
+
return unless manifest.readme
|
96
|
+
|
97
|
+
decorator.call(manifest.readme)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Return an array of objects that currently exist on the kubernetesa
|
101
|
+
# API.
|
102
|
+
#
|
103
|
+
# @return [Array<Hash>]
|
104
|
+
def live_objects(pruneable_only: false)
|
105
|
+
los = get(all_objects.keys.join(','), '--selector', 'app.kubernetes.io/managed-by=hippo')
|
106
|
+
los.each_with_object([]) do |live_obj, array|
|
107
|
+
local = all_objects.dig(live_obj.kind, live_obj.name)
|
108
|
+
pruneable = local.nil? && (live_obj.kind != 'Secret' && live_obj.name != 'hippo-secret-key')
|
109
|
+
|
110
|
+
next if pruneable_only && !pruneable
|
111
|
+
|
112
|
+
array << {
|
113
|
+
live: live_obj,
|
114
|
+
local: local,
|
115
|
+
pruneable: pruneable
|
116
|
+
}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Remove any objects which are prunable
|
121
|
+
#
|
122
|
+
# @return [void]
|
123
|
+
def delete_pruneable_objects
|
124
|
+
live_objects(pruneable_only: true).each do |object|
|
125
|
+
object = object[:live]
|
126
|
+
delete(object.kind, object.name)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
77
130
|
def objects(path)
|
78
|
-
|
131
|
+
manifest.objects(path, decorator: decorator)
|
79
132
|
end
|
80
133
|
|
81
134
|
def secret_manager
|
@@ -111,6 +164,19 @@ module Hippo
|
|
111
164
|
@jobs[type] ||= Util.create_object_definitions(objects("jobs/#{type}"), self)
|
112
165
|
end
|
113
166
|
|
167
|
+
# Return an array of all objects that should be managed by Hippo
|
168
|
+
#
|
169
|
+
# @return [Hash]
|
170
|
+
def all_objects
|
171
|
+
@all_objects ||= begin
|
172
|
+
all = (deployments | services | configs | jobs('install') | jobs('deploy'))
|
173
|
+
all.each_with_object({}) do |object, hash|
|
174
|
+
hash[object.kind] ||= {}
|
175
|
+
hash[object.kind][object.name] = object
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
114
180
|
# Return a hash of all packages available in the stage
|
115
181
|
#
|
116
182
|
# @return [Hash<String, Hippo::Package>]
|
@@ -144,33 +210,17 @@ module Hippo
|
|
144
210
|
# @param objects [Array<Hippo::ObjectDefinition>]
|
145
211
|
# @return [Hash]
|
146
212
|
def apply(objects)
|
147
|
-
yaml_to_apply = objects.map(&:yaml_to_apply).join("\n")
|
148
|
-
|
149
213
|
command = ['kubectl']
|
150
214
|
command += ['--context', context] if context
|
151
215
|
command += ['apply', '-f', '-']
|
152
|
-
Open3.popen3(command.join(' ')) do |stdin, stdout, stderr, wt|
|
153
|
-
stdin.puts yaml_to_apply
|
154
|
-
stdin.close
|
155
216
|
|
156
|
-
|
157
|
-
stderr = stderr.read.strip
|
217
|
+
yaml_to_apply = objects.map(&:yaml_to_apply).join("\n")
|
158
218
|
|
159
|
-
|
160
|
-
stdout.split("\n").each_with_object({}) do |line, hash|
|
161
|
-
next unless line =~ %r{\A([\w\/\-\.]+) (\w+)\z}
|
219
|
+
stdout, stderr, status = Open3.capture3(command.join(' '), stdin_data: yaml_to_apply + "\n")
|
162
220
|
|
163
|
-
|
164
|
-
status = Regexp.last_match(2)
|
165
|
-
hash[object] = status
|
221
|
+
raise Error, "[kubectl] #{stderr}" unless status.success?
|
166
222
|
|
167
|
-
|
168
|
-
puts "\e[37m====> #{object} #{status}\e[0m"
|
169
|
-
end
|
170
|
-
else
|
171
|
-
raise Error, "[kubectl] #{stderr}"
|
172
|
-
end
|
173
|
-
end
|
223
|
+
Util.parse_kubectl_apply_lines(stdout)
|
174
224
|
end
|
175
225
|
|
176
226
|
# Get some data from the kubernetes API
|
@@ -193,19 +243,15 @@ module Hippo
|
|
193
243
|
# @return [Boolean]
|
194
244
|
def delete(*names)
|
195
245
|
command = kubectl('delete', *names)
|
196
|
-
Open3.
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
246
|
+
stdout, stderr, status = Open3.capture3(*command)
|
247
|
+
if status.success?
|
248
|
+
Util.parse_kubectl_apply_lines(stdout)
|
249
|
+
true
|
250
|
+
else
|
251
|
+
if stderr =~ /\" not found$/
|
252
|
+
false
|
202
253
|
else
|
203
|
-
|
204
|
-
if stderr =~ /\" not found$/
|
205
|
-
false
|
206
|
-
else
|
207
|
-
raise Error, "[kutectl] #{stderr}"
|
208
|
-
end
|
254
|
+
raise Error, "[kutectl] #{stderr}"
|
209
255
|
end
|
210
256
|
end
|
211
257
|
end
|
data/lib/hippo/util.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'yaml'
|
4
|
+
require 'open3'
|
4
5
|
require 'hippo/error'
|
5
6
|
require 'hippo/object_definition'
|
6
7
|
|
@@ -30,6 +31,14 @@ module Hippo
|
|
30
31
|
end
|
31
32
|
end
|
32
33
|
|
34
|
+
def load_objects_from_path(path, decorator: nil)
|
35
|
+
files = Dir[path]
|
36
|
+
files.each_with_object({}) do |path, objects|
|
37
|
+
file = load_yaml_from_file(path, decorator: decorator)
|
38
|
+
objects[path] = file
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
33
42
|
def create_object_definitions(hash, stage, required_kinds: nil, clean: false)
|
34
43
|
index = 0
|
35
44
|
hash.each_with_object([]) do |(path, objects), array|
|
@@ -55,13 +64,17 @@ module Hippo
|
|
55
64
|
end
|
56
65
|
|
57
66
|
def open_in_editor(name, contents)
|
67
|
+
if ENV['EDITOR'].nil?
|
68
|
+
raise Error, 'No EDITOR environment variable has been defined'
|
69
|
+
end
|
70
|
+
|
58
71
|
tmp_root = File.join(ENV['HOME'], '.hippo')
|
59
72
|
FileUtils.mkdir_p(tmp_root)
|
60
73
|
begin
|
61
74
|
tmpfile = Tempfile.new([name, '.yaml'], tmp_root)
|
62
75
|
tmpfile.write(contents)
|
63
76
|
tmpfile.close
|
64
|
-
system("#{ENV['EDITOR']} #{tmpfile.path}")
|
77
|
+
Kernel.system("#{ENV['EDITOR']} #{tmpfile.path}")
|
65
78
|
tmpfile.open
|
66
79
|
tmpfile.read
|
67
80
|
ensure
|
@@ -102,6 +115,53 @@ module Hippo
|
|
102
115
|
response = response.to_s.strip
|
103
116
|
response.empty? ? default : response
|
104
117
|
end
|
118
|
+
|
119
|
+
def system(command, stdin_data: nil)
|
120
|
+
stdout, stderr, status = Open3.capture3(command, stdin_data: stdin_data)
|
121
|
+
unless status.success?
|
122
|
+
raise Error, "Command failed to execute: #{stderr}"
|
123
|
+
end
|
124
|
+
|
125
|
+
stdout
|
126
|
+
end
|
127
|
+
|
128
|
+
def action(message)
|
129
|
+
$stdout.print message
|
130
|
+
complete_state = 'done'
|
131
|
+
color = '32'
|
132
|
+
passed_proc = proc do |value|
|
133
|
+
complete_state = value
|
134
|
+
color = '33'
|
135
|
+
end
|
136
|
+
return_value = yield(passed_proc)
|
137
|
+
puts " \e[#{color}m#{complete_state}\e[0m"
|
138
|
+
return_value
|
139
|
+
rescue StandardError => e
|
140
|
+
puts " \e[31merror\e[0m"
|
141
|
+
raise
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.parse_kubectl_apply_lines(stdout)
|
146
|
+
stdout.split("\n").each_with_object({}) do |line, hash|
|
147
|
+
if line =~ %r{\A([\w\/\-\.]+) (\w+)\z}
|
148
|
+
object = Regexp.last_match(1)
|
149
|
+
status = Regexp.last_match(2)
|
150
|
+
elsif line =~ %r{\A[\w\.\/\-]+ \"([\w\-]+)\" (\w+)\z}
|
151
|
+
object = Regexp.last_match(1)
|
152
|
+
status = Regexp.last_match(2)
|
153
|
+
else
|
154
|
+
next
|
155
|
+
end
|
156
|
+
hash[object] = status
|
157
|
+
|
158
|
+
color = '32'
|
159
|
+
color = '31' if status == 'deleted'
|
160
|
+
color = '33' if status == 'configured'
|
161
|
+
|
162
|
+
status = "\e[#{color}m#{status}\e[0m" unless status == 'unchanged'
|
163
|
+
puts "\e[37m====> #{object} #{status}\e[0m"
|
164
|
+
end
|
105
165
|
end
|
106
166
|
end
|
107
167
|
end
|
data/lib/hippo/version.rb
CHANGED
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hippo/manifest'
|
4
|
+
require 'hippo/util'
|
5
|
+
require 'hippo/error'
|
6
|
+
|
7
|
+
module Hippo
|
8
|
+
class WorkingDirectory
|
9
|
+
attr_reader :root
|
10
|
+
|
11
|
+
def initialize(root = FileUtils.pwd)
|
12
|
+
@root = root
|
13
|
+
end
|
14
|
+
|
15
|
+
# Return the path to the config file in this working directory
|
16
|
+
#
|
17
|
+
# @return [String]
|
18
|
+
def config_path
|
19
|
+
File.join(@root, 'manifest.yaml')
|
20
|
+
end
|
21
|
+
|
22
|
+
# Return the path to the local config file
|
23
|
+
#
|
24
|
+
# @return [String]
|
25
|
+
def local_config_path
|
26
|
+
File.join(@root, 'manifest.local.yaml')
|
27
|
+
end
|
28
|
+
|
29
|
+
# Return all the options configured in this working directory
|
30
|
+
#
|
31
|
+
# @return [Hash]
|
32
|
+
def options
|
33
|
+
return @options if @options
|
34
|
+
|
35
|
+
if File.file?(config_path)
|
36
|
+
@options = YAML.load_file(config_path)
|
37
|
+
if File.file?(local_config_path)
|
38
|
+
@options = @options.deep_merge(YAML.load_file(local_config_path))
|
39
|
+
end
|
40
|
+
@options
|
41
|
+
else
|
42
|
+
raise Error, "No manifest config file found at #{config_path}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Return the manifest objet for this working directory
|
47
|
+
#
|
48
|
+
# @return [Hippo::Manifest]
|
49
|
+
def manifest(update: true)
|
50
|
+
if update && !@updated_manifest
|
51
|
+
update_from_remote if can_update?
|
52
|
+
@updated_manifest = true
|
53
|
+
end
|
54
|
+
|
55
|
+
raise Error, 'No manifest path could be determined' if manifest_path.nil?
|
56
|
+
|
57
|
+
@manifest ||= Manifest.load_from_file(File.join(manifest_path, 'Hippofile'))
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return the path to the manifest directory
|
61
|
+
#
|
62
|
+
# @return [String]
|
63
|
+
def manifest_path
|
64
|
+
case source_type
|
65
|
+
when 'local'
|
66
|
+
options.dig('source', 'localOptions', 'path')
|
67
|
+
when 'remote'
|
68
|
+
path = [remote_path] if remote_path
|
69
|
+
File.join(remote_root_path, *path)
|
70
|
+
else
|
71
|
+
raise Error, "Invalid source.type ('#{source_type}')"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Return the type of manifest
|
76
|
+
#
|
77
|
+
# @return [String]
|
78
|
+
def source_type
|
79
|
+
options.dig('source', 'type')
|
80
|
+
end
|
81
|
+
|
82
|
+
# Return the path on the local filesystem that the remote repository
|
83
|
+
# should be stored in.
|
84
|
+
#
|
85
|
+
# @return [String]
|
86
|
+
def remote_root_path
|
87
|
+
repo_ref = Digest::SHA1.hexdigest([remote_repository, remote_branch].join('---'))
|
88
|
+
File.join(Hippo.tmp_root, 'manifests', repo_ref)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Return the branch to use from the remote repository
|
92
|
+
#
|
93
|
+
# @return [String]
|
94
|
+
def remote_branch
|
95
|
+
options.dig('source', 'remoteOptions', 'branch') || 'master'
|
96
|
+
end
|
97
|
+
|
98
|
+
# Return the URL to the remote repository
|
99
|
+
#
|
100
|
+
# @return [String]
|
101
|
+
def remote_repository
|
102
|
+
options.dig('source', 'remoteOptions', 'repository')
|
103
|
+
end
|
104
|
+
|
105
|
+
# Return the path within the remote repository that we wish to work
|
106
|
+
# with.
|
107
|
+
#
|
108
|
+
# @return [String]
|
109
|
+
def remote_path
|
110
|
+
options.dig('source', 'remoteOptions', 'path')
|
111
|
+
end
|
112
|
+
|
113
|
+
# Update the local cached copy of the manifest from the remote
|
114
|
+
#
|
115
|
+
# @return [Boolean]
|
116
|
+
def update_from_remote(verbose: false)
|
117
|
+
return false unless source_type == 'remote'
|
118
|
+
|
119
|
+
Util.action "Updating manifest from #{remote_repository}..." do
|
120
|
+
if File.directory?(remote_root_path)
|
121
|
+
Util.system("git -C #{remote_root_path} fetch")
|
122
|
+
else
|
123
|
+
FileUtils.mkdir_p(File.dirname(remote_root_path))
|
124
|
+
Util.system("git clone #{remote_repository} #{remote_root_path}")
|
125
|
+
end
|
126
|
+
|
127
|
+
Util.system("git -C #{remote_root_path} checkout origin/#{remote_branch}")
|
128
|
+
File.open(update_timestamp_path, 'w') { |f| f.write(Time.now.to_i.to_s + "\n") }
|
129
|
+
end
|
130
|
+
|
131
|
+
if verbose
|
132
|
+
puts
|
133
|
+
puts " Repository....: \e[33m#{wd.remote_repository}\e[0m"
|
134
|
+
puts " Branch........: \e[33m#{wd.remote_branch}\e[0m"
|
135
|
+
puts " Path..........: \e[33m#{wd.remote_path}\e[0m"
|
136
|
+
puts
|
137
|
+
end
|
138
|
+
|
139
|
+
true
|
140
|
+
end
|
141
|
+
|
142
|
+
# Return the time this manifest was last updated
|
143
|
+
#
|
144
|
+
# @return [Time, nil]
|
145
|
+
def last_updated_at
|
146
|
+
if File.file?(update_timestamp_path)
|
147
|
+
timestamp = File.read(update_timestamp_path)
|
148
|
+
Time.at(timestamp.strip.to_i)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Return the path to the file where the last updated timestamp
|
153
|
+
# is stored
|
154
|
+
#
|
155
|
+
# @return [String]
|
156
|
+
def update_timestamp_path
|
157
|
+
File.join(remote_root_path + '.uptime-timestamp')
|
158
|
+
end
|
159
|
+
|
160
|
+
# Can this working directory be updated?
|
161
|
+
#
|
162
|
+
# @return [Boolean]
|
163
|
+
def can_update?
|
164
|
+
source_type == 'remote'
|
165
|
+
end
|
166
|
+
|
167
|
+
# Load all stages that are available in this working directory
|
168
|
+
#
|
169
|
+
# @return [Hash<Symbol, Hippo::Stage>]
|
170
|
+
def stages
|
171
|
+
objects = Util.load_objects_from_path(File.join(@root, '*', 'config.{yml,yaml}'))
|
172
|
+
objects.each_with_object({}) do |(path, objects), hash|
|
173
|
+
objects.each do |obj|
|
174
|
+
stage = Stage.new(self, File.dirname(path), obj)
|
175
|
+
hash[stage.name] = stage
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|