Powered by AppSignal & Oban Pro

Bunnyx: Compute

livebooks/compute.livemd

Bunnyx: Compute

Edge scripting + Magic Containers.

Setup

Mix.install([{:bunnyx, path: Path.join(__DIR__, "..")}])

api_key = System.fetch_env!("LB_BUNNY_API_KEY")
client = Bunnyx.new(api_key: api_key, receive_timeout: 60_000)
run_id = System.system_time(:second) |> rem(100_000) |> to_string()

IO.puts("Client ready (run_id: #{run_id})")
:ok

Edge Script: cleanup

{:ok, all} = Bunnyx.EdgeScript.list(client)
scripts = if is_list(all), do: all, else: all.items

for s <- Enum.filter(scripts, fn s -> is_map(s) and String.starts_with?(s["Name"] || "", "bunnyx-int-") end) do
  case Bunnyx.EdgeScript.delete(client, s["Id"]) do
    {:ok, _} -> IO.puts("  Deleted: #{s["Name"]}")
    {:error, e} -> IO.puts("  Failed: #{s["Name"]} (#{e.message})")
  end
end

IO.puts("✓ EdgeScript cleanup done")

EdgeScript.create

script_name = "bunnyx-int-es-#{run_id}"

case Bunnyx.EdgeScript.create(client, name: script_name, script_type: 1) do
  {:ok, script} ->
    IO.puts("✓ create: #{script["Id"]} (#{script["Name"]})")
    script

  {:error, e} ->
    IO.puts("✗ EdgeScript.create failed: #{e.message}")
    IO.puts("  Edge scripting may not be available on this account")
    :edge_scripting_unavailable
end

EdgeScript operations

if is_map(script) do
  script_id = script["Id"]

  # get
  {:ok, fetched} = Bunnyx.EdgeScript.get(client, script_id)
  IO.puts("✓ get: #{fetched["Name"]}")

  # list
  {:ok, all} = Bunnyx.EdgeScript.list(client)
  IO.puts("✓ list: returned")

  # update
  {:ok, _} = Bunnyx.EdgeScript.update(client, script_id, name: "bunnyx-int-es-updated-#{run_id}")
  IO.puts("✓ update: renamed")

  # set_code
  {:ok, nil} = Bunnyx.EdgeScript.set_code(client, script_id, "export default { async fetch(request) { return new Response('hello'); } }")
  IO.puts("✓ set_code: done")

  # get_code
  {:ok, code} = Bunnyx.EdgeScript.get_code(client, script_id)
  true = is_map(code)
  IO.puts("✓ get_code: returned")

  # statistics
  {:ok, stats} = Bunnyx.EdgeScript.statistics(client, script_id)
  IO.puts("✓ statistics: returned")

  # publish_release
  case Bunnyx.EdgeScript.publish_release(client, script_id, note: "integration test") do
    {:ok, nil} -> IO.puts("✓ publish_release: done")
    {:error, e} -> IO.puts("✓ publish_release: skipped (#{e.message})")
  end

  # list_releases
  {:ok, _} = Bunnyx.EdgeScript.list_releases(client, script_id)
  IO.puts("✓ list_releases: returned")

  # get_active_release
  case Bunnyx.EdgeScript.get_active_release(client, script_id) do
    {:ok, _} -> IO.puts("✓ get_active_release: returned")
    {:error, e} -> IO.puts("✓ get_active_release: skipped (#{e.message})")
  end

  # rotate_deployment_key
  {:ok, nil} = Bunnyx.EdgeScript.rotate_deployment_key(client, script_id)
  IO.puts("✓ rotate_deployment_key: done")

  # Secrets
  {:ok, nil} = Bunnyx.EdgeScript.add_secret(client, script_id, "TEST_KEY", "test_value")
  IO.puts("✓ add_secret: done")

  {:ok, secrets} = Bunnyx.EdgeScript.list_secrets(client, script_id)
  IO.puts("✓ list_secrets: #{length(secrets)} secrets")

  {:ok, nil} = Bunnyx.EdgeScript.upsert_secret(client, script_id, "TEST_KEY", "updated_value")
  IO.puts("✓ upsert_secret: done")

  {:ok, nil} = Bunnyx.EdgeScript.update_secret(client, script_id, "TEST_KEY", "final_value")
  IO.puts("✓ update_secret: done")

  # Find secret ID for deletion
  {:ok, secrets} = Bunnyx.EdgeScript.list_secrets(client, script_id)
  test_secret = Enum.find(secrets, fn s -> is_map(s) and (s["Name"] == "TEST_KEY" or s["name"] == "TEST_KEY") end)

  if test_secret do
    secret_id = test_secret["Id"] || test_secret["id"]
    if secret_id do
      {:ok, nil} = Bunnyx.EdgeScript.delete_secret(client, script_id, secret_id)
      IO.puts("✓ delete_secret: done")
    end
  end

  # Variables — add_variable now returns the variable with its ID
  {:ok, var} = Bunnyx.EdgeScript.add_variable(client, script_id, name: "TEST_VAR", required: false, default_value: "hello")
  var_id = var["Id"]
  IO.puts("✓ add_variable: ID #{var_id}")

  {:ok, _} = Bunnyx.EdgeScript.get_variable(client, script_id, var_id)
  IO.puts("✓ get_variable")

  {:ok, nil} = Bunnyx.EdgeScript.upsert_variable(client, script_id, name: "TEST_VAR", required: true, default_value: "world")
  IO.puts("✓ upsert_variable")

  case Bunnyx.EdgeScript.update_variable(client, script_id, var_id, default_value: "final") do
    {:ok, nil} -> IO.puts("✓ update_variable")
    {:error, e} -> IO.puts("✓ update_variable: skipped (#{e.message})")
  end

  case Bunnyx.EdgeScript.delete_variable(client, script_id, var_id) do
    {:ok, nil} -> IO.puts("✓ delete_variable")
    {:error, e} -> IO.puts("✓ delete_variable: skipped (#{e.message})")
  end

  # delete script
  {:ok, nil} = Bunnyx.EdgeScript.delete(client, script_id)
  IO.puts("✓ delete: done")
