lib/pakman/push.ex

defmodule Pakman.Push do
  @moduledoc """
  Push built packages to storage
  """

  alias Pakman.Instellar
  alias Pakman.FileExt

  alias ExAws.S3

  require Logger

  defmodule Error do
    defexception message: "Push failed"
  end

  def perform(options \\ [concurrency: 2]) do
    archive = Keyword.get(options, :archive, "packages.zip")
    config_file = Keyword.get(options, :config, "instellar.yml")
    workspace = System.get_env("GITHUB_WORKSPACE")
    hash = System.get_env("WORKFLOW_SHA") || System.get_env("GITHUB_SHA")

    config =
      workspace
      |> Path.join(config_file)
      |> YamlElixir.read_from_file!()

    with {:ok, token} <- Instellar.authenticate(),
         {:ok, :not_found} <- Instellar.get_deployment(token, hash),
         {:ok, %{"attributes" => storage}} <- Instellar.get_storage(token),
         {:ok, %{archive: archive_path}} <-
           push_files(storage, archive, options),
         {:ok, deployment_message, response} <-
           Instellar.create_deployment(token, archive_path, config),
         {:ok, configuration_message, _response} <-
           Instellar.create_configuration(
             token,
             response["attributes"]["id"],
             config
           ) do
      print_deployment_message(deployment_message)
      print_configuration_message(configuration_message)

      {:ok, :pushed}
    else
      {:error, error} ->
        raise Error, message: "[Pakman.Push] #{inspect(error)}"

      {:ok, %{"attributes" => _}} ->
        Logger.info("[Pakman.Push] Deployment already exists...")

        {:ok, :already_exists}

      _ ->
        raise Error, message: "[Pakman.Push] Deployment creation failed..."
    end
  end

  def push_files(storage, archive, options) do
    home = System.get_env("HOME")
    sha = System.get_env("WORKFLOW_SHA") || System.get_env("GITHUB_SHA")

    packages_dir = Path.join(home, "packages")
    archive_path = Path.join(home, archive)

    files =
      FileExt.ls_r(packages_dir)
      |> Enum.map(fn path -> {:deployments, path, sha} end)
      |> Enum.concat([{:archives, archive_path, Uniq.UUID.uuid4()}])

    storage = %{
      config:
        ExAws.Config.new(:s3,
          access_key_id: storage["credential"]["access_key_id"],
          secret_access_key: storage["credential"]["secret_access_key"],
          host: storage["host"],
          port: storage["port"],
          scheme: storage["scheme"],
          region: storage["region"]
        ),
      bucket: storage["bucket"]
    }

    stream =
      Task.Supervisor.async_stream(
        Pakman.TaskSupervisor,
        files,
        __MODULE__,
        :push,
        [storage],
        max_concurrency: Keyword.get(options, :concurrency, 2),
        timeout: 60_000
      )

    uploads = Enum.to_list(stream)

    successful_uploads =
      Enum.filter(uploads, fn {result, _} -> result == :ok end)

    if Enum.count(successful_uploads) == Enum.count(files) do
      Logger.info("[Pakman.Push] completed - #{sha}")

      {:ok, upload} =
        Enum.find(successful_uploads, fn {:ok, result} ->
          result.type == :archive
        end)

      {:ok, %{archive: upload.path}}
    else
      comment = "partially failed"
      message = "[Pakman.Push] #{comment} - #{sha}"
      Logger.error(message)
      raise Error, message: message
    end
  end

  def push({:archives, path, id}, storage) do
    storage_path = Path.join(["archives", id, Path.basename(path)])

    path
    |> S3.Upload.stream_file()
    |> S3.upload(storage.bucket, storage_path)
    |> ExAws.request(Keyword.new(storage.config))
    |> case do
      {:ok, _result} ->
        %{type: :archive, path: storage_path}

      error ->
        error
    end
  end

  def push({:deployments, path, sha}, storage) do
    %{organization: organization, name: name} = Pakman.Environment.repository()

    file_with_arch_name =
      path
      |> Path.split()
      |> Enum.take(-2)
      |> Path.join()

    storage_path =
      Path.join([
        "deployments",
        organization,
        name,
        sha,
        file_with_arch_name
      ])

    path
    |> S3.Upload.stream_file()
    |> S3.upload(storage.bucket, storage_path)
    |> ExAws.request(Keyword.new(storage.config))
    |> case do
      {:ok, _result} ->
        %{type: :deployment, path: storage_path}

      error ->
        error
    end
  end

  defp print_deployment_message(:created),
    do: Logger.info("[Pakman.Push] Deployment successfully created...")

  defp print_deployment_message(:already_exists),
    do: Logger.info("[Pakman.Push] Deployment already exists...")

  defp print_configuration_message(:created),
    do: Logger.info("[Pakman.Push] Configuration successfully created...")

  defp print_configuration_message(:already_exists),
    do: Logger.info("[Pakman.Push] Configuration already exists...")

  defp print_configuration_message(:no_configuration),
    do: Logger.info("[Pakman.Push] Configuration not found...")
end