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:
parent
1a21bce0d2
commit
818b20f775
26 changed files with 650 additions and 215 deletions
1
Vagrantfile
vendored
1
Vagrantfile
vendored
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
13
app/lib/prymn/agents/agent.ex
Normal file
13
app/lib/prymn/agents/agent.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
||||
|
|
|
@ -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 """
|
||||
|
|
|
@ -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
|
||||
|
|
199
app/lib/prymn/apps/wordpress.ex
Normal file
199
app/lib/prymn/apps/wordpress.ex
Normal 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
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
46
app/lib/prymn/worker.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
1
mix.exs
1
mix.exs
|
@ -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},
|
||||
|
|
7
mix.lock
7
mix.lock
|
@ -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"},
|
||||
|
|
|
@ -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
|
||||
|
|
11
priv/repo/migrations/20231130171151_add_oban_jobs_table.exs
Normal file
11
priv/repo/migrations/20231130171151_add_oban_jobs_table.exs
Normal 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
|
|
@ -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: ""
|
||||
})
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue