kuby-core 0.11.16 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/Gemfile +1 -2
  4. data/kuby-core.gemspec +1 -1
  5. data/lib/kuby.rb +2 -20
  6. data/lib/kuby/commands.rb +13 -67
  7. data/lib/kuby/docker.rb +27 -25
  8. data/lib/kuby/docker/alpine.rb +2 -1
  9. data/lib/kuby/docker/app_image.rb +19 -0
  10. data/lib/kuby/docker/cli.rb +4 -12
  11. data/lib/kuby/docker/docker_uri.rb +18 -7
  12. data/lib/kuby/docker/errors.rb +1 -19
  13. data/lib/kuby/docker/image.rb +115 -0
  14. data/lib/kuby/docker/layer.rb +0 -7
  15. data/lib/kuby/docker/local_tags.rb +9 -10
  16. data/lib/kuby/docker/package_phase.rb +0 -5
  17. data/lib/kuby/docker/packages.rb +1 -0
  18. data/lib/kuby/docker/remote_tags.rb +10 -5
  19. data/lib/kuby/docker/setup_phase.rb +17 -9
  20. data/lib/kuby/docker/spec.rb +29 -62
  21. data/lib/kuby/docker/timestamp_tag.rb +8 -1
  22. data/lib/kuby/docker/timestamped_image.rb +113 -0
  23. data/lib/kuby/environment.rb +1 -10
  24. data/lib/kuby/kubernetes/bare_metal_provider.rb +16 -4
  25. data/lib/kuby/kubernetes/deployer.rb +1 -1
  26. data/lib/kuby/kubernetes/docker_desktop_provider.rb +0 -15
  27. data/lib/kuby/kubernetes/spec.rb +21 -17
  28. data/lib/kuby/plugin.rb +2 -2
  29. data/lib/kuby/plugins/rails_app.rb +1 -0
  30. data/lib/kuby/plugins/rails_app/assets.rb +60 -70
  31. data/lib/kuby/plugins/rails_app/assets_image.rb +55 -0
  32. data/lib/kuby/plugins/rails_app/plugin.rb +53 -236
  33. data/lib/kuby/tasks.rb +30 -69
  34. data/lib/kuby/version.rb +1 -1
  35. data/spec/docker/spec_spec.rb +9 -118
  36. data/spec/docker/timestamped_image_spec.rb +123 -0
  37. data/spec/spec_helper.rb +10 -11
  38. metadata +9 -10
  39. data/lib/kuby/dev_setup.rb +0 -346
  40. data/lib/kuby/docker/dev_spec.rb +0 -202
  41. data/lib/kuby/docker/metadata.rb +0 -90
  42. data/lib/kuby/docker/tags.rb +0 -92
  43. data/lib/kuby/rails_commands.rb +0 -84
  44. data/spec/docker/metadata_spec.rb +0 -73
@@ -9,8 +9,15 @@ module Kuby
9
9
 
10
10
  FORMAT = T.let('%Y%m%d%H%M%S'.freeze, String)
11
11
 
12
- sig { params(str: String).returns(T.nilable(TimestampTag)) }
12
+ sig { params(str: T.nilable(String)).returns(T.nilable(TimestampTag)) }
13
13
  def self.try_parse(str)
14
+ return nil unless str
15
+
16
+ # The strptime function stops scanning after the pattern has been matched, so
17
+ # we check for all numbers here to prevent things like 20210424165405-assets
18
+ # from being treated as a timestamp tag.
19
+ return nil unless str =~ /\A\d+\z/
20
+
14
21
  time = begin
15
22
  Time.strptime(str, FORMAT)
16
23
  rescue ArgumentError
