Skip to content

Commit

Permalink
Use devcontainer features for optional dependencies
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
andrewn617 committed Apr 3, 2024
1 parent 02f6c29 commit aa29316
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 54 deletions.
35 changes: 35 additions & 0 deletions railties/lib/rails/generators/devcontainer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ def devcontainer_volumes
@devcontainer_volumes
end

def devcontainer_features
return @devcontainer_features if @devcontainer_features

@devcontainer_features = {
"ghcr.io/devcontainers/features/github-cli:1" => {}
}

@devcontainer_features["ghcr.io/rails/devcontainer/features/activestorage"] = {} unless options[:skip_active_storage]
@devcontainer_features.merge!(db_feature_for_devcontainer) if db_feature_for_devcontainer

@devcontainer_features
end

def devcontainer_needs_redis?
!(options.skip_action_cable? && options.skip_active_job?)
end
Expand Down Expand Up @@ -80,6 +93,13 @@ def db_service_for_devcontainer(database = options[:database])
end
end

def db_feature_for_devcontainer(database = options[:database])
case database
when "mysql" then mysql_feature
when "postgresql" then postgres_feature
end
end

def postgres_service
{
"postgres" => {
Expand Down Expand Up @@ -127,6 +147,21 @@ def mariadb_service
def db_service_names
["mysql", "mariadb", "postgres"]
end

def mysql_feature
{ "ghcr.io/rails/devcontainer/features/mysql-client" => {} }
end

def postgres_feature
{ "ghcr.io/rails/devcontainer/features/postgres-client" => {} }
end

def db_features
[
"ghcr.io/rails/devcontainer/features/mysql-client",
"ghcr.io/rails/devcontainer/features/postgres-client"
]
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=<%= gem_ruby_version %>
FROM ghcr.io/rails/devcontainer/images/ruby:$RUBY_VERSION

<%- unless options.skip_active_storage -%>
# Install packages needed to build gems
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y \
<%= db_package_for_dockerfile %> libvips \
# For video thumbnails
ffmpeg \
# For pdf thumbnails. If you want to use mupdf instead of poppler,
# you can install the following packages instead:
# mupdf mupdf-tools
poppler-utils
<%- end -%>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

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

<%- if !devcontainer_variables.empty? -%>
Expand All @@ -17,9 +17,6 @@
},
<%- end -%>

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,27 +101,10 @@ def gem_entry_for(*gem_name_and_version)
end

def edit_devcontainer_json
devcontainer_json_path = File.expand_path(".devcontainer/devcontainer.json", destination_root)
return unless File.exist?(devcontainer_json_path)

container_env = JSON.parse(File.read(devcontainer_json_path))["containerEnv"]
db_name = db_name_for_devcontainer

if container_env["DB_HOST"]
if db_name
container_env["DB_HOST"] = db_name
else
container_env.delete("DB_HOST")
end
else
if db_name
container_env["DB_HOST"] = db_name
end
end
return unless devcontainer_json

new_json = JSON.pretty_generate(container_env, indent: " ", object_nl: "\n ")

gsub_file(".devcontainer/devcontainer.json", /("containerEnv"\s*:\s*){[^}]*}/, "\\1#{new_json}")
update_devcontainer_db_host
update_devcontainer_db_feature
end

def edit_compose_yaml
Expand Down Expand Up @@ -152,6 +135,52 @@ def edit_compose_yaml

File.write(compose_yaml_path, compose_config.to_yaml)
end

def update_devcontainer_db_host
container_env = devcontainer_json["containerEnv"]
db_name = db_name_for_devcontainer

if container_env["DB_HOST"]
if db_name
container_env["DB_HOST"] = db_name
else
container_env.delete("DB_HOST")
end
else
if db_name
container_env["DB_HOST"] = db_name
end
end

new_json = JSON.pretty_generate(container_env, indent: " ", object_nl: "\n ")

gsub_file(".devcontainer/devcontainer.json", /("containerEnv"\s*:\s*)(.|\n)*?(^\s{2}})/, "\\1#{new_json}")
end

def update_devcontainer_db_feature
features = devcontainer_json["features"]
db_feature = db_feature_for_devcontainer

db_features.each do |feature|
features.delete(feature)
end

features.merge!(db_feature) if db_feature

