Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ config :console, Console.Cron.Scheduler,
{"15 * * * *", {Console.Deployments.Cron, :prune_vuln_reports, []}},
{"*/15 * * * *", {Console.Deployments.Cron, :pr_governance, []}},
{"15 3 * * *", {Console.Deployments.Cron, :prune_dangling_templates, []}},
{"20 3 * * *", {Console.Deployments.Cron, :prune_dangling_policy_bindings, []}},
{"30 3 * * *", {Console.Deployments.Cron, :prune_insight_components, []}},
{"0 4 * * *", {Console.Deployments.Cron, :prune_helm_repositories, []}},
{"0 5 * * *", {Console.Deployments.Cron, :prune_agent_run_repositories, []}},
Expand Down
15 changes: 15 additions & 0 deletions lib/console/deployments/cron.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule Console.Deployments.Cron do
AppNotification,
Alert,
ClusterAuditLog,
PolicyBinding,
PolicyConstraint,
VulnerabilityReport,
ServiceTemplate,
Expand Down Expand Up @@ -298,6 +299,20 @@ defmodule Console.Deployments.Cron do
|> Stream.run()
end

def prune_dangling_policy_bindings() do
PolicyBinding.dangling()
|> PolicyBinding.ordered(asc: :id)
|> Repo.stream(method: :keyset)
|> Stream.chunk_every(100)
|> Stream.each(fn bindings ->
ids = Enum.map(bindings, & &1.id)
Logger.info "pruning #{length(ids)} dangling policy bindings"
PolicyBinding.for_ids(ids)
|> Repo.delete_all(timeout: 10_000)
end)
|> Stream.run()
end

def add_ignore_crds(search) do
Service.search(search)
|> Repo.stream(method: :keyset)
Expand Down
28 changes: 28 additions & 0 deletions lib/console/schema/policy_binding.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,34 @@ defmodule Console.Schema.PolicyBinding do
timestamps()
end

def dangling(query \\ __MODULE__) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slightly scary thing about this is if another table is added that sets bindings, it's pretty likely it'll be forgotten that a clause is needed here to validate. Ideally we have some static check that confirms this, i just don't know how to organize it.

from(p in query,
where:
fragment("NOT EXISTS(SELECT 1 FROM services WHERE write_policy_id = ? OR read_policy_id = ?)", p.policy_id, p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM clusters WHERE write_policy_id = ? OR read_policy_id = ?)", p.policy_id, p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM projects WHERE write_policy_id = ? OR read_policy_id = ?)", p.policy_id, p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM pipelines WHERE write_policy_id = ? OR read_policy_id = ?)", p.policy_id, p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM stacks WHERE write_policy_id = ? OR read_policy_id = ?)", p.policy_id, p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM catalogs WHERE write_policy_id = ? OR read_policy_id = ? OR create_policy_id = ?)", p.policy_id, p.policy_id, p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM deployment_settings WHERE write_policy_id = ? OR read_policy_id = ? OR create_policy_id = ? OR git_policy_id = ?)", p.policy_id, p.policy_id, p.policy_id, p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM pr_automations WHERE write_policy_id = ? OR create_policy_id = ?)", p.policy_id, p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM flows WHERE write_policy_id = ? OR read_policy_id = ?)", p.policy_id, p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM cluster_providers WHERE write_policy_id = ? OR read_policy_id = ?)", p.policy_id, p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM mcp_servers WHERE write_policy_id = ? OR read_policy_id = ?)", p.policy_id, p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM oidc_providers WHERE bindings_id = ? OR write_policy_id = ?)", p.policy_id, p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM personas WHERE bindings_id = ?)", p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM watchman_users WHERE assume_policy_id = ?)", p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM pull_requests WHERE notifications_policy_id = ?)", p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM cloud_connections WHERE read_policy_id = ?)", p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM compliance_report_generators WHERE read_policy_id = ?)", p.policy_id) and
fragment("NOT EXISTS(SELECT 1 FROM agent_runtimes WHERE create_policy_id = ?)", p.policy_id)
)
end

def ordered(query \\ __MODULE__, order \\ [asc: :id]) do
from(p in query, order_by: ^order)
end

@valid ~w(user_id group_id policy_id)a

def changeset(model, attrs \\ %{}) do
Expand Down
27 changes: 27 additions & 0 deletions test/console/deployments/cron_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -349,4 +349,31 @@ defmodule Console.Deployments.CronTest do
assert refetch(keep)
end
end

describe "#prune_dangling_policy_bindings/0" do
test "it will prune dangling policy bindings" do
user = insert(:user)

# Create a project with write_bindings - these should be kept
project = insert(:project, write_bindings: [%{user_id: user.id}])
%{write_bindings: [kept_binding]} = Console.Repo.preload(project, [:write_bindings])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i actually don't think this preload should be needed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it's needed if we want a kept_binding that we can run assertions against after the cronjob


# Create orphaned policy bindings with random policy_ids that don't exist anywhere
orphaned = for _ <- 1..3 do
%Console.Schema.PolicyBinding{}
|> Console.Schema.PolicyBinding.changeset(%{
policy_id: Ecto.UUID.generate(),
user_id: user.id
})
|> Console.Repo.insert!()
end
:ok = Cron.prune_dangling_policy_bindings()

# Referenced binding should still exist
assert refetch(kept_binding)

# Orphaned bindings should be deleted
for binding <- orphaned, do: refute refetch(binding)
end
end
end
Loading