@@ -0,0 +1,113 @@
1
+ # typed: strict
2
+
3
+ require 'docker/remote'
4
+
5
+ module Kuby
6
+ module Docker
7
+ class TimestampedImage < Image
8
+ extend T::Sig
9
+
10
+ sig {
11
+ params(
12
+ dockerfile: T.any(Dockerfile, T.proc.returns(Dockerfile)),
13
+ image_url: String,
14
+ credentials: Credentials,
15
+ main_tag: T.nilable(String),
16
+ alias_tags: T::Array[String]
17
+ ).void
18
+ }
19
+ def initialize(dockerfile, image_url, credentials, main_tag = nil, alias_tags = [])
20
+ @new_version = T.let(@new_version, T.nilable(Image))
21
+ @current_version = T.let(@current_version, T.nilable(Image))
22
+ @previous_version = T.let(@previous_version, T.nilable(Image))
23
+
24
+ @remote_client = T.let(@remote_client, T.nilable(::Docker::Remote::Client))
25
+ @local = T.let(@local, T.nilable(LocalTags))
26
+ @remote = T.let(@remote, T.nilable(RemoteTags))
27
+
28
+ super
29
+ end
30
+
31
+ sig { returns(Image) }
32
+ def new_version
33
+ @new_version ||= duplicate_with_tags(
34
+ TimestampTag.new(Time.now).to_s, [Kuby::Docker::LATEST_TAG]
35
+ )
36
+ end
37
+
38
+ sig { returns(Image) }
39
+ def current_version
40
+ @current_version ||= duplicate_with_tags(
41
+ latest_timestamp_tag.to_s, [Kuby::Docker::LATEST_TAG]
42
+ )
43
+ end
44
+
45
+ sig { params(current_tag: T.nilable(String)).returns(Image) }
46
+ def previous_version(current_tag = nil)
47
+ @previous_version ||= duplicate_with_tags(
48
+ previous_timestamp_tag(current_tag).to_s, []
49
+ )
50
+ end
51
+
52
+ sig { params(current_tag: T.nilable(String)).returns(TimestampTag) }
53
+ def previous_timestamp_tag(current_tag = nil)
54
+ current_tag = TimestampTag.try_parse(current_tag || latest_timestamp_tag.to_s)
55
+ raise MissingTagError, 'could not find current timestamp tag' unless current_tag
56
+
57
+ all_tags = timestamp_tags.sort
58
+
59
+ idx = all_tags.index do |tag|
60
+ tag.time == current_tag.time
61
+ end
62
+
63
+ idx ||= 0
64
+ raise MissingTagError, 'could not find previous timestamp tag' unless idx > 0
65
+
66
+ T.must(all_tags[idx - 1])
67
+ end
68
+
69
+ sig { returns(TimestampTag) }
70
+ def latest_timestamp_tag
71
+ tag = timestamp_tags.sort.last
72
+ raise MissingTagError, 'could not find latest timestamp tag' unless tag
73
+ tag
74
+ end
75
+
76
+ sig { params(build_args: T::Hash[String, String]).void }
77
+ def build(build_args = {})
78
+ docker_cli.build(new_version, build_args: build_args)
79
+ @current_version = new_version
80
+ @new_version = nil
81
+ end
82
+
83
+ sig { params(tag: String).void }
84
+ def push(tag)
85
+ docker_cli.push(image_url, tag)
86
+ end
87
+
88
+ private
89
+
90
+ sig { returns(::Docker::Remote::Client) }
91
+ def remote_client
92
+ @remote_client ||= ::Docker::Remote::Client.new(
93
+ image_host, image_repo, credentials.username, credentials.password,
94
+ )
95
+ end
96
+
97
+ sig { returns(T::Array[TimestampTag]) }
98
+ def timestamp_tags
99
+ (local.timestamp_tags + remote.timestamp_tags).uniq
100
+ end
101
+
102
+ sig { returns(LocalTags) }
103
+ def local
104
+ @local ||= LocalTags.new(docker_cli, image_url)
105
+ end
106
+
107
+ sig { returns(RemoteTags) }
108
+ def remote
109
+ @remote ||= RemoteTags.new(remote_client, image_url)
110
+ end
111
+ end
112
+ end
113
+ end
@@ -12,12 +12,7 @@ module Kuby
12
12
  end
13
13
 
14
14
  def docker(&block)
15
- @docker ||= if development?
16
- Docker::DevSpec.new(self)
17
- else
18
- Docker::Spec.new(self)
19
- end
20
-
15
+ @docker ||= Docker::Spec.new(self)
21
16
  @docker.instance_eval(&block) if block
22
17
  @docker
23
18
  end
@@ -31,9 +26,5 @@ module Kuby
31
26
  def app_name
32
27
  definition.app_name
33
28
  end
34
-
35
- def development?
36
- name == 'development'
37
- end
38
29
  end
39
30
  end
@@ -1,10 +1,13 @@
1
- # typed: false
1
+ # typed: strict
2
+
2
3
  require 'kube-dsl'
3
4
 
4
5
  module Kuby
5
6
  module Kubernetes
6
7
  class BareMetalProvider < Provider
7
- STORAGE_CLASS_NAME = 'hostpath'.freeze
8
+ extend T::Sig
9
+
10
+ STORAGE_CLASS_NAME = T.let('hostpath'.freeze, String)
8
11
 
9
12
  class Config
10
13
  extend ::KubeDSL::ValueFields
@@ -12,25 +15,34 @@ module Kuby
12
15
  value_fields :kubeconfig
13
16
  end
14
17
 
18
+ sig { returns(Config) }
15
19
  attr_reader :config
16
20
 
21
+ sig { params(environment: Environment).void }
22
+ def initialize(environment)
23
+ @config = T.let(Config.new, Config)
24
+ super
25
+ end
26
+
27
+ sig { params(block: T.proc.void).void }
17
28
  def configure(&block)
18
29
  config.instance_eval(&block) if block
19
30
  end
20
31
 
32
+ sig { returns(String) }
21
33
  def kubeconfig_path
22
34
  config.kubeconfig
23
35
  end
24
36
 
37
+ sig { returns(String) }
25
38
  def storage_class_name
26
39
  STORAGE_CLASS_NAME
27
40
  end
28
41
 
29
42
  private
30
43
 
44
+ sig { void }
31
45
  def after_initialize
32
- @config = Config.new
33
-
34
46
  configure do
35
47
  # default kubeconfig path
36
48
  kubeconfig File.join(ENV['HOME'], '.kube', 'config')
@@ -102,7 +102,7 @@ module Kuby
102
102
 
103
103
  def restart_rails_deployment_if_necessary
104
104
  deployed_image = nil
105
- current_image = "#{docker.metadata.image_url}:#{docker.tag}"
105
+ current_image = "#{docker.image.image_url}:#{kubernetes.tag}"
106
106
 
107
107
  if rails_app = kubernetes.plugin(:rails_app)
108
108
  deployment_name = rails_app.deployment.metadata.name
@@ -18,21 +18,6 @@ module Kuby
18
18
  config.instance_eval(&block) if block
19
19
  end
20
20
 
21
- def after_configuration
22
- if rails_app = spec.plugin(:rails_app)
23
- # Remove ingress and change service type from ClusterIP to
24
- # LoadBalancer. No need to set up ingress for Docker Desktop
25
- # since it handles all the localhost mapping, etc if you set
26
- # up a service LB.
27
- rails_app.resources.delete(rails_app.ingress)
28
- rails_app.service.spec { type 'LoadBalancer' }
29
- end
30
-
31
- if assets = spec.plugin(:rails_assets)
32
- assets.service.spec { type 'LoadBalancer' }
33
- end
34
- end
35
-
36
21
  def kubeconfig_path
37
22
  config.kubeconfig
38
23
  end
@@ -72,7 +72,7 @@ module Kuby
72
72
  end
73
73
 
74
74
  def before_deploy
75
- @tag ||= docker.tag
75
+ @tag ||= docker.image.current_version.main_tag
76
76
 
77
77
  provider.before_deploy(resources)
78
78
  @plugins.each { |_, plg| plg.before_deploy(resources) }
@@ -81,7 +81,7 @@ module Kuby
81
81
  end
82
82
 
83
83
  def after_deploy
84
- @tag ||= docker.tag
84
+ @tag ||= docker.image.current_version.main_tag
85
85
 
86
86
  @plugins.each { |_, plg| plg.after_deploy(resources) }
87
87
  provider.after_deploy(resources)
@@ -144,24 +144,22 @@ module Kuby
144
144
  def registry_secret(&block)
145
145
  spec = self
146
146
 
