Skip to content
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

mix tasks to setup ProxyEndpoint and create new Endpoints #723

Merged
merged 53 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
484bf06
wip
APB9785 Jan 11, 2025
0c340cf
use igniter for web module name
APB9785 Jan 13, 2025
553ffba
fix web module
APB9785 Jan 13, 2025
dcc74ef
inspect otp_app atom
APB9785 Jan 13, 2025
3f36083
inspect web module too
APB9785 Jan 13, 2025
3f95a90
resolve warning
APB9785 Jan 13, 2025
27b8cb3
missing arg
APB9785 Jan 13, 2025
f16ae50
typo
APB9785 Jan 13, 2025
91c5c55
config
APB9785 Jan 13, 2025
2959a44
update runtime config
APB9785 Jan 13, 2025
ef0c4b2
application and router
APB9785 Jan 14, 2025
10ba70a
improve proxy gen
APB9785 Jan 15, 2025
63cd932
only configure runtime in prod
APB9785 Jan 15, 2025
64d61c6
gen site test fix
APB9785 Jan 15, 2025
ce917ed
add proxy_endpoint module to application start
APB9785 Jan 15, 2025
f986289
update port for existing endpoints
APB9785 Jan 15, 2025
d3dc83b
update line number
APB9785 Jan 16, 2025
6ff999e
better handling of ports
APB9785 Jan 16, 2025
728c82a
proxy endpoint finished
APB9785 Jan 17, 2025
a45e4ad
finish site gen
APB9785 Jan 17, 2025
e81540a
Merge branch 'main' into apb/proxy-igniter
APB9785 Jan 17, 2025
ee9ba65
add docs for opts
APB9785 Jan 17, 2025
d1625e6
add localhost to router scope host
APB9785 Jan 17, 2025
2ca68dd
Merge branch 'main' into apb/proxy-igniter
APB9785 Jan 17, 2025
8fc8a74
endpoint ordering
APB9785 Jan 17, 2025
65cb15a
endpoint server: true
APB9785 Jan 17, 2025
0f15ef0
update line numbers
APB9785 Jan 17, 2025
a6ef4ba
remove socket route from existing endpoints
APB9785 Jan 17, 2025
4720cfa
changelog
APB9785 Jan 17, 2025
fece7c6
small fixes
leandrocp Jan 22, 2025
81bd65c
docs and deps
leandrocp Jan 22, 2025
fdc945a
handle special chars in host
leandrocp Jan 22, 2025
f7b1c78
fix gen.site test
leandrocp Jan 22, 2025
f89a432
fix: replace @session_options with app env
leandrocp Jan 23, 2025
c7e9bb7
fix: ignore endpoints without socket call
leandrocp Jan 23, 2025
ea9cab9
check_origin dynamically
leandrocp Jan 24, 2025
6466600
docs
leandrocp Jan 24, 2025
5ce9daa
use the dynamic check_origin
leandrocp Jan 24, 2025
32e107f
Merge branch 'main' into apb/proxy-igniter
APB9785 Jan 24, 2025
a9c3318
update test
APB9785 Jan 24, 2025
9a28ada
use variables
APB9785 Jan 25, 2025
0425f9a
init proxy endpoint config
APB9785 Jan 27, 2025
31855fa
add debug diff
leandrocp Jan 27, 2025
163892e
fix gen_proxy_endpoint
APB9785 Jan 27, 2025
a13a861
Merge branch 'apb/proxy-igniter' of https://github.com/BeaconCMS/beac…
APB9785 Jan 27, 2025
9611fb5
fix gen_site
APB9785 Jan 27, 2025
9188117
Merge branch 'main' into apb/proxy-igniter
leandrocp Jan 29, 2025
29e3ea8
docs and small fixes
leandrocp Jan 29, 2025
0657897
skip config which would be duplicated
APB9785 Jan 30, 2025
390d32a
Merge branch 'apb/proxy-igniter' of https://github.com/BeaconCMS/beac…
APB9785 Jan 30, 2025
05cc4c5
small changes
leandrocp Jan 30, 2025
7111d64
fix: resolve to fallback endpoint first
leandrocp Jan 31, 2025
09a738d
docs
leandrocp Jan 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@

## Unreleased

### Enhancements

- Added `--host` option for `mix beacon.gen.site` to serve your site at an alternative domain

