Skip to content

Commit 16afdbe

Browse files
committed
Use devcontainer features for optional dependencies
We have created our own features for optional Rails dependencies needed for active storage, postgres and mysql. Features provide a bit better ergonomics for adding or removing these from the devcontainer, and previously we were always installing these dependencies via the devcontainer's dockerfile, whether the app was using them or not. With this change, when we generate the app, we just add the features we need to the devcontainer.json. And also, we swap features in and out as need when doing db:system:change.
1 parent d3b055e commit 16afdbe

File tree

7 files changed

+109
-64
lines changed

7 files changed

+109
-64
lines changed

railties/lib/rails/generators/devcontainer.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@ def devcontainer_volumes
3939
@devcontainer_volumes
4040
end
4141

42+
def devcontainer_features
43+
return @devcontainer_features if @devcontainer_features
44+
45+
@devcontainer_features = {
46+
"ghcr.io/devcontainers/features/github-cli:1" => {}
47+
}
48+
49+
@devcontainer_features["ghcr.io/rails/devcontainer/features/activestorage"] = {} unless options[:skip_active_storage]
50+
@devcontainer_features.merge!(db_feature_for_devcontainer) if db_feature_for_devcontainer
51+
52+
@devcontainer_features
53+
end
54+
4255
def devcontainer_mounts
4356
return @devcontainer_mounts if @devcontainer_mounts
4457

@@ -90,6 +103,13 @@ def db_service_for_devcontainer(database = options[:database])
90103
end
91104
end
92105

106+
def db_feature_for_devcontainer(database = options[:database])
107+
case database
108+
when "mysql" then mysql_feature
109+
when "postgresql" then postgres_feature
110+
end
111+
end
112+
93113
def postgres_service
94114
{
95115
"postgres" => {
@@ -138,6 +158,21 @@ def db_service_names
138158
["mysql", "mariadb", "postgres"]
139159
end
140160

161+
def mysql_feature
162+
{ "ghcr.io/rails/devcontainer/features/mysql-client" => {} }
163+
end
164+
165+
def postgres_feature
166+
{ "ghcr.io/rails/devcontainer/features/postgres-client" => {} }
167+
end
168+
169+
def db_features
170+
[
171+
"ghcr.io/rails/devcontainer/features/mysql-client",
172+
"ghcr.io/rails/devcontainer/features/postgres-client"
173+
]
174+
end
175+
141176
def local_rails_mount
142177
{
143178
type: "bind",
Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,3 @@
11
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
22
ARG RUBY_VERSION=<%= gem_ruby_version %>
33
FROM ghcr.io/rails/devcontainer/images/ruby:$RUBY_VERSION
4-
5-
<%- unless options.skip_active_storage -%>
6-
# Install packages needed to build gems
7-
RUN apt-get update -qq && \
8-
apt-get install --no-install-recommends -y \
9-
<%= db_package_for_dockerfile %> libvips \
10-
# For video thumbnails
11-
ffmpeg \
12-
# For pdf thumbnails. If you want to use mupdf instead of poppler,
13-
# you can install the following packages instead:
14-
# mupdf mupdf-tools
15-
poppler-utils
16-
<%- end -%>

railties/lib/rails/generators/rails/app/templates/.devcontainer/devcontainer.json.tt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
// Features to add to the dev container. More info: https://containers.dev/features.
1010
"features": {
11-
"ghcr.io/devcontainers/features/github-cli:1": {}
11+
<%= devcontainer_features.map { |key, value| "\"#{key}\": #{value}" }.join(",\n ") %>
1212
},
1313

1414
<%- if !devcontainer_variables.empty? -%>

railties/lib/rails/generators/rails/db/system/change/change_generator.rb

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -101,27 +101,10 @@ def gem_entry_for(*gem_name_and_version)
101101
end
102102

103103
def edit_devcontainer_json
104-
devcontainer_json_path = File.expand_path(".devcontainer/devcontainer.json", destination_root)
105-
return unless File.exist?(devcontainer_json_path)
106-
107-
container_env = JSON.parse(File.read(devcontainer_json_path))["containerEnv"]
108-
db_name = db_name_for_devcontainer
109-
110-
if container_env["DB_HOST"]
111-
if db_name
112-
container_env["DB_HOST"] = db_name
113-
else
114-
container_env.delete("DB_HOST")
115-
end
116-
else
117-
if db_name
118-
container_env["DB_HOST"] = db_name
119-
end
120-
end
104+
return unless devcontainer_json
121105

122-
new_json = JSON.pretty_generate(container_env, indent: " ", object_nl: "\n ")
123-
124-
gsub_file(".devcontainer/devcontainer.json", /("containerEnv"\s*:\s*){[^}]*}/, "\\1#{new_json}")
106+
update_devcontainer_db_host
107+
update_devcontainer_db_feature
125108
end
126109

127110
def edit_compose_yaml
@@ -152,6 +135,52 @@ def edit_compose_yaml
152135

153136
File.write(compose_yaml_path, compose_config.to_yaml)
154137
end
138+
139+
def update_devcontainer_db_host
140+
container_env = devcontainer_json["containerEnv"]
141+
db_name = db_name_for_devcontainer
142+
143+
if container_env["DB_HOST"]
144+
if db_name
145+
container_env["DB_HOST"] = db_name
146+
else
147+
container_env.delete("DB_HOST")
148+
end
149+
else
150+
if db_name
151+
container_env["DB_HOST"] = db_name
152+
end
153+
end
154+
155+
new_json = JSON.pretty_generate(container_env, indent: " ", object_nl: "\n ")
156+
157+
gsub_file(".devcontainer/devcontainer.json", /("containerEnv"\s*:\s*)(.|\n)*?(^\s{2}})/, "\\1#{new_json}")
158+
end
159+
160+
def update_devcontainer_db_feature
161+
features = devcontainer_json["features"]
162+
db_feature = db_feature_for_devcontainer
163+
164+
db_features.each do |feature|
165+
features.delete(feature)
166+
end
167+
168+
features.merge!(db_feature) if db_feature
169+
170+
new_json = JSON.pretty_generate(features, indent: " ", object_nl: "\n ")
171+
172+
gsub_file(".devcontainer/devcontainer.json", /("features"\s*:\s*)(.|\n)*?(^\s{2}})/, "\\1#{new_json}")
173+
end
174+
175+
def devcontainer_json
176+
return unless File.exist?(devcontainer_json_path)
177+
178+
@devcontainer_json ||= JSON.parse(File.read(devcontainer_json_path))
179+
end
180+
181+
def devcontainer_json_path
182+
File.expand_path(".devcontainer/devcontainer.json", destination_root)
183+
end
155184
end
156185
end
157186
end

