add functionality that creates wordpress sites

Reviewed-on: https://git.nikos.gg/prymn/prymn/pulls/9
Co-authored-by: Nikos Papadakis <nikos@papadakis.xyz>
Co-committed-by: Nikos Papadakis <nikos@papadakis.xyz>
This commit is contained in:
Nikos Papadakis 2023-12-14 12:27:05 +00:00 committed by nikos
parent 1a21bce0d2
commit 818b20f775
26 changed files with 650 additions and 215 deletions

1
Vagrantfile vendored
View file

@ -8,4 +8,5 @@ Vagrant.configure("2") do |config|
config.vm.box = "debian/bullseye64"
config.vm.network "forwarded_port", guest: 50012, host: 50012, host_ip: "127.0.0.1"
config.vm.network "forwarded_port", guest: 80, host: 8000, host_ip: "127.0.0.1"
end

View file

@ -1,5 +1,6 @@
use std::{pin::Pin, process::Stdio, sync::Mutex};
use tokio::process::Command;
use tokio_stream::{
wrappers::{ReceiverStream, WatchStream},
Stream, StreamExt,
@ -116,29 +117,47 @@ impl agent_server::Agent for AgentService<'static> {
async fn exec(&self, req: Request<ExecRequest>) -> AgentResult<Self::ExecStream> {
use exec_response::Out;
let ExecRequest { program, args } = req.get_ref();
let ExecRequest {
user,
program,
args,
} = req.get_ref();
let mut command = tokio::process::Command::new(program)
if user.is_empty() {
return Err(Status::invalid_argument("you must specify a user"));
}
if program.is_empty() {
return Err(Status::invalid_argument("you must specify a program"));
}
let mut command = if user != "root" {
let mut cmd = Command::new("sudo");
cmd.arg("-iu").arg(user).arg("--").arg(program);
cmd
} else {
Command::new(program)
};
let mut io = command
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdout =
FramedRead::new(command.stdout.take().unwrap(), BytesCodec::new()).map(|stdout| {
let stdout = stdout.unwrap();
Out::Stdout(String::from_utf8_lossy(&stdout[..]).to_string())
});
let stdout = FramedRead::new(io.stdout.take().unwrap(), BytesCodec::new()).map(|stdout| {
let stdout = stdout.unwrap();
Out::Stdout(String::from_utf8_lossy(&stdout[..]).to_string())
});
let stderr =
FramedRead::new(command.stderr.take().unwrap(), BytesCodec::new()).map(|stderr| {
let stderr = stderr.unwrap();
Out::Stderr(String::from_utf8_lossy(&stderr[..]).to_string())
});
let stderr = FramedRead::new(io.stderr.take().unwrap(), BytesCodec::new()).map(|stderr| {
let stderr = stderr.unwrap();
Out::Stderr(String::from_utf8_lossy(&stderr[..]).to_string())
});
let exit = TaskBuilder::new(format!("exec {program}"))
.health_monitor(self.health.clone())
.add_step(async move { command.wait().await.unwrap() })
.add_step(async move { io.wait().await.unwrap() })
.build()
.into_stream();

View file

@ -31,6 +31,10 @@ config :prymn, PrymnWeb.Endpoint,
# at the `config/runtime.exs`.
config :prymn, Prymn.Mailer, adapter: Swoosh.Adapters.Local
config :prymn, Oban,
repo: Prymn.Repo,
queues: [default: 10]
# Configure esbuild (the version is required)
config :esbuild,
version: "0.17.11",

View file

@ -5,31 +5,82 @@ defmodule Prymn.Agents do
the Prymn.Agents.ConnectionSupervisor and are book-kept using the
Prymn.Agents.Registry.
Agents are only valid when a `Prymn.Servers.Server` is considered registered.
"""
alias Prymn.Agents.Connection
alias Prymn.Agents.Health
require Logger
alias Prymn.Agents.{Connection, Health, Agent}
alias PrymnProto.Prymn.Agent.Stub
alias PrymnProto.Prymn.{ExecRequest, SysUpdateRequest}
@doc """
Establish a connection with a Server if one does not already exist, and
return an Agent that interfaces with the rest of the system.
## Examples
iex> Prymn.Servers.get_server_by_ip!("127.0.0.1") |> Prymn.Agents.from_server()
%Prymn.Agents.Agent{}
"""
def from_server(%Prymn.Servers.Server{status: :registered} = server) do
case start_connection(server.public_ip) do
{:ok, _pid} -> Agent.new(server.public_ip)
{:error, error} -> {:error, error}
end
end
def from_server(%Prymn.Servers.Server{}) do
Logger.error("Tried to establish a connection with an unregistered server.")
{:error, :unauthorized_action}
end
@doc """
Establish a connection with a Server if one does not already exist for a
given App. Returns an [Agent] that interfaces with the rest of the system.
"""
def from_app(%Prymn.Apps.App{} = app) do
app = Prymn.Repo.preload(app, :server)
from_server(app.server)
end
@doc """
Starts a new connection with `host_address` if one does not exist.
## Examples
iex> Prymn.Agents.start_connection("127.0.0.1")
{:ok, <PID:1234>}
iex> Prymn.Agents.start_connection("127.0.0.1")
{:ok, <PID:1234>}
"""
def start_connection(host_address) do
spec = {Connection, host_address}
case DynamicSupervisor.start_child(Prymn.Agents.ConnectionSupervisor, spec) do
{:ok, _pid} -> :ok
{:error, {:already_started, _pid}} -> :ok
{:ok, pid} -> {:ok, pid}
{:error, {:already_started, pid}} -> {:ok, pid}
{:error, error} -> {:error, error}
end
end
@doc """
Subscribe to the host's Health using Phoenix.PubSub Broadcasted messages are
the Health struct:
Subscribe to the host's Health using Phoenix.PubSub.
Broadcasted messages are the Health struct:
%Prymn.Agents.Health{}
"""
def subscribe_to_health(%Agent{} = agent) do
:ok = Health.subscribe(agent.host_address)
agent
end
def subscribe_to_health(host_address) do
:ok = Health.subscribe(host_address)
end
# TODO
# def alive?(host_address) do
# end
@doc """
Return the last known health status of the Agent, or `nil` if it doesn't
exist.
@ -41,30 +92,49 @@ defmodule Prymn.Agents do
@doc """
Get the system's information (CPU, Memory usage, etc.).
"""
def get_sys_info(host_address) do
case lookup_connection(host_address) do
nil -> nil
pid -> Connection.get_sys_info(pid)
def get_sys_info(%Agent{} = agent) do
with {:ok, channel} <- get_channel(agent),
{:ok, result} <- Stub.get_sys_info(channel, %Google.Protobuf.Empty{}) do
result
else
{:error, error} -> {:error, error}
end
end
@doc """
Perform a system update.
## Asynchronous call
Messages are sent to the caller in the form of the struct:
%PrymnProto.Prymn.SysUpdateResponse{}
Run a command.
"""
def sys_update(host_address, dry_run) when is_boolean(dry_run) do
lookup_connection(host_address)
|> Connection.sys_update(dry_run)
def exec(%Agent{} = agent, %ExecRequest{} = request) do
with {:ok, channel} <- get_channel(agent),
{:ok, result} <- Stub.exec(channel, request) do
result
else
{:error, error} -> {:error, error}
end
end
defp lookup_connection(host_address) when is_binary(host_address) do
case Registry.lookup(Prymn.Agents.Registry, host_address) do
[{pid, _}] -> pid
[] -> nil
def exec(%Agent{} = agent, request) when is_map(request),
do: exec(agent, struct(ExecRequest, request))
@doc """
Perform a system update.
"""
def sys_update(%Agent{} = agent, %SysUpdateRequest{} = request) do
with {:ok, channel} <- get_channel(agent),
{:ok, result} <- Stub.sys_update(channel, request) do
result
else
{:error, error} -> {:error, error}
end
end
def sys_update(%Agent{} = agent, request) when is_map(request),
do: sys_update(agent, struct(SysUpdateRequest, request))
defp get_channel(%Agent{} = agent) do
case start_connection(agent.host_address) do
{:ok, pid} -> {:ok, Connection.get_channel(pid)}
{:error, error} -> {:error, error}
end
end
end

View file

@ -0,0 +1,13 @@
defmodule Prymn.Agents.Agent do
@moduledoc false
defstruct [:host_address]
@type t :: %__MODULE__{
host_address: String.t()
}
def new(host_address) when is_binary(host_address) do
%__MODULE__{host_address: host_address}
end
end

View file

@ -13,18 +13,10 @@ defmodule Prymn.Agents.Connection do
GenServer.start_link(__MODULE__, host_address, name: via(host_address))
end
def get_channel(server) do
def get_channel(server) when is_pid(server) do
GenServer.call(server, :get_channel)
end
def get_sys_info(server) when is_pid(server) do
GenServer.call(server, :get_sys_info)
end
def sys_update(server, dry_run) when is_pid(server) and is_boolean(dry_run) do
GenServer.call(server, {:sys_update, dry_run})
end
##
## Server callbacks
##
@ -69,21 +61,10 @@ defmodule Prymn.Agents.Connection do
end
@impl true
def handle_call(:get_channel, _from, {_, channel} = state) do
def handle_call(:get_channel, _, {_, channel} = state) do
{:reply, channel, state, @timeout}
end
def handle_call(:get_sys_info, _from, {_, channel} = state) do
reply = Stub.get_sys_info(channel, %Google.Protobuf.Empty{})
{:reply, reply, state, @timeout}
end
def handle_call({:sys_update, dry_run}, {from, _}, {_, channel} = state) do
request = %PrymnProto.Prymn.SysUpdateRequest{dry_run: dry_run}
streaming_call(fn -> Stub.sys_update(channel, request) end, from)
{:reply, :ok, state, @timeout}
end
@impl true
def handle_info(%GRPC.Channel{} = channel, {host, _}) do
{:noreply, {host, channel}, {:continue, :health}}
@ -156,18 +137,4 @@ defmodule Prymn.Agents.Connection do
receive_loop(pid)
end
defp streaming_call(fun, from) do
Task.start_link(fn ->
case fun.() do
{:ok, stream} ->
stream
|> Stream.each(fn {:ok, data} -> send(from, data) end)
|> Enum.to_list()
{:error, _error} ->
:todo
end
end)
end
end

View file

@ -8,20 +8,13 @@ defmodule Prymn.Application do
@impl true
def start(_type, _args) do
children = [
# Start the Telemetry supervisor
PrymnWeb.Telemetry,
# Start the Ecto repository
Prymn.Repo,
# Start the PubSub system
{DNSCluster, query: Application.get_env(:prymn, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: Prymn.PubSub},
# Start the Finch HTTP client for sending emails
{Finch, name: Prymn.Finch},
# Start the Agents (dynamic GRPC connections) supervisor
{Oban, Application.fetch_env!(:prymn, Oban)},
Prymn.Agents.Supervisor,
# Start a worker by calling: SampleApp.Worker.start_link(arg)
# {SampleApp.Worker, arg},
# Start to serve requests, typically the last entry
PrymnWeb.Endpoint
]

View file

@ -18,40 +18,36 @@ defmodule Prymn.Apps do
"""
def list_apps do
Repo.all(App)
|> Repo.preload(:server)
end
@doc """
Gets a single app.
Raises `Ecto.NoResultsError` if the App does not exist.
## Examples
iex> get_app!(123)
%App{}
iex> get_app!(456)
** (Ecto.NoResultsError)
"""
def get_app!(id), do: Repo.get!(App, id)
@doc """
Creates a app.
## Examples
iex> create_app(%{field: value})
{:ok, %App{}}
iex> create_app(%{field: bad_value})
{:error, %Ecto.Changeset{}}
Creates an app and prepares it for deployment.
"""
def create_app(attrs \\ %{}) do
%App{}
|> App.changeset(attrs)
def create_app(%Prymn.Servers.Server{} = server, attrs \\ %{}) do
# TODO: Any app..
server
|> Ecto.build_assoc(:apps)
|> App.change_wordpress(attrs)
|> Repo.insert()
|> case do
{:ok, %App{} = app} ->
%{app_id: app.id}
|> Prymn.Worker.new()
|> Oban.insert()
app
{:error, changeset} ->
changeset
end
end
@doc """

View file

@ -1,9 +1,15 @@
defmodule Prymn.Apps.App do
use Ecto.Schema
import Ecto.Changeset
schema "apps" do
belongs_to :server, Prymn.Servers.Server
field :name, :string
field :type, :string
field :status, Ecto.Enum, values: [:initialized, :deployed], default: :initialized
embeds_one :wordpress, Prymn.Apps.Wordpress, source: :metadata, on_replace: :update
# embeds_one :html, Prymn.Apps.Html, source: :metadata, on_replace: :update
timestamps()
end
@ -12,6 +18,16 @@ defmodule Prymn.Apps.App do
def changeset(app, attrs) do
app
|> cast(attrs, [:name])
|> validate_required([:name])
|> validate_required([:name, :server_id])
end
@doc false
def change_wordpress(app, attrs \\ %{}) do
app
|> changeset(attrs)
|> cast_embed(:wordpress)
|> validate_required(:wordpress)
|> put_change(:type, "wordpress")
|> put_change(:status, :initialized)
end
end

View file

@ -0,0 +1,199 @@
defmodule Prymn.Apps.Wordpress do
use Ecto.Schema
import Ecto.Changeset
alias Prymn.Apps.App
alias Prymn.Agents
alias PrymnProto.Prymn.{ExecRequest, ExecResponse}
@primary_key false
embedded_schema do
field :path, :string
field :db_host, :string
field :db_name, :string
field :db_user, :string
field :db_pass, :string
field :admin_username, :string
field :admin_email, :string
end
def changeset(app, attrs) do
app
|> cast(attrs, [
:path,
:db_host,
:db_name,
:db_user,
:db_pass,
:admin_username,
:admin_email
])
|> validate_required([
:path,
:db_user,
:db_host,
:db_user,
:db_pass,
:admin_email,
:admin_username
])
end
@max_steps 6
# TODO: We need a mechanism to drive some messages to the pubsub, maybe using Health:
# Health.change_task(%Health.Task{
# key: "create_db_user",
# message: "Creating db user...",
# curr_step: 0,
# max_steps: 5
# })
# |> Health.update_and_broadcast()
def deploy(%App{type: "wordpress"} = app, agent, notify_fun) do
# TODO: Run sanity checks
# e.g Database does not exist, domain does not exist, etc.
deploy(app, agent, notify_fun, 1)
end
defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 1 = step) do
# TODO: We need a mechanism to wait for the agent to connect before proceeding,
# this is executed faster than the connection (which happens on deploy_app in Worker)
Agents.exec(agent, %ExecRequest{
user: "root",
program: "mysql",
args: [
"-e",
# TODO: Sanitize the string to protect from injection
"create user '#{wp.db_user}'@'#{wp.db_host}' identified by '#{wp.db_pass}';"
]
})
|> then(&get_results/1)
|> case do
{_, _, exit_code} = data when exit_code != 0 ->
notify_fun.(:error, data, 1 / 5 * 100)
data ->
notify_fun.(:progress, data, step / @max_steps * 100)
deploy(app, agent, notify_fun, step + 1)
end
end
defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 2 = step) do
Agents.exec(agent, %ExecRequest{
user: "root",
program: "mysql",
args: [
"-e",
# TODO: Sanitize the string to protect from injection
"grant all privileges on #{wp.db_name}.* to '#{wp.db_user}'@'#{wp.db_host}';"
]
})
|> then(&get_results/1)
|> case do
{_, _, exit_code} = data when exit_code != 0 ->
notify_fun.(:error, data, step / @max_steps * 100)
data ->
notify_fun.(:progress, data, step / @max_steps * 100)
deploy(app, agent, notify_fun, step + 1)
end
end
defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 3 = step) do
Agents.exec(agent, %ExecRequest{
user: "vagrant",
program: "wp",
args: ["core", "download", "--path=#{wp.path}"]
})
|> then(&get_results/1)
|> case do
{_, _, exit_code} = data when exit_code != 0 ->
notify_fun.(:error, data, step / @max_steps * 100)
data ->
notify_fun.(:progress, data, step / @max_steps * 100)
deploy(app, agent, notify_fun, step + 1)
end
end
defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 4 = step) do
Agents.exec(agent, %ExecRequest{
user: "vagrant",
program: "wp",
args: [
"config",
"create",
"--path=#{wp.path}",
"--dbhost=#{wp.db_host}",
"--dbname=#{wp.db_name}",
"--dbuser=#{wp.db_user}",
"--dbpass=#{wp.db_pass}"
]
})
|> then(&get_results/1)
|> case do
{_, _, exit_code} = data when exit_code != 0 ->
notify_fun.(:error, data, step / @max_steps * 100)
data ->
notify_fun.(:progress, data, step / @max_steps * 100)
deploy(app, agent, notify_fun, step + 1)
end
end
defp deploy(%App{wordpress: %__MODULE__{} = wp} = app, agent, notify_fun, 5 = step) do
Agents.exec(agent, %ExecRequest{
user: "vagrant",
program: "wp",
args: ["db", "create", "--path=#{wp.path}"]
})
|> then(&get_results/1)
|> case do
{_, _, exit_code} = data when exit_code != 0 ->
notify_fun.(:error, data, step / @max_steps * 100)
data ->
notify_fun.(:progress, data, step / @max_steps * 100)
deploy(app, agent, notify_fun, step + 1)
end
end
defp deploy(%App{name: name, wordpress: %__MODULE__{} = wp}, agent, notify_fun, 6 = step) do
Agents.exec(agent, %ExecRequest{
user: "vagrant",
program: "wp",
args: [
"core",
"install",
"--path=#{wp.path}",
"--url=http://site.test",
"--title=#{name}",
"--admin_user=#{wp.admin_username}",
"--admin_email=#{wp.admin_email}"
]
})
|> then(&get_results/1)
|> case do
{_, _, exit_code} = data when exit_code != 0 ->
notify_fun.(:error, data, step / @max_steps * 100)
data ->
notify_fun.(:complete, data, step / @max_steps * 100)
end
end
defp get_results(stream) do
Enum.reduce_while(stream, {"", "", nil}, fn
{:ok, %ExecResponse{out: {:exit_code, exit_code}}}, {stdout, stderr, _} ->
{:halt, {stdout, stderr, exit_code}}
{:ok, %ExecResponse{out: {:stdout, stdout}}}, {acc_stdout, stderr, exit_code} ->
{:cont, {acc_stdout <> stdout, stderr, exit_code}}
{:ok, %ExecResponse{out: {:stderr, stderr}}}, {stdout, acc_stderr, exit_code} ->
{:cont, {stdout, acc_stderr <> stderr, exit_code}}
end)
end
end

