kuby-core 0.8.1 → 0.9.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -1
  3. data/README.md +11 -1
  4. data/bin/kuby +4 -0
  5. data/kuby-core.gemspec +5 -2
  6. data/lib/kuby.rb +46 -18
  7. data/lib/kuby/basic_logger.rb +13 -0
  8. data/lib/kuby/cli_base.rb +81 -8
  9. data/lib/kuby/commands.rb +220 -0
  10. data/lib/kuby/definition.rb +1 -3
  11. data/lib/kuby/dev_setup.rb +255 -0
  12. data/lib/kuby/docker.rb +1 -0
  13. data/lib/kuby/docker/bundler_phase.rb +3 -3
  14. data/lib/kuby/docker/cli.rb +13 -1
  15. data/lib/kuby/docker/dev_spec.rb +131 -0
  16. data/lib/kuby/docker/dockerfile.rb +16 -1
  17. data/lib/kuby/docker/layer_stack.rb +4 -0
  18. data/lib/kuby/docker/local_tags.rb +4 -0
  19. data/lib/kuby/docker/metadata.rb +0 -22
  20. data/lib/kuby/docker/setup_phase.rb +3 -2
  21. data/lib/kuby/docker/spec.rb +31 -5
  22. data/lib/kuby/environment.rb +10 -1
  23. data/lib/kuby/kubernetes.rb +9 -9
  24. data/lib/kuby/kubernetes/deploy_task.rb +4 -0
  25. data/lib/kuby/kubernetes/deployer.rb +63 -11
  26. data/lib/kuby/kubernetes/{minikube_provider.rb → docker_desktop_provider.rb} +4 -4
  27. data/lib/kuby/kubernetes/provider.rb +8 -4
  28. data/lib/kuby/kubernetes/spec.rb +23 -22
  29. data/lib/kuby/plugin_registry.rb +27 -0
  30. data/lib/kuby/plugins/rails_app/generators/kuby.rb +3 -15
  31. data/lib/kuby/plugins/rails_app/plugin.rb +230 -40
  32. data/lib/kuby/rails_commands.rb +89 -0
  33. data/lib/kuby/railtie.rb +0 -4
  34. data/lib/kuby/tasks.rb +76 -23
  35. data/lib/kuby/version.rb +1 -1
  36. data/spec/docker/metadata_spec.rb +0 -108
  37. data/spec/docker/spec_spec.rb +266 -0
  38. data/spec/spec_helper.rb +8 -1
  39. metadata +44 -9
  40. data/lib/kuby/tasks/kuby.rake +0 -70
@@ -3,10 +3,6 @@ require 'rails/railtie'
3
3
 
4
4
  module Kuby
5
5
  class Railtie < ::Rails::Railtie
6
- rake_tasks do
7
- load File.expand_path(File.join('tasks', 'kuby.rake'), __dir__)
8
- end
9
-
10
6
  initializer 'kuby.health_check_middleware' do |app|
11
7
  app.middleware.use Kuby::Middleware::HealthCheck
12
8
  end
@@ -21,24 +21,25 @@ module Kuby
21
21
  end
22
22
 
23
23
  def build
24
+ build_args = {}
25
+
26
+ unless ENV.fetch('RAILS_MASTER_KEY', '').empty?
27
+ build_args['RAILS_MASTER_KEY'] = ENV['RAILS_MASTER_KEY']
28
+ end
29
+
24
30
  docker.cli.build(
25
31
  dockerfile: docker.to_dockerfile,
26
32
  image_url: docker.metadata.image_url,
27
- tags: docker.metadata.tags
28
- )
29
- end
30
-
31
- def run
32
- dockerfile = docker.to_dockerfile
33
-
34
- docker.cli.run(
35
- image_url: docker.metadata.image_url,
36
- tag: 'latest',
37
- ports: dockerfile.exposed_ports
33
+ tags: docker.metadata.tags,
34
+ build_args: build_args
38
35
  )
39
36
  end
40
37
 
41
38
  def push
39
+ if environment.development?
40
+ fail 'Cannot push Docker images built for the development environment'
41
+ end
42
+
42
43
  hostname = docker.metadata.image_hostname
43
44
 
44
45
  unless docker.cli.auths.include?(hostname)
