Skip to content

Commit

Permalink
Generate a .devcontainer folder and its contents when creating a new …
Browse files Browse the repository at this point in the history
…app.

The devcontainer folder includes everything needed to boot the app and do development in a remote container.

The container setup includes:
 - A redis container for Sidekiq and Action Cable
 - A database (SQLite, Postgres, MySQL or MariaDB)
 - A Headless chrome container for system tests
 - Active Storage configured to use the local disk and with preview features working

If any of these options are skipped in the app setup they will not be included in the container configuration.

Co-authored-by: Rafael Mendonça França <rafael@franca.dev>

wip
  • Loading branch information
andrewn617 committed Jan 30, 2024
1 parent 74e52ee commit 7dda73c
Show file tree
Hide file tree
Showing 14 changed files with 372 additions and 3 deletions.
2 changes: 1 addition & 1 deletion actionpack/lib/action_dispatch/system_test_case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {
self.driver = SystemTesting::Driver.new(driver, **driver_options, &capabilities)
end

# Configuration for the System Test application server
# Configuration for the System Test application server.
#
# By default this is localhost. This method allows the host and port to be specified manually.
def self.served_by(host:, port:)
Expand Down
20 changes: 20 additions & 0 deletions railties/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
* Generate a .devcontainer folder and its contents when creating a new app.

The .devcontainer folder includes everything needed to boot the app and do development in a remote container.

The container setup includes:
- A redis container for Sidekiq and Action Cable
- A database (SQLite, Postgres, MySQL or MariaDB)
- A Headless chrome container for system tests
- Active Storage configured to use the local disk and with preview features working

If any of these options are skipped in the app setup they will not be included in the container configuration.

These files can be skipped using the `--skip-devcontainer` option.

Additionally, a `--skip-devcontainer` option has been added to the `db:system:change` command. It is set to `true`
by default so devcontainer config will not be added to the `database.yml` for existing apps using the command. But
the user can opt in to the devcontainer config by setting the flag to false: `db:system:change --to=postgresql --skip-devcontainer=false`

*Andrew Novoselac & Rafael Mendonça França*

* Introduce `SystemTestCase#servered_by` for configuring the System Test application server

By default this is localhost. This method allows the host and port to be specified manually.
Expand Down
7 changes: 7 additions & 0 deletions railties/lib/rails/generators/app_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ def self.add_shared_options_for(name)
class_option :skip_ci, type: :boolean, default: nil,
desc: "Skip GitHub CI files"

class_option :skip_devcontainer, type: :boolean, default: nil,
desc: "Skip devcontainer files"

class_option :dev, type: :boolean, default: nil,
desc: "Set up the #{name} with Gemfile pointing to your Rails checkout"

Expand Down Expand Up @@ -400,6 +403,10 @@ def skip_ci?
options[:skip_ci]
end

def skip_devcontainer?
options[:skip_devcontainer]
end

class GemfileEntry < Struct.new(:name, :version, :comment, :options, :commented_out)
def initialize(name, version, comment, options = {}, commented_out = false)
super
Expand Down
8 changes: 8 additions & 0 deletions railties/lib/rails/generators/database.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ def mysql_socket
"/opt/lampp/var/mysql/mysql.sock" # xampp for linux
].find { |f| File.exist?(f) } unless Gem.win_platform?
end

def mysql_database_host
if options[:skip_devcontainer]
"localhost"
else
"<%= ENV.fetch(\"RAILS_DATABASE_HOST\") { \"localhost\" } %>"
end
end
end
end
end
43 changes: 43 additions & 0 deletions railties/lib/rails/generators/rails/app/app_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ def vendor
def config_target_version
@config_target_version || Rails::VERSION::STRING.to_f
end

def devcontainer
empty_directory ".devcontainer"

template ".devcontainer/devcontainer.json"
template ".devcontainer/Dockerfile"
template ".devcontainer/docker-compose.yml"
end
end

module Generators
Expand Down Expand Up @@ -455,6 +463,11 @@ def create_storage_files
build(:storage)
end

def create_devcontainer_files
return if skip_devcontainer? || options[:dummy_app]
build(:devcontainer)
end

def delete_app_assets_if_api_option
if options[:api]
remove_dir "app/assets"
Expand Down Expand Up @@ -592,6 +605,36 @@ def after_bundle(&block) # :doc:
def get_builder_class
defined?(::AppBuilder) ? ::AppBuilder : Rails::AppBuilder
end

def devcontainer_dependencies
return @devcontainer_dependencies if @devcontainer_dependencies

@devcontainer_dependencies = []

@devcontainer_dependencies << "selenium" if depends_on_system_test?
@devcontainer_dependencies << "redis" unless options.skip_action_cable? && options.skip_active_job?
@devcontainer_dependencies << "postgres" if options.database == "postgresql"
@devcontainer_dependencies << "mysql" if options.database == "mysql"
@devcontainer_dependencies << "mariadb" if options.database == "trilogy"