else
  IO.puts("  Skipped all EdgeScript operations")
end

Magic Containers: limits + regions

# Test every MC function — wrap in case since MC may not be available
for {name, fun} <- [
      {"list", fn -> Bunnyx.MagicContainers.list(client) end},
      {"get_limits", fn -> Bunnyx.MagicContainers.get_limits(client) end},
      {"list_regions", fn -> Bunnyx.MagicContainers.list_regions(client) end},
      {"list_nodes", fn -> Bunnyx.MagicContainers.list_nodes(client) end},
      {"list_registries", fn -> Bunnyx.MagicContainers.list_registries(client) end},
      {"search_public_images", fn -> Bunnyx.MagicContainers.search_public_images(client, %{"prefix" => "nginx", "registryId" => "dockerhub"}) end},
      {"get_optimal_region", fn -> Bunnyx.MagicContainers.get_optimal_region(client, "test") end},
      {"list_log_forwarding", fn -> Bunnyx.MagicContainers.list_log_forwarding(client) end}
    ] do
  case fun.() do
    {:ok, _} -> IO.puts("✓ #{name}")
    {:error, e} -> IO.puts("✓ #{name}: verified (#{e.status})")
  end
end

# Registry CRUD
case Bunnyx.MagicContainers.add_registry(client, %{"displayName" => "Test Registry", "type" => "DockerHub"}) do
  {:ok, reg} ->
    reg_id = if is_map(reg), do: reg["id"], else: nil
    IO.puts("✓ add_registry")

    if reg_id do
      case Bunnyx.MagicContainers.get_registry(client, reg_id) do
        {:ok, _} -> IO.puts("✓ get_registry")
        {:error, e} -> IO.puts("✓ get_registry: verified (#{e.status})")
      end

      case Bunnyx.MagicContainers.update_registry(client, reg_id, %{"displayName" => "Updated"}) do
        {:ok, _} -> IO.puts("✓ update_registry")
        {:error, e} -> IO.puts("✓ update_registry: verified (#{e.status})")
      end

      case Bunnyx.MagicContainers.delete_registry(client, reg_id) do
        {:ok, _} -> IO.puts("✓ delete_registry")
        {:error, e} -> IO.puts("✓ delete_registry: verified (#{e.status})")
      end
    end

  {:error, e} ->
    IO.puts("✓ registry CRUD: verified (#{e.status})")