### Fixes

- Fixed a bug where MediaLibrary could check for file contents on the wrong node in multi-node deployments

### Breaking Changes

- `beacon.install` - removed command aliases `-s` and `-p`
- `beacon.gen.site` - removed command aliases `-s` and `-p`
- `beacon.gen.tailwind_config` - removed command alias `-s`

## 0.3.3 (2024-12-13)

### Fixes
Expand Down
150 changes: 32 additions & 118 deletions guides/deployment/deployment-topologies.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The routing system in Phoenix combined with the OTP distribution model opens man

Code examples might be abbreviated and infrastructure details like load balancers are not covered in this guide to keep it short.

Clustering your application is also not covered in this guide but you can find the documentation on the platform's site you're using,
Clustering your application is also not covered in this guide but you can find the documentation on the platform's docs,
for example https://fly.io/docs/elixir/the-basics/clustering and https://www.gigalixir.com/docs/cluster.

## Core Concepts
Expand Down Expand Up @@ -32,7 +32,7 @@ end
Results in:

```
http://mysite.com/2024/campaigns/christmas`
http://mysite.com/2024/campaigns/christmas
^ ^ ^ ^
| | | |
endpoint | | page path
Expand All @@ -59,7 +59,7 @@ scope host: "siteb.com" do
end
```

The downside of having such flexibility is creating a configuration that is either invalid or not optimized. Take for instance this configuration:
The downside of having such flexibility is that it's also easy to create a configuration that is invalid or not optimized. Take for instance this configuration:

```elixir
scope host: "sitea.com" do
Expand All @@ -76,7 +76,7 @@ You can already tell that starting `:site_b` in the node that is hosting `sitea.
which is not a big problem when you have a couple of small sites, but that becomes a problem as your environment grows.

To avoid this problem, Beacon will selectively boot only the sites that are reachable in the current host, so in the example above,
only `:site_a` will be booted in the node hosting `sitea.com` and only `:site_b` in the node hosting `siteb.com`.
only `:site_a` will boot in the node hosting `sitea.com` and only `:site_b` in the node hosting `siteb.com`.

Or this other example:

Expand All @@ -91,7 +91,8 @@ The macro `beacon_site` creates a catch-all route `/*` so the second site will n
is a valid route for the first site.

Those might look obvious but that's a common source of confusion, especially in long and more complex router files.
So Beacon won't try to boot sites that can't be reached, but a warning will be displayed.

For these cases, whenever possible, Beacon will emit warnings during the boot process.

### Admin Sites Discovery

Expand Down Expand Up @@ -153,7 +154,7 @@ With these constraints in mind, let's check some deployment strategies.
Below we'll describe some common deployment strategies but Beacon is not limited to the strategies below,
you can adapt to your needs.

### 1. Single application on same host
### 1. Single application on the same host
The most simple strategy is a single project with one or more sites and the admin interface in the same host.

```elixir
Expand Down Expand Up @@ -193,6 +194,8 @@ flowchart TD
### 2. Clustered single applications
Same project as the previous strategy but with multiple nodes deployed in the same cluster.

Gives more capacity to serve more requests but still sharing the same Endpoint and same host.

```mermaid
flowchart TD
subgraph Node1["Node1"]
Expand Down Expand Up @@ -236,7 +239,9 @@ if you start booting more site and more nodes.
So an optimization is to move the Admin interface into its own project and node (a new Phoenix project),
and keep the sites in their own projects.

Note that in order to Admin find the sites, all the apps must be connected in the same cluster.
That scenario also opens the possibility to deploy the admin interface behind a VPN to increase security.

Note that in order for BeaconLiveAdmin to find all running sites, all the apps must be connected in the same cluster.

```elixir
# endpoint
Expand Down Expand Up @@ -281,126 +286,35 @@ flowchart TD

A huge benefit of this topology is the flexibility to protect the Admin interface behind a VPN or scale it independently from the main applications.

### 4. Multiple hosts in single project, separated hosting apps
Still a single project but now serving multiple sites at the root path for different dynamic hosts.

In this case we're still deploying just one application but serving multiple domains for each site:

- :campaigns -> campaigns.mysite.com
- :root -> mysite.com

TODO: diagram

TODO: gen task and constraints
### 4. Multiple hosts in single project
Still a single project but now it will serve each site on its own host (domain).

### 5. Multiple hosts in single project, separated hosting apps
Similar to the previous strategy but this time we're splitting each domain into its own app:

- App1 -> mysite.com
- App2 -> campaigns.mysite.com

That means deploying isolated apps for each domain/site, not connected to each other,
but still sharing the same codebase.

```elixir
# endpoint
host = System.get_env("PHX_HOST")
config :my_app, MyAppWeb.Endpoint, url: [host: host]

# router
scope "/", host: "campaigns.mysite.com" do
beacon_live_admin "/admin"
beacon_site "/", site: :campaigns
end
This scenario introduces a [Proxy Endpoint](https://hexdocs.pm/beacon/Beacon.ProxyEndpoint.html) to route requests to the appropriate Endpoint serving the site,
this configuration can be generated with `mix beacon.gen.site --site my_site --host mysite.com` - see [docs](https://hexdocs.pm/beacon/Mix.Tasks.Beacon.Gen.Site.html) for more info.

scope "/", host: "mysite.com" do
beacon_live_admin "/admin"
beacon_site "/", site: :root
end
```
In this case we're still deploying just one application but serving multiple domains for each site:

```mermaid
flowchart TD
subgraph Node1["Node1"]
n1_site["/, site: :root"]
subgraph Node1Admin["/admin"]
n1_admin_site[":root"]
end
end
subgraph Node2["Node1"]
n2_site["/, site: :campaigns"]
subgraph Node2Admin["/admin"]
n2_admin_site[":campaigns"]
proxy["ProxyEndpoint"]
site_a["site: :my_site, endpoint: MySiteEndpoint"]
site_b["site: :campaigns, endpoint: CampaignsEndpoint"]
subgraph Admin["/admin"]
admin_site_a[":my_site"]
admin_site_b[":campaigns"]
end
end
subgraph App1["App mysite.com"]
subgraph Cluster1["Cluster"]
Node1
end
end
subgraph App2["App campaigns.mysite.com"]
subgraph Cluster2["Cluster"]
Node2
end
end

n1_site --> n1_admin_site
n2_site --> n2_admin_site
r1["mysite.com/campaigns/christmas"] --> n2_site
r2["mysite.com/contact"] --> n1_site
r3["mysite.com/admin"] --> Node1Admin
r4["campaigns.mysite.com/admin"] --> Node2Admin
```

### 6. Multiple hosts in single project, connected hosting apps
Similar setup as the previous strategy but now connecting the apps in the same cluster with a separated admin interface.

```elixir
# endpoint
host = System.get_env("PHX_HOST")
config :my_app, MyAppWeb.Endpoint, url: [host: host]

# router
scope "/admin", host: "admin.mysite.com" do
beacon_live_admin "/"
end

scope "/", host: "campaigns.mysite.com" do
beacon_site "/", site: :campaigns
end

scope "/", host: "mysite.com" do
beacon_site "/", site: :root
end
```

```mermaid
flowchart TD
subgraph Node1["Node1"]
n1_site["/, site: :root"]
end
subgraph Node2["Node2"]
n2_site["/, site: :campaigns"]
end
subgraph Admin["NodeAdmin"]
admin_site_a[":root"]
admin_site_b[":campaigns"]
end
subgraph Cluster["Cluster"]
subgraph App1["App1 mysite.com"]
Node1
end
subgraph App2["App2 campaigns.mysite.com"]
Node2
end
subgraph AppAdmin["App admin.mysite.com"]
Admin
end
Node1
Admin
end

n1_site --> admin_site_a
n2_site --> admin_site_b
r1["mysite.com/campaigns/christmas"] --> n2_site
r2["mysite.com/contact"] --> n1_site
r3["admin.mysite.com"] --> Admin
site_a --> admin_site_a
site_b --> admin_site_b
r1["mysite.com"] --> proxy
r2["campaigns.mysite.com"] --> proxy
proxy --> site_a
proxy --> site_b
r3["mysite.com/admin"] --> Admin
```
54 changes: 54 additions & 0 deletions lib/beacon/igniter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,58 @@ defmodule Beacon.Igniter do
found -> found
end
end

def move_to_constant(zipper, name) do
case Sourceror.Zipper.find(zipper, &match?({:@, _, [{^name, _, _}]}, &1)) do
nil -> :error
value -> {:ok, value}
end
end

def move_to_variable(zipper, name) do
case Sourceror.Zipper.find(zipper, &match?({:=, _, [{^name, _, _}, _]}, &1)) do
nil -> :error
value -> {:ok, value}
end
end

def move_to_variable!(zipper, name) do
{:ok, zipper} = move_to_variable(zipper, name)
zipper
end

def move_to_import(zipper, name) when is_atom(name) do
module_as_list =
name
|> inspect()
|> String.split(".")
|> Enum.map(&String.to_atom/1)

move_to_import(zipper, module_as_list)
end

def move_to_import(zipper, name) when is_binary(name) do
module_as_list =
name
|> String.split(".")
|> Enum.map(&String.to_atom/1)

move_to_import(zipper, module_as_list)
end

def move_to_import(zipper, module_list) when is_list(module_list) do
with nil <- Sourceror.Zipper.find(zipper, &match?({:import, _, [{_, _, ^module_list}]}, &1)),
nil <- Sourceror.Zipper.find(zipper, &match?({:import, _, [{_, _, ^module_list}, _]}, &1)) do
:error
else
value -> {:ok, value}
end
end

def diff_file(igniter, file) do
igniter.rewrite.sources
|> Map.fetch!(file)
|> Rewrite.Source.diff()
|> IO.iodata_to_binary()
end
end
54 changes: 43 additions & 11 deletions lib/beacon/proxy_endpoint.ex
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
defmodule Beacon.ProxyEndpoint do
@moduledoc false

# Proxy Endpoint to redirect requests to each site endpoint in a multiple domains setup.
#
# TODO: beacon.deploy.add_domain fobar
#
# TODO: use Beacon.ProxyEndpoint, otp_app: :my_app, endpoints: [MyAppWeb.EndpointSiteA, MyAppWeb.EndpointSiteB]

defmacro __using__(opts) do
quote location: :keep, generated: true do
otp_app = Keyword.get(unquote(opts), :otp_app) || raise Beacon.RuntimeError, "missing required option :otp_app in Beacon.ProxyEndpoint"
Expand All @@ -27,11 +21,9 @@ defmodule Beacon.ProxyEndpoint do

plug :proxy

def proxy(conn, opts) do
%{host: host} = conn

# TODO: cache endpoint resolver
endpoint =
# TODO: cache endpoint resolver
def proxy(%{host: host} = conn, opts) do
matching_endpoint = fn ->
Enum.reduce_while(Beacon.Registry.running_sites(), @__beacon_proxy_fallback__, fn site, default ->
%{endpoint: endpoint} = Beacon.Config.fetch!(site)

Expand All @@ -41,9 +33,49 @@ defmodule Beacon.ProxyEndpoint do
{:cont, default}
end
end)
end

# fallback endpoint has higher priority in case of conflicts,
# for eg when all endpoints' host are localhost
endpoint =
if @__beacon_proxy_fallback__.host() == host do
@__beacon_proxy_fallback__
else
matching_endpoint.()
end

endpoint.call(conn, endpoint.init(opts))
end

@doc """
Check origin dynamically.

Used in the ProxyEndpoint `:check_origin` config to check the origin request
against the fallback endpoint and all running site's endpoints.

It checks if the requested scheme://host is the same as any of the available endpoints.

It doesn't check the scheme if not available, so in some cases it might check only the host.
Port is never checked since the proxied (children) endpoints don't use the same port as
as the requested URI.
"""
def check_origin(%URI{} = uri) do
check_origin_fallback_endpoint = fn ->
url = @__beacon_proxy_fallback__.config(:url)
check_origin(uri, url[:scheme], url[:host])
end

Enum.any?(Beacon.Registry.running_sites(), fn site ->
url = Beacon.Config.fetch!(site).endpoint.config(:url)
check_origin(uri, url[:scheme], url[:host])
end) || check_origin_fallback_endpoint.()
end

def check_origin(_), do: false

defp check_origin(%{scheme: scheme, host: host}, scheme, host) when is_binary(scheme) and is_binary(host), do: true
defp check_origin(%{host: host}, nil, host) when is_binary(host), do: true
defp check_origin(_, _), do: false
end
end
end
Loading
Loading