kuby-core 0.7.2 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/Gemfile +1 -0
  4. data/README.md +11 -1
  5. data/bin/kuby +4 -0
  6. data/kuby-core.gemspec +9 -4
  7. data/lib/kuby.rb +58 -19
  8. data/lib/kuby/basic_logger.rb +13 -0
  9. data/lib/kuby/cli_base.rb +81 -8
  10. data/lib/kuby/commands.rb +244 -0
  11. data/lib/kuby/definition.rb +1 -11
  12. data/lib/kuby/dev_setup.rb +255 -0
  13. data/lib/kuby/docker.rb +1 -0
  14. data/lib/kuby/docker/bundler_phase.rb +3 -3
  15. data/lib/kuby/docker/cli.rb +13 -1
  16. data/lib/kuby/docker/dev_spec.rb +131 -0
  17. data/lib/kuby/docker/dockerfile.rb +16 -1
  18. data/lib/kuby/docker/layer.rb +4 -4
  19. data/lib/kuby/docker/layer_stack.rb +4 -0
  20. data/lib/kuby/docker/local_tags.rb +4 -0
  21. data/lib/kuby/docker/metadata.rb +18 -38
  22. data/lib/kuby/docker/package_phase.rb +2 -2
  23. data/lib/kuby/docker/setup_phase.rb +3 -2
  24. data/lib/kuby/docker/spec.rb +42 -16
  25. data/lib/kuby/docker/timestamp_tag.rb +6 -0
  26. data/lib/kuby/environment.rb +15 -2
  27. data/lib/kuby/kubernetes.rb +9 -11
  28. data/lib/kuby/kubernetes/deploy_task.rb +4 -1
  29. data/lib/kuby/kubernetes/deployer.rb +70 -15
  30. data/lib/kuby/kubernetes/{minikube_provider.rb → docker_desktop_provider.rb} +8 -4
  31. data/lib/kuby/kubernetes/provider.rb +12 -8
  32. data/lib/kuby/kubernetes/spec.rb +30 -29
  33. data/lib/kuby/plugin.rb +59 -0
  34. data/lib/kuby/plugin_registry.rb +27 -0
  35. data/lib/kuby/plugins.rb +6 -0
  36. data/lib/kuby/plugins/nginx_ingress.rb +71 -0
  37. data/lib/kuby/plugins/rails_app.rb +18 -0
  38. data/lib/kuby/plugins/rails_app/asset_copy_task.rb +117 -0
  39. data/lib/kuby/plugins/rails_app/assets.rb +347 -0
  40. data/lib/kuby/plugins/rails_app/database.rb +74 -0
  41. data/lib/kuby/{kubernetes/plugins → plugins}/rails_app/generators/kuby.rb +14 -16
  42. data/lib/kuby/plugins/rails_app/mysql.rb +152 -0
  43. data/lib/kuby/plugins/rails_app/plugin.rb +593 -0
  44. data/lib/kuby/plugins/rails_app/postgres.rb +143 -0
  45. data/lib/kuby/plugins/rails_app/rewrite_db_config.rb +11 -0
  46. data/lib/kuby/plugins/rails_app/sqlite.rb +32 -0
  47. data/lib/kuby/plugins/rails_app/tasks.rake +36 -0
  48. data/lib/kuby/rails_commands.rb +89 -0
  49. data/lib/kuby/railtie.rb +0 -4
  50. data/lib/kuby/tasks.rb +85 -31
  51. data/lib/kuby/version.rb +1 -1
  52. data/spec/docker/metadata_spec.rb +84 -0
  53. data/spec/docker/spec_spec.rb +266 -0
  54. data/spec/docker/timestamp_tag_spec.rb +54 -4
  55. data/spec/dummy/Gemfile +54 -0
  56. data/spec/dummy/Gemfile.lock +223 -0
  57. data/spec/dummy/README.md +24 -0
  58. data/spec/dummy/Rakefile +6 -0
  59. data/spec/dummy/app/assets/config/manifest.js +2 -0
  60. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  61. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  62. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  63. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  64. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  65. data/spec/dummy/app/javascript/channels/consumer.js +6 -0
  66. data/spec/dummy/app/javascript/channels/index.js +5 -0
  67. data/spec/dummy/app/javascript/packs/application.js +17 -0
  68. data/spec/dummy/app/jobs/application_job.rb +7 -0
  69. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  70. data/spec/dummy/app/models/application_record.rb +3 -0
  71. data/spec/dummy/app/views/layouts/application.html.erb +15 -0
  72. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  73. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  74. data/spec/dummy/bin/bundle +114 -0
  75. data/spec/dummy/bin/rails +9 -0
  76. data/spec/dummy/bin/rake +9 -0
  77. data/spec/dummy/bin/setup +36 -0
  78. data/spec/dummy/bin/spring +17 -0
  79. data/spec/dummy/bin/yarn +11 -0
  80. data/spec/dummy/config.ru +5 -0
  81. data/spec/dummy/config/application.rb +19 -0
  82. data/spec/dummy/config/boot.rb +4 -0
  83. data/spec/dummy/config/cable.yml +10 -0
  84. data/spec/dummy/config/credentials.yml.enc +1 -0
  85. data/spec/dummy/config/database.yml +25 -0
  86. data/spec/dummy/config/environment.rb +5 -0
  87. data/spec/dummy/config/environments/development.rb +62 -0
  88. data/spec/dummy/config/environments/production.rb +112 -0
  89. data/spec/dummy/config/environments/test.rb +49 -0
  90. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  91. data/spec/dummy/config/initializers/assets.rb +14 -0
  92. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  93. data/spec/dummy/config/initializers/content_security_policy.rb +30 -0
  94. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  95. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  96. data/spec/dummy/config/initializers/inflections.rb +16 -0
  97. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  98. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  99. data/spec/dummy/config/locales/en.yml +33 -0
  100. data/spec/dummy/config/master.key +1 -0
  101. data/spec/dummy/config/puma.rb +38 -0
  102. data/spec/dummy/config/routes.rb +3 -0
  103. data/spec/dummy/config/spring.rb +6 -0
  104. data/spec/dummy/config/storage.yml +34 -0
  105. data/spec/dummy/db/seeds.rb +7 -0
  106. data/spec/dummy/package.json +11 -0
  107. data/spec/dummy/public/404.html +67 -0
  108. data/spec/dummy/public/422.html +67 -0
  109. data/spec/dummy/public/500.html +66 -0
  110. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  111. data/spec/dummy/public/apple-touch-icon.png +0 -0
  112. data/spec/dummy/public/favicon.ico +0 -0
  113. data/spec/dummy/public/robots.txt +1 -0
  114. data/spec/dummy/test/application_system_test_case.rb +5 -0
  115. data/spec/dummy/test/channels/application_cable/connection_test.rb +11 -0
  116. data/spec/dummy/test/test_helper.rb +13 -0
  117. data/spec/dummy/tmp/cache/bootsnap-load-path-cache +0 -0
  118. data/spec/spec_helper.rb +77 -2
  119. data/spec/support/docker/fake_cli.rb +54 -0
  120. data/spec/support/docker/remote/fake_client.rb +16 -0
  121. data/spec/trailing_hash_spec.rb +23 -0
  122. metadata +139 -30
  123. data/lib/ext/krane/kubernetes_resource.rb +0 -16
  124. data/lib/kuby/kubernetes/plugin.rb +0 -55
  125. data/lib/kuby/kubernetes/plugins.rb +0 -8
  126. data/lib/kuby/kubernetes/plugins/nginx_ingress.rb +0 -73
  127. data/lib/kuby/kubernetes/plugins/rails_app.rb +0 -16
  128. data/lib/kuby/kubernetes/plugins/rails_app/database.rb +0 -79
  129. data/lib/kuby/kubernetes/plugins/rails_app/mysql.rb +0 -154
  130. data/lib/kuby/kubernetes/plugins/rails_app/plugin.rb +0 -379
  131. data/lib/kuby/kubernetes/plugins/rails_app/postgres.rb +0 -142
  132. data/lib/kuby/kubernetes/plugins/rails_app/rewrite_db_config.rb +0 -13
  133. data/lib/kuby/kubernetes/plugins/rails_app/sqlite.rb +0 -30
  134. data/lib/kuby/kubernetes/plugins/rails_app/tasks.rake +0 -28
  135. data/lib/kuby/tasks/kuby.rake +0 -70
