kuby-core 0.8.1 → 0.11.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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -1
  3. data/Gemfile +1 -0
  4. data/README.md +15 -3
  5. data/bin/kuby +4 -0
  6. data/kuby-core.gemspec +7 -3
  7. data/lib/kuby-core.rb +1 -0
  8. data/lib/kuby.rb +60 -18
  9. data/lib/kuby/basic_logger.rb +14 -0
  10. data/lib/kuby/cli_base.rb +126 -4
  11. data/lib/kuby/commands.rb +245 -0
  12. data/lib/kuby/definition.rb +2 -3
  13. data/lib/kuby/dev_setup.rb +256 -0
  14. data/lib/kuby/docker.rb +4 -0
  15. data/lib/kuby/docker/alpine.rb +16 -12
  16. data/lib/kuby/docker/assets_phase.rb +5 -0
  17. data/lib/kuby/docker/bundler_phase.rb +39 -5
  18. data/lib/kuby/docker/cli.rb +60 -3
  19. data/lib/kuby/docker/copy_phase.rb +11 -3
  20. data/lib/kuby/docker/credentials.rb +1 -0
  21. data/lib/kuby/docker/debian.rb +14 -10
  22. data/lib/kuby/docker/dev_spec.rb +195 -0
  23. data/lib/kuby/docker/distro.rb +43 -0
  24. data/lib/kuby/docker/dockerfile.rb +81 -18
  25. data/lib/kuby/docker/errors.rb +9 -0
  26. data/lib/kuby/docker/inline_layer.rb +7 -0
  27. data/lib/kuby/docker/layer.rb +11 -0
  28. data/lib/kuby/docker/layer_stack.rb +47 -6
  29. data/lib/kuby/docker/local_tags.rb +27 -2
  30. data/lib/kuby/docker/metadata.rb +35 -29
  31. data/lib/kuby/docker/package_list.rb +22 -3
  32. data/lib/kuby/docker/package_phase.rb +25 -5
  33. data/lib/kuby/docker/packages.rb +1 -0
  34. data/lib/kuby/docker/packages/managed_package.rb +20 -2
  35. data/lib/kuby/docker/packages/nodejs.rb +8 -0
  36. data/lib/kuby/docker/packages/package.rb +15 -1
  37. data/lib/kuby/docker/packages/simple_managed_package.rb +10 -1
  38. data/lib/kuby/docker/packages/yarn.rb +14 -0
  39. data/lib/kuby/docker/remote_tags.rb +21 -2
  40. data/lib/kuby/docker/setup_phase.rb +37 -5
  41. data/lib/kuby/docker/spec.rb +114 -12
  42. data/lib/kuby/docker/tags.rb +37 -2
  43. data/lib/kuby/docker/timestamp_tag.rb +14 -2
  44. data/lib/kuby/docker/webserver_phase.rb +40 -5
  45. data/lib/kuby/docker/yarn_phase.rb +5 -0
  46. data/lib/kuby/environment.rb +14 -1
  47. data/lib/kuby/kubernetes.rb +10 -9
  48. data/lib/kuby/kubernetes/deploy_task.rb +5 -0
  49. data/lib/kuby/kubernetes/deployer.rb +67 -11
  50. data/lib/kuby/kubernetes/docker_config.rb +1 -0
  51. data/lib/kuby/kubernetes/{minikube_provider.rb → docker_desktop_provider.rb} +5 -4
  52. data/lib/kuby/kubernetes/errors.rb +1 -0
  53. data/lib/kuby/kubernetes/manifest.rb +1 -0
  54. data/lib/kuby/kubernetes/provider.rb +9 -4
  55. data/lib/kuby/kubernetes/registry_secret.rb +1 -0
  56. data/lib/kuby/kubernetes/spec.rb +24 -22
  57. data/lib/kuby/middleware.rb +1 -0
  58. data/lib/kuby/middleware/health_check.rb +1 -0
  59. data/lib/kuby/plugin.rb +1 -0
  60. data/lib/kuby/plugin_registry.rb +28 -0
  61. data/lib/kuby/plugins.rb +1 -0
  62. data/lib/kuby/plugins/nginx_ingress.rb +1 -0
  63. data/lib/kuby/plugins/rails_app.rb +1 -0
  64. data/lib/kuby/plugins/rails_app/asset_copy_task.rb +1 -0
  65. data/lib/kuby/plugins/rails_app/assets.rb +1 -0
  66. data/lib/kuby/plugins/rails_app/database.rb +10 -9
  67. data/lib/kuby/plugins/rails_app/generators/kuby.rb +45 -44
  68. data/lib/kuby/plugins/rails_app/mysql.rb +1 -3
  69. data/lib/kuby/plugins/rails_app/plugin.rb +244 -44
  70. data/lib/kuby/plugins/rails_app/postgres.rb +1 -0
  71. data/lib/kuby/plugins/rails_app/rewrite_db_config.rb +1 -0
  72. data/lib/kuby/plugins/rails_app/sqlite.rb +1 -0
  73. data/lib/kuby/rails_commands.rb +84 -0
  74. data/lib/kuby/railtie.rb +1 -4
  75. data/lib/kuby/tasks.rb +78 -23
  76. data/lib/kuby/trailing_hash.rb +5 -3
  77. data/lib/kuby/version.rb +3 -1
  78. data/spec/docker/metadata_spec.rb +1 -108
  79. data/spec/docker/spec_spec.rb +303 -0
  80. data/spec/docker/timestamp_tag_spec.rb +1 -0
  81. data/spec/dummy/app/channels/application_cable/channel.rb +1 -0
  82. data/spec/dummy/app/channels/application_cable/connection.rb +1 -0
  83. data/spec/dummy/app/controllers/application_controller.rb +1 -0
  84. data/spec/dummy/app/helpers/application_helper.rb +1 -0
  85. data/spec/dummy/app/jobs/application_job.rb +1 -0
  86. data/spec/dummy/app/mailers/application_mailer.rb +1 -0
  87. data/spec/dummy/app/models/application_record.rb +1 -0
  88. data/spec/dummy/config/application.rb +1 -0
  89. data/spec/dummy/config/boot.rb +1 -0
  90. data/spec/dummy/config/environment.rb +1 -0
  91. data/spec/dummy/config/environments/development.rb +1 -0
  92. data/spec/dummy/config/environments/production.rb +1 -0
  93. data/spec/dummy/config/environments/test.rb +1 -0
  94. data/spec/dummy/config/initializers/application_controller_renderer.rb +1 -0
  95. data/spec/dummy/config/initializers/assets.rb +1 -0
  96. data/spec/dummy/config/initializers/backtrace_silencers.rb +1 -0
  97. data/spec/dummy/config/initializers/content_security_policy.rb +1 -0
  98. data/spec/dummy/config/initializers/cookies_serializer.rb +1 -0
  99. data/spec/dummy/config/initializers/filter_parameter_logging.rb +1 -0
  100. data/spec/dummy/config/initializers/inflections.rb +1 -0
  101. data/spec/dummy/config/initializers/mime_types.rb +1 -0
  102. data/spec/dummy/config/initializers/wrap_parameters.rb +1 -0
  103. data/spec/dummy/config/puma.rb +1 -0
  104. data/spec/dummy/config/routes.rb +1 -0
  105. data/spec/dummy/config/spring.rb +1 -0
  106. data/spec/dummy/db/seeds.rb +1 -0
  107. data/spec/dummy/test/application_system_test_case.rb +1 -0
  108. data/spec/dummy/test/channels/application_cable/connection_test.rb +1 -0
  109. data/spec/dummy/test/test_helper.rb +1 -0
  110. data/spec/spec_helper.rb +18 -1
  111. data/spec/support/docker/fake_cli.rb +1 -0
  112. data/spec/support/docker/remote/fake_client.rb +1 -0
  113. data/spec/trailing_hash_spec.rb +2 -0
  114. metadata +62 -11
  115. data/lib/kuby/tasks/kuby.rake +0 -70