new_json = JSON.pretty_generate(features, indent: " ", object_nl: "\n ")

gsub_file(".devcontainer/devcontainer.json", /("features"\s*:\s*)(.|\n)*?(^\s{2}})/, "\\1#{new_json}")
end

def devcontainer_json
return unless File.exist?(devcontainer_json_path)

@devcontainer_json ||= JSON.parse(File.read(devcontainer_json_path))
end

def devcontainer_json_path
File.expand_path(".devcontainer/devcontainer.json", destination_root)
end
end
end
end
Expand Down
3 changes: 0 additions & 3 deletions railties/test/fixtures/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
"REDIS_URL": "redis://redis:6379/1"
},

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

Expand Down
22 changes: 8 additions & 14 deletions railties/test/generators/app_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1246,11 +1246,11 @@ def test_devcontainer
assert_match(/"REDIS_URL": "redis:\/\/redis:6379\/1"/, content)
assert_match(/"CAPYBARA_SERVER_PORT": "45678"/, content)
assert_match(/"SELENIUM_HOST": "selenium"/, content)
assert_match(/"ghcr.io\/rails\/devcontainer\/features\/activestorage": {}/, content)
assert_match(/"ghcr.io\/devcontainers\/features\/github-cli:1": {}/, content)
end
assert_file(".devcontainer/Dockerfile") do |content|
assert_match(/libvips/, content)
assert_match(/ffmpeg/, content)
assert_match(/poppler-utils/, content)
assert_match(/ARG RUBY_VERSION=#{RUBY_VERSION}/, content)
end
assert_compose_file do |compose_config|
expected_rails_app_config = {
Expand Down Expand Up @@ -1319,13 +1319,11 @@ def test_devonctainer_postgresql
end
assert_file(".devcontainer/devcontainer.json") do |content|
assert_match(/"DB_HOST": "postgres"/, content)
assert_match(/"ghcr.io\/rails\/devcontainer\/features\/postgres-client": {}/, content)
end
assert_file("config/database.yml") do |content|
assert_match(/host: <%= ENV\["DB_HOST"\] %>/, content)
end
assert_file(".devcontainer/Dockerfile") do |content|
assert_match(/libpq-dev/, content)
end
end

def test_devonctainer_mysql
Expand All @@ -1350,13 +1348,11 @@ def test_devonctainer_mysql
end
assert_file(".devcontainer/devcontainer.json") do |content|
assert_match(/"DB_HOST": "mysql"/, content)
assert_match(/"ghcr.io\/rails\/devcontainer\/features\/mysql-client": {}/, content)
end
assert_file("config/database.yml") do |content|
assert_match(/host: <%= ENV.fetch\("DB_HOST"\) \{ "localhost" } %>/, content)
end
assert_file(".devcontainer/Dockerfile") do |content|
assert_match(/default-libmysqlclient-dev/, content)
end
end

def test_devonctainer_mariadb
Expand Down Expand Up @@ -1397,13 +1393,11 @@ def test_devcontainer_no_selenium_when_skipping_system_test
end
end

def test_devcontainer_no_Dockerfile_packages_when_skipping_active_storage
def test_devcontainer_no_feature_when_skipping_active_storage
run_generator [ destination_root, "--skip-active-storage" ]

assert_file(".devcontainer/Dockerfile") do |content|
assert_no_match(/libvips/, content)
assert_no_match(/ffmpeg/, content)
assert_no_match(/poppler-utils/, content)
assert_file(".devcontainer/devcontainer.json") do |content|
assert_no_match(/"ghcr.io\/rails\/devcontainer\/features\/activestorage": {}/, content)
end
end

Expand Down
3 changes: 3 additions & 0 deletions railties/test/generators/db_system_change_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class ChangeGeneratorTest < Rails::Generators::TestCase

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

assert_compose_file do |compose_config|
Expand Down Expand Up @@ -95,6 +96,7 @@ class ChangeGeneratorTest < Rails::Generators::TestCase

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

assert_compose_file do |compose_config|
Expand Down Expand Up @@ -203,6 +205,7 @@ class ChangeGeneratorTest < Rails::Generators::TestCase

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

assert_compose_file do |compose_config|
Expand Down

0 comments on commit aa29316

Please sign in to comment.