@devcontainer_dependencies
end

def devcontainer_variables
return @devcontainer_variables if @devcontainer_variables

@devcontainer_variables = {}

@devcontainer_variables["CAPYBARA_SERVER_PORT"] = "45678" if depends_on_system_test?
@devcontainer_variables["SELENIUM_HOST"] = "selenium" if depends_on_system_test?
@devcontainer_variables["JOBS_REDIS_URL"] = "redis://redis:6379/1" unless options.skip_active_job?
@devcontainer_variables["CABLE_REDIS_URL"] = "redis://redis:6379/1" unless options.skip_action_cable?
@devcontainer_variables["RAILS_DATABASE_HOST"] = "postgres" if options.database == "postgresql"
@devcontainer_variables["RAILS_DATABASE_HOST"] = "mysql" if options.database == "mysql"
@devcontainer_variables["RAILS_DATABASE_HOST"] = "mariadb" if options.database == "trilogy"

@devcontainer_variables
end
end

# This class handles preparation of the arguments before the AppGenerator is
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM mcr.microsoft.com/devcontainers/ruby:1-3-bookworm

<%- unless options.skip_active_storage -%>
# Install packages needed to build gems
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y \
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
@@ -0,0 +1,34 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/ruby
{
"name": "<%= app_name %>",
"dockerComposeFile": "docker-compose.yml",
"service": "rails-app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",

// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
},

<%- if !devcontainer_variables.empty? -%>
"containerEnv": {
<%= devcontainer_variables.map { |key, value| "\"#{key}\": \"#{value}\"" }.join(",\n ") %>
},
<%- 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": [],

// Configure tool-specific properties.
// "customizations": {},

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root",

// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "bin/setup"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
services:
rails-app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile

volumes:
- ../..:/workspaces:cached

# Overrides default command so things don't shut down after the process ends.
command: sleep infinity

networks:
- default

# Uncomment the next line to use a non-root user for all processes.
# user: vscode

# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
ports:
- 45678:45678
<%- if !devcontainer_dependencies.empty? -%>
depends_on:
<%- devcontainer_dependencies.each do |dependency| -%>
- <%= dependency %>
<%- end -%>
<%- end -%>

<%- if depends_on_system_test? -%>
selenium:
image: seleniarm/standalone-chromium
restart: unless-stopped
networks:
- default
<%- end -%>

<%- unless options.skip_active_job? && options.skip_action_cable? -%>
redis:
image: redis:7.2
restart: unless-stopped
networks:
- default
ports:
- 6379:6379
<%- end -%>

<%- if options.database == "postgresql" -%>
postgres:
image: postgres:16.1
restart: unless-stopped
networks:
- default
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
<%- end -%>

<%- if options.database == "mysql" -%>
mysql:
image: mysql/mysql-server:8.0
restart: unless-stopped
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_ROOT_HOST: "%"
networks:
- default
<%- end -%>

<%- if options.database == "trilogy" -%>
mariadb:
image: mariadb:10.5
restart: unless-stopped
networks:
- default
environment:
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true
<%- end -%>
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ default: &default
<% if mysql_socket -%>
socket: <%= mysql_socket %>
<% else -%>
host: localhost
host: <%= mysql_database_host %>
<% end -%>

development:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ default: &default
# For details on connection pooling, see Rails configuration guide
# https://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
<% unless options[:skip_devcontainer] -%>
<%% if ENV["RAILS_DATABASE_HOST"] %>
host: <%%= ENV["RAILS_DATABASE_HOST"] %>
username: postgres
password: postgres
<%% end %>
<% end %>

development:
<<: *default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ default: &default
<% if mysql_socket -%>
socket: <%= mysql_socket %>
<% else -%>
host: localhost
host: <%= mysql_database_host %>
<% end -%>

development:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
<% if skip_devcontainer? -%>
driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ]
<% else -%>
if ENV["CAPYBARA_SERVER_PORT"]
served_by host: "rails-app", port: ENV["CAPYBARA_SERVER_PORT"]

driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ], options: {
browser: :remote,
url: "http://#{ENV["SELENIUM_HOST"]}:4444",
}
else
driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ]
end
<% end -%>
end
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class ChangeGenerator < Base # :nodoc:
class_option :to, required: true,
desc: "The database system to switch to."

class_option :skip_devcontainer, type: :boolean, default: true,
desc: "Skip devcontainer config in database.yml"

def self.default_generator_root
path = File.expand_path(File.join(base_name, "app"), base_root)
path if File.exist?(path)
Expand Down
Loading

0 comments on commit 7dda73c

Please sign in to comment.