@@ -0,0 +1,245 @@
1
+ # typed: true
2
+ require 'kuby/version'
3
+ require 'gli'
4
+
5
+ module Kuby
6
+ class Commands
7
+ extend GLI::App
8
+
9
+ # GLI doesn't have a wildcard option, so it's impossible to tell it to
10
+ # slurp up all args after a certain point. In our case, we want to be
11
+ # able to invoke `rails` commands and pass through all the original
12
+ # flags, switches, etc. To get around GLI's limitations, we identify
13
+ # `rails` commands in this hijacked `run` method and only use GLI to
14
+ # parse global options (like -e). The rest of the Rails options are
15
+ # captured in an instance variable and thereby made available to the
16
+ # Rails command handlers defined below. We use Module#prepend here to
17
+ # avoid the usual series of cryptic alias_method calls (note that there
18
+ # is no singleton class version of #prepend in the Ruby language).
19
+ singleton_class.send(:prepend, Module.new do
20
+ def run(args)
21
+ if idx = args.index('rails') || idx = args.index('rake')
22
+ @rails_options = args[idx..-1]
23
+ super(args[0..idx])
24
+ else
25
+ super
26
+ end
27
+ end
28
+ end)
29
+
30
+ def self.tasks
31
+ Kuby::Tasks.new(Kuby.environment)
32
+ end
33
+
34
+ def self.must_be_dev_env!
35
+ unless Kuby.environment.development?
36
+ fail "Command not supported in the '#{Kuby.environment.name}' environment"
37
+ end
38
+ end
39
+
40
+ program_desc 'Kuby command-line interface. Kuby is a convention '\
41
+ 'over configuration approach for running Rails apps in Kubernetes.'
42
+
43
+ version Kuby::VERSION
44
+
45
+ subcommand_option_handling :normal
46
+ arguments :loose
47
+
48
+ desc 'The Kuby environment to use. Overrides KUBY_ENV.'
49
+ flag [:e, :environment], type: String, required: false
50
+
51
+ desc 'Path to your Kuby config file. Overrides KUBY_CONFIG.'
52
+ default_value './kuby.rb'
53
+ flag [:c, :config]
54
+
55
+ pre do |global_options, options, args|
56
+ Kuby.env = global_options[:environment] if global_options[:environment]
57
+ Kuby.load!(global_options[:config])
58
+
59
+ # GLI will abort unless this block returns a truthy value
60
+ true
61
+ end
62
+
63
+ # These are only stubs included to fill out the help screens. Rails
64
+ # commands are handled by the RailsCommands class.
65
+ desc 'Runs a Rails command.'
66
+ command :rails do |rc|
67
+ rc.action do |global_options, options, args|
68
+ must_be_dev_env!
69
+ exit 1 unless tasks.dev_deployment_ok
70
+ Kuby::RailsCommands.run(@rails_options)
71
+ end
72
+
73
+ rc.desc 'Runs the rails server (run `rails server --help` for options)'
74
+ rc.command [:server, :s] do |c|
75
+ c.action do |global_options, options, args|
76
+ must_be_dev_env!
77
+ exit 1 unless tasks.dev_deployment_ok
78
+ Kuby::RailsCommands.run(@rails_options)
79
+ end
80
+ end
81
+
82
+ rc.desc 'Runs a script in the Rails environment (run `rails runner --help` for options)'
83
+ rc.command [:runner, :r] do |c|
84
+ c.action do |global_options, options, args|
85
+ must_be_dev_env!
86
+ exit 1 unless tasks.dev_deployment_ok
87
+ Kuby::RailsCommands.run(@rails_options)
88
+ end
89
+ end
90
+
91
+ rc.desc 'Starts an interactive Ruby console with the Rails environment loaded '\
92
+ '(run `rails console --help` for options)'
93
+ rc.command [:console, :c] do |c|
94
+ c.action do |global_options, options, args|
95
+ must_be_dev_env!
96
+ exit 1 unless tasks.dev_deployment_ok
97
+ Kuby::RailsCommands.run(@rails_options)
98
+ end
99
+ end
100
+ end
101
+
102
+ desc 'Runs a rake task.'
103
+ command :rake do |rc|
104
+ rc.action do |global_options, options, args|
105
+ must_be_dev_env!
106
+ exit 1 unless tasks.dev_deployment_ok
107
+ Kuby::RailsCommands.run(@rails_options)
108
+ end
109
+ end
110
+
111
+ desc 'Builds the Docker image.'
112
+ command :build do |c|
113
+ c.action do |global_options, options, args|
114
+ tasks.build
115
+ end
116
+ end
117
+
118
+ desc 'Pushes the Docker image to the configured registry.'
119
+ command :push do |c|
120
+ c.action do |global_options, options, args|
121
+ tasks.push
122
+ end
123
+ end
124
+
125
+ desc 'Gets your Kubernetes cluster ready to run your Rails app.'
126
+ command :setup do |c|
127
+ c.action do |global_options, options, args|
128
+ tasks.setup
129
+ end
130
+ end
131
+
132
+ desc 'Prints the effective Dockerfile used to build the Docker image.'
133
+ command :dockerfile do |c|
134
+ c.action do |global_options, options, args|
135
+ tasks.print_dockerfile
136
+ end
137
+ end
138
+
139
+ desc 'Deploys the application.'
140
+ command :deploy do |c|
141
+ c.desc 'The Docker tag to deploy. Defaults to the most recent tag.'
142
+ c.flag [:t, :tag], required: false
143
+ c.action do |global_options, options, args|
144
+ tasks.deploy(options[:tag])
145
+ end
146
+ end
147
+
148
+ desc 'Rolls back to the previous Docker tag.'
149
+ command :rollback do |c|
150
+ c.action do |global_options, options, args|
151
+ tasks.rollback
152
+ end
153
+ end
154
+
155
+ desc 'Prints the effective Kubernetes resources that will be applied on deploy.'
156
+ command :resources do |c|
157
+ c.action do |global_options, options, args|
158
+ tasks.print_resources
159
+ end
160
+ end
161
+
162
+ desc 'Prints out the contents of the kubeconfig Kuby is using to communicate with your cluster.'
163
+ command :kubeconfig do |c|
164
+ c.action do |global_options, options, args|
165
+ tasks.print_kubeconfig
166
+ end
167
+ end
168
+
169
+ desc 'Runs an arbitrary kubectl command.'
170
+ command :kubectl do |c|
171
+ c.desc 'Prefixes the kubectl command with the namespace associated with '\
172
+ 'the current environment. For example, if the Kuby env is "production", '\
173
+ 'this option will prefix the kubectl command with "-n myapp-production".'
174
+ c.switch [:N, :namespaced], default: false
175
+ c.action do |global_options, options, args|
176
+ if options[:namespaced]
177
+ # sorry Demeter
178
+ namespace = Kuby.definition.environment.kubernetes.namespace.metadata.name
179
+ args = ['-n', namespace, *args]
180
+ end
181
+
182
+ tasks.kubectl(*args)
183
+ end
184
+ end
185
+
186
+ desc 'Runs commands, etc against the Kubernetes cluster.'
187
+ command :remote do |rc|
188
+ rc.desc 'Tails (i.e. continuously streams) the Rails log from your running application.'
189
+ rc.command :logs do |c|
190
+ c.action do |global_options, options, args|
191
+ exit 1 unless tasks.dev_deployment_ok
192
+ tasks.remote_logs
193
+ end
194
+ end
195
+
196
+ rc.desc 'Lists running Kubernetes pods.'
197
+ rc.command :status do |c|
198
+ c.action do |global_options, options, args|
199
+ exit 1 unless tasks.dev_deployment_ok
200
+ tasks.remote_status
201
+ end
202
+ end
203
+
204
+ rc.desc 'Runs an arbitrary command inside a running Rails pod.'
205
+ rc.command :exec do |c|
206
+ c.action do |global_options, options, args|
207
+ exit 1 unless tasks.dev_deployment_ok
208
+ tasks.remote_exec(args)
209
+ end
210
+ end
211
+
212
+ rc.desc 'Establishes a shell inside a running Rails pod.'
213
+ rc.command :shell do |c|
214
+ c.action do |global_options, options, args|
215
+ exit 1 unless tasks.dev_deployment_ok
216
+ tasks.remote_shell
217
+ end
218
+ end
219
+
220
+ rc.desc 'Establishes a Rails console inside a running Rails pod.'
221
+ rc.command :console do |c|
222
+ c.action do |global_options, options, args|
223
+ exit 1 unless tasks.dev_deployment_ok
224
+ tasks.remote_console
225
+ end
226
+ end
227
+
228
+ rc.desc 'Establishes a database console inside a running Rails pod.'
229
+ rc.command :dbconsole do |c|
230
+ c.action do |global_options, options, args|
231
+ exit 1 unless tasks.dev_deployment_ok
232
+ tasks.remote_dbconsole
233
+ end
234
+ end
235
+
236
+ rc.desc "Restarts the Rails app's web pods."
237
+ rc.command :restart do |c|
238
+ c.action do |global_options, options, args|
239
+ exit 1 unless tasks.dev_deployment_ok
240
+ tasks.remote_restart
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  module Kuby
2
3
  class Definition