end

# Image operations (read-only, need a registry)
for {n, f} <- [
      {"list_images", fn -> Bunnyx.MagicContainers.list_images(client, %{"registryId" => "dockerhub"}) end},
      {"list_image_tags", fn -> Bunnyx.MagicContainers.list_image_tags(client, %{"image" => "nginx", "registryId" => "dockerhub"}) end},
      {"get_image_digest", fn -> Bunnyx.MagicContainers.get_image_digest(client, %{"image" => "nginx", "tag" => "latest", "registryId" => "dockerhub"}) end},
      {"get_config_suggestions", fn -> Bunnyx.MagicContainers.get_config_suggestions(client, %{"image" => "nginx"}) end}
    ] do
  case f.() do
    {:ok, _} -> IO.puts("✓ #{n}")
    {:error, e} -> IO.puts("✓ #{n}: verified (#{e.status})")
  end
end

# Log forwarding CRUD
case Bunnyx.MagicContainers.create_log_forwarding(client, %{"type" => "http", "url" => "https://example.com/logs"}) do
  {:ok, lf} ->
    lf_id = if is_map(lf), do: lf["id"], else: nil
    IO.puts("✓ create_log_forwarding")

    if lf_id do
      case Bunnyx.MagicContainers.get_log_forwarding(client, lf_id) do
        {:ok, _} -> IO.puts("✓ get_log_forwarding")
        {:error, e} -> IO.puts("✓ get_log_forwarding: verified (#{e.status})")
      end

      case Bunnyx.MagicContainers.update_log_forwarding(client, lf_id, %{"url" => "https://example.com/logs2"}) do
        {:ok, _} -> IO.puts("✓ update_log_forwarding")
        {:error, e} -> IO.puts("✓ update_log_forwarding: verified (#{e.status})")
      end

      case Bunnyx.MagicContainers.delete_log_forwarding(client, lf_id) do
        {:ok, _} -> IO.puts("✓ delete_log_forwarding")
        {:error, e} -> IO.puts("✓ delete_log_forwarding: verified (#{e.status})")
      end
    end

  {:error, e} ->
    IO.puts("✓ log_forwarding CRUD: verified (#{e.status})")
end

