lib/helmex.ex

defmodule Helmex do
  @moduledoc """
  Execute helm CLI commands.

  ## Examples
      iex> Helmex.init(kubeconfig: "/path/to/kube/config")
      ...> |> Helmex.Repo.add("kvaps", "https://kvaps.github.io/charts")
      ...> |> Helmex.call()

  ## Global options

  Option  |  Type
  :------ | :------
  `:burst-limit`   | value
  `:debug`   | flag
  `:kubeconfig`   | value
  `:namespace`   | value
  """

  alias Helmex.Helm
  alias Helmex.Option

  @global_options %{
    debug: %Option{name: "--debug"},
    burst_limit: %Option{name: "--burst-limit", require_arg: true},
    kubeconfig: %Option{name: "--kubeconfig", require_arg: true},
    namespace: %Option{name: "--namespace", require_arg: true}
  }

  @type result :: {:ok, String.t()} | {:error, String.t()}

  defdelegate install(helm, name, chart, opts \\ []), to: Helmex.Install

  @doc """
  Initialize helm command with an optional global flags

  ## Examples
      iex> Helmex.init()
      %Helmex.Helm{command: nil, global_options: [], options: [], args: []}

      iex> Helmex.init(kubeconfig: "/path/to/the/kube/config")
      %Helmex.Helm{
        command: nil,
        global_options: [
          %Helmex.Option{
            name: "--kubeconfig",
            require_arg: true,
            value: "/path/to/the/kube/config"
          }
        ],
        options: [],
        args: []
        }
  """
  @spec init(opts :: keyword) :: Helm.t()
  def init(opts \\ []) do
    %Helm{global_options: parse_options(opts, @global_options)}
  end

  @doc """
  Executes the helm command
  """
  @spec call(helm :: Helm.t()) :: result()
  def call(%Helm{command: nil}),
    do: raise(ArgumentError, "Please specify the helm command")

  def call(%Helm{} = helm) do
    case Rambo.run(helm_path(), build_args(helm)) do
      {:ok, %Rambo{out: out}} ->
        {:ok, out}

      {:error, %Rambo{err: err}} ->
        {:error, err}

      {:error, reason} ->
        {:error, reason}
    end
  end

  @doc """
  Filter and parse raw options into the %Helmex.Option structs
  """
  def parse_options(opts, allowed_options) do
    opts
    |> Enum.filter(&filter_options(&1, allowed_options))
    |> Enum.map(fn {opt, val} ->
      option = Map.get(allowed_options, opt)

      if option.require_arg do
        %Option{option | value: val}
      else
        option
      end
    end)
  end

  defp filter_options({option, _value}, allowed_options),
    do: Map.has_key?(allowed_options, option)

  defp build_args(%Helm{
         global_options: global_options,
         options: options,
         command: cmd,
         args: args
       }) do
    [
      [
        [Enum.map(global_options, &build_options/1) | [cmd]] | args
      ]
      | Enum.map(options, &build_options/1)
    ]
    |> List.flatten()
  end

  defp build_options(%Option{name: name, require_arg: false}) do
    ~w(#{name})
  end

  defp build_options(%Option{name: name, value: value, require_arg: true}) do
    ~w(#{name} #{value})
  end

  defp helm_path do
    case Application.get_env(:helmex, :helm_path, nil) do
      nil -> System.find_executable("helm")
      path -> path
    end
  end
end