railties/test/fixtures/.devcontainer/devcontainer.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717
"REDIS_URL": "redis://redis:6379/1"
1818
},
1919

20-
// Features to add to the dev container. More info: https://containers.dev/features.
21-
// "features": {},
22-
2320
// Use 'forwardPorts' to make a list of ports inside the container available locally.
2421
// "forwardPorts": [],
2522

railties/test/generators/app_generator_test.rb

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,16 +1241,16 @@ def test_name_option
12411241
def test_devcontainer
12421242
run_generator [destination_root, "--name=my-app"]
12431243

1244-
assert_file(".devcontainer/devcontainer.json") do |content|
1245-
assert_match(/"name": "my_app"/, content)
1246-
assert_match(/"REDIS_URL": "redis:\/\/redis:6379\/1"/, content)
1247-
assert_match(/"CAPYBARA_SERVER_PORT": "45678"/, content)
1248-
assert_match(/"SELENIUM_HOST": "selenium"/, content)
1244+
assert_devcontainer_json_file do |content|
1245+
assert_equal "my_app", content["name"]
1246+
assert_equal "redis://redis:6379/1", content["containerEnv"]["REDIS_URL"]
1247+
assert_equal "45678", content["containerEnv"]["CAPYBARA_SERVER_PORT"]
1248+
assert_equal "selenium", content["containerEnv"]["SELENIUM_HOST"]
1249+
assert_equal({}, content["features"]["ghcr.io/rails/devcontainer/features/activestorage"])
1250+
assert_equal({}, content["features"]["ghcr.io/devcontainers/features/github-cli:1"])
12491251
end
12501252
assert_file(".devcontainer/Dockerfile") do |content|
1251-
assert_match(/libvips/, content)
1252-
assert_match(/ffmpeg/, content)
1253-
assert_match(/poppler-utils/, content)
1253+
assert_match(/ARG RUBY_VERSION=#{RUBY_VERSION}/, content)
12541254
end
12551255
assert_compose_file do |compose_config|
12561256
expected_rails_app_config = {
@@ -1317,15 +1317,13 @@ def test_devonctainer_postgresql
13171317
assert_equal expected_postgres_config, compose_config["services"]["postgres"]
13181318
assert_includes compose_config["volumes"].keys, "postgres-data"
13191319
end
1320-
assert_file(".devcontainer/devcontainer.json") do |content|
1321-
assert_match(/"DB_HOST": "postgres"/, content)
1320+
assert_devcontainer_json_file do |content|
1321+
assert_equal "postgres", content["containerEnv"]["DB_HOST"]
1322+
assert_equal({}, content["features"]["ghcr.io/rails/devcontainer/features/postgres-client"])
13221323
end
13231324
assert_file("config/database.yml") do |content|
13241325
assert_match(/host: <%= ENV\["DB_HOST"\] %>/, content)
13251326
end
1326-
assert_file(".devcontainer/Dockerfile") do |content|
1327-
assert_match(/libpq-dev/, content)
1328-
end
13291327
end
13301328

13311329
def test_devonctainer_mysql
@@ -1348,15 +1346,13 @@ def test_devonctainer_mysql
13481346
assert_equal expected_mysql_config, compose_config["services"]["mysql"]
13491347
assert_includes compose_config["volumes"].keys, "mysql-data"
13501348
end
1351-
assert_file(".devcontainer/devcontainer.json") do |content|
1352-
assert_match(/"DB_HOST": "mysql"/, content)
1349+
assert_devcontainer_json_file do |content|
1350+
assert_equal "mysql", content["containerEnv"]["DB_HOST"]
1351+
assert_equal({}, content["features"]["ghcr.io/rails/devcontainer/features/mysql-client"])
13531352
end
13541353
assert_file("config/database.yml") do |content|
13551354
assert_match(/host: <%= ENV.fetch\("DB_HOST"\) \{ "localhost" } %>/, content)
13561355
end
1357-
assert_file(".devcontainer/Dockerfile") do |content|
1358-
assert_match(/default-libmysqlclient-dev/, content)
1359-
end
13601356
end
13611357

13621358
def test_devonctainer_mariadb
@@ -1377,8 +1373,8 @@ def test_devonctainer_mariadb
13771373
assert_equal expected_mariadb_config, compose_config["services"]["mariadb"]
13781374
assert_includes compose_config["volumes"].keys, "mariadb-data"
13791375
end
1380-
assert_file(".devcontainer/devcontainer.json") do |content|
1381-
assert_match(/"DB_HOST": "mariadb"/, content)
1376+
assert_devcontainer_json_file do |content|
1377+
assert_equal "mariadb", content["containerEnv"]["DB_HOST"]
13821378
end
13831379
assert_file("config/database.yml") do |content|
13841380
assert_match(/host: <%= ENV.fetch\("DB_HOST"\) \{ "localhost" } %>/, content)
@@ -1392,18 +1388,16 @@ def test_devcontainer_no_selenium_when_skipping_system_test
13921388
assert_not_includes compose_config["services"]["rails-app"]["depends_on"], "selenium"
13931389
assert_not_includes compose_config["services"].keys, "selenium"
13941390
end
1395-
assert_file(".devcontainer/devcontainer.json") do |content|
1396-
assert_no_match(/CAPYBARA_SERVER_PORT/, content)
1391+
assert_devcontainer_json_file do |content|
1392+
assert_nil content["containerEnv"]["CAPYBARA_SERVER_PORT"]
13971393
end
13981394
end
13991395

1400-
def test_devcontainer_no_Dockerfile_packages_when_skipping_active_storage
1396+
def test_devcontainer_no_feature_when_skipping_active_storage
14011397
run_generator [ destination_root, "--skip-active-storage" ]
14021398

1403-
assert_file(".devcontainer/Dockerfile") do |content|
1404-
assert_no_match(/libvips/, content)
1405-
assert_no_match(/ffmpeg/, content)
1406-
assert_no_match(/poppler-utils/, content)
1399+
assert_devcontainer_json_file do |content|
1400+
assert_nil content["features"]["ghcr.io/rails/devcontainer/features/activestorage"]
14071401
end
14081402
end
14091403

railties/test/generators/db_system_change_generator_test.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class ChangeGeneratorTest < Rails::Generators::TestCase
5454

5555
assert_file(".devcontainer/devcontainer.json") do |content|
5656
assert_match(/"DB_HOST": "postgres"/, content)
57+
assert_match(/"ghcr.io\/rails\/devcontainer\/features\/postgres-client":/, content)
5758
end
5859

5960
assert_compose_file do |compose_config|
@@ -95,6 +96,7 @@ class ChangeGeneratorTest < Rails::Generators::TestCase
9596

9697
assert_file(".devcontainer/devcontainer.json") do |content|
9798
assert_match(/"DB_HOST": "mysql"/, content)
99+
assert_match(/"ghcr.io\/rails\/devcontainer\/features\/mysql-client":/, content)
98100
end
99101

100102
assert_compose_file do |compose_config|
@@ -203,6 +205,7 @@ class ChangeGeneratorTest < Rails::Generators::TestCase
203205

204206
assert_file(".devcontainer/devcontainer.json") do |content|
205207
assert_no_match(/"DB_HOST"/, content)
208+
assert_no_match(/"ghcr.io\/rails\/devcontainer\/features\/mysql-client":/, content)
206209
end
207210

208211
assert_compose_file do |compose_config|

0 commit comments

Comments
 (0)