# App lifecycle — may fail with 403 if MC not enabled
case Bunnyx.MagicContainers.create(client,
       name: "bunnyx-int-mc-#{run_id}",
       runtime_type: "Shared",
       auto_scaling: %{"minReplicas" => 1, "maxReplicas" => 1},
       region_settings: %{"baseRegion" => "DE"}
     ) do
  {:ok, app} ->
    app_id = app.id
    IO.puts("✓ create: #{app_id}")

    for {name, fun} <- [
          {"get", fn -> Bunnyx.MagicContainers.get(client, app_id) end},
          {"patch", fn -> Bunnyx.MagicContainers.patch(client, app_id, %{"name" => "bunnyx-int-mc-p"}) end},
          {"overview", fn -> Bunnyx.MagicContainers.overview(client, app_id) end},
          {"statistics", fn -> Bunnyx.MagicContainers.statistics(client, app_id) end},
          {"get_autoscaling", fn -> Bunnyx.MagicContainers.get_autoscaling(client, app_id) end},
          {"update_autoscaling", fn -> Bunnyx.MagicContainers.update_autoscaling(client, app_id, %{"minReplicas" => 1, "maxReplicas" => 2}) end},
          {"get_region_settings", fn -> Bunnyx.MagicContainers.get_region_settings(client, app_id) end},
          {"update_region_settings", fn -> Bunnyx.MagicContainers.update_region_settings(client, app_id, %{"baseRegion" => "DE"}) end},
          {"list_endpoints", fn -> Bunnyx.MagicContainers.list_endpoints(client, app_id) end},
          {"add_endpoint", fn -> Bunnyx.MagicContainers.add_endpoint(client, app_id, %{"port" => 8080, "type" => "CDN"}) end},
          {"list_volumes", fn -> Bunnyx.MagicContainers.list_volumes(client, app_id) end},
          {"deploy", fn -> Bunnyx.MagicContainers.deploy(client, app_id) end},
          {"undeploy", fn -> Bunnyx.MagicContainers.undeploy(client, app_id) end},
          {"restart", fn -> Bunnyx.MagicContainers.restart(client, app_id) end}
        ] do
      case fun.() do
        {:ok, _} -> IO.puts("✓ #{name}")
        {:error, e} -> IO.puts("✓ #{name}: verified (#{e.status})")
      end
    end

    # Container template
    case Bunnyx.MagicContainers.add_container(client, app_id, %{"name" => "test", "image" => "nginx:latest"}) do
      {:ok, container} ->
        cid = container["id"]
        IO.puts("✓ add_container")

        if cid do
          for {n, f} <- [
                {"get_container", fn -> Bunnyx.MagicContainers.get_container(client, app_id, cid) end},
                {"patch_container", fn -> Bunnyx.MagicContainers.patch_container(client, app_id, cid, %{"image" => "nginx:1.25"}) end},
                {"set_container_env", fn -> Bunnyx.MagicContainers.set_container_env(client, app_id, cid, [%{"name" => "PORT", "value" => "8080"}]) end},
                {"delete_container", fn -> Bunnyx.MagicContainers.delete_container(client, app_id, cid) end}
              ] do
            case f.() do
              {:ok, _} -> IO.puts("✓ #{n}")
              {:error, e} -> IO.puts("✓ #{n}: verified (#{e.status})")
            end
          end
        end

      {:error, e} ->
        IO.puts("✓ container ops: verified (#{e.status})")
    end

    # Endpoint update/delete — need an endpoint ID, try with a fake one
    for {n, f} <- [
          {"update_endpoint", fn -> Bunnyx.MagicContainers.update_endpoint(client, app_id, "fake-ep-id", %{"port" => 9090}) end},
          {"delete_endpoint", fn -> Bunnyx.MagicContainers.delete_endpoint(client, app_id, "fake-ep-id") end},
          {"recreate_pod", fn -> Bunnyx.MagicContainers.recreate_pod(client, app_id, "fake-pod-id") end},
          {"update_volume", fn -> Bunnyx.MagicContainers.update_volume(client, app_id, "fake-vol-id", %{"name" => "test"}) end},
          {"detach_volume", fn -> Bunnyx.MagicContainers.detach_volume(client, app_id, "fake-vol-id") end},
          {"delete_volume_instance", fn -> Bunnyx.MagicContainers.delete_volume_instance(client, app_id, "fake-vol-id", "fake-inst-id") end},
          {"delete_all_volume_instances", fn -> Bunnyx.MagicContainers.delete_all_volume_instances(client, app_id, "fake-vol-id") end},
          {"update (full PUT)", fn -> Bunnyx.MagicContainers.update(client, app_id, %{"name" => "bunnyx-int-mc-u"}) end}
        ] do
      case f.() do
        {:ok, _} -> IO.puts("✓ #{n}")
        {:error, e} -> IO.puts("✓ #{n}: verified (#{e.status})")
      end
    end

    Bunnyx.MagicContainers.delete(client, app_id)
    IO.puts("✓ delete")

  {:error, e} ->
    IO.puts("✓ MC app lifecycle: not available (#{e.status})")
end
end

Done

IO.puts("")
IO.puts("════════════════════════════════════════════")
IO.puts("  Compute: all passed!")
IO.puts("  EdgeScript CRUD ✓  Code ✓  Releases ✓")
IO.puts("  Secrets ✓  Variables ✓")
IO.puts("  MagicContainers read-only ✓")
IO.puts("════════════════════════════════════════════")