View file

@ -21,6 +21,11 @@ defmodule Prymn.Servers do
Repo.all(Server |> order_by(desc: :inserted_at))
end
def list_registered_servers() do
query = from s in Server, select: s, where: s.status == :registered
Repo.all(query)
end
@doc """
Gets a single server.

View file

@ -33,6 +33,8 @@ defmodule Prymn.Servers.Server do
values: [:unregistered, :registered],
default: :unregistered
has_many :apps, Prymn.Apps.App
timestamps()
end

46
app/lib/prymn/worker.ex Normal file
View file

@ -0,0 +1,46 @@
defmodule Prymn.Worker do
use Oban.Worker
alias Prymn.{Agents, Apps}
@impl true
def perform(%Oban.Job{args: %{"app_id" => app_id}}) do
deploy_app(app_id)
await_deploy("app:#{app_id}")
:ok
end
defp deploy_app(app_id) do
pid = self()
app = Apps.get_app!(app_id)
agent = Agents.from_app(app)
Task.start_link(fn ->
notify_fun = fn
:progress, data, progress -> send(pid, {:progress, data, progress})
:complete, data, _progress -> send(pid, {:complete, data})
:error, data, _progress -> send(pid, {:error, data})
end
Apps.Wordpress.deploy(app, agent, notify_fun)
end)
end
defp await_deploy(channel) do
receive do
{:progress, data, progress} ->
Phoenix.PubSub.broadcast!(Prymn.PubSub, channel, {:progress, data, progress})
await_deploy(channel)
{:complete, data} ->
Phoenix.PubSub.broadcast!(Prymn.PubSub, channel, {:complete, data})
{:error, data} ->
Phoenix.PubSub.broadcast!(Prymn.PubSub, channel, {:error, data})
# raise "Error occured during deployment: #{inspect(data)}"
after
60_000 ->
raise RuntimeError, "no progress after 1 minute"
end
end
end

View file

@ -1,6 +1,24 @@
defmodule PrymnWeb.CreateApp do
use PrymnWeb, :live_component
alias Prymn.Apps
@impl true
def mount(socket) do
servers = Prymn.Servers.list_registered_servers()
{:ok, assign(socket, :servers, servers)}
end
@impl true
def update(assigns, socket) do
changeset = Apps.change_app(%Apps.App{server: nil})
{:ok,
socket
|> assign(assigns)
|> assign(:form, to_form(changeset))}
end
@impl true
def render(assigns) do
~H"""
@ -26,52 +44,107 @@ defmodule PrymnWeb.CreateApp do
</span>
<span>
<input
id="plain_html"
id="plain"
type="radio"
name="app_type"
value="plain_html"
value="plain"
class="peer hidden"
checked={@app_type == "plain_html"}
checked={@app_type == "plain"}
/>
<label
for="plain_html"
for="plain"
class="inline-block cursor-pointer rounded p-5 shadow peer-checked:bg-black peer-checked:text-white"
phx-click={JS.patch(~p"/apps/new?app_type=plain_html")}
phx-click={JS.patch(~p"/apps/new?app_type=plain")}
>
Plain HTML
</label>
</span>
</fieldset>
<.wordpress_app_form :if={assigns.app_type == "wordpress"} servers={assigns[:servers]} />
<.wordpress_app_form
:if={@app_type == "wordpress"}
form={@form}
servers={@servers}
myself={@myself}
/>
<.plain_app_form :if={@app_type == "plain"} form={@form} servers={@servers} myself={@myself} />
</div>
"""
end
@impl true
def handle_event("create_wordpress_app", %{"app" => params}, socket) do
server =
Enum.find(socket.assigns.servers, %Prymn.Servers.Server{}, fn server ->
Integer.to_string(server.id) == params["server_id"]
end)
socket =
case Apps.create_app(server, params) do
%Ecto.Changeset{valid?: false} = changeset -> assign(socket, :form, to_form(changeset))
%Apps.App{} -> push_navigate(socket, to: ~p"/apps")
end
{:noreply, socket}
end
def handle_event("create_plain_app", _params, socket) do
{:noreply, socket}
end
defp wordpress_app_form(assigns) do
~H"""
<.simple_form id="test" for={nil}>
<.simple_form method="POST" for={@form} phx-submit="create_wordpress_app" phx-target={@myself}>
<.input
:if={assigns.servers != nil}
id="server"
type="select"
name="server"
prompt="Select a server to host this app..."
options={[]}
value={nil}
label="Hosting Server"
prompt="Select a server"
field={@form[:server_id]}
options={Enum.map(@servers, &{&1.name, &1.id})}
/>
<.input id="name" type="text" name="app_name" value={nil} label="WordPress Site Name" required />
<.input id="domain" type="text" name="domain" value={nil} label="Domain Name" required />
<.input type="checkbox" name="create_database?" label="Create a new database?" />
<.input type="text" name="db_name" value={nil} label="Database name" />
<.input type="text" label="App Name" field={@form[:name]} />
<.input type="text" label="Domain Name" field={@form[:domain]} />
<fieldset>
<legend class="text-sm font-semibold leading-6 text-gray-900">
WordPress Settings
</legend>
<div class="mt-6 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-2">
<.inputs_for :let={wordpress} field={@form[:wordpress]}>
<.input type="email" label="Admin Email" field={wordpress[:admin_email]} />
<.input type="text" label="Admin Username" field={wordpress[:admin_username]} />
<.input type="text" label="Installation Path" field={wordpress[:path]} />
<.input
type="text"
label="Database Host"
field={wordpress[:db_host]}
value="127.0.0.1"
readonly
/>
<.input type="text" label="Database Name" field={wordpress[:db_name]} />
<.input type="text" label="Database User" field={wordpress[:db_user]} />
<.input type="password" label="Database Password" field={wordpress[:db_pass]} />
</.inputs_for>
</div>
</fieldset>
<:actions>
<Button.primary type="submit">Create</Button.primary>
</:actions>
</.simple_form>
"""
end
<.input type="select" name="db_name" value={nil} options={["db1", "db2"]} label="Database" />
<.input type="text" name="admin_username" value={nil} label="Admin Username" />
<.input type="email" name="admin_email" value={nil} label="Admin Email" />
<.input type="password" name="admin_password" value={nil} label="Admin Password" />
<Button.primary type="submit">Create</Button.primary>
defp plain_app_form(assigns) do
~H"""
<.simple_form method="POST" for={@form} phx-submit="create_plain_app" phx-target={@myself}>
<.input
type="select"
label="Hosting Server"
prompt="Select a server"
field={@form[:server_id]}
options={Enum.map(@servers, &{&1.name, &1.id})}
/>
<:actions>
<Button.primary type="submit">Create</Button.primary>
</:actions>
</.simple_form>
"""
end

View file

@ -5,37 +5,29 @@
>
<%= gettext("Skip to content") %>
</a>
<div class="flex items-stretch justify-between border-b border-zinc-300 bg-gray-100 px-4 text-sm sm:px-6 lg:px-8">
<div class="my-auto leading-tight">
<p class="text-[9px]">codename</p>
<a href="/" class="text-lg font-medium">
<div class="flex h-14 items-center justify-between border-b border-slate-100 bg-violet-100 px-4 text-sm sm:px-6 lg:px-8">
<div class="my-auto leading-tight text-violet-700">
<p class="text-[10px]">codename</p>
<a
href="/"
class="bg-gradient-to-br from-violet-600 to-pink-600 bg-clip-text text-lg font-black text-transparent"
>
Prymn
</a>
<p class="inline text-xs">
v<%= Application.spec(:prymn, :vsn) %>
</p>
</div>
<div class="flex items-center text-zinc-900">
<.dropdown position="right">
<:button variant="tertiary">
<.icon name="hero-user-solid" />
</:button>
<:item href={~p"/users/settings"}>Settings</:item>
<:item method="DELETE" href={~p"/auth/log_out"}>Log out</:item>
</.dropdown>
</div>
<.dropdown position="right">
<:button variant="tertiary">
<.icon name="hero-user-solid" />
</:button>
<:item href={~p"/users/settings"}>Settings</:item>
<:item method="DELETE" href={~p"/auth/log_out"}>Log out</:item>
</.dropdown>
</div>
</header>
<main id="main" class="px-4 sm:px-6 lg:px-10">
<div class="mb-8 border-b border-zinc-900 pt-10 pb-2 font-medium">
<ul class="flex gap-4">
<li>Projects</li>
<li>Usage</li>
<li>Activities</li>
<li>Limits</li>
<li>Support</li>
</ul>
</div>
<main id="main" class="mt-10 px-4 sm:px-6 lg:px-10">
<div class="pb-20">
<.flash_group flash={@flash} />
<%= @inner_content %>

View file

@ -8,11 +8,10 @@ defmodule PrymnWeb.SystemInfo do
def update(assigns, socket) do
{:ok,
socket
|> assign(:ip, assigns.ip)
|> assign(:agent, assigns.agent)
|> assign(:sys_info, AsyncResult.loading())
|> start_async(:get_sys_info, fn ->
Logger.debug("getting initial system info for #{assigns.ip}...")
Prymn.Agents.get_sys_info(assigns.ip)
Prymn.Agents.get_sys_info(assigns.agent)
end)}
end
@ -61,17 +60,16 @@ defmodule PrymnWeb.SystemInfo do
end
def handle_async(:get_sys_info, {:ok, {:ok, sys_info}}, socket) do
%{sys_info: sys_info_result, ip: host_address} = socket.assigns
%{sys_info: sys_info_result, agent: agent} = socket.assigns
{:noreply,
socket
|> assign(:sys_info, AsyncResult.ok(sys_info_result, sys_info))
|> start_async(:get_sys_info, fn ->
Logger.debug("getting more system info for #{host_address}...")
# 10 seconds is >5 which is gun's timeout duration (which might have a race
# condition if they are equal)
Process.sleep(:timer.seconds(10))
Prymn.Agents.get_sys_info(host_address)
Prymn.Agents.get_sys_info(agent)
end)}
end

View file

@ -1,15 +1,17 @@
defmodule PrymnWeb.AppIndexLive do
use PrymnWeb, :live_view
alias Prymn.Apps
@impl true
def mount(_, _, socket) do
apps = Prymn.Apps.list_apps()
servers = Prymn.Servers.list_servers()
apps = Apps.list_apps()
{:ok,
socket
|> assign(:servers, servers)
|> assign(:apps, apps)}
for %Apps.App{} = app <- apps do
Phoenix.PubSub.subscribe(Prymn.PubSub, "app:#{app.id}")
end
{:ok, assign(socket, :apps, apps)}
end
@impl true
@ -18,12 +20,7 @@ defmodule PrymnWeb.AppIndexLive do
<%= cond do %>
<% assigns.live_action == :new -> %>
<.back navigate={~p"/apps"}>Go back</.back>
<.live_component
id={:new}
module={PrymnWeb.CreateApp}
app_type={assigns[:app_type]}
servers={@servers}
/>
<.live_component id={:new} module={PrymnWeb.CreateApp} app_type={assigns[:app_type]} />
<% assigns.apps == [] -> %>
<.onboarding />
<% true -> %>
@ -33,12 +30,25 @@ defmodule PrymnWeb.AppIndexLive do
<:subtitle>
All of your apps accross all projects.
</:subtitle>
<:actions>
<Button.primary patch={~p"/apps/new"}>Create app</Button.primary>
</:actions>
</.header>
<div :for={app <- @apps} class="mt-5 bg-violet-100 p-5">
<p>App: <%= app.name %></p>
<p>Server: <%= app.server.name %></p>
</div>
</div>
<% end %>
"""
end
@impl true
def handle_info(msg, socket) do
dbg(msg, label: "Incoming message from pubsub")
{:noreply, socket}
end
@impl true
def handle_params(%{"app_type" => app_type}, _, socket) do
{:noreply, assign(socket, app_type: app_type)}
@ -52,12 +62,14 @@ defmodule PrymnWeb.AppIndexLive do
defp onboarding(assigns) do
~H"""
<div class="mx-auto max-w-2xl text-center">
<h1 class="text-3xl font-medium">You have no Apps.</h1>
<h2 class="text-xl">Create your first App here!</h2>
<Button.primary class="mt-10" size="lg" patch={~p"/apps/new"}>
Create a new App
</Button.primary>
<div class="grid h-screen items-center">
<div class="pb-64 text-center">
<h1 class="text-3xl font-medium">You have no Apps.</h1>
<h2 class="text-xl">Create your first App here!</h2>
<Button.primary class="mt-10" size="lg" patch={~p"/apps/new"}>
Create a new App
</Button.primary>
</div>
</div>
"""
end

View file

@ -9,7 +9,7 @@ defmodule PrymnWeb.DashboardLive do
<h1 class="text-3xl font-bold leading-snug">Good morning, <%= @current_user.email %>!</h1>
<h2 class="text-lg font-medium">Your overview</h2>
</div>
<div class="flex flex-wrap justify-around gap-2">
<div class="flex flex-wrap justify-center space-x-4 md:flex-nowrap">
<div class="basis-4/12 rounded-lg p-3 shadow-md">
<span class="text-7xl font-medium">0</span>
<h2 class="mt-5 font-medium leading-snug text-gray-600">Projects</h2>
@ -24,7 +24,7 @@ defmodule PrymnWeb.DashboardLive do
<.icon class="h-3 w-4" name="hero-arrow-right" /> View your servers
</.link>
</div>
<div class="basis-3/12 rounded-lg p-3 shadow-md">
<div class="basis-4/12 rounded-lg p-3 shadow-md">
<span class="text-7xl font-medium">0</span>
<h2 class="mt-5 font-medium leading-snug text-gray-600">Apps</h2>
<.link class="text-sm text-blue-600" navigate={~p"/apps"}>

View file

@ -11,9 +11,10 @@ defmodule PrymnWeb.ServerLive.Index do
healths =
if connected?(socket) do
for %Servers.Server{status: :registered, public_ip: ip} <- servers, into: %{} do
Agents.subscribe_to_health(ip)
Agents.start_connection(ip)
for %Servers.Server{status: :registered, public_ip: ip} = server <- servers, into: %{} do
Agents.from_server(server)
|> Agents.subscribe_to_health()
{ip, Agents.get_health(ip)}
end
else
@ -51,15 +52,15 @@ defmodule PrymnWeb.ServerLive.Index do
</div>
<div class="flex flex-row flex-wrap justify-between lg:text-sm">
<span>IP: <%= server.public_ip || "N/A" %></span>
<span
:if={@healths[server.public_ip] && Enum.count(@healths[server.public_ip].tasks)}
class="text-right text-xs text-slate-700"
>
<%= for {name, task} <- Enum.take(@healths[server.public_ip].tasks, 1) do %>
<%= if @healths[server.public_ip] do %>
<span
:for={{name, task} <- @healths[server.public_ip].tasks || []}
class="text-right text-xs text-slate-700"
>
<div>In progress: <%= name %></div>
<div><%= task.progress %></div>
<% end %>
</span>
</span>
<% end %>
</div>
</.link>
</div>

View file

@ -75,7 +75,7 @@ defmodule PrymnWeb.ServerLive.Show do
<.live_component
id={"system_info-#{@server.name}"}
module={PrymnWeb.SystemInfo}
ip={@server.public_ip}
agent={assigns[:agent]}
/>
<section class="mt-4">
<form phx-change="change_dry_run">
@ -131,10 +131,14 @@ defmodule PrymnWeb.ServerLive.Show do
def handle_params(%{"id" => id}, _, socket) do
server = Servers.get_server!(id)
if connected?(socket) and server.status == :registered do
Agents.subscribe_to_health(server.public_ip)
Agents.start_connection(server.public_ip)
end
socket =
if connected?(socket) and server.status == :registered do
agent = Agents.from_server(server)
Agents.subscribe_to_health(agent)
assign(socket, :agent, agent)
else
socket
end
health = Agents.get_health(server.public_ip)
@ -167,20 +171,24 @@ defmodule PrymnWeb.ServerLive.Show do
@impl true
def handle_event("system_update", _params, socket) do
host_address = get_in(socket.assigns, [:server, Access.key(:public_ip)])
server_name = get_in(socket.assigns, [:server, Access.key(:name)])
pid = self()
socket =
if host_address do
Agents.sys_update(host_address, socket.assigns.dry_run)
put_flash(socket, :info, "Started a system update on server #{server_name}.")
else
put_flash(
socket,
:error,
"Could not perform the update. Your server does not seem to have an address"
)
end
if agent = socket.assigns[:agent] do
# TODO: This is ugly
Task.start_link(fn ->
Agents.sys_update(agent, %{dry_run: socket.assigns.dry_run})
|> Stream.each(fn
{:ok, msg} -> send(pid, msg)
{:error, error} -> Logger.error("error during system update call: #{inspect(error)}")
end)
|> Enum.to_list()
end)
put_flash(socket, :info, "Started a system update on server #{server_name}.")
else
put_flash(socket, :error, "Could not perform the update.")
end
{:noreply, socket}
end

View file

@ -54,6 +54,7 @@ defmodule Prymn.MixProject do
{:grpc, "~> 0.7"},
{:protobuf, "~> 0.12.0"},
{:google_protos, "~> 0.3.0"},
{:oban, "~> 2.17"},
# Test
{:floki, ">= 0.30.0", only: :test},

View file

@ -11,8 +11,8 @@
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"dialyxir": {:hex, :dialyxir, "1.4.2", "764a6e8e7a354f0ba95d58418178d486065ead1f69ad89782817c296d0d746a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "516603d8067b2fd585319e4b13d3674ad4f314a5902ba8130cd97dc902ce6bbd"},
"dns_cluster": {:hex, :dns_cluster, "0.1.1", "73b4b2c3ec692f8a64276c43f8c929733a9ab9ac48c34e4c0b3d9d1b5cd69155", [:mix], [], "hexpm", "03a3f6ff16dcbb53e219b99c7af6aab29eb6b88acf80164b4bd76ac18dc890b3"},
"ecto": {:hex, :ecto, "3.11.0", "ff8614b4e70a774f9d39af809c426def80852048440e8785d93a6e91f48fec00", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7769dad267ef967310d6e988e92d772659b11b09a0c015f101ce0fff81ce1f81"},
"ecto_sql": {:hex, :ecto_sql, "3.11.0", "c787b24b224942b69c9ff7ab9107f258ecdc68326be04815c6cce2941b6fad1c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "77aa3677169f55c2714dda7352d563002d180eb33c0dc29cd36d39c0a1a971f5"},
"ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"},
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
@ -30,6 +30,7 @@
"mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
"oban": {:hex, :oban, "2.17.1", "42d6221a1c17b63d81c19e3bad9ea82b59e39c47c1f9b7670ee33628569a449b", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c02686ada7979b00e259c0efbafeae2749f8209747b3460001fe695c5bdbeee6"},
"phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
"phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"},
@ -41,7 +42,7 @@
"plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"},
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
"postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"},
"postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"},
"protobuf": {:hex, :protobuf, "0.12.0", "58c0dfea5f929b96b5aa54ec02b7130688f09d2de5ddc521d696eec2a015b223", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "75fa6cbf262062073dd51be44dd0ab940500e18386a6c4e87d5819a58964dc45"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"swoosh": {:hex, :swoosh, "1.14.1", "d8813699ba410509008dd3dfdb2df057e3fce367d45d5e6d76b146a7c9d559cd", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87da72260b4351678f96aec61db5c2acc8a88cda2cf2c4f534eb4c9c461350c7"},

View file

@ -3,7 +3,11 @@ defmodule Prymn.Repo.Migrations.CreateApps do
def change do
create table(:apps) do
add :server_id, references("servers")
add :name, :string
add :type, :string
add :status, :string
add :metadata, :map
timestamps()
end

View file

@ -0,0 +1,11 @@
defmodule Prymn.Repo.Migrations.AddObanJobsTable do
use Ecto.Migration
def up do
Oban.Migration.up()
end
def down do
Oban.Migration.down(version: 1)
end
end

View file

@ -1,11 +1,13 @@
# Script for populating the database. You can run it as:
#
# mix run priv/repo/seeds.exs
#
# Inside the script, you can read and write to any of your
# repositories directly:
#
# Prymn.Repo.insert!(%Prymn.SomeSchema{})
#
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.
Prymn.Accounts.register_user(%{email: "dev@test", password: "password"})
Prymn.Repo.insert!(%Prymn.Servers.Server{
status: :registered,
public_ip: "127.0.0.1",
name: "local server",
provider: :Custom,
registration_token: ""
})

View file

@ -46,8 +46,9 @@ message SysInfoResponse {
}
message ExecRequest {
string program = 1;
repeated string args = 2;
string user = 1;
string program = 2;
repeated string args = 3;
}
message ExecResponse {