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.

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

Co-authored-by: Rafael Mendonça França <rafael@franca.dev>
  • Loading branch information
andrewn617 and rafaelfranca committed Feb 5, 2024
1 parent 6db7e83 commit 03e3b10
Show file tree
Hide file tree
Showing 13 changed files with 348 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
16 changes: 16 additions & 0 deletions railties/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
* 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 Kredis, ActionCable etc.
- 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 `--no-devcontainer` option.

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

* Introduce `SystemTestCase#served_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 :devcontainer, type: :boolean, default: true,
desc: "Generate 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[: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[:devcontainer]
"<%= ENV.fetch(\"DB_HOST\") { \"localhost\" } %>"
else
"localhost"
end
end
end
end
end
42 changes: 42 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,35 @@ 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["REDIS_URL"] = "redis://redis:6379/1" unless options.skip_active_job? && options.skip_action_cable?
@devcontainer_variables["DB_HOST"] = "postgres" if options.database == "postgresql"
@devcontainer_variables["DB_HOST"] = "mysql" if options.database == "mysql"
@devcontainer_variables["DB_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 } %>
<% if options[:devcontainer] -%>
<%% if ENV["DB_HOST"] %>
host: <%%= ENV["DB_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
Loading

0 comments on commit 03e3b10

Please sign in to comment.