defmodule K8s.Version do @moduledoc """ Kubernetes [API Versioning](https://kubernetes.io/docs/concepts/overview/kubernetes-api/#api-versioning) """ alias K8s.Version @type t :: %__MODULE__{major: pos_integer, minor: binary, patch: pos_integer} defstruct [:major, :minor, :patch] @pattern ~r/v(?\d{1,})(?(alpha|beta)?)(?\d{0,})/ @doc """ Returns the recommended version from a list of versions. It will return the most stable version if any, otherwise the newest "*edge*" version. ## Examples iex> K8s.Version.recommended(["v1beta1", "v1", "v2"]) "v2" iex> K8s.Version.recommended(["v1beta1", "v1"]) "v1" iex> K8s.Version.recommended(["v1beta2", "v1beta1"]) "v1beta2" iex> K8s.Version.recommended(["v1alpha1", "v1alpha2", "v1beta1"]) "v1beta1" """ @spec recommended(list(binary)) :: binary def recommended(versions) do versions |> parse_list |> sort |> List.first() |> serialize end @doc """ Sorts a list of `K8s.Version`s ## Examples iex> raw_versions = Enum.shuffle(["v1", "v2beta1", "v2", "v1alpha1", "v1alpha2"]) iex> versions = raw_versions |> Enum.map(&(K8s.Version.parse(&1))) iex> sorted_versions = versions |> K8s.Version.sort() iex> sorted_versions |> Enum.map(&(K8s.Version.serialize(&1))) ["v2", "v1", "v2beta1", "v1alpha2", "v1alpha1"] """ @spec sort(list(t)) :: list(t) def sort(versions), do: Enum.sort(versions, &compare/2) @spec compare(t, t) :: boolean defp compare(%Version{minor: "stable", major: a}, %Version{minor: "stable", major: b}), do: a >= b defp compare(%Version{minor: "stable"}, _), do: true defp compare(_, %Version{minor: "stable"}), do: false defp compare(%Version{minor: "beta"}, %Version{minor: "alpha"}), do: true defp compare(%Version{minor: "alpha"}, %Version{minor: "beta"}), do: false # Guard here catches case where minor is the same and major is greater. # If using >= it will get a flakey result when everything is the same except patch level defp compare(%Version{minor: m, major: a}, %Version{minor: m, major: b}) when a > b, do: true defp compare(%Version{patch: a}, %Version{patch: b}), do: a >= b defp compare(_, _), do: false @doc """ Returns only stable versions ## Examples iex> K8s.Version.serialize(%K8s.Version{major: 1, minor: "stable", patch: nil}) "v1" iex> K8s.Version.serialize(%K8s.Version{major: 1, minor: "beta", patch: 1}) "v1beta1" iex> K8s.Version.serialize(%K8s.Version{major: 1, minor: "alpha", patch: 1}) "v1alpha1" """ @spec serialize(Version.t()) :: binary def serialize(%Version{major: major, minor: "stable"}), do: "v#{major}" def serialize(%Version{major: major, minor: minor, patch: patch}), do: "v#{major}#{minor}#{patch}" @doc """ Returns only stable versions ## Examples iex> K8s.Version.stable(["v1alpha1", "v1", "v2beta1"]) [%K8s.Version{major: 1, minor: "stable", patch: nil}] """ @spec stable(list(binary)) :: list(Version.t()) def stable(versions), do: parse_filter(versions, "stable") @doc """ Returns only alpha versions ## Examples iex> K8s.Version.alpha(["v1alpha1", "v1", "v2beta1"]) [%K8s.Version{major: 1, minor: "alpha", patch: 1}] """ @spec alpha(list(binary)) :: list(Version.t()) def alpha(versions), do: parse_filter(versions, "alpha") @doc """ Returns only beta versions ## Examples iex> K8s.Version.beta(["v1alpha1", "v1", "v2beta1"]) [%K8s.Version{major: 2, minor: "beta", patch: 1}] """ @spec beta(list(binary)) :: list(Version.t()) def beta(versions), do: parse_filter(versions, "beta") @doc """ Retuns all non-stable versions ## Examples iex> K8s.Version.edge(["v1alpha1", "v1", "v2beta1"]) [%K8s.Version{major: 1, minor: "alpha", patch: 1}, %K8s.Version{major: 2, minor: "beta", patch: 1}] """ @spec edge(list(binary)) :: list(Version.t()) def edge(versions), do: alpha(versions) ++ beta(versions) @spec parse_filter(list(binary), binary) :: list(Version.t()) defp parse_filter(versions, keep) do versions |> parse_list |> Enum.filter(&is(&1, keep)) end @spec is(Version.t(), binary) :: boolean() defp is(%Version{minor: minor}, minor), do: true defp is(_, _), do: false @doc """ Parses a Kubernetes API version into a `K8s.Version` struct ## Examples iex> K8s.Version.parse("v1") %K8s.Version{major: 1, minor: "stable", patch: nil} iex> K8s.Version.parse("v1beta2") %K8s.Version{major: 1, minor: "beta", patch: 2} iex> K8s.Version.parse("v1alpha1") %K8s.Version{major: 1, minor: "alpha", patch: 1} """ @spec parse(binary()) :: Version.t() def parse(version) do %{"major" => major, "minor" => minor, "patch" => patch} = Regex.named_captures(@pattern, version) format(major, minor, patch) end @spec format(binary | integer, binary, binary | integer | nil) :: t defp format(major, "", "") when is_integer(major), do: format(major, "stable", nil) defp format(major, minor, patch) when is_binary(major), do: major |> String.to_integer() |> format(minor, patch) defp format(major, minor, patch) when is_binary(patch), do: format(major, minor, String.to_integer(patch)) defp format(major, minor, patch), do: %__MODULE__{major: major, minor: minor, patch: patch} # Parse a list of string versions @spec parse_list(list(binary)) :: list(K8s.Version.t()) defp parse_list(versions), do: parse_list(versions, []) @spec parse_list(list(binary), list(K8s.Version.t()) | nil) :: list(K8s.Version.t()) defp parse_list([], acc), do: acc defp parse_list([version | tail], acc), do: parse_list(tail, [parse(version) | acc]) end