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.
@@ -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