lib/mix/tasks/pot.utils.ex

defmodule PotUtils do
  @moduledoc """
  Utils for running Pot tasks. Most of the logic of Pot tasks are contained here.
  """
  require Logger

  @doc """
  Get the current container run time to use. The return values can
  be the strings of "docker", "podman", or "nerdctl". Currently
  does not allow for choosing your runtime, this is to be added later.
  """
  def get_runtime do
    get_nerdctl()
  end

  @doc """
  Get the application name. Thils is either the value of `:app` in the
  application configuration or the return value of `Application.get_all_env(__MODULE__)`
  """
  def app_name do
    config = Mix.Project.config()

    if Keyword.has_key?(config, :app) do
      config[:app] |> Atom.to_string()
    else
      Application.get_application(__MODULE__)
    end
  end

  @doc """
  Trys to run the program `nerdctl`. If it errors, it will move
  on to try the next runtime `podman`.
  """
  def get_nerdctl do
    try do
      System.cmd("nerdctl", ["-v"])
      "nerdctl"
    catch
      _, _-> get_podman()
    end
  end

  @doc """
  Trys to run the program `podman`. If it errors, it will move
  on to try the next runtime `docker`.
  """
  def get_podman do
    try do
      System.cmd("podman", ["-v"])
      "podman"
    catch
      _, _ -> get_docker()
    end
  end

  @doc """
  Trys to run the program `docker`. If it errors, it will raise
  and error because no suitable container runtime can be found.
  """
  def get_docker do
    try do
      System.cmd("docker", ["-v"])
      "docker"
    catch
      _, _ ->
        raise """
        Pots requires a container runtime to function. The currently
        supported container run times are as follows. Please install one;
        - docker (https://docs.docker.com/engine/install/)
        - podman (https://podman.io/docs/installation)
        - nerdctl (https://github.com/containerd/nerdctl)
        """
    end
  end

  @doc """
  Gets the name of the Dockerfile. If `:no_name` is passed in then it
  simply returns `Dockerfile.pot`. If any other string is passed to it, it will
  return `Docerfile.<string>.pot`.

  ## Example
    iex> PotUtils.get_docker_file_for_pot(:no_name)
    Dockerfile.pot
    iex> PotUtils.get_docker_file_for_pot("my_project")
    Dockerfile.my_project.pot
  """
  def get_docker_file_for_pot(pot_name) do
    case pot_name do
      :no_name -> "Dockerfile.pot"
      _ -> "Docker.#{pot_name}.pot"
    end
  end

  @doc """
  Returns a list of `Dockerfiles` for Pot. It looks for files that start
  with `Docker` and end with `.pot`.
  """
  def get_all_docker_files do
    {:ok, files} = File.ls()
    files |> Enum.filter(&(String.starts_with?(&1, "Docker") && String.ends_with?(&1, ".pot")))
  end

  @doc """
  Remove all container images created by Pot. This will only touch images labled appropriately
  by pot. This runs the command `<container-runtime> image rm <img-id>`
  """
  def remove_image(img) do
    id = img["Id"]
    runtime_cmd("image rm #{id}")
  end

  @doc """
  Stop all running containers created by Pot. This should
  also remove them as all containers are started with the
  options `--rm` which means to remove them once stopped.
  Uses the command in the format of `<container-runtime> stop <container-id`
  """
  def stop_container(container) do
    id = container["Id"]
    runtime_cmd("container stop #{id}")
  end

  @doc """
  Returns a list of all docker containers, filtered by the label of
  `pot_<app-name>` in JSON format. If none are returned, it returns
  an empty list. Uses the command in the format of
  `<container-runtime> container ls --filter lable=pot_<app-name> --format {{json}}`
  """
  def get_docker_containers do
    {output, _} = runtime_cmd_output("container ls --filter label=pot_#{app_name()} --format {{json}}")
    case output do
      [] -> []
      _ -> Jason.decode!(output)
    end
  end

  @doc """
  Similar to `PotUtils.get_docker_containers/0`, it filters all containers on the label of
  `pot_<app-name>` and simply prints the output of the command. Uses the command
  in the format of
  `<container-runtime> container ls --filter lable=pot_<app-name>`
  """
  def print_containers do
    {output, _} = runtime_cmd_output("container ls --filter label=pot_#{app_name()}")
    IO.puts("Containers")
    IO.puts(output)
  end

  @doc """
  Returns a list of all docker images, filtered by the label of
  `pot_<app-name>` in JSON format. If none are returned, it returns
  an empty list. Uses the command in the format of
  `<container-runtime> images --filter lable=pot_<app-name> --format {{json}}`
  """
  def get_docker_images do
    {output, _} = runtime_cmd_output("images --filter label=pot_#{app_name()} --format {{json}}")
    case output do
      [] -> []
      _ -> Jason.decode!(output)
    end
  end

  @doc """
  Similar to `PotUtils.get_docker_images/0`, it filters all images on the label of
  `pot_<app-name>` and simply prints the output of the command. Uses the command
  in the format of
  `<container-runtime> images --filter lable=pot_<app-name>`
  """
  def print_images do
    {output, _} = runtime_cmd_output("images --filter label=pot_#{app_name()}")
    IO.puts("Images")
    IO.puts(output)
  end

  @doc """
  Takes in the command to be run using the runtime returned from `PotUtils.get_runtime/0`.
  Splits the `cmd` up and passes it to `System.cmd`. This one returns the output from
  the command run unlike `runtime_cmd/1`
  """
  def runtime_cmd_output(cmd) do
    System.cmd(get_runtime(), String.split(cmd, " "))
  end

  @doc """
  Takes in the command to be run using the runtime returned from `PotUtils.get_runtime/0`.
  Splits the `cmd` up and passes it to `System.cmd`.
  """
  def runtime_cmd(cmd) do
    System.cmd(get_runtime(), String.split(cmd, " "), into: IO.stream(:stdio, :line))
  end
end