@@ -2,7 +2,7 @@ require 'kube-dsl'
2
2
 
3
3
  module Kuby
4
4
  module Kubernetes
5
- class MinikubeProvider < Provider
5
+ class DockerDesktopProvider < Provider
6
6
  STORAGE_CLASS_NAME = 'hostpath'.freeze
7
7
 
8
8
  class Config
@@ -20,12 +20,16 @@ module Kuby
20
20
  def after_configuration
21
21
  if rails_app = spec.plugin(:rails_app)
22
22
  # Remove ingress and change service type from ClusterIP to
23
- # LoadBalancer. No need to set up ingress for minikube since
24
- # it handles all the localhost mapping, etc if you set up a
25
- # service LB.
23
+ # LoadBalancer. No need to set up ingress for Docker Desktop
24
+ # since it handles all the localhost mapping, etc if you set
25
+ # up a service LB.
26
26
  rails_app.resources.delete(rails_app.ingress)
27
27
  rails_app.service.spec { type 'LoadBalancer' }
28
28
  end
29
+
30
+ if assets = spec.plugin(:rails_assets)
31
+ assets.service.spec { type 'LoadBalancer' }
32
+ end
29
33
  end
30
34
 
31
35
  def kubeconfig_path
@@ -3,10 +3,10 @@ require 'kubernetes-cli'
3
3
  module Kuby
4
4
  module Kubernetes
5
5
  class Provider
6
- attr_reader :definition
6
+ attr_reader :environment
7
7
 
8
- def initialize(definition)
9
- @definition = definition
8
+ def initialize(environment)
9
+ @environment = environment
10
10
  after_initialize
11
11
  end
12
12
 
@@ -55,22 +55,26 @@ module Kuby
55
55
  @kubernetes_cli ||= ::KubernetesCLI.new(kubeconfig_path)
56
56
  end
57
57
 
58
+ def helm_cli
59
+ @helm_cli ||= ::HelmCLI.new(kubeconfig_path)
60
+ end
61
+
58
62
  def kubeconfig_path
59
63
  raise NotImplementedError, "please define #{__method__} in #{self.class.name}"
60
64
  end
61
65
 
66
+ def deployer
67
+ @deployer ||= Kuby::Kubernetes::Deployer.new(environment)
68
+ end
69
+
62
70
  private
63
71
 
64
72
  def after_initialize
65
73
  # override this in derived classes
66
74
  end
67
75
 
68
- def deployer
69
- @deployer ||= Kuby::Kubernetes::Deployer.new(definition)
70
- end
71
-
72
76
  def spec
73
- definition.kubernetes
77
+ environment.kubernetes
74
78
  end
75
79
  end
76
80
  end
@@ -5,10 +5,10 @@ module Kuby
5
5
  class Spec
6
6
  extend ::KubeDSL::ValueFields
7
7
 
8
- attr_reader :definition, :plugins, :tag
8
+ attr_reader :environment, :plugins, :tag
9
9
 
10
- def initialize(definition)
11
- @definition = definition
10
+ def initialize(environment)
11
+ @environment = environment
12
12
  @plugins = TrailingHash.new
13
13
 
14
14
  # default plugins
@@ -18,7 +18,7 @@ module Kuby
18
18
  def provider(provider_name = nil, &block)
19
19
  if provider_name
20
20
  if @provider || provider_klass = Kuby.providers[provider_name]
21
- @provider ||= provider_klass.new(definition)
21
+ @provider ||= provider_klass.new(environment)
22
22
  @provider.configure(&block)
23
23
  else
24
24
  msg = if provider_name
@@ -36,13 +36,12 @@ module Kuby
36
36
  end
37
37
 
38
38
  def configure_plugin(plugin_name, &block)
39
- if @plugins[plugin_name] || plugin_klass = Kuby.plugins[plugin_name]
40
- @plugins[plugin_name] ||= plugin_klass.new(definition)
41
- @plugins[plugin_name].configure(&block) if block
42
- else
43
- raise MissingPluginError, "no plugin registered with name #{plugin_name}, "\
44
- 'do you need to add a gem to your Gemfile?'
39
+ unless @plugins.include?(plugin_name)
40
+ plugin_klass = Kuby.plugins.find(plugin_name)
41
+ @plugins[plugin_name] = plugin_klass.new(environment)
45
42
  end
43
+
44
+ @plugins[plugin_name].configure(&block) if block
46
45
  end
47
46
 
48
47
  alias_method :add_plugin, :configure_plugin
@@ -57,7 +56,7 @@ module Kuby
57
56
  end
58
57
 
59
58
  def before_deploy
60
- @tag ||= docker.metadata.tag
59
+ @tag ||= docker.tag
61
60
 
62
61
  provider.before_deploy(resources)
63
62
  @plugins.each { |_, plg| plg.before_deploy(resources) }
@@ -66,7 +65,7 @@ module Kuby
66
65
  end
67
66
 
68
67
  def after_deploy
69
- @tag ||= docker.metadata.tag
68
+ @tag ||= docker.tag
70
69
 
71
70
  @plugins.each { |_, plg| plg.after_deploy(resources) }
72
71
  provider.after_deploy(resources)
@@ -118,7 +117,7 @@ module Kuby
118
117
 
119
118
  @namespace ||= KubeDSL.namespace do
120
119
  metadata do
121
- name "#{spec.selector_app}-#{spec.definition.environment.name}"
120
+ name "#{spec.selector_app}-#{spec.environment.name}"
122
121
  end
123
122
  end
124
123
 
@@ -129,22 +128,24 @@ module Kuby
129
128
  def registry_secret(&block)
130
129
  spec = self
131
130
 
132
- @registry_secret ||= RegistrySecret.new do
133
- metadata do
134
- name "#{spec.selector_app}-registry-secret"
135
- namespace spec.namespace.metadata.name
136
- end
131
+ unless environment.development?
132
+ @registry_secret ||= RegistrySecret.new do
133
+ metadata do
134
+ name "#{spec.selector_app}-registry-secret"
135
+ namespace spec.namespace.metadata.name
136
+ end
137
137
 
138
- docker_config do
139
- registry_host spec.docker.metadata.image_host
140
- username spec.docker.credentials.username
141
- password spec.docker.credentials.password
142
- email spec.docker.credentials.email
138
+ docker_config do
139
+ registry_host spec.docker.metadata.image_host
140
+ username spec.docker.credentials.username
141
+ password spec.docker.credentials.password
142
+ email spec.docker.credentials.email
143
+ end
143
144
  end
144
- end
145
145
 
146
- @registry_secret.instance_eval(&block) if block
147
- @registry_secret
146
+ @registry_secret.instance_eval(&block) if block
147
+ @registry_secret
148
+ end
148
149
  end
149
150
 
150
151
  def resources
@@ -152,15 +153,15 @@ module Kuby
152
153
  namespace,
153
154
  registry_secret,
154
155
  *@plugins.flat_map { |_, plugin| plugin.resources }
155
- ])
156
+ ].compact)
156
157
  end
157
158
 
158
159
  def selector_app
159
- @selector_app ||= definition.app_name.downcase
160
+ @selector_app ||= environment.app_name.downcase
160
161
  end
161
162
 
162
163
  def docker
163
- definition.docker
164
+ environment.docker
164
165
  end
165
166
  end
166
167
  end
@@ -0,0 +1,59 @@
1
+ module Kuby
2
+ class Plugin
3
+ attr_reader :environment
4
+
5
+ def initialize(environment)
6
+ @environment = environment
7
+ after_initialize
8
+ end
9
+
10
+ def configure(&block)
11
+ # do nothing by default
12
+ end
13
+
14
+ def setup
15
+ # do nothing by default
16
+ end
17
+
18
+ # additional kubernetes resources that should be deployed
19
+ def resources
20
+ []
21
+ end
22
+
23
+ # additional dockerfiles that should be built and pushed
24
+ def dockerfiles
25
+ []
26
+ end
27
+
28
+ # called after all plugins have been configured
29
+ def after_configuration
30
+ # do nothing by default
31
+ end
32
+
33
+ # called before any plugins have been setup
34
+ def before_setup
35
+ # do nothing by default
36
+ end
37
+
38
+ # called after all plugins have been setup
39
+ def after_setup
40
+ # do nothing by default
41
+ end
42
+
43
+ # called before deploying any resources
44
+ def before_deploy(manifest)
45
+ # do nothing by default
46
+ end
47
+
48
+ # called after deploying all resources
49
+ def after_deploy(manifest)
50
+ # do nothing by default
51
+ end
52
+
53
+ private
54
+
55
+ def after_initialize
56
+ # override this in derived classes
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,27 @@
1
+ module Kuby
2
+ class PluginRegistry
3
+ ANY = 'any'.freeze
4
+
5
+ def register(plugin_name, plugin_klass, environment: ANY)
6
+ plugins[plugin_name] ||= {}
7
+ plugins[plugin_name][environment] ||= plugin_klass
8
+ end
9
+
10
+ def find(plugin_name, environment: Kuby.env)
11
+ plugins_by_env = plugins[plugin_name]
12
+
13
+ unless plugins_by_env
14
+ raise MissingPluginError, "no plugin registered with name #{plugin_name}, "\
15
+ 'do you need to add a gem to your Gemfile?'
16
+ end
17
+
18
+ plugins_by_env[environment] || plugins_by_env[ANY]
19
+ end
20
+
21
+ private
22
+
23
+ def plugins
24
+ @plugins ||= {}
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ module Kuby
2
+ module Plugins
3
+ autoload :NginxIngress, 'kuby/plugins/nginx_ingress'
4
+ autoload :RailsApp, 'kuby/plugins/rails_app'
5
+ end
6
+ end
@@ -0,0 +1,71 @@
1
+ require 'kube-dsl'
2
+
3
+ module Kuby
4
+ module Plugins
5
+ class NginxIngress < ::Kuby::Plugin
6
+ class Config
7
+ extend ::KubeDSL::ValueFields
8
+
9
+ value_fields :provider
10
+ end
11
+
12
+ VERSION = '0.27.1'.freeze
13
+ DEFAULT_PROVIDER = 'cloud-generic'.freeze
14
+ NAMESPACE = 'ingress-nginx'.freeze
15
+ SERVICE_NAME = 'ingress-nginx'.freeze
16
+
17
+ SETUP_RESOURCES = [
18
+ "https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-#{VERSION}/deploy/static/mandatory.yaml",
19
+ "https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-#{VERSION}/deploy/static/provider/%{provider}.yaml"
20
+ ].freeze
21
+
22
+ def configure(&block)
23
+ @config.instance_eval(&block) if block
24
+ end
25
+
26
+ def setup
27
+ Kuby.logger.info('Deploying nginx ingress resources')
28
+
29
+ if already_deployed?
30
+ Kuby.logger.info('Nginx ingress already deployed, skipping')
31
+ return
32
+ end
33
+
34
+ SETUP_RESOURCES.each do |uri|
35
+ uri = uri % { provider: @config.provider || DEFAULT_PROVIDER }
36
+ kubernetes_cli.apply_uri(uri)
37
+ end
38
+
39
+ Kuby.logger.info('Nginx ingress resources deployed!')
40
+ rescue => e
41
+ Kuby.logger.fatal(e.message)
42
+ raise
43
+ end
44
+
45
+ def namespace
46
+ NAMESPACE
47
+ end
48
+
49
+ def service_name
50
+ SERVICE_NAME
51
+ end
52
+
53
+ private
54
+
55
+ def already_deployed?
56
+ kubernetes_cli.get_object('Service', 'ingress-nginx', 'ingress-nginx')
57
+ true
58
+ rescue KubernetesCLI::GetResourceError
59
+ return false
60
+ end
61
+
62
+ def after_initialize
63
+ @config = Config.new
64
+ end
65
+
66
+ def kubernetes_cli
67
+ environment.kubernetes.provider.kubernetes_cli
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,18 @@
1
+ module Kuby
2
+ module Plugins
3
+ module RailsApp
4
+ autoload :AssetCopyTask, 'kuby/plugins/rails_app/asset_copy_task'
5
+ autoload :Assets, 'kuby/plugins/rails_app/assets'
6
+ autoload :Database, 'kuby/plugins/rails_app/database'
7
+ autoload :MySQL, 'kuby/plugins/rails_app/mysql'
8
+ autoload :Plugin, 'kuby/plugins/rails_app/plugin'
9
+ autoload :Postgres, 'kuby/plugins/rails_app/postgres'
10
+ autoload :RewriteDbConfig, 'kuby/plugins/rails_app/rewrite_db_config'
11
+ autoload :Sqlite, 'kuby/plugins/rails_app/sqlite'
12
+ end
13
+ end
14
+ end
15
+
16
+ Kuby.register_plugin(:rails_assets, Kuby::Plugins::RailsApp::Assets)
17
+
18
+ load File.expand_path(File.join('rails_app', 'tasks.rake'), __dir__)
@@ -0,0 +1,117 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+
4
+ module Kuby
5
+ module Plugins
6
+ module RailsApp
7
+ # Works by maintaining a directory structure where each deploy creates
8
+ # a new directory. Each directory is given a timestamped name. Assets
9
+ # are symlinked into a special 'current' directory from each of the
10
+ # timestamped directories. Each invocation overrides existing symlinks
11
+ # if they already exist. This technique ensures assets from the previous
12
+ # deploy remain available while the web servers are restarting.
13
+ class AssetCopyTask
14
+ TIMESTAMP_FORMAT = '%Y%m%d%H%M%S'.freeze
15
+ KEEP = 5
16
+
17
+ attr_reader :dest_path, :source_path
18
+
19
+ def initialize(to:, from:)
20
+ @dest_path = to
21
+ @source_path = from
22
+ end
23
+
24
+ def run
25
+ FileUtils.mkdir_p(ts_dir)
26
+ FileUtils.mkdir_p(current_dir)
27
+
28
+ copy_new_assets
29
+ delete_old_assets
30
+
31
+ nil
32
+ end
33
+
34
+ private
35
+
36
+ def copy_new_assets
37
+ # Copy all assets to new timestamp directory
38
+ #
39
+ # "source_path/." is special syntax. From the Ruby docs:
40
+ # cp_r('src', 'dest') makes dest/src, but cp_r('src/.', 'dest') doesn't
41
+ FileUtils.cp_r(File.join(source_path, '.'), ts_dir)
42
+
43
+ relative_source_files = Dir.chdir(ts_dir) do
44
+ Dir.glob(File.join('**', '*'))
45
+ end
46
+
47
+ relative_source_files.each do |relative_source_file|
48
+ source_file = Pathname(File.join(current_dir, relative_source_file))
49
+ source_ts_file = Pathname(File.join(ts_dir, relative_source_file))
50
+ next unless File.file?(source_ts_file)
51
+
52
+ # create individual symlinks for each file in source dir
53
+ target_file = File.join(current_dir, relative_source_file)
54
+ FileUtils.mkdir_p(File.dirname(target_file))
55
+ source_ln_file = source_ts_file.relative_path_from(source_file.dirname)
56
+ FileUtils.ln_s(source_ln_file, target_file, force: true)
57
+ Kuby.logger.info("Linked #{source_ln_file} -> #{target_file}")
58
+ end
59
+ end
60
+
61
+ def delete_old_assets
62
+ # find all asset directories; directories have timestamp names
63
+ asset_dirs = (Dir.glob(File.join(dest_path, '*')) - [current_dir])
64
+ .select { |dir| File.directory?(dir) && try_parse_ts(File.basename(dir)) }
65
+ .sort_by { |dir| parse_ts(File.basename(dir)) }
66
+ .reverse
67
+
68
+ # only keep the n most recent directories
69
+ dirs_to_delete = asset_dirs[KEEP..-1] || []
70
+
71
+ dirs_to_delete.each do |dir_to_delete|
72
+ relative_files_to_delete = Dir.chdir(dir_to_delete) do
73
+ Dir.glob(File.join('**', '*'))
74
+ end
75
+
76
+ relative_files_to_delete.each do |relative_file_to_delete|
77
+ file_to_delete = File.join(dir_to_delete, relative_file_to_delete)
78
+ next unless File.file?(file_to_delete)
79
+
80
+ link = File.join(current_dir, relative_file_to_delete)
81
+ next unless File.symlink?(link)
82
+
83
+ # Only remove a symlink if it still points to a resource
84
+ # in the directory we're currently deleting. Othewise, leave
85
+ # it there - it was added by another deploy.
86
+ if File.readlink(link) == file_to_delete
87
+ File.unlink(link)
88
+ end
89
+ end
90
+
91
+ FileUtils.rm_r(dir_to_delete)
92
+ end
93
+ end
94
+
95
+ def try_parse_ts(ts)
96
+ parse_ts(ts)
97
+ rescue ArgumentError
98
+ return nil
99
+ end
100
+
101
+ def parse_ts(ts)
102
+ Time.strptime(ts, TIMESTAMP_FORMAT)
103
+ end
104
+
105
+ def ts_dir
106
+ @ts_dir ||= File.join(
107
+ dest_path, Time.now.strftime(TIMESTAMP_FORMAT)
108
+ )
109
+ end
110
+
111
+ def current_dir
112
+ @current_dir ||= File.join(dest_path, 'current')
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end