3
4
  attr_reader :app_name
@@ -9,9 +10,7 @@ module Kuby
9
10
  def environment(name = Kuby.env, &block)
10
11
  name = name.to_s
11
12
 
12
- if name
13
- environments[name] ||= Environment.new(name, self)
14
- end
13
+ environments[name] ||= Environment.new(name, self)
15
14
 
16
15
  if block_given?
17
16
  environments[name].instance_eval(&block)
@@ -0,0 +1,256 @@
1
+ # typed: true
2
+ module Kuby
3
+ class Spinner
4
+ PIECES = %w(- \\ | /).freeze
5
+ INTERVAL = 0.2 # seconds
6
+
7
+ def self.spin(message)
8
+ yield new(message)
9
+ end
10
+
11
+ attr_reader :message, :status
12
+
13
+ def initialize(message)
14
+ @message = message
15
+ @status = :running
16
+ @thread = Thread.new do
17
+ counter = 0
18
+
19
+ while true
20
+ case status
21
+ when :running
22
+ piece = PIECES[counter % PIECES.size]
23
+ STDOUT.write "\r[#{piece}] #{message}"
24
+ sleep INTERVAL
25
+ counter += 1
26
+ when :success
27
+ STDOUT.write("\r[+] #{message}")
28
+ break
29
+ when :failure
30
+ STDOUT.write("\r[×] #{message}")
31
+ break
32
+ end
33
+ end
34
+
35
+ puts
36
+ end
37
+ end
38
+
39
+ def success
40
+ @status = :success
41
+ @thread.join
42
+ end
43
+
44
+ def failure
45
+ @status = :failure
46
+ @thread.join
47
+ end
48
+ end
49
+
50
+ class SetupTask
51
+ attr_reader :message, :callable
52
+
53
+ def initialize(message, callable)
54
+ @message = message
55
+ @callable = callable
56
+ end
57
+
58
+ def run
59
+ callable.call
60
+ end
61
+ end
62
+
63
+ class Pipe
64
+ attr_reader :name, :cli, :out, :err
65
+
66
+ def initialize(name, cli)
67
+ @name = name
68
+ @cli = cli
69
+ @out = StringIO.new
70
+ @err = StringIO.new
71
+ end
72
+
73
+ def wrap(&block)
74
+ cli.with_pipes(out, err) do
75
+ block.call
76
+ end
77
+ end
78
+
79
+ def success?
80
+ cli.last_status.nil? || cli.last_status.success?
81
+ end
82
+ end
83
+
84
+ class Pipes
85
+ include Enumerable
86
+
87
+ attr_reader :pipes, :ex
88
+
89
+ def self.build(clis)
90
+ new(clis.map { |name, cli| Pipe.new(name, cli) })
91
+ end
92
+
93
+ def initialize(pipes)
94
+ @pipes = pipes
95
+ end
96
+
97
+ def each(&block)
98
+ pipes.each(&block)
99
+ end
100
+
101
+ def wrap(&block)
102
+ do_wrap(pipes, &block)
103
+ end
104
+
105
+ def success?
106
+ pipes.all?(&:success?) && !ex
107
+ end
108
+
109
+ private
110
+
111
+ def do_wrap(remaining_pipes, &block)
112
+ if remaining_pipes.empty?
113
+ begin
114
+ yield
115
+ rescue => e
116
+ @ex = e
117
+ end
118
+
119
+ return
120
+ end
121
+
122
+ remaining_pipes[0].wrap do
123
+ do_wrap(remaining_pipes[1..-1], &block)
124
+ end
125
+ end
126
+ end
127
+
128
+ class SetupTaskList
129
+ attr_reader :tasks, :clis
130
+
131
+ def initialize(tasks, clis)
132
+ @tasks = tasks
133
+ @clis = clis
134
+ end
135
+
136
+ def run
137
+ tasks.each do |task|
138
+ pipes = Pipes.build(clis)
139
+
140
+ Spinner.spin(task.message) do |spinner|
141
+ pipes.wrap { task.run }
142
+
143
+ if pipes.success?
144
+ spinner.success
145
+ else
146
+ spinner.failure
147
+ print_error(pipes.ex)
148
+
149
+ pipes.each do |pipe|
150
+ print_streams(pipe)
151
+ end
152
+
153
+ return false
154
+ end
155
+ end
156
+ end
157
+
158
+ true
159
+ end
160
+
161
+ private
162
+
163
+ def print_streams(pipe)
164
+ unless pipe.out.string.strip.empty?
165
+ puts("========= #{pipe.name.upcase} STDOUT ========")
166
+ puts pipe.out.string
167
+ end
168
+
169
+ unless pipe.err.string.strip.empty?
170
+ puts("========= #{pipe.name.upcase} STDERR ========")
171
+ puts pipe.err.string
172
+ end
173
+ end
174
+
175
+ def print_error(ex)
176
+ return unless ex
177
+ puts("========= RUBY ERROR ========")
178
+ puts(ex.message)
179
+ puts(ex.backtrace.join("\n"))
180
+ end
181
+ end
182
+
183
+ class DevSetup
184
+ attr_reader :environment
185
+
186
+ def initialize(environment)
187
+ @environment = environment
188
+ end
189
+
190
+ def run
191
+ SetupTaskList.new(setup_tasks, clis).run
192
+ end
193
+
194
+ private
195
+
196
+ def setup_tasks
197
+ @setup_tasks ||= [
198
+ SetupTask.new(
199
+ 'Building dev Docker image', -> { tasks.build }
200
+ ),
201
+
202
+ SetupTask.new(
203
+ 'Setting up local Kubernetes cluster', -> { tasks.setup }
204
+ ),
205
+
206
+ SetupTask.new(
207
+ 'Deploying dev environment', -> { tasks.deploy }
208
+ ),
209
+
210
+ SetupTask.new(
211
+ 'Installing bundler', -> {
212
+ tasks.remote_system("gem install bundler -v #{Bundler::VERSION}")
213
+ }
214
+ ),
215
+
216
+ SetupTask.new(
217
+ 'Installing bundled gems', -> { tasks.remote_system('bundle install') }
218
+ ),
219
+
220
+ SetupTask.new(
221
+ 'Installing Javascript packages', -> { tasks.remote_system('yarn install') }
222
+ ),
223
+
224
+ SetupTask.new(
225
+ 'Creating database', -> { tasks.remote_system('bundle exec rake db:create') }
226
+ ),
227
+
228
+ SetupTask.new(
229
+ 'Migrating database', -> { tasks.remote_system('bundle exec rake db:migrate') }
230
+ )
231
+ ]
232
+ end
233
+
234
+ def kubernetes
235
+ environment.kubernetes
236
+ end
237
+
238
+ def docker
239
+ environment.docker
240
+ end
241
+
242
+ def clis
243
+ @clis ||= {
244
+ kubectl: kubernetes.provider.kubernetes_cli,
245
+ helm: kubernetes.provider.helm_cli,
246
+ krane: kubernetes.provider.deployer,
247
+ docker: docker.cli,
248
+ kuby: Kuby.logger
249
+ }
250
+ end
251
+
252
+ def tasks
253
+ @tasks ||= Kuby::Tasks.new(environment)
254
+ end
255
+ end
256
+ end