kuby-core 0.11.16 → 0.12.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.
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