147
- unless environment.development?
148
- @registry_secret ||= RegistrySecret.new do
149
- metadata do
150
- name "#{spec.selector_app}-registry-secret"
151
- namespace spec.namespace.metadata.name
152
- end
153
-
154
- docker_config do
155
- registry_host spec.docker.metadata.image_hostname
156
- username spec.docker.credentials.username
157
- password spec.docker.credentials.password
158
- email spec.docker.credentials.email
159
- end
147
+ @registry_secret ||= RegistrySecret.new do
148
+ metadata do
149
+ name "#{spec.selector_app}-registry-secret"
150
+ namespace spec.namespace.metadata.name
160
151
  end
161
152
 
162
- @registry_secret.instance_eval(&block) if block
163
- @registry_secret
153
+ docker_config do
154
+ registry_host spec.docker.image.image_hostname
155
+ username spec.docker.image.credentials.username
156
+ password spec.docker.image.credentials.password
157
+ email spec.docker.image.credentials.email
158
+ end
164
159
  end
160
+
161
+ @registry_secret.instance_eval(&block) if block
162
+ @registry_secret
165
163
  end
166
164
 
167
165
  def resources
@@ -172,6 +170,12 @@ module Kuby
172
170
  ].compact)
173
171
  end
174
172
 
173
+ def docker_images
174
+ @docker_images ||= [
175
+ docker.image, *@plugins.flat_map { |_, plugin| plugin.docker_images }
176
+ ]
177
+ end
178
+
175
179
  def selector_app
176
180
  @selector_app ||= environment.app_name.downcase
177
181
  end
data/lib/kuby/plugin.rb CHANGED
@@ -21,8 +21,8 @@ module Kuby
21
21
  []
22
22
  end
23
23
 
24
- # additional dockerfiles that should be built and pushed
25
- def dockerfiles
24
+ # additional docker images that should be built and pushed
25
+ def docker_images
26
26
  []
27
27
  end
28
28
 
@@ -3,6 +3,7 @@ module Kuby
3
3
  module Plugins
4
4
  module RailsApp
5
5
  autoload :AssetCopyTask, 'kuby/plugins/rails_app/asset_copy_task'
6
+ autoload :AssetsImage, 'kuby/plugins/rails_app/assets_image'
6
7
  autoload :Assets, 'kuby/plugins/rails_app/assets'
7
8
  autoload :Database, 'kuby/plugins/rails_app/database'
8
9
  autoload :MySQL, 'kuby/plugins/rails_app/mysql'
@@ -47,38 +47,6 @@ module Kuby
47
47
  end
48
48
  end
49
49
 
50
- def configure_deployment(deployment, docker_image)
51
- spec = self
52
-
53
- deployment.spec.template.spec do
54
- init_container(:copy_assets) do
55
- name "#{spec.selector_app}-copy-assets"
56
- command %w(bundle exec rake kuby:rails_app:assets:copy)
57
- image docker_image
58
-
59
- volume_mount do
60
- name 'assets'
61
- mount_path RAILS_MOUNT_PATH
62
- end
63
- end
64
-
65
- container(:web) do
66
- volume_mount do
67
- name 'assets'
68
- mount_path NGINX_MOUNT_PATH
69
- end
70
- end
71
-
72
- volume do
73
- name 'assets'
74
-
75
- persistent_volume_claim do
76
- claim_name spec.volume_claim.metadata.name
77
- end
78
- end
79
- end
80
- end
81
-
82
50
  def copy_task
