-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Spike] Refactor Jupiter Routes/Controllers/Layout to Be Able To Serve Multiple Sites #1707
Comments
What do we want?We want to allow Jupiter to present different homepages (front-doors), differently themed pages, and have the ability to search from different data. What are possible solutions to this problem?
With different applications hopefully we can agree this is probably not ideal for applications that are very similar in nature. Discovery and NEOSDiscovery is probably the best example of the pain around this. Both these applications probably share 90% or more of their codebase, with essentially just some different theming and minor content changes. As a result, we end up copying and pasting a lot of the code around and it's a challenge to keep both applications up to date with the latest changes. Configuration could work. Configuration works really well for theming. But when we start potentially having different pages with different content (e.g: item/theses vs newspapers/maps) it's a bit harder to make this work through a shared configuration. It could work though. However, probably the biggest pain I think would be on the system admin side. Having us host and maintain multiple applications could be quite a challenge. As an example, If we needed to get a security update out, and wanted to get that out simultaneously to all our applications, this could be quite the undertaking with our current setup. As a result, I think subdomains could be the best solution. We can change the theme and different content via the ingress of a user by subdomain. We can still use the same deployment and codebase, so no major change from that point of view. So this seems like the best option of the three for our current requirements. SubdomainsFiltering by Host vs SubdomainThere's a couple of ways to handle subdomains in Rails. First is by filtering by host ( constraints(subdomain: 'admin') do
resources :users, only: [:index, :show] do
member do
patch :suspend
patch :unsuspend
patch :grant_admin
patch :revoke_admin
post :login_as_user
end
end
end This will allow these routes to only work on traffic to DevelopmentGetting Subdomains to work in developmentDevelopment with subdomains is pretty easy. The big change is changing what url you use to load your application. Instead of going There are a few ways to use real domains in development which I’ll talk about here. The first is a nifty little domain called
So if we wanted to test the above admin subdomain, instead of navigating to Whitelisting hostsSince Rails 6, you will also need to whitelist hosts for development. Rails 6 includes a new middleware named Host Authorization to help prevent DNS rebinding attacks. By default this feature allows requests from 0.0.0.0, ::, and localhost. There are basically two ways to work around this. Rails.application.configure do
# Whitelist one hostname
config.hosts << "hostname"
# Whitelist a test domain. Rails adds \A and \z around
# your regular expressions.
config.hosts << /application\.local/
end Or you can just allow all requests through: Rails.application.configure do
config.hosts.clear
end If using # whitelist our subdomains as valid hosts
config.hosts << /.*\.lvh\.me/ TestFor integration/controller tests, any subdomain routes will result in 404 errors if the test request does not have a proper subdomain. To get around this, Rails provides a # Configuring subdomain in Rails integration tests
setup do
host! 'admin.example.com'
end For System tests, we should be able to do something similar. In system tests we can use the setup do
host! 'http://admin.example.me'
end Note: Testing is a can of worms and I have had some success and lots of randomness that I can't really explain. It basically boils down to the trouble of jumping between subdomains. Which is common as for majority of our tests, we want to login as a user (which means potentially hitting another subdomain) then doing our actual testing (which could be on an entirely different subdomain). I did get some success in integration/controller tests especially if we are making sure we are using url helpers but its very finicky. Testing will require some major investigation and figuring out if we want to pursuit jumping between subdomains. Overall I'm sure a solution is possible, and maybe when we start building this we can find one. However for right now and maybe best practice regardless is I think the rule of thumb is try to keep subdomains self contained and isolated from each other. Each subdomain should maybe have what it requires to be self sufficient. If we move all routes in Jupiter under ERA subdomain then everything works. If we add a new subdomain like Peel, it will then need everything it requires available under its own subdomain. So if it needs authentication then it needs access to its own (maybe authentication routes are exposed to all subdomains, or we just duplicate these routes and share as much code as possible with ERA). ProductionFrom a development point of view nothing major needs to happen on our end for production. There will probably be some work with our DNS/Apache/etc configuration to allow traffic to be served from these subdomains GotchasMultiple subdomainsIf you have multiple levels of subdomains, you need to do a bit more work. Take for example we run the same code on our staging url which may be If you look at what our subdomain is for the above staging url, its as follows: request.subdomain #=> admin.staging Which means our The parsing of the request's subdomain is managed by the # config/application.rb
config.action_dispatch.tld_length = Integer(ENV['TLD_LENGTH'] || 1) We can set it using an environment variable (or maybe better this is configured from secrets/credentials yml) so that we can use the same code in our staging environment as well as in the production environment. With this configuration made our routing setup will now work for AuthenticationBy default cookies are set by the browser on the request’s domain. So if we login into our application at Rails.application.config.session_store :cookie_store, key: "_jupiter_session", domain: :all By setting (you can also pass a list of domains to the You will also have to set the Rails.application.config.session_store :cookie_store,
key: "_jupiter_session",
domain: :all,
tld_length: 2 Note: This URL Helpers with subdomainsURL helpers seem to be working with subdomains. One caveat is we can no longer can use path helpers like Root RoutesRails has a special root route which is basically the default route of the application. When we have all of our routes under any one of the subdomains, then there can be situations where we don’t have any root route defined at all. Certain gems might depend on the presence of a root route and we need to add checks and balances accordingly. You also cannot have multiple Root Routes in a Rails application (application will crash). So this puts us in a special place. So for example this is not allowed as we have two constraints(subdomain: 'admin') do
root to: 'dashboard#index'
resources :users, only: [:index, :show] do
member do
patch :suspend
patch :unsuspend
patch :grant_admin
patch :revoke_admin
post :login_as_user
end
end
end
root to: 'welcome#index' How to get around this? There is a simple solution and that is using namespace helpers with an empty path option. If we wrap the routes within the subdomain constraint in a namespace, we are then allowed to have multiple Plus we get an interesting bonus which is the fact that everything inside this namespace by default is namespaced when looking up controllers and views. We do this quite a bit with our admin section already. For example, all admin controllers will be expected to live in a Of course you have the option to opt out of this namespace. But given the benefits it could be an easy way to organize our code for each subdomain. So this might be the best route we will want to take when we do subdomains. So by using a namespace, this now works: constraints(subdomain: 'admin') do
namespace :admin, path: '' do
root to: 'dashboard#index'
resources :users, only: [:index, :show] do
member do
patch :suspend
patch :unsuspend
patch :grant_admin
patch :revoke_admin
post :login_as_user
end
end
end
end
root to: 'welcome#index' By setting an empty path, we can continue to serve these routes on the home path ( Note: The generic catch all Public folderBy default, static content in the public folder would remain exposed to all subdomains (favicon/images, error pages, robots.txt, etc). This might be mostly okay? If we need to override these, we can add routes/controllers to intercept this traffic and serve it yourself (for example we want Peel to have a separate robots.txt from ERA). Final Thoughts / ConclusionOverall as this document hopefully outlines, subdomains for the most part are pretty easy to develop with. It has some trade offs and added complexity but most of these are resolvable. There's a couple gotchas as noted above, but this largely depends on how we want to design our subdomains. I assume we want to move every route into its own subdomain? So we probably want Peel or any new "frontend" routes into their own subdomains, everything currently in Jupiter under an ERA Subdomain. We could also split these current routes up further such as OAI/API/Admin routes into their own subdomains instead of everything under the ERA subdomain. But this has some major gotchas with testing (which hopefully we can find a solution once we deep dive more into subdomains) as noted above. I think if we keep each "frontend" under a separate subdomain (like peel.library.ualberta.ca) and everything currently in ERA under its own separate subdomain (like era.library.ualberta.ca) then this should be pretty straight forward. In the end, hopefully this allows us to share as much code as we possibility can between "frontends" (as its still just one code base) and still be able to have our entire app hosted on a single application/process without too much pain/complexity. |
Overview
Spike out some ideas for implementing #1684, to see how this approach might work and figure out any pros & cons.
I'm leaning towards serving both sites out of the same underlying webprocess here; rather than trying to make the controllers super-generic and parameterize the models they deal with via config, just having different controllers handle each site and moving shared logic into services. This seems easier overall to test than the alternative – you can test controllers directly without worrying about how the subdomain interacts with routing, etc. With the config route we'd really need to run tests twice, once with each config set.
Basic tasks as I see them (but add or change as needed)
Wrap parts of routes in an ERA subdomain constraint:
items, sitemap, robots, static, redirects
can leave admin, oai, aip, and auth stuff exposed to all subdomains?
search & profile look like we can leave it out of the subdomain restriction, see below
bunch of ways we can access this in dev, but the dirt simplest way is just to add '127.0.0.1 era.library.dev' to your hosts file? (may need to see also https://gist.github.com/indiesquidge/b836647f851179589765)
replace
root to:
with an era_root declaration instead. swap out existing calls to root_url or root_path for era_root_url & era_root_pathcreate an intermediary controller class (EraController or something?) between ApplicationController and the other controllers related to ERA materials:
collections_controller, communities_controller, items_controller, search_controller, robots_controller, sitemap_controller, static_pages_controller, welcome_controller
that a PeelController could set different navbar contents? Same deal with the footer.
css files that maybe set a different color for navbar and buttons, maybe? basically keep the same overall visual feel but give
them a slightly different identity via changing the dominant color
@search_models
determined by the parent controller; the way we've implemented it, the actual results rendering is determined by what models it brings back, so that should all just work™ if we tell it to search something else.test: only tests that go through routes should be impacted, so that's system & integration afaik
outstanding Qs:
The text was updated successfully, but these errors were encountered: