hippo-cli 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +3 -0
- data/bin/hippo +36 -0
- data/cli/apply_config.rb +17 -0
- data/cli/apply_services.rb +15 -0
- data/cli/build.rb +16 -0
- data/cli/console.rb +31 -0
- data/cli/deploy.rb +45 -0
- data/cli/edit-secret.rb +45 -0
- data/cli/help.rb +21 -0
- data/cli/init.rb +25 -0
- data/cli/install.rb +44 -0
- data/cli/kubectl.rb +16 -0
- data/cli/objects.rb +34 -0
- data/cli/publish.rb +17 -0
- data/cli/secrets.rb +23 -0
- data/cli/status.rb +15 -0
- data/lib/hippo/build_spec.rb +32 -0
- data/lib/hippo/cli_steps.rb +274 -0
- data/lib/hippo/error.rb +18 -0
- data/lib/hippo/kubernetes.rb +200 -0
- data/lib/hippo/recipe.rb +126 -0
- data/lib/hippo/repository.rb +122 -0
- data/lib/hippo/secret.rb +165 -0
- data/lib/hippo/secret_manager.rb +99 -0
- data/lib/hippo/stage.rb +34 -0
- data/lib/hippo/util.rb +50 -0
- data/lib/hippo/version.rb +3 -0
- data/lib/hippo/yaml_part.rb +47 -0
- data/lib/hippo.rb +7 -0
- data/template/Hippofile +19 -0
- data/template/config/production/env-vars.yaml +7 -0
- data/template/deployments/web.yaml +29 -0
- data/template/deployments/worker.yaml +26 -0
- data/template/jobs/install/load-schema.yaml +22 -0
- data/template/jobs/upgrade/db-migration.yaml +22 -0
- data/template/services/main.ingress.yaml +13 -0
- data/template/services/web.svc.yaml +11 -0
- data/template/stages/production.yaml +6 -0
- data.tar.gz.sig +0 -0
- metadata +163 -0
- metadata.gz.sig +1 -0
@@ -0,0 +1,274 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'hippo/error'
|
5
|
+
require 'hippo/recipe'
|
6
|
+
|
7
|
+
module Hippo
|
8
|
+
class CLISteps
|
9
|
+
attr_reader :recipe
|
10
|
+
attr_reader :stage
|
11
|
+
|
12
|
+
def initialize(recipe, stage)
|
13
|
+
@recipe = recipe
|
14
|
+
@stage = stage
|
15
|
+
end
|
16
|
+
|
17
|
+
# Prepare the repository for this build by getting the latest
|
18
|
+
# version from the remote and checking out the branch.
|
19
|
+
def prepare_repository(fetch: true)
|
20
|
+
info "Using repository #{@recipe.repository.url}"
|
21
|
+
if fetch
|
22
|
+
if @recipe.repository.cloned?
|
23
|
+
info 'Repository is already cloned'
|
24
|
+
action 'Fetching the latest repository data...'
|
25
|
+
@recipe.repository.fetch
|
26
|
+
else
|
27
|
+
info 'Repository is not yet cloned.'
|
28
|
+
action 'Cloning repository...'
|
29
|
+
@recipe.repository.clone
|
30
|
+
end
|
31
|
+
|
32
|
+
elsif !fetch && !@recipe.repository.cloned?
|
33
|
+
raise Error, 'Repository is not cloned yet so cannot continue'
|
34
|
+
else
|
35
|
+
info 'Not fetching latest repository, using cached copy'
|
36
|
+
end
|
37
|
+
|
38
|
+
action "Checking out '#{@stage.branch}' branch..."
|
39
|
+
@recipe.repository.checkout(@stage.branch)
|
40
|
+
@commit = @recipe.repository.commit
|
41
|
+
info "Latest commit on branch is #{@commit.objectish}"
|
42
|
+
info "Message: #{@commit.message.split("\n").first}"
|
43
|
+
@commit
|
44
|
+
end
|
45
|
+
|
46
|
+
def build
|
47
|
+
if @commit.nil?
|
48
|
+
raise Error, 'You cannot build without first preparing the repository'
|
49
|
+
end
|
50
|
+
|
51
|
+
Dir.chdir(@recipe.repository.path) do
|
52
|
+
@recipe.build_specs.each do |_, build_spec|
|
53
|
+
if build_spec.image_name.nil?
|
54
|
+
raise Error, "No image-name has been specified for build #{build_spec.name}"
|
55
|
+
end
|
56
|
+
|
57
|
+
image_name = build_spec.image_name_for_commit(@commit)
|
58
|
+
action "Building #{build_spec.name} with tag #{image_name}"
|
59
|
+
|
60
|
+
command = [
|
61
|
+
'docker', 'build', '.',
|
62
|
+
'-f', build_spec.dockerfile,
|
63
|
+
'-t', image_name
|
64
|
+
]
|
65
|
+
external_command do
|
66
|
+
if system(*command)
|
67
|
+
@built_commit = @commit
|
68
|
+
success "Successfully built image #{build_spec.image_name} for #{build_spec.name}"
|
69
|
+
else
|
70
|
+
raise Error, "Image for #{build_spec.name} did not succeed. Check output and try again."
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def publish
|
78
|
+
if @built_commit.nil?
|
79
|
+
raise Error, 'You cannot publish without first building the image'
|
80
|
+
end
|
81
|
+
|
82
|
+
Dir.chdir(@recipe.repository.path) do
|
83
|
+
@recipe.build_specs.each do |_, build_spec|
|
84
|
+
if build_spec.image_name.nil?
|
85
|
+
raise Error, "No image-name has been specified for build #{build_spec.name}"
|
86
|
+
end
|
87
|
+
|
88
|
+
image_name = build_spec.image_name_for_commit(@built_commit.objectish)
|
89
|
+
action "Publishing #{build_spec.name} with tag #{image_name}"
|
90
|
+
|
91
|
+
command = ['docker', 'push', image_name]
|
92
|
+
external_command do
|
93
|
+
if system(*command)
|
94
|
+
success "Successfully published image #{image_name} for #{build_spec.name}"
|
95
|
+
else
|
96
|
+
raise Error, "Image for #{build_spec.name} was not published successfully. Check output and try again."
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def run_install_jobs
|
104
|
+
run_jobs('install')
|
105
|
+
end
|
106
|
+
|
107
|
+
def run_deploy_jobs
|
108
|
+
run_jobs('deploy')
|
109
|
+
end
|
110
|
+
|
111
|
+
def deploy
|
112
|
+
action 'Applying deployments'
|
113
|
+
deployments = @recipe.kubernetes.objects('deployments', @stage, @commit)
|
114
|
+
|
115
|
+
if deployments.nil?
|
116
|
+
info 'No deployments file configured. Not applying any deployments'
|
117
|
+
end
|
118
|
+
|
119
|
+
external_command do
|
120
|
+
@recipe.kubernetes.apply_with_kubectl(deployments)
|
121
|
+
end
|
122
|
+
success 'Deployments applied successfully'
|
123
|
+
puts 'You can watch the deployment progressing using the command below:'
|
124
|
+
puts
|
125
|
+
puts " ⏰ #{@stage.kubectl('get pods --watch')}"
|
126
|
+
deployments.each do |deployment|
|
127
|
+
puts ' 👩🏼💻 ' + @stage.kubectl("describe deployment #{deployment['metadata']['name']}")
|
128
|
+
end
|
129
|
+
puts
|
130
|
+
end
|
131
|
+
|
132
|
+
def apply_services
|
133
|
+
action 'Applying services'
|
134
|
+
objects = @recipe.kubernetes.objects('services', @stage, @commit)
|
135
|
+
if objects.empty?
|
136
|
+
info 'No services have been defined'
|
137
|
+
else
|
138
|
+
external_command do
|
139
|
+
@recipe.kubernetes.apply_with_kubectl(objects)
|
140
|
+
end
|
141
|
+
success 'Services applied successfully'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def apply_namespace
|
146
|
+
action 'Applying namespace'
|
147
|
+
external_command { @recipe.kubernetes.apply_namespace(@stage) }
|
148
|
+
success 'Namespace applied successfully'
|
149
|
+
end
|
150
|
+
|
151
|
+
def apply_config
|
152
|
+
action 'Applying configuration'
|
153
|
+
objects = @recipe.kubernetes.objects('config', @stage, @commit)
|
154
|
+
if objects.empty?
|
155
|
+
info 'No configuration files have been defined'
|
156
|
+
else
|
157
|
+
external_command do
|
158
|
+
@recipe.kubernetes.apply_with_kubectl(objects)
|
159
|
+
end
|
160
|
+
success 'Configuration applied successfully'
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def apply_secrets
|
165
|
+
require 'hippo/secret_manager'
|
166
|
+
action 'Applying secrets'
|
167
|
+
manager = SecretManager.new(@recipe, @stage)
|
168
|
+
unless manager.key_available?
|
169
|
+
error 'No secret encryption key was available. Not applying secrets.'
|
170
|
+
return
|
171
|
+
end
|
172
|
+
|
173
|
+
yamls = manager.secrets.map(&:to_secret_yaml).join("---\n")
|
174
|
+
external_command do
|
175
|
+
@recipe.kubernetes.apply_with_kubectl(yamls)
|
176
|
+
end
|
177
|
+
success 'Secrets applicated successfully'
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
def info(text)
|
183
|
+
puts text
|
184
|
+
end
|
185
|
+
|
186
|
+
def success(text)
|
187
|
+
puts "\e[32m#{text}\e[0m"
|
188
|
+
end
|
189
|
+
|
190
|
+
def action(text)
|
191
|
+
puts "\e[33m#{text}\e[0m"
|
192
|
+
end
|
193
|
+
|
194
|
+
def error(text)
|
195
|
+
puts "\e[31m#{text}\e[0m"
|
196
|
+
end
|
197
|
+
|
198
|
+
def external_command
|
199
|
+
$stdout.print "\e[37m"
|
200
|
+
yield
|
201
|
+
ensure
|
202
|
+
$stdout.print "\e[0m"
|
203
|
+
end
|
204
|
+
|
205
|
+
def run_jobs(type)
|
206
|
+
objects = @recipe.kubernetes.objects("jobs/#{type}", @stage, @commit)
|
207
|
+
if objects.empty?
|
208
|
+
info "No #{type} jobs exist so not applying anything"
|
209
|
+
return true
|
210
|
+
end
|
211
|
+
|
212
|
+
action "Applying #{type} job objects objects to Kubernetes"
|
213
|
+
|
214
|
+
result = nil
|
215
|
+
external_command do
|
216
|
+
# Remove any previous jobs that might have been running before
|
217
|
+
objects.each do |job|
|
218
|
+
@recipe.kubernetes.delete_job(@stage, job['metadata']['name'])
|
219
|
+
end
|
220
|
+
|
221
|
+
result = @recipe.kubernetes.apply_with_kubectl(objects)
|
222
|
+
end
|
223
|
+
|
224
|
+
puts 'Waiting for all scheduled jobs to finish...'
|
225
|
+
timeout, jobs = @recipe.kubernetes.wait_for_jobs(@stage, result.keys)
|
226
|
+
success_jobs = []
|
227
|
+
failed_jobs = []
|
228
|
+
jobs.each do |job|
|
229
|
+
if job['status']['succeeded']
|
230
|
+
success_jobs << job
|
231
|
+
else
|
232
|
+
failed_jobs << job
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
if success_jobs.size == jobs.size
|
237
|
+
success 'All jobs completed successfully.'
|
238
|
+
puts 'You can review the logs for these by running the commands below.'
|
239
|
+
puts
|
240
|
+
result = true
|
241
|
+
else
|
242
|
+
error 'Not all install jobs completed successfully.'
|
243
|
+
puts 'You should review the logs for these using the commands below.'
|
244
|
+
puts
|
245
|
+
result = false
|
246
|
+
end
|
247
|
+
|
248
|
+
jobs.each do |job|
|
249
|
+
icon = if job['status']['succeeded']
|
250
|
+
'✅'
|
251
|
+
else
|
252
|
+
'❌'
|
253
|
+
end
|
254
|
+
puts " #{icon} " + @stage.kubectl("logs job/#{job['metadata']['name']}")
|
255
|
+
end
|
256
|
+
puts
|
257
|
+
|
258
|
+
result
|
259
|
+
end
|
260
|
+
|
261
|
+
class << self
|
262
|
+
def setup(context)
|
263
|
+
recipe = Hippo::Recipe.load_from_file(context.options[:hippofile] || './Hippofile')
|
264
|
+
|
265
|
+
stage = recipe.stages[CURRENT_STAGE]
|
266
|
+
if stage.nil?
|
267
|
+
raise Error, "Invalid stage name `#{CURRENT_STAGE}`. Check this has been defined in in your stages directory with a matching name?"
|
268
|
+
end
|
269
|
+
|
270
|
+
new(recipe, stage)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
data/lib/hippo/error.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hippo
|
4
|
+
class Error < StandardError
|
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
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'liquid'
|
4
|
+
require 'hippo/util'
|
5
|
+
|
6
|
+
module Hippo
|
7
|
+
class Kubernetes
|
8
|
+
OBJECT_DIRECTORY_NAMES = %w[config deployments jobs/install jobs/deploy services].freeze
|
9
|
+
|
10
|
+
include Hippo::Util
|
11
|
+
|
12
|
+
def initialize(recipe, options)
|
13
|
+
@recipe = recipe
|
14
|
+
@options = options
|
15
|
+
end
|
16
|
+
|
17
|
+
# Load and return a set of objects from a given path.
|
18
|
+
# Parse them through the templating and return them in the appropriate
|
19
|
+
# context.
|
20
|
+
#
|
21
|
+
# @param stage [Hippo::Stage]
|
22
|
+
# @param commit [String] the commit ref
|
23
|
+
# @param path [String]
|
24
|
+
# @return
|
25
|
+
def objects(path, stage, commit)
|
26
|
+
time = Time.now
|
27
|
+
|
28
|
+
yamls = load_yaml_from_directory(path)
|
29
|
+
yamls |= load_yaml_from_directory(File.join(path, stage.name))
|
30
|
+
|
31
|
+
yamls.map do |yaml_part|
|
32
|
+
object = yaml_part.parse(@recipe, stage, commit)
|
33
|
+
|
34
|
+
# Unless a namespace has been specified in the metadata we will
|
35
|
+
# want to add the namespace for the current stage.
|
36
|
+
if object['metadata'].nil? || object['metadata']['namespace'].nil?
|
37
|
+
object['metadata'] ||= {}
|
38
|
+
object['metadata']['namespace'] = stage.namespace
|
39
|
+
end
|
40
|
+
|
41
|
+
# Add our own details to the metadata of all objets created by us so
|
42
|
+
# we know where they came from.
|
43
|
+
object['metadata']['annotations'] ||= {}
|
44
|
+
object['metadata']['annotations']['hippo.adam.ac/builtAt'] ||= time.to_s
|
45
|
+
object['metadata']['annotations']['hippo.adam.ac/builtBy'] ||= ENV['USER'] || 'unknown'
|
46
|
+
|
47
|
+
add_default_labels(object, stage)
|
48
|
+
|
49
|
+
# Add some information to Deployments to reflect the latest
|
50
|
+
# information about this deployment.
|
51
|
+
if object['kind'] == 'Deployment'
|
52
|
+
object['metadata']['annotations']['hippo.adam.ac/deployID'] ||= time.to_i.to_s
|
53
|
+
if commit
|
54
|
+
object['metadata']['annotations']['hippo.adam.ac/commitRef'] ||= commit.objectish
|
55
|
+
object['metadata']['annotations']['hippo.adam.ac/commitMessage'] ||= commit.message
|
56
|
+
end
|
57
|
+
|
58
|
+
if pod_metadata = object.dig('spec', 'template', 'metadata')
|
59
|
+
pod_metadata['annotations'] ||= {}
|
60
|
+
pod_metadata['annotations']['hippo.adam.ac/deployID'] ||= time.to_i.to_s
|
61
|
+
if commit
|
62
|
+
pod_metadata['annotations']['hippo.adam.ac/commitRef'] ||= commit.objectish
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
object
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def apply_namespace(stage)
|
72
|
+
namespace = {
|
73
|
+
'kind' => 'Namespace',
|
74
|
+
'apiVersion' => 'v1',
|
75
|
+
'metadata' => {
|
76
|
+
'name' => stage.namespace
|
77
|
+
}
|
78
|
+
}
|
79
|
+
add_default_labels(namespace, stage)
|
80
|
+
apply_with_kubectl(namespace.to_yaml)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Apply the given configuration with kubectl
|
84
|
+
#
|
85
|
+
# @param config [Array<Hippo::YAMLPart>, String]
|
86
|
+
# @return [void]
|
87
|
+
def apply_with_kubectl(yaml_parts)
|
88
|
+
unless yaml_parts.is_a?(String)
|
89
|
+
yaml_parts = [yaml_parts] unless yaml_parts.is_a?(Array)
|
90
|
+
yaml_parts = yaml_parts.map { |yp| yp.hash.to_yaml }.join("\n---\n")
|
91
|
+
end
|
92
|
+
|
93
|
+
Open3.popen3('kubectl apply -f -') do |stdin, stdout, stderr, wt|
|
94
|
+
stdin.puts yaml_parts
|
95
|
+
stdin.close
|
96
|
+
|
97
|
+
stdout = stdout.read.strip
|
98
|
+
stderr = stderr.read.strip
|
99
|
+
|
100
|
+
if wt.value.success?
|
101
|
+
puts stdout
|
102
|
+
stdout.split("\n").each_with_object({}) do |line, hash|
|
103
|
+
if line =~ %r{\A([\w\/\-\.]+) (\w+)\z}
|
104
|
+
hash[Regexp.last_match(1)] = Regexp.last_match(2)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
else
|
108
|
+
raise Error, "[kubectl] #{stderr}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Get details of objects using kubectl.
|
114
|
+
#
|
115
|
+
# @param stage [Hippo::Stage]
|
116
|
+
# @param names [Array<String>]
|
117
|
+
# @raises [Hippo::Error]
|
118
|
+
# @return [Array<Hash>]
|
119
|
+
def get_with_kubectl(stage, *names)
|
120
|
+
command = [
|
121
|
+
'kubectl',
|
122
|
+
'-n', stage.namespace,
|
123
|
+
'get',
|
124
|
+
names,
|
125
|
+
'-o', 'yaml'
|
126
|
+
].flatten.reject(&:nil?)
|
127
|
+
|
128
|
+
Open3.popen3(*command) do |_, stdout, stderr, wt|
|
129
|
+
if wt.value.success?
|
130
|
+
yaml = YAML.safe_load(stdout.read, permitted_classes: [Time])
|
131
|
+
yaml['items'] || [yaml]
|
132
|
+
else
|
133
|
+
raise Error, "[kutectl] #{stderr.read}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Delete a named job from the cluster
|
139
|
+
#
|
140
|
+
# @param stage [Hippo::Stage]
|
141
|
+
# @param name [String]
|
142
|
+
# @raises [Hippo::Error]
|
143
|
+
# @return [void]
|
144
|
+
def delete_job(stage, name)
|
145
|
+
command = [
|
146
|
+
'kubectl',
|
147
|
+
'-n', stage.namespace,
|
148
|
+
'delete',
|
149
|
+
'job',
|
150
|
+
name
|
151
|
+
]
|
152
|
+
|
153
|
+
Open3.popen3(*command) do |_, stdout, stderr, wt|
|
154
|
+
if wt.value.success?
|
155
|
+
puts stdout.read
|
156
|
+
true
|
157
|
+
else
|
158
|
+
stderr = stderr.read
|
159
|
+
if stderr =~ /\"#{name}\" not found/
|
160
|
+
false
|
161
|
+
else
|
162
|
+
raise Error, "[kutectl] #{stderr.read}"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Poll the named jobs and return them when all are complete
|
169
|
+
# or the number of checks is exceeded.
|
170
|
+
#
|
171
|
+
# @param stage [Hippo::Stage]
|
172
|
+
# @param names [Array<String>]
|
173
|
+
# @param times [Integer]
|
174
|
+
# @return [Array<Boolean, Array<Hash>]
|
175
|
+
def wait_for_jobs(stage, names, times = 120)
|
176
|
+
jobs = nil
|
177
|
+
times.times do
|
178
|
+
jobs = get_with_kubectl(stage, *names)
|
179
|
+
|
180
|
+
# Are all the jobs completed?
|
181
|
+
if jobs.all? { |j| j['status']['active'].nil? }
|
182
|
+
return [false, jobs]
|
183
|
+
else
|
184
|
+
sleep 2
|
185
|
+
end
|
186
|
+
end
|
187
|
+
[true, jobs]
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
def add_default_labels(object, stage)
|
193
|
+
object['metadata']['labels'] ||= {}
|
194
|
+
object['metadata']['labels']['app.kubernetes.io/name'] = @recipe.name
|
195
|
+
object['metadata']['labels']['app.kubernetes.io/instance'] = stage.name
|
196
|
+
object['metadata']['labels']['app.kubernetes.io/managed-by'] = 'hippo'
|
197
|
+
object
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
data/lib/hippo/recipe.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'hippo/build_spec'
|
5
|
+
require 'hippo/error'
|
6
|
+
require 'hippo/kubernetes'
|
7
|
+
require 'hippo/repository'
|
8
|
+
require 'hippo/stage'
|
9
|
+
require 'hippo/util'
|
10
|
+
|
11
|
+
module Hippo
|
12
|
+
class Recipe
|
13
|
+
include Util
|
14
|
+
|
15
|
+
class RecipeNotFound < Error
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
# Load a new Recipe class from a given file.
|
20
|
+
#
|
21
|
+
# @param path [String] path to recipe file.
|
22
|
+
# @return [Hippo::Recipe]
|
23
|
+
def load_from_file(path)
|
24
|
+
unless File.file?(path)
|
25
|
+
raise RecipeNotFound, "No recipe file found at #{path}"
|
26
|
+
end
|
27
|
+
|
28
|
+
hash = YAML.load_file(path)
|
29
|
+
new(hash, path)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param hash [Hash] the raw hash from the underlying yaml file
|
34
|
+
def initialize(hash, hippofile_path = nil)
|
35
|
+
@hash = hash
|
36
|
+
@hippofile_path = hippofile_path
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :path
|
40
|
+
|
41
|
+
# Return the root directory where the Hippofile is located
|
42
|
+
#
|
43
|
+
def root
|
44
|
+
File.dirname(@hippofile_path)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return the repository that this manifest should be working with
|
48
|
+
#
|
49
|
+
# @return [Hippo::Repository]
|
50
|
+
def repository
|
51
|
+
return unless @hash['repository']
|
52
|
+
|
53
|
+
@repository ||= Repository.new(@hash['repository'])
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return the app name
|
57
|
+
#
|
58
|
+
# @return [String, nil]
|
59
|
+
def name
|
60
|
+
@hash['name']
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return kubernetes configuration
|
64
|
+
#
|
65
|
+
# @return [Hippo::Kubernetes]
|
66
|
+
def kubernetes
|
67
|
+
@kubernetes ||= Kubernetes.new(self, @hash['kubernetes'] || {})
|
68
|
+
end
|
69
|
+
|
70
|
+
# Return the stages for this recipe
|
71
|
+
#
|
72
|
+
# @return [Hash<Hippo::Stage>]
|
73
|
+
def stages
|
74
|
+
@stages ||= begin
|
75
|
+
yamls = load_yaml_from_directory(File.join(root, 'stages'))
|
76
|
+
yamls.each_with_object({}) do |yaml, hash|
|
77
|
+
stage = Stage.new(yaml)
|
78
|
+
hash[stage.name] = stage
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Return the builds for this recipe
|
84
|
+
#
|
85
|
+
# @return [Hash<Hippo::BuildSpec>]
|
86
|
+
def build_specs
|
87
|
+
@build_specs ||= @hash['builds'].each_with_object({}) do |(key, options), hash|
|
88
|
+
hash[key] = BuildSpec.new(self, key, options)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return configuration
|
93
|
+
#
|
94
|
+
# @return []
|
95
|
+
def console
|
96
|
+
@hash['console']
|
97
|
+
end
|
98
|
+
|
99
|
+
# Return the template variables that should be exposed
|
100
|
+
#
|
101
|
+
# @return [Hash]
|
102
|
+
def template_vars
|
103
|
+
{
|
104
|
+
'repository' => repository ? repository.template_vars : nil,
|
105
|
+
'builds' => build_specs.each_with_object({}) { |(_, bs), h| h[bs.name] = bs.template_vars }
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
# Parse a string through the template parser
|
110
|
+
#
|
111
|
+
# @param string [String]
|
112
|
+
# @return [String]
|
113
|
+
def parse(stage, commit, string)
|
114
|
+
template = Liquid::Template.parse(string)
|
115
|
+
template_variables = template_vars
|
116
|
+
template_variables['stage'] = stage.template_vars
|
117
|
+
if commit
|
118
|
+
template_variables['commit'] = {
|
119
|
+
'ref' => commit.objectish,
|
120
|
+
'message' => commit.message
|
121
|
+
}
|
122
|
+
end
|
123
|
+
template.render(template_variables)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|