CRITICAL: - Membership: remove :role/:org_id/:user_id from mass-assignment cast; use explicit create_changeset/4 and role_update_changeset/2 - GraphQL member resolver: add authorize_invite/3 checking admin/owner role and role hierarchy - REST invitations controller: add auth check for invite creation HIGH: - ApiToken: remove :organization_id/:user_id from cast; use explicit create_changeset/4 MEDIUM: - Move 8 LiveView Ecto queries into context modules (Admin, Alerts, Coverages, OnCall, Snmp) - Replace Process.put/Process.get with socket assigns for unresolved_alert_count (user_auth + layouts + 50 templates) - Add batch get_utilization_for_interfaces/1 to eliminate N+1 capacity queries in device show - Replace Process.sleep with Process.monitor/assert_receive or Process.send_after in 5 test files LOW: - Add handle_params/3 to UserResetPasswordLive, UserRegistrationLive, StatusPageLive - Remove redundant Repo.preload calls; add preloads to list_site_devices/1 - Fix @impl annotations and credo nesting warnings
552 lines
22 KiB
Text
552 lines
22 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
active_page="agents"
|
|
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
|
|
>
|
|
<.header>
|
|
{@page_title}
|
|
<:subtitle>{t("Manage remote agents for local SNMP polling")}</:subtitle>
|
|
<:actions>
|
|
<.button phx-click={JS.show(to: "#new-agent-form")} variant="primary">
|
|
<.icon name="hero-plus" class="h-5 w-5" /> New Agent
|
|
</.button>
|
|
</:actions>
|
|
</.header>
|
|
|
|
<!-- New Agent Form -->
|
|
<div id="new-agent-form" class="hidden mt-6">
|
|
<div class="bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
{t("Create New Agent")}
|
|
</h3>
|
|
<.form id="create-agent-form" for={@agent_form} phx-submit="create_agent">
|
|
<div class="space-y-4">
|
|
<.input
|
|
field={@agent_form[:name]}
|
|
type="text"
|
|
label={t("Agent Name")}
|
|
placeholder="e.g., Datacenter A"
|
|
required
|
|
/>
|
|
<%= if @is_superuser do %>
|
|
<.input
|
|
field={@agent_form[:is_cloud_poller]}
|
|
type="checkbox"
|
|
label={t("Cloud Poller (Application-wide agent, not tied to this organization)")}
|
|
/>
|
|
<% end %>
|
|
<div class="flex gap-3">
|
|
<.button type="submit" variant="primary">
|
|
{t("Create Agent")}
|
|
</.button>
|
|
<button
|
|
type="button"
|
|
phx-click={JS.hide(to: "#new-agent-form")}
|
|
class="px-3 py-2 text-sm font-semibold text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
|
|
>
|
|
{t("Cancel")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</.form>
|
|
</div>
|
|
</div>
|
|
|
|
<%= if !@has_agents do %>
|
|
<div class="text-center py-16">
|
|
<.icon name="hero-server" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
|
|
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">{t("No agents")}</h3>
|
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
{t("Get started by creating your first remote agent.")}
|
|
</p>
|
|
<div class="mt-6">
|
|
<.button phx-click={JS.show(to: "#new-agent-form")} variant="primary">
|
|
<.icon name="hero-plus" class="h-5 w-5" /> New Agent
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<div class="mt-6 overflow-x-auto">
|
|
<.table
|
|
id="agents-table"
|
|
rows={@streams.agent_tokens}
|
|
row_link={fn {_id, agent} -> ~p"/agents/#{agent.id}" end}
|
|
>
|
|
<:col :let={{_id, agent}} label={t("Name")}>
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-medium">{agent.name}</span>
|
|
<%= if agent.is_cloud_poller do %>
|
|
<span class="inline-flex items-center gap-1 rounded-full bg-purple-50 dark:bg-purple-900/30 px-2 py-0.5 text-xs font-medium text-purple-700 dark:text-purple-300 ring-1 ring-inset ring-purple-600/20 dark:ring-purple-400/30">
|
|
<.icon name="hero-cloud" class="h-3 w-3" /> Cloud
|
|
</span>
|
|
<% end %>
|
|
<%= if agent.allow_remote_debug do %>
|
|
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 dark:bg-amber-900/30 px-2 py-0.5 text-xs font-medium text-amber-700 dark:text-amber-300 ring-1 ring-inset ring-amber-600/20 dark:ring-amber-400/30">
|
|
<.icon name="hero-bug-ant" class="h-3 w-3" /> Debug
|
|
</span>
|
|
<% end %>
|
|
</div>
|
|
</:col>
|
|
|
|
<:col :let={{_id, agent}} label={t("Status")}>
|
|
<% {status, label} = agent_status(agent) %>
|
|
<span class={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
|
|
<span class={"h-1.5 w-1.5 rounded-full #{status_dot_class(status)}"} />
|
|
{label}
|
|
</span>
|
|
</:col>
|
|
|
|
<:col :let={{_id, agent}} label={t("Device")}>
|
|
<% counts = Map.get(@device_counts, agent.id, %{direct: 0, total: 0}) %>
|
|
<div class="text-sm text-gray-900 dark:text-white">
|
|
{counts.total} total
|
|
</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
{counts.direct} direct
|
|
<%= if counts.total > counts.direct do %>
|
|
· {counts.total - counts.direct} inherited
|
|
<% end %>
|
|
</div>
|
|
</:col>
|
|
|
|
<:col :let={{_id, agent}} label={t("Last Seen")}>
|
|
<div class="text-sm text-gray-900 dark:text-white">
|
|
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} now={@now} />
|
|
</div>
|
|
<%= if agent.last_seen_at do %>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} format="absolute" />
|
|
</div>
|
|
<% end %>
|
|
<%= if agent.last_ip do %>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-mono">
|
|
{agent.last_ip}
|
|
</div>
|
|
<% end %>
|
|
</:col>
|
|
|
|
<:col :let={{_id, agent}} label={t("Version")}>
|
|
<%= if agent.metadata["version"] do %>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
|
|
{format_agent_version(agent.metadata["version"])}
|
|
</div>
|
|
<% end %>
|
|
</:col>
|
|
|
|
<:col :let={{_id, agent}} label={t("Created")}>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
|
{ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)}
|
|
</div>
|
|
</:col>
|
|
|
|
<:action :let={{_id, agent}}>
|
|
<%= if agent.enabled do %>
|
|
<div class="flex items-center gap-3">
|
|
<.button
|
|
id={"show-setup-#{agent.id}"}
|
|
type="button"
|
|
phx-click="show_setup"
|
|
phx-value-id={agent.id}
|
|
variant="secondary"
|
|
>
|
|
{t("View Setup")}
|
|
</.button>
|
|
<.button
|
|
id={"delete-agent-#{agent.id}"}
|
|
type="button"
|
|
phx-click="delete_agent"
|
|
phx-value-id={agent.id}
|
|
data-confirm={
|
|
t(
|
|
"Are you sure you want to delete this agent? All device assignments will be removed, and devices will fall back to site/organization defaults or cloud polling."
|
|
)
|
|
}
|
|
variant="danger"
|
|
>
|
|
{t("Delete")}
|
|
</.button>
|
|
</div>
|
|
<% else %>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">Disabled</span>
|
|
<% end %>
|
|
</:action>
|
|
</.table>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%!-- Cloud Pollers Section (Superadmin Only) --%>
|
|
<%= if @is_superuser && @has_cloud_pollers do %>
|
|
<div class="mt-12">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
{t("Cloud Pollers")}
|
|
</h2>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
|
{t(
|
|
"Application-wide agents that can poll devices for any organization. Only visible and manageable by superadmins."
|
|
)}
|
|
</p>
|
|
<div class="overflow-x-auto">
|
|
<.table
|
|
id="cloud-pollers-table"
|
|
rows={@streams.cloud_pollers}
|
|
row_link={fn {_id, agent} -> ~p"/agents/#{agent.id}" end}
|
|
>
|
|
<:col :let={{_id, agent}} label={t("Name")}>
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-medium">{agent.name}</span>
|
|
<%= if agent.id == @global_default_cloud_poller_id do %>
|
|
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 dark:bg-green-900/30 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300 ring-1 ring-inset ring-green-600/20 dark:ring-green-400/30">
|
|
<.icon name="hero-check-circle" class="h-3 w-3" /> Default
|
|
</span>
|
|
<% end %>
|
|
<%= if agent.allow_remote_debug do %>
|
|
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 dark:bg-amber-900/30 px-2 py-0.5 text-xs font-medium text-amber-700 dark:text-amber-300 ring-1 ring-inset ring-amber-600/20 dark:ring-amber-400/30">
|
|
<.icon name="hero-bug-ant" class="h-3 w-3" /> Debug
|
|
</span>
|
|
<% end %>
|
|
</div>
|
|
</:col>
|
|
|
|
<:col :let={{_id, agent}} label={t("Status")}>
|
|
<% {status, label} = agent_status(agent) %>
|
|
<span class={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
|
|
<span class={"h-1.5 w-1.5 rounded-full #{status_dot_class(status)}"} />
|
|
{label}
|
|
</span>
|
|
</:col>
|
|
|
|
<:col :let={{_id, agent}} label={t("Device")}>
|
|
<% counts = Map.get(@cloud_poller_counts, agent.id, %{direct: 0, total: 0}) %>
|
|
<div class="text-sm text-gray-900 dark:text-white">
|
|
{counts.total} total
|
|
</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
{counts.direct} direct
|
|
<%= if counts.total > counts.direct do %>
|
|
· {counts.total - counts.direct} inherited
|
|
<% end %>
|
|
</div>
|
|
</:col>
|
|
|
|
<:col :let={{_id, agent}} label={t("Last Seen")}>
|
|
<div class="text-sm text-gray-900 dark:text-white">
|
|
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} now={@now} />
|
|
</div>
|
|
<%= if agent.last_seen_at do %>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} format="absolute" />
|
|
</div>
|
|
<% end %>
|
|
<%= if agent.last_ip do %>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-mono">
|
|
{agent.last_ip}
|
|
</div>
|
|
<% end %>
|
|
</:col>
|
|
|
|
<:col :let={{_id, agent}} label={t("Version")}>
|
|
<%= if agent.metadata["version"] do %>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
|
|
{format_agent_version(agent.metadata["version"])}
|
|
</div>
|
|
<% end %>
|
|
</:col>
|
|
|
|
<:col :let={{_id, agent}} label={t("Created")}>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
|
{ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)}
|
|
</div>
|
|
</:col>
|
|
|
|
<:action :let={{_id, agent}}>
|
|
<%= if agent.enabled do %>
|
|
<div class="flex items-center gap-3">
|
|
<.button
|
|
id={"show-setup-#{agent.id}"}
|
|
type="button"
|
|
phx-click="show_setup"
|
|
phx-value-id={agent.id}
|
|
variant="secondary"
|
|
>
|
|
{t("View Setup")}
|
|
</.button>
|
|
<.button
|
|
id={"delete-agent-#{agent.id}"}
|
|
type="button"
|
|
phx-click="delete_agent"
|
|
phx-value-id={agent.id}
|
|
data-confirm={
|
|
t(
|
|
"Are you sure you want to delete this cloud poller? This will affect all organizations using it."
|
|
)
|
|
}
|
|
variant="danger"
|
|
>
|
|
{t("Delete")}
|
|
</.button>
|
|
</div>
|
|
<% else %>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">Disabled</span>
|
|
<% end %>
|
|
</:action>
|
|
</.table>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%!-- Global Default Cloud Poller (Superadmin Only) --%>
|
|
<%= if @is_superuser do %>
|
|
<div class="mt-8">
|
|
<div class="bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800/30 rounded-lg p-6">
|
|
<h3 class="text-lg font-semibold text-amber-900 dark:text-amber-200 mb-2">
|
|
{t("Global Default Cloud Poller")}
|
|
</h3>
|
|
<p class="text-sm text-amber-700 dark:text-amber-300 mb-4">
|
|
{t(
|
|
"Fallback agent for organizations without a default agent configured. Devices with no assignment at any level will use this agent."
|
|
)}
|
|
</p>
|
|
|
|
<.form
|
|
id="global-default-form"
|
|
for={%{}}
|
|
phx-change="update_selected_global_default"
|
|
class="max-w-md"
|
|
>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
|
|
{t("Select Global Default")}
|
|
</label>
|
|
<select
|
|
name="agent_token_id"
|
|
value={@selected_global_default}
|
|
class="block w-full rounded-md border-amber-300 dark:border-amber-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow-sm focus:border-amber-500 focus:ring-amber-500 sm:text-sm"
|
|
>
|
|
<option value="">No global default (direct Phoenix polling)</option>
|
|
<%= for cloud_poller <- @cloud_pollers_list do %>
|
|
<option value={cloud_poller.id}>
|
|
{cloud_poller.name}
|
|
<%= if cloud_poller.id == @global_default_cloud_poller_id do %>
|
|
(Current)
|
|
<% end %>
|
|
</option>
|
|
<% end %>
|
|
</select>
|
|
</div>
|
|
|
|
<%= if @global_default_cloud_poller_id do %>
|
|
<% selected_poller =
|
|
Enum.find(@cloud_pollers_list, &(&1.id == @global_default_cloud_poller_id)) %>
|
|
<%= if selected_poller do %>
|
|
<p class="text-xs text-amber-600 dark:text-amber-400">
|
|
<.icon name="hero-check-circle" class="h-4 w-4 inline" /> Currently using:
|
|
<span class="font-medium">{selected_poller.name}</span>
|
|
</p>
|
|
<% else %>
|
|
<p class="text-xs text-red-600 dark:text-red-400">
|
|
<.icon name="hero-exclamation-triangle" class="h-4 w-4 inline" />
|
|
{t("Warning: Selected agent no longer exists. Please choose a new agent.")}
|
|
</p>
|
|
<% end %>
|
|
<% else %>
|
|
<p class="text-xs text-amber-600 dark:text-amber-400">
|
|
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
|
|
{t(
|
|
"No global default configured. Devices without assignments will use direct Phoenix cluster polling."
|
|
)}
|
|
</p>
|
|
<% end %>
|
|
|
|
<div class="flex gap-3">
|
|
<.button
|
|
id="save-global-default"
|
|
type="button"
|
|
phx-click="save_global_default"
|
|
variant="primary"
|
|
disabled={@selected_global_default == (@global_default_cloud_poller_id || "")}
|
|
>
|
|
<.icon name="hero-check" class="h-4 w-4" /> Save Changes
|
|
</.button>
|
|
<%= if @selected_global_default != (@global_default_cloud_poller_id || "") do %>
|
|
<button
|
|
type="button"
|
|
phx-click={
|
|
JS.push("update_selected_global_default",
|
|
value: %{agent_token_id: @global_default_cloud_poller_id || ""}
|
|
)
|
|
}
|
|
class="px-3 py-2 text-sm font-semibold text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
|
|
>
|
|
{t("Cancel")}
|
|
</button>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</.form>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- Token Display Modal -->
|
|
<%= if @show_token_modal && @new_token do %>
|
|
<div
|
|
id="token-modal"
|
|
class="fixed inset-0 z-50 overflow-y-auto"
|
|
phx-mounted={JS.show()}
|
|
>
|
|
<div class="flex min-h-screen items-center justify-center p-4">
|
|
<div
|
|
class="fixed inset-0 bg-gray-500/75 dark:bg-gray-950/75 transition-opacity"
|
|
phx-click="close_token_modal"
|
|
>
|
|
</div>
|
|
|
|
<div class="relative bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-2xl w-full">
|
|
<div class="p-6">
|
|
<div class="flex items-start justify-between mb-4">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{t("Agent Setup Instructions")}
|
|
</h3>
|
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
{t("Use the token and Docker Compose configuration below to deploy this agent.")}
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
phx-click="close_token_modal"
|
|
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
>
|
|
<.icon name="hero-x-mark" class="h-6 w-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
{t("Agent Name")}
|
|
</label>
|
|
<div class="text-base font-mono text-gray-900 dark:text-white">
|
|
{@new_token.agent_token.name}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
{t("Authentication Token")}
|
|
</label>
|
|
<div class="relative">
|
|
<input
|
|
type="text"
|
|
readonly
|
|
value={String.trim(@new_token.token)}
|
|
class="w-full font-mono text-sm bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded px-3 py-2 pr-28 text-gray-900 dark:text-white select-all"
|
|
id="agent-token-input"
|
|
onclick="this.select()"
|
|
/>
|
|
<button
|
|
type="button"
|
|
phx-hook="CopyToClipboard"
|
|
phx-update="ignore"
|
|
data-target="#agent-token-input"
|
|
id={"copy-token-#{@new_token.agent_token.id}"}
|
|
class="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 rounded shadow-sm transition-colors flex items-center gap-1.5"
|
|
>
|
|
<.icon name="hero-clipboard" class="h-3.5 w-3.5" />
|
|
<span>{t("Copy")}</span>
|
|
</button>
|
|
</div>
|
|
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
|
{t("Click the token to select all, or use the Copy button")}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-2">
|
|
{t("Docker Compose Setup with Auto-Updates")}
|
|
</h4>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 mb-3">
|
|
{t(
|
|
"This configuration includes Watchtower for automatic agent updates. The agent will automatically update to the latest version every 12 hours."
|
|
)}
|
|
</p>
|
|
<pre class="text-xs font-mono text-gray-600 dark:text-gray-400 overflow-x-auto"><code>services:
|
|
towerops-agent:
|
|
image: {@agent_image}
|
|
container_name: towerops-agent
|
|
restart: unless-stopped
|
|
environment:
|
|
- TOWEROPS_API_URL={url(@socket, ~p"/")}
|
|
- TOWEROPS_AGENT_TOKEN={@new_token.token}
|
|
labels:
|
|
- "com.centurylinklabs.watchtower.enable=true"
|
|
- "com.centurylinklabs.watchtower.scope=towerops"
|
|
|
|
watchtower:
|
|
image: containrrr/watchtower:latest
|
|
container_name: towerops-watchtower
|
|
restart: unless-stopped
|
|
environment:
|
|
# Check for updates every 12 hours (43200 seconds)
|
|
- WATCHTOWER_POLL_INTERVAL=43200
|
|
- WATCHTOWER_LABEL_ENABLE=true
|
|
- WATCHTOWER_SCOPE=towerops
|
|
- WATCHTOWER_CLEANUP=true
|
|
- WATCHTOWER_LOG_LEVEL=info
|
|
# Use latest Docker API version
|
|
- DOCKER_API_VERSION=1.44
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock</code></pre>
|
|
<button
|
|
type="button"
|
|
phx-hook="CopyToClipboard"
|
|
phx-update="ignore"
|
|
data-target="#docker-compose-example"
|
|
id={"copy-docker-compose-#{@new_token.agent_token.id}"}
|
|
class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium text-white bg-gray-700 hover:bg-gray-600 dark:bg-gray-600 dark:hover:bg-gray-500 rounded transition-colors"
|
|
>
|
|
<.icon name="hero-clipboard" class="h-3 w-3" /> Copy to clipboard
|
|
</button>
|
|
<textarea id="docker-compose-example" class="sr-only" readonly>services:
|
|
towerops-agent:
|
|
image: {@agent_image}
|
|
container_name: towerops-agent
|
|
restart: unless-stopped
|
|
environment:
|
|
- TOWEROPS_API_URL={url(@socket, ~p"/")}
|
|
- TOWEROPS_AGENT_TOKEN={@new_token.token}
|
|
labels:
|
|
- "com.centurylinklabs.watchtower.enable=true"
|
|
- "com.centurylinklabs.watchtower.scope=towerops"
|
|
|
|
watchtower:
|
|
image: containrrr/watchtower:latest
|
|
container_name: towerops-watchtower
|
|
restart: unless-stopped
|
|
environment:
|
|
# Check for updates every 12 hours
|
|
- WATCHTOWER_POLL_INTERVAL=43200
|
|
- WATCHTOWER_LABEL_ENABLE=true
|
|
- WATCHTOWER_CLEANUP=true
|
|
- WATCHTOWER_LOG_LEVEL=info
|
|
# Use latest Docker API version
|
|
- DOCKER_API_VERSION=1.44
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 flex justify-end">
|
|
<.button id="close-token-modal" phx-click="close_token_modal" variant="primary">
|
|
{t("Close")}
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</Layouts.authenticated>
|