@@ -72,8 +73,8 @@ module Kuby
72
73
  end
73
74
  end
74
75
 
75
- def deploy
76
- environment.kubernetes.deploy
76
+ def deploy(tag = nil)
77
+ environment.kubernetes.deploy(tag)
77
78
  end
78
79
 
79
80
  def rollback
@@ -106,26 +107,64 @@ module Kuby
106
107
  kubernetes_cli.run_cmd(['-n', namespace, 'get', 'pods'])
107
108
  end
108
109
 
109
- def remote_shell
110
+ def remote_exec(cmd)
110
111
  first_pod = get_first_pod
111
- shell = docker.distro_spec.shell_exe
112
- kubernetes_cli.exec_cmd(shell, namespace, first_pod.dig('metadata', 'name'))
112
+ kubernetes_cli.exec_cmd(cmd, namespace, first_pod.dig('metadata', 'name'))
113
113
  end
114
114
 
115
- def remote_console
115
+ def remote_system(cmd)
116
116
  first_pod = get_first_pod
117
+ kubernetes_cli.system_cmd(cmd, namespace, first_pod.dig('metadata', 'name'))
118
+ end
117
119
 
118
- kubernetes_cli.exec_cmd(
119
- 'bundle exec rails console', namespace, first_pod.dig('metadata', 'name')
120
- )
120
+ def remote_shell
121
+ remote_exec(docker.distro_spec.shell_exe)
122
+ end
123
+
124
+ def remote_console
125
+ remote_exec('bundle exec rails console')
121
126
  end
122
127
 
123
128
  def remote_dbconsole
124
- first_pod = get_first_pod
129
+ remote_exec('bundle exec rails dbconsole')
130
+ end
131
+
132
+ def remote_restart
133
+ deployment = rails_app.deployment.metadata.name
134
+ kubernetes_cli.restart_deployment(namespace, deployment)
135
+ end
136
+
137
+ def dev_deployment_ok
138
+ return true unless Kuby.environment.development?
125
139
 
126
- kubernetes_cli.exec_cmd(
127
- 'bundle exec rails dbconsole', namespace, first_pod.dig('metadata', 'name')
140
+ deployments = kubernetes_cli.get_objects(
141
+ 'deployments', namespace, match_labels.serialize
128
142
  )
143
+
144
+ if deployments.empty?
145
+ puts 'No development environment detected.'
146
+ STDOUT.write('Set up development environment? (y/n): ')
147
+ answer = STDIN.gets.strip.downcase
148
+ return false unless answer =~ /ye?s?/
149
+ else
150
+ depl = deployments.first
151
+ deployed_checksum = depl.dig('metadata', 'annotations', 'getkuby.io/dockerfile-checksum')
152
+ current_checksum = docker.to_dockerfile.checksum
153
+
154
+ if deployed_checksum != current_checksum
155
+ puts "Development environment appears to be out-of-date."
156
+ puts "Environment checksum: #{deployed_checksum}"
157
+ puts "Current checksum: #{current_checksum}"
158
+ STDOUT.write('Update development environment? (y/n): ')
159
+ answer = STDIN.gets.strip.downcase
160
+ # return true here to prevent letting an out-of-date deployment
161
+ # stop us from running commands
162
+ return true unless answer =~ /ye?s?/
163
+ DevSetup.new(environment).run
164
+ end
165
+ end
166
+
167
+ true
129
168
  end
130
169
 
131
170
  private
@@ -135,6 +174,12 @@ module Kuby
135
174
  'pods', namespace, match_labels.serialize
136
175
  )
137
176
 
177
+ # consider only "Running" pods that aren't marked for deletion
178
+ pods.select! do |pod|
179
+ pod.dig('status', 'phase') == 'Running' &&
180
+ !pod.dig('metadata', 'deletionTimestamp')
181
+ end
182
+
138
183
  if pods.empty?
139
184
  raise Kuby::Kubernetes::MissingResourceError,
140
185
  "Couldn't find any running pods in namespace '#{namespace}' :("
@@ -161,6 +206,14 @@ module Kuby
161
206
  kubernetes.provider.kubernetes_cli
162
207
  end
163
208
 
209
+ def helm_cli
210
+ kubernetes.provider.helm_cli
211
+ end
212
+
213
+ def docker_cli
214
+ docker.cli
215
+ end
216
+
164
217
  def kubernetes
165
218
  Kuby.environment.kubernetes
166
219
  end
@@ -1,3 +1,3 @@
1
1
  module Kuby
2
- VERSION = '0.8.1'
2
+ VERSION = '0.9.0'
3
3
  end
@@ -70,114 +70,6 @@ describe Kuby::Docker::Metadata do
70
70
  end
71
71
  end
72
72
 
73
- describe '#tag' do
74
- let(:tag) { make_ts_tag(Time.now) }
75
-
76
- subject { metadata.tag }
77
-
78
- context 'with no local or remote tags' do
79
- it 'raises an error' do
80
- expect { subject }.to raise_error(Kuby::Docker::MissingTagError)
81
- end
82
- end
83
-
84
- context 'with an available remote tag' do
85
- before { docker_remote_client.tags << tag }
86
-
87
- it { is_expected.to eq(tag) }
88
- end
89
-
90
- context 'with an available local tag' do
91
- before do
92
- docker_cli.build(
93
- dockerfile: nil,
94
- image_url: docker_image_url,
95
- tags: [tag]
96
- )
97
- end
98
-
99
- it { is_expected.to eq(tag) }
100
- end
101
-
102
- context 'with multiple remote tags' do
103
- let(:time) { Time.now }
104
-
105
- before do
106
- docker_remote_client.tags +=
107
- [time - 5, time + 10, time - 10, time + 15].map do |t|
108
- make_ts_tag(t)
109
- end
110
- end
111
-
112
- it { is_expected.to eq(make_ts_tag(time + 15)) }
113
- end
114
-
115
- context 'with multiple local and remote tags' do
116
- let(:time) { Time.now }
117
-
118
- before do
119
- docker_remote_client.tags +=
120
- [time - 5, time + 10, time - 10, time + 15].map do |t|
121
- make_ts_tag(t)
122
- end
123
-
124
- docker_cli.build(
125
- dockerfile: nil,
126
- image_url: docker_image_url,
127
- tags: [time - 3, time + 6, time - 6, time + 18].map do |t|
128
- make_ts_tag(t)
129
- end
130
- )
131
- end
132
-
133
- it { is_expected.to eq(make_ts_tag(time + 18)) }
134
- end
135
- end
136
-
137
- describe '#previous_tag' do
138
- let(:time) { Time.now }
139
- let(:current_tag) { make_ts_tag(time) }
140
-
141
- before do
142
- docker_remote_client.tags << current_tag
143
- docker_cli.build(
144
- dockerfile: nil,
145
- image_url: docker_image_url,
146
- tags: [current_tag]
147
- )
148
- end
149
-
150
- subject { metadata.previous_tag(current_tag) }
151
-
152
- context 'with no previous local or remote tag' do
153
- it 'raises an error' do
154
- expect { subject }.to raise_error(Kuby::Docker::MissingTagError)
155
- end
156
- end
157
-
158
- context 'with an available previous remote tag' do
159
- let(:previous_tag) { make_ts_tag(time - 5) }
160
-
161
- before { docker_remote_client.tags << previous_tag }
162
-
163
- it { is_expected.to eq(previous_tag) }
164
- end
165
-
166
- context 'with an available previous local tag' do
167
- let(:previous_tag) { make_ts_tag(time - 5) }
168
-
169
- before do
170
- docker_cli.build(
171
- dockerfile: nil,
172
- image_url: docker_image_url,
173
- tags: [previous_tag]
174
- )
175
- end
176
-
177
- it { is_expected.to eq(previous_tag) }
178
- end
179
- end
180
-
181
73
  describe '#distro' do
182
74
  subject { metadata.distro }
183
75
 
@@ -0,0 +1,266 @@
1
+ require 'spec_helper'
2
+ require 'timecop'
3
+
4
+ describe Kuby::Docker::Spec do
5
+ let(:spec) { definition.environment.docker }
6
+
7
+ describe '#base_image' do
8
+ subject { spec.to_dockerfile.to_s }
9
+
10
+ it 'uses the default base image for Debian' do
11
+ expect(subject).to include("FROM ruby:#{RUBY_VERSION}\n")
12
+ end
13
+
14
+ context 'when the distro is set to Alpine' do
15
+ before { spec.distro(:alpine) }
16
+
17
+ it 'uses the Alpine base image' do
18
+ expect(subject).to include("FROM ruby:#{RUBY_VERSION}-alpine\n")
19
+ end
20
+ end
21
+
22
+ context 'when the base image is set to something custom' do
23
+ before { spec.base_image('foo/bar') }
24
+
25
+ it 'uses the base image as given' do
26
+ expect(subject).to include("FROM foo/bar\n")
27
+ end
28
+ end
29
+ end
30
+
31
+ describe '#working_dir' do
32
+ let(:default_working_dir) do
33
+ Kuby::Docker::SetupPhase::DEFAULT_WORKING_DIR
34
+ end
35
+
36
+ subject { spec.to_dockerfile.to_s }
37
+
38
+ it 'uses the default working dir' do
39
+ expect(subject).to(
40
+ include("WORKDIR #{default_working_dir}\n")
41
+ )
42
+ end
43
+
44
+ context 'when the working dir is set to something custom' do
45
+ before { spec.working_dir('/foo/bar') }
46
+
47
+ it 'uses the working dir as given' do
48
+ expect(subject).to include("WORKDIR /foo/bar\n")
49
+ end
50
+ end
51
+ end
52
+
53
+ describe '#rails_env' do
54
+ subject { spec.to_dockerfile.to_s }
55
+
56
+ it 'uses the name of the current Kuby environment' do
57
+ expect(subject).to include("ENV RAILS_ENV=#{spec.environment.name}\n")
58
+ expect(subject).to include("ENV KUBY_ENV=#{spec.environment.name}\n")
59
+ end
60
+
61
+ context 'when the environment is set to something custom' do
62
+ before { spec.rails_env('foobar') }
63
+
64
+ it 'uses the environment as given' do
65
+ expect(subject).to include("ENV RAILS_ENV=foobar\n")
66
+ end
67
+
68
+ it "doesn't change the Kuby env" do
69
+ expect(subject).to include("ENV KUBY_ENV=#{spec.environment.name}\n")
70
+ end
71
+ end
72
+ end
73
+
74
+ describe '#bundler_version' do
75
+ subject { spec.to_dockerfile.to_s }
76
+
77
+ it 'installs the current bundler version' do
78
+ expect(subject).to(
79
+ include("RUN gem install bundler -v #{Bundler::VERSION}\n")
80
+ )
81
+ end
82
+
83
+ context 'when the bundler version is set to something custom' do
84
+ before { spec.bundler_version('1.17.3') }
85
+
86
+ it 'installs the given version' do
87
+ expect(subject).to include("RUN gem install bundler -v 1.17.3\n")
88
+ end
89
+ end
90
+ end
91
+
92
+ describe '#gemfile' do
93
+ subject { spec.to_dockerfile.to_s }
94
+
95
+ it 'uses the default Gemfile' do
96
+ expect(subject).to include("COPY Gemfile .\n")
97
+ expect(subject).to include("COPY Gemfile.lock .\n")
98
+ expect(subject).to match(/RUN bundle install .* --gemfile Gemfile/)
99
+ end
100
+
101
+ context 'when the gemfile path is set to something custom' do
102
+ before { spec.gemfile('foo/bar/Gemfile') }
103
+
104
+ it 'uses the given gemfile' do
105
+ expect(subject).to include("COPY foo/bar/Gemfile .\n")
106
+ expect(subject).to include("COPY foo/bar/Gemfile.lock .\n")
107
+ expect(subject).to match(/RUN bundle install .* --gemfile foo\/bar\/Gemfile/)
108
+ end
109
+ end
110
+ end
111
+
112
+ describe '#package' do
113
+ subject { spec.to_dockerfile.to_s }
114
+
115
+ it 'installs the given package' do
116
+ # configured in spec_helper.rb
117
+ spec.package(:fake_package)
118
+
119
+ expect(subject).to match(/apt-get install .* fake_package/m)
120
+ end
121
+ end
122
+
123
+ describe '#files' do
124
+ subject { spec.to_dockerfile.to_s }
125
+
126
+ it 'copies the current directory contents by default' do
127
+ expect(subject).to include("COPY ./ .\n")
128
+ end
129
+
130
+ context 'when given a custom path to copy' do
131
+ before { spec.files('./foo/bar') }
132
+
133
+ it 'copies the given paths only' do
134
+ expect(subject).to include("COPY ./foo/bar .\n")
135
+ end
136
+ end
137
+ end
138
+
139
+ describe '#port' do
140
+ let(:default_port) do
141
+ Kuby::Docker::WebserverPhase::DEFAULT_PORT
142
+ end
143
+
144
+ subject { spec.to_dockerfile.to_s }
145
+
146
+ it 'exposes the default port' do
147
+ expect(subject).to include("EXPOSE #{default_port}\n")
148
+ end
149
+
150
+ context 'when given a custom port' do
151
+ before { spec.port(5555) }
152
+
153
+ it 'exposes the given port' do
154
+ expect(subject).to include("EXPOSE 5555\n")
155
+ end
156
+ end
157
+ end
158
+
159
+ describe '#tag' do
160
+ let(:tag) { make_ts_tag(Time.now) }
161
+
162
+ subject { spec.tag }
163
+
164
+ context 'with no local or remote tags' do
165
+ it 'raises an error' do
166
+ expect { subject }.to raise_error(Kuby::Docker::MissingTagError)
167
+ end
168
+ end
169
+
170
+ context 'with an available remote tag' do
171
+ before { docker_remote_client.tags << tag }
172
+
173
+ it { is_expected.to eq(tag) }
174
+ end
175
+
176
+ context 'with an available local tag' do
177
+ before do
178
+ docker_cli.build(
179
+ dockerfile: nil,
180
+ image_url: docker_image_url,
181
+ tags: [tag]
182
+ )
183
+ end
184
+
185
+ it { is_expected.to eq(tag) }
186
+ end
187
+
188
+ context 'with multiple remote tags' do
189
+ let(:time) { Time.now }
190
+
191
+ before do
192
+ docker_remote_client.tags +=
193
+ [time - 5, time + 10, time - 10, time + 15].map do |t|
194
+ make_ts_tag(t)
195
+ end
196
+ end
197
+
198
+ it { is_expected.to eq(make_ts_tag(time + 15)) }
199
+ end
200
+
201
+ context 'with multiple local and remote tags' do
202
+ let(:time) { Time.now }
203
+
204
+ before do
205
+ docker_remote_client.tags +=
206
+ [time - 5, time + 10, time - 10, time + 15].map do |t|
207
+ make_ts_tag(t)
208
+ end
209
+
210
+ docker_cli.build(
211
+ dockerfile: nil,
212
+ image_url: docker_image_url,
213
+ tags: [time - 3, time + 6, time - 6, time + 18].map do |t|
214
+ make_ts_tag(t)
215
+ end
216
+ )
217
+ end
218
+
219
+ it { is_expected.to eq(make_ts_tag(time + 18)) }
220
+ end
221
+ end
222
+
223
+ describe '#previous_tag' do
224
+ let(:time) { Time.now }
225
+ let(:current_tag) { make_ts_tag(time) }
226
+
227
+ before do
228
+ docker_remote_client.tags << current_tag
229
+ docker_cli.build(
230
+ dockerfile: nil,
231
+ image_url: docker_image_url,
232
+ tags: [current_tag]
233
+ )
234
+ end
235
+
236
+ subject { spec.previous_tag(current_tag) }
237
+
238
+ context 'with no previous local or remote tag' do
239
+ it 'raises an error' do
240
+ expect { subject }.to raise_error(Kuby::Docker::MissingTagError)
241
+ end
242
+ end
243
+
244
+ context 'with an available previous remote tag' do
245
+ let(:previous_tag) { make_ts_tag(time - 5) }
246
+
247
+ before { docker_remote_client.tags << previous_tag }
248
+
249
+ it { is_expected.to eq(previous_tag) }
250
+ end
251
+
252
+ context 'with an available previous local tag' do
253
+ let(:previous_tag) { make_ts_tag(time - 5) }
254
+
255
+ before do
256
+ docker_cli.build(
257
+ dockerfile: nil,
258
+ image_url: docker_image_url,
259
+ tags: [previous_tag]
260
+ )
261
+ end
262
+
263
+ it { is_expected.to eq(previous_tag) }
264
+ end
265
+ end
266
+ end