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 12, 2024
1 parent d3b055e commit 16afdbe
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 64 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_mounts
return @devcontainer_mounts if @devcontainer_mounts

Expand Down Expand Up @@ -90,6 +103,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 @@ -138,6 +158,21 @@ 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

def local_rails_mount
{
type: "bind",
Expand Down
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 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
48 changes: 21 additions & 27 deletions railties/test/generators/app_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1241,16 +1241,16 @@ def test_name_option
def test_devcontainer
run_generator [destination_root, "--name=my-app"]

assert_file(".devcontainer/devcontainer.json") do |content|
assert_match(/"name": "my_app"/, content)
assert_match(/"REDIS_URL": "redis:\/\/redis:6379\/1"/, content)
assert_match(/"CAPYBARA_SERVER_PORT": "45678"/, content)
assert_match(/"SELENIUM_HOST": "selenium"/, content)
assert_devcontainer_json_file do |content|
assert_equal "my_app", content["name"]
assert_equal "redis://redis:6379/1", content["containerEnv"]["REDIS_URL"]
assert_equal "45678", content["containerEnv"]["CAPYBARA_SERVER_PORT"]
assert_equal "selenium", content["containerEnv"]["SELENIUM_HOST"]
assert_equal({}, content["features"]["ghcr.io/rails/devcontainer/features/activestorage"])
assert_equal({}, content["features"]["ghcr.io/devcontainers/features/github-cli:1"])
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 @@ -1317,15 +1317,13 @@ def test_devonctainer_postgresql
assert_equal expected_postgres_config, compose_config["services"]["postgres"]
assert_includes compose_config["volumes"].keys, "postgres-data"
end
assert_file(".devcontainer/devcontainer.json") do |content|
assert_match(/"DB_HOST": "postgres"/, content)
assert_devcontainer_json_file do |content|
assert_equal "postgres", content["containerEnv"]["DB_HOST"]
assert_equal({}, content["features"]["ghcr.io/rails/devcontainer/features/postgres-client"])
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 @@ -1348,15 +1346,13 @@ def test_devonctainer_mysql
assert_equal expected_mysql_config, compose_config["services"]["mysql"]
assert_includes compose_config["volumes"].keys, "mysql-data"
end
assert_file(".devcontainer/devcontainer.json") do |content|
assert_match(/"DB_HOST": "mysql"/, content)
assert_devcontainer_json_file do |content|
assert_equal "mysql", content["containerEnv"]["DB_HOST"]
assert_equal({}, content["features"]["ghcr.io/rails/devcontainer/features/mysql-client"])
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 All @@ -1377,8 +1373,8 @@ def test_devonctainer_mariadb
assert_equal expected_mariadb_config, compose_config["services"]["mariadb"]
assert_includes compose_config["volumes"].keys, "mariadb-data"
end
assert_file(".devcontainer/devcontainer.json") do |content|
assert_match(/"DB_HOST": "mariadb"/, content)
assert_devcontainer_json_file do |content|
assert_equal "mariadb", content["containerEnv"]["DB_HOST"]
end
assert_file("config/database.yml") do |content|
assert_match(/host: <%= ENV.fetch\("DB_HOST"\) \{ "localhost" } %>/, content)
Expand All @@ -1392,18 +1388,16 @@ def test_devcontainer_no_selenium_when_skipping_system_test
assert_not_includes compose_config["services"]["rails-app"]["depends_on"], "selenium"
assert_not_includes compose_config["services"].keys, "selenium"
end
assert_file(".devcontainer/devcontainer.json") do |content|
assert_no_match(/CAPYBARA_SERVER_PORT/, content)
assert_devcontainer_json_file do |content|
assert_nil content["containerEnv"]["CAPYBARA_SERVER_PORT"]
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_devcontainer_json_file do |content|
assert_nil content["features"]["ghcr.io/rails/devcontainer/features/activestorage"]
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 16afdbe

Please sign in to comment.