-
Notifications
You must be signed in to change notification settings - Fork 438
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add docker deployment instructions to app panel (#2276)
- Loading branch information
1 parent
87ef031
commit ae1371d
Showing
11 changed files
with
1,062 additions
and
275 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,284 @@ | ||
defmodule Livebook.Hubs.Dockerfile do | ||
# This module is responsible for building Dockerfile to deploy apps. | ||
|
||
import Ecto.Changeset | ||
|
||
alias Livebook.Hubs | ||
|
||
@type config :: %{ | ||
deploy_all: boolean(), | ||
docker_tag: String.t(), | ||
zta_provider: atom() | nil, | ||
zta_key: String.t() | nil | ||
} | ||
|
||
@doc """ | ||
Builds a changeset for app Dockerfile configuration. | ||
""" | ||
@spec config_changeset(map()) :: Ecto.Changeset.t() | ||
def config_changeset(attrs \\ %{}) do | ||
default_image = Livebook.Config.docker_images() |> hd() | ||
|
||
data = %{deploy_all: false, docker_tag: default_image.tag, zta_provider: nil, zta_key: nil} | ||
|
||
zta_types = | ||
for provider <- Livebook.Config.identity_providers(), | ||
not provider.read_only, | ||
do: provider.type | ||
|
||
types = %{ | ||
deploy_all: :boolean, | ||
docker_tag: :string, | ||
zta_provider: Ecto.ParameterizedType.init(Ecto.Enum, values: zta_types), | ||
zta_key: :string | ||
} | ||
|
||
cast({data, types}, attrs, [:deploy_all, :docker_tag, :zta_provider, :zta_key]) | ||
|> validate_required([:deploy_all, :docker_tag]) | ||
end | ||
|
||
@doc """ | ||
Builds Dockerfile definition for app deployment. | ||
""" | ||
@spec build_dockerfile( | ||
config(), | ||
Hubs.Provider.t(), | ||
list(Livebook.Secrets.Secret.t()), | ||
list(Livebook.FileSystem.t()), | ||
Livebook.FileSystem.File.t() | nil, | ||
list(Livebook.Notebook.file_entry()), | ||
list(Livebook.Session.Data.secrets()) | ||
) :: String.t() | ||
def build_dockerfile(config, hub, hub_secrets, hub_file_systems, file, file_entries, secrets) do | ||
base_image = Enum.find(Livebook.Config.docker_images(), &(&1.tag == config.docker_tag)) | ||
|
||
image = """ | ||
FROM ghcr.io/livebook-dev/livebook:#{base_image.tag} | ||
""" | ||
|
||
image_envs = format_envs(base_image.env) | ||
|
||
hub_type = Hubs.Provider.type(hub) | ||
used_secrets = used_secrets(config, hub, secrets, hub_secrets) |> Enum.sort_by(& &1.name) | ||
hub_config = format_hub_config(hub_type, config, hub, hub_file_systems, used_secrets) | ||
|
||
apps_config = """ | ||
# Apps configuration | ||
ENV LIVEBOOK_APPS_PATH "/apps" | ||
ENV LIVEBOOK_APPS_PATH_WARMUP "manual" | ||
ENV LIVEBOOK_APPS_PATH_HUB_ID "#{hub.id}" | ||
""" | ||
|
||
notebook = | ||
if config.deploy_all do | ||
""" | ||
# Notebooks and files | ||
COPY . /apps | ||
""" | ||
else | ||
notebook_file_name = Livebook.FileSystem.File.name(file) | ||
|
||
notebook = | ||
""" | ||
# Notebook | ||
COPY #{notebook_file_name} /apps/ | ||
""" | ||
|
||
attachments = | ||
file_entries | ||
|> Enum.filter(&(&1.type == :attachment)) | ||
|> Enum.sort_by(& &1.name) | ||
|
||
if attachments == [] do | ||
notebook | ||
else | ||
list = Enum.map_join(attachments, " ", &"files/#{&1.name}") | ||
|
||
""" | ||
# Files | ||
COPY #{list} /apps/files/ | ||
#{notebook}\ | ||
""" | ||
end | ||
end | ||
|
||
apps_warmup = """ | ||
# Cache apps setup at build time | ||
RUN /app/bin/warmup_apps.sh | ||
""" | ||
|
||
[ | ||
image, | ||
image_envs, | ||
hub_config, | ||
apps_config, | ||
notebook, | ||
apps_warmup | ||
] | ||
|> Enum.reject(&is_nil/1) | ||
|> Enum.join("\n") | ||
end | ||
|
||
defp format_hub_config("team", config, hub, hub_file_systems, used_secrets) do | ||
base_env = | ||
""" | ||
ARG TEAMS_KEY="#{hub.teams_key}" | ||
# Teams Hub configuration for airgapped deployment | ||
ENV LIVEBOOK_TEAMS_KEY ${TEAMS_KEY} | ||
ENV LIVEBOOK_TEAMS_NAME "#{hub.hub_name}" | ||
ENV LIVEBOOK_TEAMS_OFFLINE_KEY "#{hub.org_public_key}" | ||
""" | ||
|
||
secrets = | ||
if used_secrets != [] do | ||
""" | ||
ENV LIVEBOOK_TEAMS_SECRETS "#{encrypt_secrets_to_dockerfile(used_secrets, hub)}" | ||
""" | ||
end | ||
|
||
file_systems = | ||
if hub_file_systems != [] do | ||
""" | ||
ENV LIVEBOOK_TEAMS_FS "#{encrypt_file_systems_to_dockerfile(hub_file_systems, hub)}" | ||
""" | ||
end | ||
|
||
zta = | ||
if zta_configured?(config) do | ||
""" | ||
ENV LIVEBOOK_IDENTITY_PROVIDER "#{config.zta_provider}:#{config.zta_key}" | ||
""" | ||
end | ||
|
||
[base_env, secrets, file_systems, zta] | ||
|> Enum.reject(&is_nil/1) | ||
|> Enum.join() | ||
end | ||
|
||
defp format_hub_config("personal", _config, _hub, _hub_file_systems, used_secrets) do | ||
if used_secrets != [] do | ||
envs = used_secrets |> Enum.map(&{"LB_" <> &1.name, &1.value}) |> format_envs() | ||
|
||
""" | ||
# Personal Hub secrets | ||
#{envs}\ | ||
""" | ||
end | ||
end | ||
|
||
defp format_envs([]), do: nil | ||
|
||
defp format_envs(list) do | ||
Enum.map_join(list, fn {key, value} -> ~s/ENV #{key} "#{value}"\n/ end) | ||
end | ||
|
||
defp encrypt_secrets_to_dockerfile(secrets, hub) do | ||
secrets_map = | ||
for %{name: name, value: value} <- secrets, | ||
into: %{}, | ||
do: {name, value} | ||
|
||
encrypt_to_dockerfile(hub, secrets_map) | ||
end | ||
|
||
defp encrypt_file_systems_to_dockerfile(file_systems, hub) do | ||
file_systems = | ||
for file_system <- file_systems do | ||
file_system | ||
|> Livebook.FileSystem.dump() | ||
|> Map.put_new(:type, Livebook.FileSystems.type(file_system)) | ||
end | ||
|
||
encrypt_to_dockerfile(hub, file_systems) | ||
end | ||
|
||
defp encrypt_to_dockerfile(hub, data) do | ||
secret_key = Livebook.Teams.derive_key(hub.teams_key) | ||
|
||
data | ||
|> Jason.encode!() | ||
|> Livebook.Teams.encrypt(secret_key) | ||
end | ||
|
||
defp used_secrets(config, hub, secrets, hub_secrets) do | ||
if config.deploy_all do | ||
hub_secrets | ||
else | ||
for {_, secret} <- secrets, secret.hub_id == hub.id, do: secret | ||
end | ||
end | ||
|
||
defp zta_configured?(config) do | ||
config.zta_provider != nil and config.zta_key != nil | ||
end | ||
|
||
@doc """ | ||
Returns a list of Dockerfile-related warnings. | ||
The returned messages may include HTML. | ||
""" | ||
@spec warnings( | ||
config(), | ||
Hubs.Provider.t(), | ||
list(Livebook.Secrets.Secret.t()), | ||
Livebook.Notebook.AppSettings.t(), | ||
list(Livebook.Notebook.file_entry()), | ||
list(Livebook.Session.Data.secrets()) | ||
) :: list(String.t()) | ||
def warnings(config, hub, hub_secrets, app_settings, file_entries, secrets) do | ||
common_warnings = | ||
[ | ||
if Livebook.Session.Data.session_secrets(secrets, hub.id) != [] do | ||
"The notebook uses session secrets, but those are not available to deployed apps." <> | ||
" Convert them to Hub secrets instead." | ||
end | ||
] | ||
|
||
hub_warnings = | ||
case Hubs.Provider.type(hub) do | ||
"personal" -> | ||
[ | ||
if used_secrets(config, hub, secrets, hub_secrets) != [] do | ||
"You are deploying an app with secrets and the secrets are included in the Dockerfile" <> | ||
" as environment variables. If someone else deploys this app, they must also set the" <> | ||
" same secrets. Use Livebook Teams to automatically encrypt and synchronize secrets" <> | ||
" across your team and deployments." | ||
end, | ||
if module = find_hub_file_system(file_entries) do | ||
name = LivebookWeb.FileSystemHelpers.file_system_name(module) | ||
|
||
"The #{name} file storage, defined in your personal hub, will not be available in the Docker image." <> | ||
" You must either download all references as attachments or use Livebook Teams to automatically" <> | ||
" encrypt and synchronize file storages across your team and deployments." | ||
end, | ||
if app_settings.access_type == :public do | ||
teams_link = | ||
~s{<a class="font-medium underline text-gray-900 hover:no-underline" href="https://livebook.dev/teams?ref=LivebookApp" target="_blank">Livebook Teams</a>} | ||
|
||
"This app has no password configuration and anyone with access to the server will be able" <> | ||
" to use it. You may either configure a password or use #{teams_link} to add Zero Trust Authentication" <> | ||
" to your deployed notebooks." | ||
end | ||
] | ||
|
||
"team" -> | ||
[ | ||
if app_settings.access_type == :public and not zta_configured?(config) do | ||
"This app has no password configuration and anyone with access to the server will be able" <> | ||
" to use it. You may either configure a password or configure Zero Trust Authentication." | ||
end | ||
] | ||
end | ||
|
||
Enum.reject(common_warnings ++ hub_warnings, &is_nil/1) | ||
end | ||
|
||
defp find_hub_file_system(file_entries) do | ||
Enum.find_value(file_entries, fn entry -> | ||
entry.type == :file && entry.file.file_system_module != FileSystem.Local && | ||
entry.file.file_system_module | ||
end) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.