at 25.11-pre 29 kB view raw
1defmodule Provision do 2 alias Domain.{Repo, Accounts, Auth, Actors, Resources, Tokens, Gateways, Relays, Policies} 3 require Logger 4 5 # UUID Mapping handling 6 defmodule UuidMapping do 7 @mapping_file "provision-uuids.json" 8 9 # Loads the mapping from file 10 def load do 11 mappings = case File.read(@mapping_file) do 12 {:ok, content} -> 13 case Jason.decode(content) do 14 {:ok, mapping} -> mapping 15 _ -> %{"accounts" => %{}} 16 end 17 18 _ -> %{"accounts" => %{}} 19 end 20 Process.put(:uuid_mappings, mappings) 21 mappings 22 end 23 24 # Saves the current mapping (defaulting to the one in the process dictionary) 25 def save(mapping \\ Process.get(:uuid_mappings)) do 26 File.write!(@mapping_file, Jason.encode!(mapping)) 27 end 28 29 # Retrieves the account-level mapping from a given mapping (or from Process) 30 def get_account(mapping \\ Process.get(:uuid_mappings), account_slug) do 31 get_in(mapping, ["accounts", account_slug]) || %{} 32 end 33 34 # Retrieves the entity mapping for a specific account and type 35 def get_entities(mapping \\ Process.get(:uuid_mappings), account_slug, type) do 36 get_in(mapping, ["accounts", account_slug, type]) || %{} 37 end 38 39 # Retrieves an entity mapping for a specific account, type and external_id 40 def get_entity(mapping \\ Process.get(:uuid_mappings), account_slug, type, external_id) do 41 get_in(mapping, ["accounts", account_slug, type, external_id]) 42 end 43 44 # Updates (or creates) the account UUID mapping and stores it in the process dictionary. 45 def update_account(account_slug, uuid) do 46 mapping = Process.get(:uuid_mappings) || load() 47 mapping = ensure_account_exists(mapping, account_slug) 48 mapping = put_in(mapping, ["accounts", account_slug, "id"], uuid) 49 Process.put(:uuid_mappings, mapping) 50 mapping 51 end 52 53 # Ensures that the given account exists in the mapping. 54 def ensure_account_exists(mapping, account_slug) do 55 if not Map.has_key?(mapping["accounts"], account_slug) do 56 put_in(mapping, ["accounts", account_slug], %{}) 57 else 58 mapping 59 end 60 end 61 62 # Updates (or creates) the mapping for entities of a given type for the account. 63 def update_entities(account_slug, type, new_entries) do 64 mapping = Process.get(:uuid_mappings) || load() 65 mapping = ensure_account_exists(mapping, account_slug) 66 current = get_entities(mapping, account_slug, type) 67 mapping = put_in(mapping, ["accounts", account_slug, type], Map.merge(current, new_entries)) 68 Process.put(:uuid_mappings, mapping) 69 mapping 70 end 71 72 # Removes an entire account from the mapping. 73 def remove_account(account_slug) do 74 mapping = Process.get(:uuid_mappings) || load() 75 mapping = update_in(mapping, ["accounts"], fn accounts -> 76 Map.delete(accounts, account_slug) 77 end) 78 Process.put(:uuid_mappings, mapping) 79 mapping 80 end 81 82 # Removes a specific entity mapping for the account. 83 def remove_entity(account_slug, type, key) do 84 mapping = Process.get(:uuid_mappings) || load() 85 mapping = update_in(mapping, ["accounts", account_slug, type], fn entities -> 86 Map.delete(entities || %{}, key) 87 end) 88 Process.put(:uuid_mappings, mapping) 89 mapping 90 end 91 end 92 93 defp resolve_references(value) when is_map(value) do 94 Enum.into(value, %{}, fn {k, v} -> {k, resolve_references(v)} end) 95 end 96 97 defp resolve_references(value) when is_list(value) do 98 Enum.map(value, &resolve_references/1) 99 end 100 101 defp resolve_references(value) when is_binary(value) do 102 Regex.replace(~r/\{env:([^}]+)\}/, value, fn _, var -> 103 System.get_env(var) || raise "Environment variable #{var} not set" 104 end) 105 end 106 107 defp resolve_references(value), do: value 108 109 defp atomize_keys(map) when is_map(map) do 110 Enum.into(map, %{}, fn {k, v} -> 111 { 112 if(is_binary(k), do: String.to_atom(k), else: k), 113 if(is_map(v), do: atomize_keys(v), else: v) 114 } 115 end) 116 end 117 118 defp cleanup_account(uuid) do 119 case Accounts.fetch_account_by_id_or_slug(uuid) do 120 {:ok, value} when value.deleted_at == nil -> 121 Logger.info("Deleting removed account #{value.slug}") 122 value |> Ecto.Changeset.change(%{ deleted_at: DateTime.utc_now() }) |> Repo.update!() 123 _ -> :ok 124 end 125 end 126 127 defp cleanup_actor(uuid, subject) do 128 case Actors.fetch_actor_by_id(uuid, subject) do 129 {:ok, value} -> 130 Logger.info("Deleting removed actor #{value.name}") 131 {:ok, _} = Actors.delete_actor(value, subject) 132 _ -> :ok 133 end 134 end 135 136 defp cleanup_provider(uuid, subject) do 137 case Auth.fetch_provider_by_id(uuid, subject) do 138 {:ok, value} -> 139 Logger.info("Deleting removed provider #{value.name}") 140 {:ok, _} = Auth.delete_provider(value, subject) 141 _ -> :ok 142 end 143 end 144 145 defp cleanup_gateway_group(uuid, subject) do 146 case Gateways.fetch_group_by_id(uuid, subject) do 147 {:ok, value} -> 148 Logger.info("Deleting removed gateway group #{value.name}") 149 {:ok, _} = Gateways.delete_group(value, subject) 150 _ -> :ok 151 end 152 end 153 154 defp cleanup_relay_group(uuid, subject) do 155 case Relays.fetch_group_by_id(uuid, subject) do 156 {:ok, value} -> 157 Logger.info("Deleting removed relay group #{value.name}") 158 {:ok, _} = Relays.delete_group(value, subject) 159 _ -> :ok 160 end 161 end 162 163 defp cleanup_actor_group(uuid, subject) do 164 case Actors.fetch_group_by_id(uuid, subject) do 165 {:ok, value} -> 166 Logger.info("Deleting removed actor group #{value.name}") 167 {:ok, _} = Actors.delete_group(value, subject) 168 _ -> :ok 169 end 170 end 171 172 # Fetch resource by uuid, but follow the chain of replacements if any 173 defp fetch_resource(uuid, subject) do 174 case Resources.fetch_resource_by_id(uuid, subject) do 175 {:ok, resource} when resource.replaced_by_resource_id != nil -> fetch_resource(resource.replaced_by_resource_id, subject) 176 v -> v 177 end 178 end 179 180 defp cleanup_resource(uuid, subject) do 181 case fetch_resource(uuid, subject) do 182 {:ok, value} when value.deleted_at == nil -> 183 Logger.info("Deleting removed resource #{value.name}") 184 {:ok, _} = Resources.delete_resource(value, subject) 185 _ -> :ok 186 end 187 end 188 189 # Fetch policy by uuid, but follow the chain of replacements if any 190 defp fetch_policy(uuid, subject) do 191 case Policies.fetch_policy_by_id(uuid, subject) do 192 {:ok, policy} when policy.replaced_by_policy_id != nil -> fetch_policy(policy.replaced_by_policy_id, subject) 193 v -> v 194 end 195 end 196 197 defp cleanup_policy(uuid, subject) do 198 case fetch_policy(uuid, subject) do 199 {:ok, value} when value.deleted_at == nil -> 200 Logger.info("Deleting removed policy #{value.description}") 201 {:ok, _} = Policies.delete_policy(value, subject) 202 _ -> :ok 203 end 204 end 205 206 defp cleanup_entity_type(account_slug, entity_type, cleanup_fn, temp_admin_subject) do 207 # Get mapping for this entity type 208 existing_entities = UuidMapping.get_entities(account_slug, entity_type) 209 # Get current entities from account data 210 current_entities = Process.get(:current_entities) 211 # Determine which ones to remove 212 removed_entity_ids = Map.keys(existing_entities) -- (current_entities[entity_type] || []) 213 214 # Process each entity to remove 215 Enum.each(removed_entity_ids, fn entity_id -> 216 case existing_entities[entity_id] do 217 nil -> :ok 218 uuid -> 219 cleanup_fn.(uuid, temp_admin_subject) 220 UuidMapping.remove_entity(account_slug, entity_type, entity_id) 221 end 222 end) 223 end 224 225 defp collect_current_entities(account_data) do 226 %{ 227 "actors" => Map.keys(account_data["actors"] || %{}), 228 "providers" => Map.keys(account_data["auth"] || %{}), 229 "gateway_groups" => Map.keys(account_data["gatewayGroups"] || %{}), 230 "relay_groups" => Map.keys(account_data["relayGroups"] || %{}), 231 "actor_groups" => Map.keys(account_data["groups"] || %{}) ++ ["everyone"], 232 "resources" => Map.keys(account_data["resources"] || %{}), 233 "policies" => Map.keys(account_data["policies"] || %{}) 234 } 235 end 236 237 defp nil_if_deleted_or_not_found(value) do 238 case value do 239 nil -> nil 240 {:error, :not_found} -> nil 241 {:ok, value} when value.deleted_at != nil -> nil 242 v -> v 243 end 244 end 245 246 defp create_temp_admin(account, email_provider) do 247 temp_admin_actor_email = "firezone-provision@localhost.local" 248 temp_admin_actor_context = %Auth.Context{ 249 type: :browser, 250 user_agent: "Unspecified/0.0", 251 remote_ip: {127, 0, 0, 1}, 252 remote_ip_location_region: "N/A", 253 remote_ip_location_city: "N/A", 254 remote_ip_location_lat: 0.0, 255 remote_ip_location_lon: 0.0 256 } 257 258 {:ok, temp_admin_actor} = 259 Actors.create_actor(account, %{ 260 type: :account_admin_user, 261 name: "Provisioning" 262 }) 263 264 {:ok, temp_admin_actor_email_identity} = 265 Auth.create_identity(temp_admin_actor, email_provider, %{ 266 provider_identifier: temp_admin_actor_email, 267 provider_identifier_confirmation: temp_admin_actor_email 268 }) 269 270 {:ok, temp_admin_actor_token} = 271 Auth.create_token(temp_admin_actor_email_identity, temp_admin_actor_context, "temporarynonce", DateTime.utc_now() |> DateTime.add(1, :hour)) 272 273 {:ok, temp_admin_subject} = 274 Auth.build_subject(temp_admin_actor_token, temp_admin_actor_context) 275 276 {temp_admin_subject, temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token} 277 end 278 279 defp cleanup_temp_admin(temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token, subject) do 280 Logger.info("Cleaning up temporary admin actor") 281 {:ok, _} = Tokens.delete_token(temp_admin_actor_token, subject) 282 {:ok, _} = Auth.delete_identity(temp_admin_actor_email_identity, subject) 283 {:ok, _} = Actors.delete_actor(temp_admin_actor, subject) 284 end 285 286 def provision() do 287 Logger.info("Starting provisioning") 288 289 # Load desired state 290 json_file = "provision-state.json" 291 {:ok, raw_json} = File.read(json_file) 292 {:ok, %{"accounts" => accounts}} = Jason.decode(raw_json) 293 accounts = resolve_references(accounts) 294 295 # Load existing UUID mappings into the process dictionary. 296 UuidMapping.load() 297 298 # Clean up removed accounts first 299 current_account_slugs = Map.keys(accounts) 300 existing_accounts = Map.keys(Process.get(:uuid_mappings)["accounts"]) 301 removed_accounts = existing_accounts -- current_account_slugs 302 303 Enum.each(removed_accounts, fn slug -> 304 if uuid = get_in(Process.get(:uuid_mappings), ["accounts", slug, "id"]) do 305 cleanup_account(uuid) 306 # Remove the account from the UUID mapping. 307 UuidMapping.remove_account(slug) 308 end 309 end) 310 311 multi = Enum.reduce(accounts, Ecto.Multi.new(), fn {slug, account_data}, multi -> 312 account_attrs = atomize_keys(%{ 313 name: account_data["name"], 314 slug: slug, 315 features: Map.get(account_data, "features", %{}), 316 metadata: Map.get(account_data, "metadata", %{}), 317 limits: Map.get(account_data, "limits", %{}) 318 }) 319 320 multi = multi 321 |> Ecto.Multi.run({:account, slug}, fn repo, _changes -> 322 case Accounts.fetch_account_by_id_or_slug(slug) do 323 {:ok, acc} -> 324 Logger.info("Updating existing account #{slug}") 325 updated_acc = acc |> Ecto.Changeset.change(account_attrs) |> repo.update!() 326 {:ok, {:existing, updated_acc}} 327 _ -> 328 Logger.info("Creating new account #{slug}") 329 {:ok, account} = Accounts.create_account(account_attrs) 330 331 Logger.info("Creating internet gateway group") 332 {:ok, internet_site} = Gateways.create_internet_group(account) 333 334 Logger.info("Creating internet resource") 335 {:ok, _internet_resource} = Resources.create_internet_resource(account, internet_site) 336 337 # Store mapping of slug to UUID 338 UuidMapping.update_account(slug, account.id) 339 {:ok, {:new, account}} 340 end 341 end) 342 |> Ecto.Multi.run({:everyone_group, slug}, fn _repo, changes -> 343 case Map.get(changes, {:account, slug}) do 344 {:new, account} -> 345 Logger.info("Creating everyone group for new account") 346 {:ok, actor_group} = Actors.create_managed_group(account, %{name: "Everyone", membership_rules: [%{operator: true}]}) 347 UuidMapping.update_entities(slug, "actor_groups", %{"everyone" => actor_group.id}) 348 {:ok, actor_group} 349 {:existing, _account} -> 350 {:ok, :skipped} 351 end 352 end) 353 |> Ecto.Multi.run({:email_provider, slug}, fn _repo, changes -> 354 case Map.get(changes, {:account, slug}) do 355 {:new, account} -> 356 Logger.info("Creating default email provider for new account") 357 Auth.create_provider(account, %{name: "Email", adapter: :email, adapter_config: %{}}) 358 {:existing, account} -> 359 Auth.Provider.Query.not_disabled() 360 |> Auth.Provider.Query.by_adapter(:email) 361 |> Auth.Provider.Query.by_account_id(account.id) 362 |> Repo.fetch(Auth.Provider.Query, []) 363 end 364 end) 365 |> Ecto.Multi.run({:temp_admin, slug}, fn _repo, changes -> 366 {_, account} = changes[{:account, slug}] 367 email_provider = changes[{:email_provider, slug}] 368 {:ok, create_temp_admin(account, email_provider)} 369 end) 370 371 # Clean up removed entities for this account after we have an admin subject 372 multi = multi 373 |> Ecto.Multi.run({:cleanup_entities, slug}, fn _repo, changes -> 374 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}] 375 376 # Store current entities in process dictionary for our helper function 377 current_entities = collect_current_entities(account_data) 378 Process.put(:current_entities, current_entities) 379 380 # Define entity types and their cleanup functions 381 entity_types = [ 382 {"actors", &cleanup_actor/2}, 383 {"providers", &cleanup_provider/2}, 384 {"gateway_groups", &cleanup_gateway_group/2}, 385 {"relay_groups", &cleanup_relay_group/2}, 386 {"actor_groups", &cleanup_actor_group/2}, 387 {"resources", &cleanup_resource/2}, 388 {"policies", &cleanup_policy/2} 389 ] 390 391 # Clean up each entity type 392 Enum.each(entity_types, fn {entity_type, cleanup_fn} -> 393 cleanup_entity_type(slug, entity_type, cleanup_fn, temp_admin_subject) 394 end) 395 396 {:ok, :cleaned} 397 end) 398 399 # Create or update actors 400 multi = Enum.reduce(account_data["actors"] || %{}, multi, fn {external_id, actor_data}, multi -> 401 actor_attrs = atomize_keys(%{ 402 name: actor_data["name"], 403 type: String.to_atom(actor_data["type"]) 404 }) 405 406 Ecto.Multi.run(multi, {:actor, slug, external_id}, fn _repo, changes -> 407 {_, account} = changes[{:account, slug}] 408 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}] 409 uuid = UuidMapping.get_entity(slug, "actors", external_id) 410 case uuid && Actors.fetch_actor_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do 411 nil -> 412 Logger.info("Creating new actor #{actor_data["name"]}") 413 {:ok, actor} = Actors.create_actor(account, actor_attrs) 414 # Update the mapping without manually handling Process.get/put. 415 UuidMapping.update_entities(slug, "actors", %{external_id => actor.id}) 416 {:ok, {:new, actor}} 417 {:ok, existing_actor} -> 418 Logger.info("Updating existing actor #{actor_data["name"]}") 419 {:ok, updated_act} = Actors.update_actor(existing_actor, actor_attrs, temp_admin_subject) 420 {:ok, {:existing, updated_act}} 421 end 422 end) 423 |> Ecto.Multi.run({:actor_identity, slug, external_id}, fn repo, changes -> 424 email_provider = changes[{:email_provider, slug}] 425 case Map.get(changes, {:actor, slug, external_id}) do 426 {:new, actor} -> 427 Logger.info("Creating actor email identity") 428 Auth.create_identity(actor, email_provider, %{ 429 provider_identifier: actor_data["email"], 430 provider_identifier_confirmation: actor_data["email"] 431 }) 432 {:existing, actor} -> 433 Logger.info("Updating actor email identity") 434 {:ok, identity} = Auth.Identity.Query.not_deleted() 435 |> Auth.Identity.Query.by_actor_id(actor.id) 436 |> Auth.Identity.Query.by_provider_id(email_provider.id) 437 |> Repo.fetch(Auth.Identity.Query, []) 438 439 {:ok, identity |> Ecto.Changeset.change(%{ 440 provider_identifier: actor_data["email"] 441 }) |> repo.update!()} 442 end 443 end) 444 end) 445 446 # Create or update providers 447 multi = Enum.reduce(account_data["auth"] || %{}, multi, fn {external_id, provider_data}, multi -> 448 Ecto.Multi.run(multi, {:provider, slug, external_id}, fn repo, changes -> 449 provider_attrs = %{ 450 name: provider_data["name"], 451 adapter: String.to_atom(provider_data["adapter"]), 452 adapter_config: provider_data["adapter_config"] 453 } 454 455 {_, account} = changes[{:account, slug}] 456 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}] 457 uuid = UuidMapping.get_entity(slug, "providers", external_id) 458 case uuid && Auth.fetch_provider_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do 459 nil -> 460 Logger.info("Creating new provider #{provider_data["name"]}") 461 {:ok, provider} = Auth.create_provider(account, provider_attrs) 462 UuidMapping.update_entities(slug, "providers", %{external_id => provider.id}) 463 {:ok, provider} 464 {:ok, existing} -> 465 Logger.info("Updating existing provider #{provider_data["name"]}") 466 {:ok, existing |> Ecto.Changeset.change(provider_attrs) |> repo.update!()} 467 end 468 end) 469 end) 470 471 # Create or update gateway_groups 472 multi = Enum.reduce(account_data["gatewayGroups"] || %{}, multi, fn {external_id, gateway_group_data}, multi -> 473 Ecto.Multi.run(multi, {:gateway_group, slug, external_id}, fn _repo, changes -> 474 gateway_group_attrs = %{ 475 name: gateway_group_data["name"], 476 tokens: [%{}] 477 } 478 479 {_, account} = changes[{:account, slug}] 480 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}] 481 uuid = UuidMapping.get_entity(slug, "gateway_groups", external_id) 482 case uuid && Gateways.fetch_group_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do 483 nil -> 484 Logger.info("Creating new gateway group #{gateway_group_data["name"]}") 485 gateway_group = account 486 |> Gateways.Group.Changeset.create(gateway_group_attrs, temp_admin_subject) 487 |> Repo.insert!() 488 UuidMapping.update_entities(slug, "gateway_groups", %{external_id => gateway_group.id}) 489 {:ok, gateway_group} 490 {:ok, existing} -> 491 # Nothing to update 492 {:ok, existing} 493 end 494 end) 495 end) 496 497 # Create or update relay_groups 498 multi = Enum.reduce(account_data["relayGroups"] || %{}, multi, fn {external_id, relay_group_data}, multi -> 499 Ecto.Multi.run(multi, {:relay_group, slug, external_id}, fn _repo, changes -> 500 relay_group_attrs = %{ 501 name: relay_group_data["name"] 502 } 503 504 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}] 505 uuid = UuidMapping.get_entity(slug, "relay_groups", external_id) 506 existing_relay_group = uuid && Relays.fetch_group_by_id(uuid, temp_admin_subject) 507 case existing_relay_group do 508 v when v in [nil, {:error, :not_found}] -> 509 Logger.info("Creating new relay group #{relay_group_data["name"]}") 510 {:ok, relay_group} = Relays.create_group(relay_group_attrs, temp_admin_subject) 511 UuidMapping.update_entities(slug, "relay_groups", %{external_id => relay_group.id}) 512 {:ok, relay_group} 513 {:ok, existing} -> 514 # Nothing to update 515 {:ok, existing} 516 end 517 end) 518 end) 519 520 # Create or update actor_groups 521 multi = Enum.reduce(account_data["groups"] || %{}, multi, fn {external_id, actor_group_data}, multi -> 522 Ecto.Multi.run(multi, {:actor_group, slug, external_id}, fn _repo, changes -> 523 actor_group_attrs = %{ 524 name: actor_group_data["name"], 525 type: :static 526 } 527 528 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}] 529 uuid = UuidMapping.get_entity(slug, "actor_groups", external_id) 530 case uuid && Actors.fetch_group_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do 531 nil -> 532 Logger.info("Creating new actor group #{actor_group_data["name"]}") 533 {:ok, actor_group} = Actors.create_group(actor_group_attrs, temp_admin_subject) 534 UuidMapping.update_entities(slug, "actor_groups", %{external_id => actor_group.id}) 535 {:ok, actor_group} 536 {:ok, existing} -> 537 # Nothing to update 538 {:ok, existing} 539 end 540 end) 541 |> Ecto.Multi.run({:actor_group_members, slug, external_id}, fn repo, changes -> 542 {_, account} = changes[{:account, slug}] 543 group_uuid = UuidMapping.get_entity(slug, "actor_groups", external_id) 544 545 memberships = 546 Actors.Membership.Query.all() 547 |> Actors.Membership.Query.by_group_id(group_uuid) 548 |> Actors.Membership.Query.returning_all() 549 |> Repo.all() 550 551 existing_members = Enum.map(memberships, fn membership -> membership.actor_id end) 552 desired_members = Enum.map(actor_group_data["members"] || [], fn member -> 553 uuid = UuidMapping.get_entity(slug, "actors", member) 554 if uuid == nil do 555 raise "Cannot find provisioned actor #{member} to add to group" 556 end 557 uuid 558 end) 559 560 missing_members = desired_members -- existing_members 561 untracked_members = existing_members -- desired_members 562 563 Logger.info("Updating members for actor group #{external_id}") 564 Enum.each(missing_members || [], fn actor_uuid -> 565 Logger.info("Adding member #{external_id}") 566 Actors.Membership.Changeset.upsert(account.id, %Actors.Membership{}, %{ 567 group_id: group_uuid, 568 actor_id: actor_uuid 569 }) 570 |> repo.insert!() 571 end) 572 573 if actor_group_data["forceMembers"] == true do 574 # Remove untracked members 575 to_delete = Enum.map(untracked_members, fn actor_uuid -> {group_uuid, actor_uuid} end) 576 if to_delete != [] do 577 Actors.Membership.Query.by_group_id_and_actor_id({:in, to_delete}) 578 |> repo.delete_all() 579 end 580 end 581 582 {:ok, nil} 583 end) 584 end) 585 586 # Create or update resources 587 multi = Enum.reduce(account_data["resources"] || %{}, multi, fn {external_id, resource_data}, multi -> 588 Ecto.Multi.run(multi, {:resource, slug, external_id}, fn _repo, changes -> 589 resource_attrs = %{ 590 type: String.to_atom(resource_data["type"]), 591 name: resource_data["name"], 592 address: resource_data["address"], 593 address_description: resource_data["address_description"], 594 connections: Enum.map(resource_data["gatewayGroups"] || [], fn group -> 595 %{gateway_group_id: UuidMapping.get_entity(slug, "gateway_groups", group)} 596 end), 597 filters: Enum.map(resource_data["filters"] || [], fn filter -> 598 %{ 599 ports: filter["ports"] || [], 600 protocol: String.to_atom(filter["protocol"]) 601 } 602 end) 603 } 604 605 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}] 606 uuid = UuidMapping.get_entity(slug, "resources", external_id) 607 case uuid && fetch_resource(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do 608 nil -> 609 Logger.info("Creating new resource #{resource_data["name"]}") 610 {:ok, resource} = Resources.create_resource(resource_attrs, temp_admin_subject) 611 UuidMapping.update_entities(slug, "resources", %{external_id => resource.id}) 612 {:ok, resource} 613 {:ok, existing} -> 614 existing = Repo.preload(existing, :connections) 615 Logger.info("Updating existing resource #{resource_data["name"]}") 616 only_updated_attrs = resource_attrs 617 |> Enum.reject(fn {key, value} -> 618 case key do 619 # Compare connections by gateway_group_id only 620 :connections -> value == Enum.map(existing.connections || [], fn conn -> Map.take(conn, [:gateway_group_id]) end) 621 # Compare filters by ports and protocol only 622 :filters -> value == Enum.map(existing.filters || [], fn filter -> Map.take(filter, [:ports, :protocol]) end) 623 _ -> Map.get(existing, key) == value 624 end 625 end) 626 |> Enum.into(%{}) 627 628 if only_updated_attrs == %{} do 629 {:ok, existing} 630 else 631 resource = case existing |> Resources.update_resource(resource_attrs, temp_admin_subject) do 632 {:replaced, _old, new} -> 633 UuidMapping.update_entities(slug, "resources", %{external_id => new.id}) 634 new 635 {:updated, value} -> value 636 x -> x 637 end 638 639 {:ok, resource} 640 end 641 end 642 end) 643 end) 644 645 # Create or update policies 646 multi = Enum.reduce(account_data["policies"] || %{}, multi, fn {external_id, policy_data}, multi -> 647 Ecto.Multi.run(multi, {:policy, slug, external_id}, fn _repo, changes -> 648 policy_attrs = %{ 649 description: policy_data["description"], 650 actor_group_id: UuidMapping.get_entity(slug, "actor_groups", policy_data["group"]), 651 resource_id: UuidMapping.get_entity(slug, "resources", policy_data["resource"]) 652 } 653 654 {temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}] 655 uuid = UuidMapping.get_entity(slug, "policies", external_id) 656 case uuid && fetch_policy(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do 657 nil -> 658 Logger.info("Creating new policy #{policy_data["name"]}") 659 {:ok, policy} = Policies.create_policy(policy_attrs, temp_admin_subject) 660 UuidMapping.update_entities(slug, "policies", %{external_id => policy.id}) 661 {:ok, policy} 662 {:ok, existing} -> 663 Logger.info("Updating existing policy #{policy_data["name"]}") 664 only_updated_attrs = policy_attrs 665 |> Enum.reject(fn {key, value} -> Map.get(existing, key) == value end) 666 |> Enum.into(%{}) 667 668 if only_updated_attrs == %{} do 669 {:ok, existing} 670 else 671 policy = case existing |> Policies.update_policy(policy_attrs, temp_admin_subject) do 672 {:replaced, _old, new} -> 673 UuidMapping.update_entities(slug, "policies", %{external_id => new.id}) 674 new 675 {:updated, value} -> value 676 x -> x 677 end 678 679 {:ok, policy} 680 end 681 end 682 end) 683 end) 684 685 # Clean up temporary admin after all operations 686 multi |> Ecto.Multi.run({:cleanup_temp_admin, slug}, fn _repo, changes -> 687 {temp_admin_subject, temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token} = 688 changes[{:temp_admin, slug}] 689 690 cleanup_temp_admin(temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token, temp_admin_subject) 691 {:ok, :cleaned} 692 end) 693 end) 694 |> Ecto.Multi.run({:save_state}, fn _repo, _changes -> 695 # Save all UUID mappings to disk. 696 UuidMapping.save() 697 {:ok, :saved} 698 end) 699 700 case Repo.transaction(multi) do 701 {:ok, _result} -> 702 Logger.info("Provisioning completed successfully") 703 {:error, step, reason, _changes} -> 704 Logger.error("Provisioning failed at step #{inspect(step)}, no changes were applied: #{inspect(reason)}") 705 end 706 end 707end 708 709Provision.provision()