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.
@@ -1,6 +1,7 @@
1
1
  name: production
2
2
  branch: master
3
3
  namespace: myapp-production
4
+ context: my-k8s-cluster
4
5
  vars:
5
6
  example: Hello world!
6
7
  hostname: myapp.mydomain.com
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hippo-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Cooke
@@ -30,8 +30,28 @@ cert_chain:
30
30
  3wUJNGnT5XYq+qvTqmjkTSTfdGvZCM63C6bGdN5CAyMokGOOatGqyCMAONolWnfC
31
31
  gm3t2GWWrxY=
32
32
  -----END CERTIFICATE-----
33
- date: 2020-01-29 00:00:00.000000000 Z
33
+ date: 2020-02-01 00:00:00.000000000 Z
34
34
  dependencies:
35
+ - !ruby/object:Gem::Dependency
36
+ name: encryptor
37
+ requirement: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '3.0'
42
+ - - "<"
43
+ - !ruby/object:Gem::Version
44
+ version: '4.0'
45
+ type: :runtime
46
+ prerelease: false
47
+ version_requirements: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '3.0'
52
+ - - "<"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.0'
35
55
  - !ruby/object:Gem::Dependency
36
56
  name: git
37
57
  requirement: !ruby/object:Gem::Requirement
@@ -103,37 +123,32 @@ files:
103
123
  - bin/hippo
104
124
  - cli/apply_config.rb
105
125
  - cli/apply_services.rb
106
- - cli/build.rb
107
126
  - cli/console.rb
108
127
  - cli/deploy.rb
109
- - cli/edit-secret.rb
110
128
  - cli/help.rb
111
129
  - cli/init.rb
112
130
  - cli/install.rb
113
131
  - cli/kubectl.rb
114
- - cli/objects.rb
115
- - cli/publish.rb
116
- - cli/secrets.rb
117
- - cli/status.rb
132
+ - cli/secrets_edit.rb
133
+ - cli/secrets_key.rb
118
134
  - lib/hippo.rb
119
- - lib/hippo/build_spec.rb
120
- - lib/hippo/cli_steps.rb
135
+ - lib/hippo/cli.rb
136
+ - lib/hippo/deployment_monitor.rb
121
137
  - lib/hippo/error.rb
122
- - lib/hippo/kubernetes.rb
123
- - lib/hippo/recipe.rb
124
- - lib/hippo/repository.rb
138
+ - lib/hippo/image.rb
139
+ - lib/hippo/manifest.rb
140
+ - lib/hippo/object_definition.rb
125
141
  - lib/hippo/secret.rb
126
142
  - lib/hippo/secret_manager.rb
127
143
  - lib/hippo/stage.rb
128
144
  - lib/hippo/util.rb
129
145
  - lib/hippo/version.rb
130
- - lib/hippo/yaml_part.rb
131
146
  - template/Hippofile
132
- - template/config/production/env-vars.yaml
147
+ - template/config/env-vars.yaml
133
148
  - template/deployments/web.yaml
134
149
  - template/deployments/worker.yaml
150
+ - template/jobs/deploy/db-migration.yaml
135
151
  - template/jobs/install/load-schema.yaml
136
- - template/jobs/upgrade/db-migration.yaml
137
152
  - template/services/main.ingress.yaml
138
153
  - template/services/web.svc.yaml
139
154
  - template/stages/production.yaml
metadata.gz.sig CHANGED
Binary file
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- command :build do
4
- desc 'Build an image for the given stage'
5
-
6
- option '-h', '--hippofile [RECIPE]', 'The path to the Hippofile (defaults: ./Hippofile)' do |value, options|
7
- options[:hippofile] = value.to_s
8
- end
9
-
10
- action do |context|
11
- require 'hippo/cli_steps'
12
- steps = Hippo::CLISteps.setup(context)
13
- steps.prepare_repository
14
- steps.build
15
- end
16
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- command :'edit-secret' do
4
- desc 'Create/edit an encrypted secrets file'
5
-
6
- option '--create-key', 'Create a new encryption key if missing' do |_value, options|
7
- options[:create_key] = true
8
- end
9
-
10
- action do |context|
11
- require 'hippo/cli_steps'
12
- steps = Hippo::CLISteps.setup(context)
13
-
14
- secret_name = context.args[0]
15
- raise Hippo::Error, 'You must provide a secret name' if secret_name.nil?
16
-
17
- require 'hippo/secret_manager'
18
- manager = Hippo::SecretManager.new(steps.recipe, steps.stage)
19
- if !manager.key_available? && context.options[:create_key]
20
- manager.create_key
21
- elsif !manager.key_available?
22
- puts "\e[31mNo key has been published for this stage yet. You can create"
23
- puts "a key automatically by adding --create-key to this command.\e[0m"
24
- exit 2
25
- elsif context.options[:create_key]
26
- puts "\e[31mThe --create-key option can only be provided when a key has not already"
27
- puts "been generated. Remove the key from the Kubernetes API to regenerate.\e[0m"
28
- exit 2
29
- end
30
-
31
- secret = manager.secret(secret_name)
32
- if secret.exists?
33
- secret.edit
34
- else
35
- puts "No secret exists at #{secret.path}. Would you like to create one?"
36
- response = STDIN.gets.strip.downcase.strip
37
- if %w[y yes please].include?(response)
38
- secret.create
39
- secret.edit
40
- else
41
- puts 'Not a problem. You can make it later.'
42
- end
43
- end
44
- end
45
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- command :objects do
4
- desc 'Build and publish an image for the given stage'
5
-
6
- option '-h', '--hippofile [RECIPE]', 'The path to the Hippofile (defaults: ./Hippofile)' do |value, options|
7
- options[:hippofile] = value.to_s
8
- end
9
-
10
- option '--types [TYPES]', 'The types of objects you wish to see' do |value, options|
11
- options[:types] = value.split(/,/)
12
- end
13
-
14
- action do |context|
15
- require 'hippo/cli_steps'
16
- steps = Hippo::CLISteps.setup(context)
17
- commit = steps.prepare_repository(fetch: false)
18
-
19
- if context.options[:types].nil? || context.options[:types].include?('all')
20
- types = Hippo::Kubernetes::OBJECT_DIRECTORY_NAMES
21
- else
22
- types = context.options[:types]
23
- end
24
-
25
- objects = []
26
- types.each do |type|
27
- next unless Hippo::Kubernetes::OBJECT_DIRECTORY_NAMES.include?(type)
28
-
29
- objects += steps.recipe.kubernetes.objects(type, steps.stage, commit, deploy_id: 'preview')
30
- end
31
-
32
- puts objects.map { |o| o.hash.to_yaml }
33
- end
34
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- command :publish do
4
- desc 'Build and publish an image for the given stage'
5
-
6
- option '-h', '--hippofile [RECIPE]', 'The path to the Hippofile (defaults: ./Hippofile)' do |value, options|
7
- options[:hippofile] = value.to_s
8
- end
9
-
10
- action do |context|
11
- require 'hippo/cli_steps'
12
- steps = Hippo::CLISteps.setup(context)
13
- steps.prepare_repository
14
- steps.build
15
- steps.publish
16
- end
17
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- command :secrets do
4
- desc 'View all secrets'
5
-
6
- option '-h', '--hippofile [RECIPE]', 'The path to the Hippofile (defaults: ./Hippofile)' do |value, options|
7
- options[:hippofile] = value.to_s
8
- end
9
-
10
- action do |context|
11
- require 'hippo/cli_steps'
12
- cli = Hippo::CLISteps.setup(context)
13
-
14
- require 'hippo/secret_manager'
15
- manager = Hippo::SecretManager.new(cli.recipe, cli.stage)
16
- unless manager.key_available?
17
- puts "\e[31mNo key has been published for this stage yet.\e[0m"
18
- exit 2
19
- end
20
-
21
- puts manager.secrets.map(&:to_editable_yaml)
22
- end
23
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- command :status do
4
- desc 'Print status information from Kubernetes'
5
-
6
- option '-h', '--hippofile [RECIPE]', 'The path to the Hippofile (defaults: ./Hippofile)' do |value, options|
7
- options[:hippofile] = value.to_s
8
- end
9
-
10
- action do |context|
11
- require 'hippo/cli_steps'
12
- cli = Hippo::CLISteps.setup(context)
13
- exec cli.stage.kubectl('get pods,deployments,job,statefulset,pvc,svc,ingress')
14
- end
15
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hippo
4
- class BuildSpec
5
- attr_reader :name
6
-
7
- def initialize(recipe, name, options)
8
- @recipe = recipe
9
- @name = name
10
- @options = options
11
- end
12
-
13
- def dockerfile
14
- @options['dockerfile'] || 'Dockerfile'
15
- end
16
-
17
- def image_name
18
- @options['image-name']
19
- end
20
-
21
- def image_name_for_commit(commit_ref)
22
- "#{image_name}:#{commit_ref}"
23
- end
24
-
25
- def template_vars
26
- {
27
- 'dockerfile' => dockerfile,
28
- 'image-name' => image_name
29
- }
30
- end
31
- end
32
- end
@@ -1,315 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'open3'
4
- require 'digest'
5
- require 'hippo/error'
6
- require 'hippo/recipe'
7
-
8
- module Hippo
9
- class CLISteps
10
- attr_reader :recipe
11
- attr_reader :stage
12
-
13
- def initialize(recipe, stage)
14
- @recipe = recipe
15
- @stage = stage
16
- end
17
-
18
- # Prepare the repository for this build by getting the latest
19
- # version from the remote and checking out the branch.
20
- def prepare_repository(fetch: true)
21
- info "Using repository #{@recipe.repository.url}"
22
- if fetch
23
- if @recipe.repository.cloned?
24
- info 'Repository is already cloned'
25
- action 'Fetching the latest repository data...'
26
- @recipe.repository.fetch
27
- else
28
- info 'Repository is not yet cloned.'
29
- action 'Cloning repository...'
30
- @recipe.repository.clone
31
- end
32
-
33
- elsif !fetch && !@recipe.repository.cloned?
34
- raise Error, 'Repository is not cloned yet so cannot continue'
35
- else
36
- info 'Not fetching latest repository, using cached copy'
37
- end
38
-
39
- action "Checking out '#{@stage.branch}' branch..."
40
- @recipe.repository.checkout(@stage.branch)
41
- @commit = @recipe.repository.commit
42
- info "Latest commit on branch is #{@commit.objectish}"
43
- info "Message: #{@commit.message.split("\n").first}"
44
- @commit
45
- end
46
-
47
- def build
48
- if @commit.nil?
49
- raise Error, 'You cannot build without first preparing the repository'
50
- end
51
-
52
- Dir.chdir(@recipe.repository.path) do
53
- @recipe.build_specs.each do |_, build_spec|
54
- if build_spec.image_name.nil?
55
- raise Error, "No image-name has been specified for build #{build_spec.name}"
56
- end
57
-
58
- image_name = build_spec.image_name_for_commit(@commit)
59
- action "Building #{build_spec.name} with tag #{image_name}"
60
-
61
- command = [
62
- 'docker', 'build', '.',
63
- '-f', build_spec.dockerfile,
64
- '-t', image_name,
65
- '--build-arg', "commit_ref=#{@commit.objectish}"
66
- ]
67
- external_command do
68
- if system(*command)
69
- @built_commit = @commit
70
- success "Successfully built image #{build_spec.image_name} for #{build_spec.name}"
71
- else
72
- raise Error, "Image for #{build_spec.name} did not succeed. Check output and try again."
73
- end
74
- end
75
- end
76
- end
77
- end
78
-
79
- def publish
80
- if @built_commit.nil?
81
- raise Error, 'You cannot publish without first building the image'
82
- end
83
-
84
- Dir.chdir(@recipe.repository.path) do
85
- @recipe.build_specs.each do |_, build_spec|
86
- if build_spec.image_name.nil?
87
- raise Error, "No image-name has been specified for build #{build_spec.name}"
88
- end
89
-
90
- image_name = build_spec.image_name_for_commit(@built_commit.objectish)
91
- action "Publishing #{build_spec.name} with tag #{image_name}"
92
-
93
- command = ['docker', 'push', image_name]
94
- external_command do
95
- if system(*command)
96
- success "Successfully published image #{image_name} for #{build_spec.name}"
97
- else
98
- raise Error, "Image for #{build_spec.name} was not published successfully. Check output and try again."
99
- end
100
- end
101
- end
102
- end
103
- end
104
-
105
- def run_install_jobs
106
- run_jobs('install')
107
- end
108
-
109
- def run_deploy_jobs
110
- run_jobs('deploy')
111
- end
112
-
113
- def deploy
114
- action 'Applying deployments'
115
- @deploy_id = Digest::SHA1.hexdigest(Time.now.to_f.to_s)[0, 10]
116
- deployments = @recipe.kubernetes.objects('deployments', @stage, @commit, deploy_id: @deploy_id)
117
-
118
- if deployments.nil?
119
- info 'No deployments file configured. Not applying any deployments'
120
- end
121
-
122
- external_command do
123
- @recipe.kubernetes.apply_with_kubectl(@stage, deployments)
124
- end
125
- success 'Deployments applied successfully'
126
- puts 'Waiting for all deployments to rollout...'
127
-
128
- count = 0
129
- loop do
130
- sleep 4
131
- count += 1
132
- replica_sets = @recipe.kubernetes.get_with_kubectl(@stage, [
133
- 'rs',
134
- '--selector',
135
- 'hippo.adam.ac/deployID=' + @deploy_id
136
- ])
137
- pending_replica_sets = replica_sets.reject do |deploy|
138
- deploy['status']['availableReplicas'] == deploy['status']['replicas']
139
- end
140
-
141
- names = replica_sets.map { |d| d['metadata']['name'].split('-').first }.join(', ')
142
-
143
- if pending_replica_sets.empty?
144
- success 'All deployments have rolled out successfully.'
145
- puts
146
- replica_sets.each do |rs|
147
- name = rs['metadata']['name'].split('-').first
148
- quantity = rs['status']['availableReplicas']
149
- puts " * \e[35m#{name}\e[0m has #{quantity} #{quantity == 1 ? 'replica' : 'replicas'}"
150
- end
151
- puts
152
- break
153
- else
154
- if count == 15
155
- error "Looks like things aren't going to plan with some deployments."
156
- puts 'You can review potential issues using the commads below:'
157
- pending_replica_sets.each do |rs|
158
- puts
159
- name = rs['metadata']['name'].split('-').first
160
- puts " hippo #{stage.name} kubectl -- describe deployment \e[35m#{name}\e[0m"
161
- puts " hippo #{stage.name} kubectl -- logs deployment/\e[35m#{name}\e[0m --all-containers"
162
- end
163
- puts
164
-
165
- break
166
- else
167
- puts 'Waiting for ' + pending_replica_sets.map { |rs| rs['metadata']['name'].split('-').first }.join(', ')
168
- end
169
- end
170
- end
171
- end
172
-
173
- def apply_services
174
- action 'Applying services'
175
- objects = @recipe.kubernetes.objects('services', @stage, @commit)
176
- if objects.empty?
177
- info 'No services have been defined'
178
- else
179
- external_command do
180
- @recipe.kubernetes.apply_with_kubectl(@stage, objects)
181
- end
182
- success 'Services applied successfully'
183
- end
184
- end
185
-
186
- def apply_namespace
187
- action 'Applying namespace'
188
- external_command { @recipe.kubernetes.apply_namespace(@stage) }
189
- success 'Namespace applied successfully'
190
- end
191
-
192
- def apply_config
193
- action 'Applying configuration'
194
- objects = @recipe.kubernetes.objects('config', @stage, @commit)
195
- if objects.empty?
196
- info 'No configuration files have been defined'
197
- else
198
- external_command do
199
- @recipe.kubernetes.apply_with_kubectl(@stage, objects)
200
- end
201
- success 'Configuration applied successfully'
202
- end
203
- end
204
-
205
- def apply_secrets
206
- require 'hippo/secret_manager'
207
- action 'Applying secrets'
208
- manager = SecretManager.new(@recipe, @stage)
209
- unless manager.key_available?
210
- error 'No secret encryption key was available. Not applying secrets.'
211
- return
212
- end
213
-
214
- yamls = manager.secrets.map(&:to_secret_yaml).join("---\n")
215
- external_command do
216
- @recipe.kubernetes.apply_with_kubectl(@stage, yamls)
217
- end
218
- success 'Secrets applicated successfully'
219
- end
220
-
221
- private
222
-
223
- def info(text)
224
- puts text
225
- end
226
-
227
- def success(text)
228
- puts "\e[32m#{text}\e[0m"
229
- end
230
-
231
- def action(text)
232
- puts "\e[33m#{text}\e[0m"
233
- end
234
-
235
- def error(text)
236
- puts "\e[31m#{text}\e[0m"
237
- end
238
-
239
- def external_command
240
- $stdout.print "\e[37m"
241
- yield
242
- ensure
243
- $stdout.print "\e[0m"
244
- end
245
-
246
- def run_jobs(type)
247
- objects = @recipe.kubernetes.objects("jobs/#{type}", @stage, @commit)
248
- if objects.empty?
249
- info "No #{type} jobs exist so not applying anything"
250
- return true
251
- end
252
-
253
- action "Applying #{type} job objects objects to Kubernetes"
254
-
255
- result = nil
256
- external_command do
257
- # Remove any previous jobs that might have been running before
258
- objects.each do |job|
259
- @recipe.kubernetes.delete_job(@stage, job['metadata']['name'])
260
- end
261
-
262
- result = @recipe.kubernetes.apply_with_kubectl(@stage, objects)
263
- end
264
-
265
- puts 'Waiting for all scheduled jobs to finish...'
266
- timeout, jobs = @recipe.kubernetes.wait_for_jobs(@stage, result.keys)
267
- success_jobs = []
268
- failed_jobs = []
269
- jobs.each do |job|
270
- if job['status']['succeeded']
271
- success_jobs << job
272
- else
273
- failed_jobs << job
274
- end
275
- end
276
-
277
- if success_jobs.size == jobs.size
278
- success 'All jobs completed successfully.'
279
- puts 'You can review the logs for these by running the commands below.'
280
- puts
281
- result = true
282
- else
283
- error 'Not all install jobs completed successfully.'
284
- puts 'You should review the logs for these using the commands below.'
285
- puts
286
- result = false
287
- end
288
-
289
- jobs.each do |job|
290
- icon = if job['status']['succeeded']
291
- '✅'
292
- else
293
- '❌'
294
- end
295
- puts " #{icon} " + @stage.kubectl("logs job/#{job['metadata']['name']}")
296
- end
297
- puts
298
-
299
- result
300
- end
301
-
302
- class << self
303
- def setup(context)
304
- recipe = Hippo::Recipe.load_from_file(context.options[:hippofile] || './Hippofile')
305
-
306
- stage = recipe.stages[CURRENT_STAGE]
307
- if stage.nil?
308
- raise Error, "Invalid stage name `#{CURRENT_STAGE}`. Check this has been defined in in your stages directory with a matching name?"
309
- end
310
-
311
- new(recipe, stage)
312
- end
313
- end
314
- end
315
- end