83
51
  @copy_task ||= AssetCopyTask.new(
84
52
  from: asset_path, to: RAILS_MOUNT_PATH
@@ -238,7 +206,7 @@ module Kuby
238
206
  container(:nginx) do
239
207
  name "#{kube_spec.selector_app}-#{kube_spec.role}"
240
208
  image_pull_policy 'IfNotPresent'
241
- image NGINX_IMAGE
209
+ image "#{kube_spec.image.image_url}:#{kube_spec.kubernetes.tag || Kuby::Docker::LATEST_TAG}-assets"
242
210
 
243
211
  port do
244
212
  container_port NGINX_PORT
@@ -252,11 +220,6 @@ module Kuby
252
220
  sub_path 'nginx.conf'
253
221
  end
254
222
 
255
- volume_mount do
256
- name 'assets'
257
- mount_path NGINX_MOUNT_PATH
258
- end
259
-
260
223
  readiness_probe do
261
224
  success_threshold 1
262
225
  failure_threshold 2
@@ -272,6 +235,10 @@ module Kuby
272
235
  end
273
236
  end
274
237
 
238
+ image_pull_secret do
239
+ name kube_spec.environment.kubernetes.registry_secret.metadata.name
240
+ end
241
+
275
242
  volume do
276
243
  name 'nginx-config'
277
244
 
@@ -280,14 +247,6 @@ module Kuby
280
247
  end
281
248
  end
282
249
 
283
- volume do
284
- name 'assets'
285
-
286
- persistent_volume_claim do
287
- claim_name kube_spec.volume_claim.metadata.name
288
- end
289
- end
290
-
291
250
  restart_policy 'Always'
292
251
  service_account_name kube_spec.service_account.metadata.name
293
252
  end
@@ -299,38 +258,19 @@ module Kuby
299
258
  @deployment
300
259
  end
301
260
 
302
- def volume_claim
303
- spec = self
304
-
305
- @volume_claim ||= KubeDSL.persistent_volume_claim do
306
- metadata do
307
- name "#{spec.selector_app}-#{spec.role}"
308
- namespace spec.namespace.metadata.name
309
- end
310
-
311
- spec do
312
- access_modes ['ReadWriteOnce']
313
- storage_class_name spec.environment.kubernetes.provider.storage_class_name
314
-
315
- resources do
316
- requests do
317
- add :storage, '10Gi'
318
- end
319
- end
320
- end
321
- end
322
- end
323
-
324
261
  def resources
325
262
  @resources ||= [
326
263
  service,
327
264
  service_account,
328
265
  nginx_config,
329
- deployment,
330
- volume_claim
266
+ deployment
331
267
  ]
332
268
  end
333
269
 
270
+ def docker_images
271
+ @docker_images ||= [docker_spec]
272
+ end
273
+
334
274
  def namespace
335
275
  environment.kubernetes.namespace
336
276
  end
@@ -342,6 +282,56 @@ module Kuby
342
282
  def role
343
283
  ROLE
344
284
  end
285
+
286
+ def docker
287
+ environment.docker
288
+ end
289
+
290
+ def kubernetes
291
+ environment.kubernetes
292
+ end
293
+
294
+ def docker_images
295
+ @docker_images ||= [image]
296
+ end
297
+
298
+ def image
299
+ @image ||= RailsApp::AssetsImage.new(docker.image, -> { dockerfile })
300
+ end
301
+
302
+ private
303
+
304
+ def dockerfile
305
+ Docker::Dockerfile.new.tap do |df|
306
+ base_image = docker.image.current_version
307
+ cur_tag = base_image.main_tag
308
+ app_name = environment.app_name.downcase
309
+
310
+ tags = begin
311
+ [base_image.previous_timestamp_tag(cur_tag).to_s, cur_tag]
312
+ rescue Kuby::Docker::MissingTagError
313
+ [cur_tag, nil]
314
+ end
315
+
316
+ # this can handle more than 2 tags by virtue of using each_cons :)
317
+ tags.each_cons(2) do |prev_tag, tag|
318
+ prev_image_name = "#{app_name}-#{prev_tag}"
319
+ df.from("#{base_image.image_url}:#{prev_tag}", as: prev_image_name)
320
+ df.run("mkdir -p #{RAILS_MOUNT_PATH}")
321
+ df.run("bundle exec rake kuby:rails_app:assets:copy")
322
+
323
+ if tag
324
+ image_name = "#{app_name}-#{tag}"
325
+ df.from("#{base_image.image_url}:#{tag}", as: image_name)
326
+ df.copy("--from=#{prev_image_name} #{RAILS_MOUNT_PATH}", RAILS_MOUNT_PATH)
327
+ df.run("bundle exec rake kuby:rails_app:assets:copy")
328
+ end
329
+ end
330
+
331
+ df.from(NGINX_IMAGE)
332
+ df.copy("--from=#{"#{app_name}-#{tags.compact.last}"} #{RAILS_MOUNT_PATH}", NGINX_MOUNT_PATH)
333
+ end
334
+ end
345
335
  end
346
336
  end
347
337
  end