diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b4e659..ac86d32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,3 +151,4 @@ jobs: POSTGRES_PASSWORD: postgres POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} DEFAULT_CDN_HOST: some.domain.com + POLAR_CLOAK_KEY: ${{secrets.POLAR_CLOAK_KEY}} diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index b6a4e7d..8f70e6c 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -75,6 +75,6 @@ jobs: env: WORKFLOW_REF: ${{ github.event.workflow_run.head_branch }} WORKFLOW_SHA: ${{ github.event.workflow_run.head_sha }} - INSTELLAR_ENDPOINT: https://opsmaru.com + INSTELLAR_ENDPOINT: ${{vars.INSTELLAR_ENDPOINT}} INSTELLAR_PACKAGE_TOKEN: ${{secrets.INSTELLAR_PACKAGE_TOKEN}} INSTELLAR_AUTH_TOKEN: ${{secrets.INSTELLAR_AUTH_TOKEN}} \ No newline at end of file diff --git a/README.md b/README.md index 4428509..0a204ff 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ The build system for polar is called [icepak](https://github.com/upmaru/icepak). ## Demo -+ [Sandbox Site](https://images.opsmaru.dev) ++ [Production](https://images.opsmaru.com) ++ [Sandbox](https://images.opsmaru.dev) ## Basic Architecture diff --git a/config/config.exs b/config/config.exs index 6d1c697..d0ea3b4 100644 --- a/config/config.exs +++ b/config/config.exs @@ -11,6 +11,16 @@ config :polar, ecto_repos: [Polar.Repo], generators: [timestamp_type: :utc_datetime] +config :polar, Polar.Vault, json_library: Jason + +config :polar, Oban, + engine: Oban.Engines.Basic, + queues: [default: 3], + repo: Polar.Repo, + plugins: [ + {Oban.Plugins.Cron, crontab: [{"@daily", Polar.Streams.Version.Pruning}]} + ] + # Configures the endpoint config :polar, PolarWeb.Endpoint, url: [host: "localhost"], diff --git a/config/runtime.exs b/config/runtime.exs index 266a8ca..20327da 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -34,6 +34,21 @@ config :polar, Polar.Assets, endpoint: System.get_env("AWS_S3_ENDPOINT"), default_cdn_host: default_cdn_host +cloak_key = + System.get_env("CLOAK_KEY") || System.get_env("POLAR_CLOAK_KEY") || + raise """ + environment variable CLOAK_KEY or POLAR_CLOAK_KEY is missing. + You can generate one using 32 |> :crypto.strong_rand_bytes() |> Base.encode64() + """ + +config :polar, Polar.Vault, + ciphers: [ + default: { + Cloak.Ciphers.AES.GCM, + tag: "AES.GCM.V1", key: Base.decode64!(cloak_key) + } + ] + if config_env() == :prod do database_url = System.get_env("DATABASE_URL") || diff --git a/config/test.exs b/config/test.exs index 263374a..dc38920 100644 --- a/config/test.exs +++ b/config/test.exs @@ -23,6 +23,10 @@ config :polar, PolarWeb.Endpoint, secret_key_base: "6KFZ6zNwk0UXHww8AnEmReHHKixN5GmuKJLFVB+/YvfcvgVaKMwM3G4SvSNz5Z8s", server: false +config :polar, Oban, testing: :manual + +config :polar, :lexdee, Polar.LexdeeMock + # In test we don't send emails. config :polar, Polar.Mailer, adapter: Swoosh.Adapters.Test diff --git a/instellar.yml b/instellar.yml index d5772de..4191970 100644 --- a/instellar.yml +++ b/instellar.yml @@ -33,7 +33,7 @@ build: run: commands: - binary: polar - call: eval 'Polar.Release.migrate' + call: eval 'Polar.Release.Tasks.migrate' name: migrate - binary: polar call: remote @@ -81,4 +81,6 @@ kits: driver: database/postgresql key: DATABASE - driver: bucket/aws-s3 + driver_options: + acl: public key: AWS_S3 diff --git a/lib/polar/accounts/space/credential.ex b/lib/polar/accounts/space/credential.ex index 421022c..9884cdc 100644 --- a/lib/polar/accounts/space/credential.ex +++ b/lib/polar/accounts/space/credential.ex @@ -3,6 +3,7 @@ defmodule Polar.Accounts.Space.Credential do import Ecto.Changeset alias Polar.Accounts.Space + alias Polar.Streams.ReleaseChannel alias __MODULE__.Transitions alias __MODULE__.Event @@ -27,11 +28,13 @@ defmodule Polar.Accounts.Space.Credential do field :name, :string field :token, :binary - field :type, :string + field :type, :string, default: "lxd" field :expires_in, :integer, virtual: true field :expires_at, :utc_datetime + field :release_channel, :string, default: "active" + belongs_to :space, Space timestamps(type: :utc_datetime_usec) @@ -46,12 +49,13 @@ defmodule Polar.Accounts.Space.Credential do expires_in_range_values = Enum.map(@expires_in_range, fn r -> r.value end) credential - |> cast(attrs, [:name, :expires_in, :type]) + |> cast(attrs, [:name, :expires_in, :type, :release_channel]) |> generate_token() |> validate_inclusion(:expires_in, expires_in_range_values) |> validate_inclusion(:type, types()) |> maybe_set_expires_at() |> validate_required([:token, :type, :name]) + |> validate_inclusion(:release_channel, ReleaseChannel.valid_names()) |> unique_constraint(:name, name: :space_credentials_space_id_name_index) end diff --git a/lib/polar/application.ex b/lib/polar/application.ex index 5b1c659..ee87981 100644 --- a/lib/polar/application.ex +++ b/lib/polar/application.ex @@ -10,10 +10,12 @@ defmodule Polar.Application do children = [ PolarWeb.Telemetry, Polar.Repo, + {Oban, Application.fetch_env!(:polar, Oban)}, {DNSCluster, query: Application.get_env(:polar, :dns_cluster_query) || :ignore}, {Phoenix.PubSub, name: Polar.PubSub}, # Start the Finch HTTP client for sending emails {Finch, name: Polar.Finch}, + Polar.Vault, # Start a worker by calling: Polar.Worker.start_link(arg) # {Polar.Worker, arg}, # Start to serve requests, typically the last entry diff --git a/lib/polar/encrypted/map.ex b/lib/polar/encrypted/map.ex new file mode 100644 index 0000000..d1a1299 --- /dev/null +++ b/lib/polar/encrypted/map.ex @@ -0,0 +1,3 @@ +defmodule Polar.Encrypted.Map do + use Cloak.Ecto.Map, vault: Polar.Vault +end diff --git a/lib/polar/machines.ex b/lib/polar/machines.ex new file mode 100644 index 0000000..1e2ef74 --- /dev/null +++ b/lib/polar/machines.ex @@ -0,0 +1,27 @@ +defmodule Polar.Machines do + alias __MODULE__.Cluster + + defdelegate list_clusters(scope), + to: Cluster.Manager, + as: :list + + defdelegate create_cluster(params), + to: Cluster.Manager, + as: :create + + alias __MODULE__.Check + + defdelegate list_checks(), + to: Check.Manager, + as: :list + + defdelegate create_check(params), + to: Check.Manager, + as: :create + + alias __MODULE__.Assessment + + defdelegate get_or_create_assessment(version, params), + to: Assessment.Manager, + as: :get_or_create +end diff --git a/lib/polar/machines/assessment.ex b/lib/polar/machines/assessment.ex new file mode 100644 index 0000000..a896bda --- /dev/null +++ b/lib/polar/machines/assessment.ex @@ -0,0 +1,49 @@ +defmodule Polar.Machines.Assessment do + use Ecto.Schema + import Ecto.Changeset + + alias Polar.Streams + alias Polar.Machines.Check + alias Polar.Machines.Cluster + + alias __MODULE__.Event + alias __MODULE__.Transitions + + use Eventful.Transitable + + Transitions + |> governs(:current_state, on: Event) + + @valid_attrs ~w( + check_id + cluster_id + instance_type + )a + + @required_attrs ~w( + check_id + cluster_id + instance_type + )a + + schema "assessments" do + field :current_state, :string, default: "created" + + field :instance_type, :string + + belongs_to :check, Check + belongs_to :cluster, Cluster + + belongs_to :version, Streams.Version + + timestamps(type: :utc_datetime_usec) + end + + @doc false + def changeset(assessment, attrs) do + assessment + |> cast(attrs, @valid_attrs) + |> validate_required(@required_attrs) + |> validate_inclusion(:instance_type, ["container", "vm"]) + end +end diff --git a/lib/polar/machines/assessment/event.ex b/lib/polar/machines/assessment/event.ex new file mode 100644 index 0000000..8d192a6 --- /dev/null +++ b/lib/polar/machines/assessment/event.ex @@ -0,0 +1,12 @@ +defmodule Polar.Machines.Assessment.Event do + alias Polar.Machines.Assessment + alias Polar.Accounts.User + + use Eventful, + parent: {:assessment, Assessment}, + actor: {:user, User} + + alias Assessment.Transitions + + handle(:transitions, using: Transitions) +end diff --git a/lib/polar/machines/assessment/manager.ex b/lib/polar/machines/assessment/manager.ex new file mode 100644 index 0000000..2043d41 --- /dev/null +++ b/lib/polar/machines/assessment/manager.ex @@ -0,0 +1,29 @@ +defmodule Polar.Machines.Assessment.Manager do + alias Polar.Repo + alias Polar.Machines.Assessment + + def get_or_create(version, params) do + check_id = Map.get(params, "check_id") || params.check_id + instance_type = Map.get(params, "instance_type") || params.instance_type + + Assessment + |> Repo.get_by( + version_id: version.id, + check_id: check_id, + instance_type: instance_type + ) + |> case do + %Assessment{} = assessment -> + {:ok, assessment} + + nil -> + create(version, params) + end + end + + def create(version, params) do + %Assessment{version_id: version.id} + |> Assessment.changeset(params) + |> Repo.insert() + end +end diff --git a/lib/polar/machines/assessment/transit.ex b/lib/polar/machines/assessment/transit.ex new file mode 100644 index 0000000..6afe34d --- /dev/null +++ b/lib/polar/machines/assessment/transit.ex @@ -0,0 +1,17 @@ +defimpl Eventful.Transit, for: Polar.Machines.Assessment do + alias Polar.Machines.Assessment.Event + + def perform(assessment, user, event_name, options \\ []) do + comment = Keyword.get(options, :comment) + domain = Keyword.get(options, :domain, "transitions") + parameters = Keyword.get(options, :parameters, %{}) + + assessment + |> Event.handle(user, %{ + domain: domain, + name: event_name, + comment: comment, + parameters: parameters + }) + end +end diff --git a/lib/polar/machines/assessment/transitions.ex b/lib/polar/machines/assessment/transitions.ex new file mode 100644 index 0000000..8775338 --- /dev/null +++ b/lib/polar/machines/assessment/transitions.ex @@ -0,0 +1,36 @@ +defmodule Polar.Machines.Assessment.Transitions do + @behaviour Eventful.Handler + use Eventful.Transition, repo: Polar.Repo + + alias Polar.Machines.Assessment + + Assessment + |> transition( + [from: "created", to: "running", via: "run"], + fn changes -> transit(changes) end + ) + + Assessment + |> transition( + [from: "failed", to: "running", via: "run"], + fn changes -> transit(changes) end + ) + + Assessment + |> transition( + [from: "running", to: "running", via: "run"], + fn changes -> transit(changes) end + ) + + Assessment + |> transition( + [from: "running", to: "passed", via: "pass"], + fn changes -> transit(changes) end + ) + + Assessment + |> transition( + [from: "running", to: "failed", via: "fail"], + fn changes -> transit(changes) end + ) +end diff --git a/lib/polar/machines/check.ex b/lib/polar/machines/check.ex new file mode 100644 index 0000000..25dc4ac --- /dev/null +++ b/lib/polar/machines/check.ex @@ -0,0 +1,29 @@ +defmodule Polar.Machines.Check do + use Ecto.Schema + import Ecto.Changeset + + schema "checks" do + field :name, :string, virtual: true + + field :slug, :string + field :description, :string + + timestamps(type: :utc_datetime_usec) + end + + @doc false + def changeset(check, attrs) do + check + |> cast(attrs, [:name, :description]) + |> validate_required([:name, :description]) + |> generate_slug() + end + + defp generate_slug(changeset) do + if name = get_change(changeset, :name) do + put_change(changeset, :slug, Slug.slugify(name)) + else + changeset + end + end +end diff --git a/lib/polar/machines/check/manager.ex b/lib/polar/machines/check/manager.ex new file mode 100644 index 0000000..34cfcff --- /dev/null +++ b/lib/polar/machines/check/manager.ex @@ -0,0 +1,14 @@ +defmodule Polar.Machines.Check.Manager do + alias Polar.Repo + alias Polar.Machines.Check + + def list() do + Repo.all(Check) + end + + def create(params) do + %Check{} + |> Check.changeset(params) + |> Repo.insert() + end +end diff --git a/lib/polar/machines/cluster.ex b/lib/polar/machines/cluster.ex new file mode 100644 index 0000000..02edfeb --- /dev/null +++ b/lib/polar/machines/cluster.ex @@ -0,0 +1,101 @@ +defmodule Polar.Machines.Cluster do + use Ecto.Schema + import Ecto.Changeset + + alias __MODULE__.Credential + alias __MODULE__.Transitions + alias __MODULE__.Event + + use Eventful.Transitable + + Transitions + |> governs(:current_state, on: Event) + + @valid_attrs ~w( + name + type + arch + credential_endpoint + credential_password + credential_password_confirmation + )a + + @required_attrs ~w( + name + type + arch + credential_endpoint + credential_password + credential_password_confirmation + )a + + schema "clusters" do + field :name, :string + field :current_state, :string, default: "created" + + field :type, :string, default: "lxd" + field :arch, :string + + field :credential_endpoint, :string, virtual: true + field :credential_password, :string, virtual: true + field :credential_password_confirmation, :string, virtual: true + field :credential, Polar.Encrypted.Map + + embeds_many :instance_wait_times, __MODULE__.WaitTime, on_replace: :delete + + timestamps(type: :utc_datetime_usec) + end + + @doc false + def changeset(cluster, attrs) do + cluster + |> cast(attrs, @valid_attrs) + |> validate_required(@required_attrs) + |> validate_inclusion(:type, ["lxd", "incus"]) + |> validate_inclusion(:arch, ["amd64", "arm64"]) + |> cast_embed(:instance_wait_times) + |> process_credential() + end + + def update_changeset(cluster, attrs) do + cluster + |> cast(attrs, [:credential_endpoint]) + |> cast_embed(:instance_wait_times) + |> maybe_update_credential() + end + + defp maybe_update_credential(%{data: %{credential: credential}} = changeset) do + if changeset.valid? do + endpoint = get_change(changeset, :credential_endpoint) + + credential = + %Credential{ + endpoint: credential["endpoint"], + private_key: credential["private_key"], + certificate: credential["certificate"] + } + |> Credential.update!(%{endpoint: "https://#{endpoint}"}) + + put_change(changeset, :credential, credential) + else + changeset + end + end + + defp process_credential(changeset) do + if changeset.valid? do + endpoint = get_change(changeset, :credential_endpoint) + + credential = + Credential.create!(%{ + endpoint: "https://#{endpoint}", + password: get_change(changeset, :credential_password), + password_confirmation: get_change(changeset, :credential_password_confirmation) + }) + + put_change(changeset, :credential, credential) + else + changeset + end + end +end diff --git a/lib/polar/machines/cluster/connect.ex b/lib/polar/machines/cluster/connect.ex new file mode 100644 index 0000000..6ba574f --- /dev/null +++ b/lib/polar/machines/cluster/connect.ex @@ -0,0 +1,42 @@ +defmodule Polar.Machines.Cluster.Connect do + use Oban.Worker, queue: :default + + alias Polar.Repo + alias Polar.Accounts.User + alias Polar.Machines.Cluster + + @lexdee Application.compile_env(:polar, :lexdee) || Lexdee + + def perform(%Job{args: %{"user_id" => user_id, "cluster_id" => cluster_id}}) do + user = Repo.get(User, user_id) + %{credential: credential} = cluster = Repo.get(Cluster, cluster_id) + + client = + Lexdee.create_client( + credential["endpoint"], + credential["certificate"], + credential["private_key"] + ) + + params = %{ + "password" => credential["password"], + "certificate" => credential["certificate"] + } + + case @lexdee.create_certificate(client, params) do + {:ok, _response} -> + Eventful.Transit.perform(cluster, user, "healthy") + + {:error, %{"error" => "Certificate already in trust store"}} -> + Eventful.Transit.perform(cluster, user, "healthy") + + {:error, %{"error" => reason, "error_code" => 403}} -> + Eventful.Transit.perform(cluster, user, "revert", comment: reason) + + _ -> + Eventful.Transit.perform(cluster, user, "revert", + comment: "Create certificate failed, make sure port is open." + ) + end + end +end diff --git a/lib/polar/machines/cluster/credential.ex b/lib/polar/machines/cluster/credential.ex new file mode 100644 index 0000000..306d1c4 --- /dev/null +++ b/lib/polar/machines/cluster/credential.ex @@ -0,0 +1,72 @@ +defmodule Polar.Machines.Cluster.Credential do + use Ecto.Schema + import Ecto.Changeset + + @valid_attrs ~w(endpoint password password_confirmation)a + @required_attrs ~w(endpoint password private_key certificate)a + + @derive Jason.Encoder + + @primary_key false + embedded_schema do + field :endpoint, :string + + field :password, :string + field :password_confirmation, :string, virtual: true + + field :private_key, :string + field :certificate, :string + end + + @spec create!(map) :: %__MODULE__{} + def create!(attrs) do + %__MODULE__{} + |> changeset(attrs) + |> apply_action!(:insert) + end + + def update!(credential, attrs) do + credential + |> cast(attrs, [:endpoint]) + |> apply_action!(:insert) + end + + def changeset(credential, attrs) do + credential + |> cast(attrs, @valid_attrs) + |> generate_certificate() + |> validate_password() + |> validate_required(@required_attrs) + end + + defp validate_password(changeset) do + validate_change(changeset, :password, fn :password, password -> + password_confirmation = get_change(changeset, :password_confirmation) + + if password == password_confirmation do + [] + else + [password: "do not match"] + end + end) + end + + defp generate_certificate(changeset) do + ca_key = X509.PrivateKey.new_ec(:secp256r1) + + ca = + X509.Certificate.self_signed( + ca_key, + "/C=US/ST=DE/L=Newark/O=Upmaru/CN=instellar.app", + template: :root_ca + ) + + ca_key = X509.PrivateKey.to_pem(ca_key) + + ca = X509.Certificate.to_pem(ca) + + changeset + |> put_change(:private_key, ca_key) + |> put_change(:certificate, ca) + end +end diff --git a/lib/polar/machines/cluster/event.ex b/lib/polar/machines/cluster/event.ex new file mode 100644 index 0000000..b1d5dc7 --- /dev/null +++ b/lib/polar/machines/cluster/event.ex @@ -0,0 +1,12 @@ +defmodule Polar.Machines.Cluster.Event do + alias Polar.Machines.Cluster + alias Polar.Accounts.User + + use Eventful, + parent: {:cluster, Cluster}, + actor: {:user, User} + + alias Cluster.Transitions + + handle(:transitions, using: Transitions) +end diff --git a/lib/polar/machines/cluster/manager.ex b/lib/polar/machines/cluster/manager.ex new file mode 100644 index 0000000..ed8e0a9 --- /dev/null +++ b/lib/polar/machines/cluster/manager.ex @@ -0,0 +1,18 @@ +defmodule Polar.Machines.Cluster.Manager do + alias Polar.Repo + alias Polar.Machines.Cluster + + import Ecto.Query, only: [where: 3] + + def list(:for_testing) do + Cluster + |> where([c], c.current_state == "healthy") + |> Repo.all() + end + + def create(params) do + %Cluster{} + |> Cluster.changeset(params) + |> Repo.insert() + end +end diff --git a/lib/polar/machines/cluster/transit.ex b/lib/polar/machines/cluster/transit.ex new file mode 100644 index 0000000..d2ed892 --- /dev/null +++ b/lib/polar/machines/cluster/transit.ex @@ -0,0 +1,17 @@ +defimpl Eventful.Transit, for: Polar.Machines.Cluster do + alias Polar.Machines.Cluster.Event + + def perform(cluster, user, event_name, options \\ []) do + comment = Keyword.get(options, :comment) + domain = Keyword.get(options, :domain, "transitions") + parameters = Keyword.get(options, :parameters, %{}) + + cluster + |> Event.handle(user, %{ + domain: domain, + name: event_name, + comment: comment, + parameters: parameters + }) + end +end diff --git a/lib/polar/machines/cluster/transitions.ex b/lib/polar/machines/cluster/transitions.ex new file mode 100644 index 0000000..ed91f65 --- /dev/null +++ b/lib/polar/machines/cluster/transitions.ex @@ -0,0 +1,30 @@ +defmodule Polar.Machines.Cluster.Transitions do + @behaviour Eventful.Handler + use Eventful.Transition, repo: Polar.Repo + + alias Polar.Machines.Cluster + + Cluster + |> transition( + [from: "created", to: "connecting", via: "connect"], + fn changes -> transit(changes, Cluster.Triggers) end + ) + + Cluster + |> transition( + [from: "healthy", to: "connecting", via: "connect"], + fn changes -> transit(changes, Cluster.Triggers) end + ) + + Cluster + |> transition( + [from: "connecting", to: "healthy", via: "healthy"], + fn changes -> transit(changes) end + ) + + Cluster + |> transition( + [from: "connecting", to: "created", via: "revert"], + fn changes -> transit(changes) end + ) +end diff --git a/lib/polar/machines/cluster/triggers.ex b/lib/polar/machines/cluster/triggers.ex new file mode 100644 index 0000000..bab4450 --- /dev/null +++ b/lib/polar/machines/cluster/triggers.ex @@ -0,0 +1,13 @@ +defmodule Polar.Machines.Cluster.Triggers do + use Eventful.Trigger + + alias Polar.Machines.Cluster + alias Polar.Machines.Cluster.Connect + + Cluster + |> trigger([currently: "connecting"], fn event, cluster -> + %{user_id: event.user_id, cluster_id: cluster.id} + |> Connect.new() + |> Oban.insert() + end) +end diff --git a/lib/polar/machines/cluster/wait_time.ex b/lib/polar/machines/cluster/wait_time.ex new file mode 100644 index 0000000..80eaaf5 --- /dev/null +++ b/lib/polar/machines/cluster/wait_time.ex @@ -0,0 +1,18 @@ +defmodule Polar.Machines.Cluster.WaitTime do + use Ecto.Schema + import Ecto.Changeset + + @derive Jason.Encoder + + @primary_key false + embedded_schema do + field :type, :string + field :duration, :integer + end + + def changeset(wait_time, params) do + wait_time + |> cast(params, [:type, :duration]) + |> validate_inclusion(:type, ["container", "vm"]) + end +end diff --git a/lib/polar/release.ex b/lib/polar/release/tasks.ex similarity index 93% rename from lib/polar/release.ex rename to lib/polar/release/tasks.ex index 050fc00..e60a004 100644 --- a/lib/polar/release.ex +++ b/lib/polar/release/tasks.ex @@ -1,4 +1,4 @@ -defmodule Polar.Release do +defmodule Polar.Release.Tasks do @app :polar def migrate do diff --git a/lib/polar/streams.ex b/lib/polar/streams.ex index 39410fe..5f68124 100644 --- a/lib/polar/streams.ex +++ b/lib/polar/streams.ex @@ -23,7 +23,7 @@ defmodule Polar.Streams do to: Version.Manager, as: :create - defdelegate deactivate_previous_versions(version), + defdelegate deactivate_previous_versions(event, version), to: Version.Manager, as: :deactivate_previous diff --git a/lib/polar/streams/product.ex b/lib/polar/streams/product.ex index 34237d4..3df2605 100644 --- a/lib/polar/streams/product.ex +++ b/lib/polar/streams/product.ex @@ -37,6 +37,7 @@ defmodule Polar.Streams.Product do has_one :latest_version, Version, where: [current_state: "active"] has_many :active_versions, Version, where: [current_state: "active"] + has_many :testing_versions, Version, where: [current_state: "testing"] timestamps(type: :utc_datetime_usec) end @@ -54,6 +55,15 @@ defmodule Polar.Streams.Product do |> validate_inclusion(:arch, ["arm64", "amd64"]) end + def scope(:testing, queryable) do + from( + p in queryable, + join: v in assoc(p, :testing_versions), + where: not is_nil(v.product_id), + group_by: [:id] + ) + end + def scope(:active, queryable) do from( p in queryable, diff --git a/lib/polar/streams/release_channel.ex b/lib/polar/streams/release_channel.ex new file mode 100644 index 0000000..36aa3ea --- /dev/null +++ b/lib/polar/streams/release_channel.ex @@ -0,0 +1,17 @@ +defmodule Polar.Streams.ReleaseChannel do + def entries, + do: %{ + "active" => %{ + scope: [:active], + preload: [active_versions: [:items]] + }, + "testing" => %{ + scope: [:testing], + preload: [testing_versions: [:items]] + } + } + + def valid_names do + Map.keys(entries()) + end +end diff --git a/lib/polar/streams/version.ex b/lib/polar/streams/version.ex index 56faa1b..cf9dfe2 100644 --- a/lib/polar/streams/version.ex +++ b/lib/polar/streams/version.ex @@ -16,7 +16,7 @@ defmodule Polar.Streams.Version do import Ecto.Query, only: [from: 2] schema "versions" do - field :current_state, :string, default: "active" + field :current_state, :string, default: "created" field :serial, :string belongs_to :product, Product diff --git a/lib/polar/streams/version/manager.ex b/lib/polar/streams/version/manager.ex index 25813ad..a11f396 100644 --- a/lib/polar/streams/version/manager.ex +++ b/lib/polar/streams/version/manager.ex @@ -9,32 +9,29 @@ defmodule Polar.Streams.Version.Manager do %Version{product_id: product.id} |> Version.changeset(attrs) |> Repo.insert() - |> case do - {:ok, version} = result -> - deactivate_previous(version) - - result - - error -> - error - end end - def deactivate_previous(version) do + def deactivate_previous(event, version) do basic_setting = Globals.get("basic") bot = Polar.Accounts.Automation.get_bot!() - from( - v in Version, - where: - v.product_id == ^version.product_id and - v.current_state == ^"active", - offset: ^basic_setting.versions_per_product, - order_by: [desc: :inserted_at] - ) - |> Repo.all() - |> Enum.map(fn v -> - Eventful.Transit.perform(v, bot, "deactivate") - end) + results = + from( + v in Version, + where: + v.product_id == ^version.product_id and + v.current_state == ^"active", + offset: ^basic_setting.versions_per_product, + order_by: [desc: :inserted_at] + ) + |> Repo.all() + |> Enum.map(fn v -> + Eventful.Transit.perform(v, bot, "deactivate", + comment: "New version activated.", + parameters: %{"event_id" => event.id} + ) + end) + + {:ok, results} end end diff --git a/lib/polar/streams/version/pruning.ex b/lib/polar/streams/version/pruning.ex new file mode 100644 index 0000000..1e95f1c --- /dev/null +++ b/lib/polar/streams/version/pruning.ex @@ -0,0 +1,31 @@ +defmodule Polar.Streams.Version.Pruning do + use Oban.Worker, queue: :default + + alias Polar.Repo + alias Polar.Accounts.Automation + alias Polar.Streams.Version + + import Ecto.Query, only: [from: 2] + + @age_days 2 + + def perform(%Job{}) do + age = + DateTime.utc_now() + |> DateTime.add(-@age_days, :day) + + versions_to_deactivate = + from(v in Version, + where: v.current_state == "testing" and v.inserted_at < ^age + ) + |> Repo.all() + + bot = Automation.get_bot!() + + Enum.each(versions_to_deactivate, fn version -> + Eventful.Transit.perform(version, bot, "deactivate", comment: "testing period expired") + end) + + :ok + end +end diff --git a/lib/polar/streams/version/transitions.ex b/lib/polar/streams/version/transitions.ex index 30e0c18..b592e8a 100644 --- a/lib/polar/streams/version/transitions.ex +++ b/lib/polar/streams/version/transitions.ex @@ -4,6 +4,36 @@ defmodule Polar.Streams.Version.Transitions do alias Polar.Streams.Version + Version + |> transition( + [from: "created", to: "testing", via: "test"], + fn changes -> transit(changes) end + ) + + Version + |> transition( + [from: "testing", to: "testing", via: "test"], + fn changes -> transit(changes) end + ) + + Version + |> transition( + [from: "inactive", to: "testing", via: "test"], + fn changes -> transit(changes) end + ) + + Version + |> transition( + [from: "testing", to: "inactive", via: "deactivate"], + fn changes -> transit(changes) end + ) + + Version + |> transition( + [from: "testing", to: "active", via: "activate"], + fn changes -> transit(changes, Version.Triggers) end + ) + Version |> transition( [from: "active", to: "inactive", via: "deactivate"], diff --git a/lib/polar/streams/version/triggers.ex b/lib/polar/streams/version/triggers.ex new file mode 100644 index 0000000..b5f62ac --- /dev/null +++ b/lib/polar/streams/version/triggers.ex @@ -0,0 +1,9 @@ +defmodule Polar.Streams.Version.Triggers do + use Eventful.Trigger + + alias Polar.Streams + alias Polar.Streams.Version + + Version + |> trigger([currently: "active"], &Streams.deactivate_previous_versions/2) +end diff --git a/lib/polar/vault.ex b/lib/polar/vault.ex new file mode 100644 index 0000000..2599083 --- /dev/null +++ b/lib/polar/vault.ex @@ -0,0 +1,3 @@ +defmodule Polar.Vault do + use Cloak.Vault, otp_app: :polar +end diff --git a/lib/polar_web/controllers/publish/event_controller.ex b/lib/polar_web/controllers/publish/event_controller.ex new file mode 100644 index 0000000..8f4e90f --- /dev/null +++ b/lib/polar_web/controllers/publish/event_controller.ex @@ -0,0 +1,40 @@ +defmodule PolarWeb.Publish.EventController do + use PolarWeb, :controller + + alias Polar.Repo + alias Polar.Streams.Version + + action_fallback PolarWeb.FallbackController + + def create(conn, %{ + "version_id" => version_id, + "event" => event_params + }) do + with %Version{} = version <- Repo.get(Version, version_id) do + transit_and_render(conn, version, event_params) + end + end + + alias Polar.Machines.Assessment + + def create(conn, %{ + "assessment_id" => assessment_id, + "event" => event_params + }) do + with %Assessment{} = assessment <- Repo.get(Assessment, assessment_id) do + transit_and_render(conn, assessment, event_params) + end + end + + defp transit_and_render( + %{assigns: %{current_user: user}} = conn, + resource, + %{"name" => event_name} + ) do + with {:ok, %{event: event}} <- Eventful.Transit.perform(resource, user, event_name) do + conn + |> put_status(:created) + |> render(:create, %{event: event}) + end + end +end diff --git a/lib/polar_web/controllers/publish/event_json.ex b/lib/polar_web/controllers/publish/event_json.ex new file mode 100644 index 0000000..2d7fe2a --- /dev/null +++ b/lib/polar_web/controllers/publish/event_json.ex @@ -0,0 +1,5 @@ +defmodule PolarWeb.Publish.EventJSON do + def create(%{event: event}) do + %{data: %{id: event.id, name: event.name}} + end +end diff --git a/lib/polar_web/controllers/publish/product_json.ex b/lib/polar_web/controllers/publish/product_json.ex index fdcce2c..1712cda 100644 --- a/lib/polar_web/controllers/publish/product_json.ex +++ b/lib/polar_web/controllers/publish/product_json.ex @@ -5,7 +5,8 @@ defmodule PolarWeb.Publish.ProductJSON do %{ data: %{ id: product.id, - key: Product.key(product) + key: Product.key(product), + requirements: product.requirements } } end diff --git a/lib/polar_web/controllers/publish/testing/assessment_controller.ex b/lib/polar_web/controllers/publish/testing/assessment_controller.ex new file mode 100644 index 0000000..8067ed6 --- /dev/null +++ b/lib/polar_web/controllers/publish/testing/assessment_controller.ex @@ -0,0 +1,27 @@ +defmodule PolarWeb.Publish.Testing.AssessmentController do + use PolarWeb, :controller + + alias Polar.Repo + alias Polar.Machines + + alias Polar.Streams.Version + + alias PolarWeb.Params.Assessment + + action_fallback PolarWeb.FallbackController + + def create(conn, %{ + "version_id" => version_id, + "assessment" => assessment_params + }) do + with %Version{} = check <- Repo.get(Version, version_id), + {:ok, assessment_params} <- Assessment.parse(assessment_params), + {:ok, assessment} <- + Machines.get_or_create_assessment(check, Map.from_struct(assessment_params)) do + assessment = Repo.preload(assessment, [:check]) + + conn + |> render(:create, %{assessment: assessment}) + end + end +end diff --git a/lib/polar_web/controllers/publish/testing/assessment_json.ex b/lib/polar_web/controllers/publish/testing/assessment_json.ex new file mode 100644 index 0000000..124bb3b --- /dev/null +++ b/lib/polar_web/controllers/publish/testing/assessment_json.ex @@ -0,0 +1,17 @@ +defmodule PolarWeb.Publish.Testing.AssessmentJSON do + alias Polar.Machines.Assessment + + def create(%{assessment: assessment}) do + %{data: data(assessment)} + end + + defp data(%Assessment{} = assessment) do + %{ + id: assessment.id, + current_state: assessment.current_state, + check: %{ + slug: assessment.check.slug + } + } + end +end diff --git a/lib/polar_web/controllers/publish/testing/check_controller.ex b/lib/polar_web/controllers/publish/testing/check_controller.ex new file mode 100644 index 0000000..6218756 --- /dev/null +++ b/lib/polar_web/controllers/publish/testing/check_controller.ex @@ -0,0 +1,13 @@ +defmodule PolarWeb.Publish.Testing.CheckController do + use PolarWeb, :controller + + alias Polar.Machines + + action_fallback PolarWeb.FallbackController + + def index(conn, _params) do + checks = Machines.list_checks() + + render(conn, :index, %{checks: checks}) + end +end diff --git a/lib/polar_web/controllers/publish/testing/check_json.ex b/lib/polar_web/controllers/publish/testing/check_json.ex new file mode 100644 index 0000000..2f06e34 --- /dev/null +++ b/lib/polar_web/controllers/publish/testing/check_json.ex @@ -0,0 +1,11 @@ +defmodule PolarWeb.Publish.Testing.CheckJSON do + alias Polar.Machines.Check + + def index(%{checks: checks}) do + %{data: Enum.map(checks, &data/1)} + end + + def data(%Check{} = check) do + %{id: check.id, slug: check.slug} + end +end diff --git a/lib/polar_web/controllers/publish/testing/cluster_controller.ex b/lib/polar_web/controllers/publish/testing/cluster_controller.ex new file mode 100644 index 0000000..1851fd4 --- /dev/null +++ b/lib/polar_web/controllers/publish/testing/cluster_controller.ex @@ -0,0 +1,13 @@ +defmodule PolarWeb.Publish.Testing.ClusterController do + use PolarWeb, :controller + + alias Polar.Machines + + action_fallback PolarWeb.FallbackController + + def index(conn, _params) do + clusters = Machines.list_clusters(:for_testing) + + render(conn, :index, %{clusters: clusters}) + end +end diff --git a/lib/polar_web/controllers/publish/testing/cluster_json.ex b/lib/polar_web/controllers/publish/testing/cluster_json.ex new file mode 100644 index 0000000..aa29e3a --- /dev/null +++ b/lib/polar_web/controllers/publish/testing/cluster_json.ex @@ -0,0 +1,20 @@ +defmodule PolarWeb.Publish.Testing.ClusterJSON do + alias Polar.Machines.Cluster + + def index(%{clusters: clusters}) do + %{ + data: Enum.map(clusters, &data/1) + } + end + + def data(%Cluster{} = cluster) do + %{ + id: cluster.id, + type: cluster.type, + arch: cluster.arch, + credential: cluster.credential, + current_state: cluster.current_state, + instance_wait_times: cluster.instance_wait_times + } + end +end diff --git a/lib/polar_web/controllers/publish/version_controller.ex b/lib/polar_web/controllers/publish/version_controller.ex index 69cafe2..59f5ca6 100644 --- a/lib/polar_web/controllers/publish/version_controller.ex +++ b/lib/polar_web/controllers/publish/version_controller.ex @@ -4,9 +4,18 @@ defmodule PolarWeb.Publish.VersionController do alias Polar.Repo alias Polar.Streams alias Polar.Streams.Product + alias Polar.Streams.Version action_fallback PolarWeb.FallbackController + def show(conn, %{"product_id" => product_id, "id" => serial}) do + version = Repo.get_by(Version, product_id: product_id, serial: serial) + + if version do + render(conn, :show, %{version: version}) + end + end + def create(conn, %{"product_id" => product_id, "version" => version_params}) do product = Repo.get(Product, product_id) diff --git a/lib/polar_web/controllers/publish/version_json.ex b/lib/polar_web/controllers/publish/version_json.ex index 73b07fc..341b707 100644 --- a/lib/polar_web/controllers/publish/version_json.ex +++ b/lib/polar_web/controllers/publish/version_json.ex @@ -1,5 +1,9 @@ defmodule PolarWeb.Publish.VersionJSON do + def show(%{version: version}) do + %{data: %{id: version.id, serial: version.serial}} + end + def create(%{version: version}) do - %{data: %{id: version.id}} + %{data: %{id: version.id, serial: version.serial}} end end diff --git a/lib/polar_web/controllers/stream_controller.ex b/lib/polar_web/controllers/stream_controller.ex index 2227cfe..7e48741 100644 --- a/lib/polar_web/controllers/stream_controller.ex +++ b/lib/polar_web/controllers/stream_controller.ex @@ -4,13 +4,19 @@ defmodule PolarWeb.StreamController do alias Polar.Accounts alias Polar.Streams + alias Polar.Streams.ReleaseChannel + action_fallback PolarWeb.FallbackController def index(conn, %{"space_token" => space_token}) do credential = Accounts.get_space_credential(token: space_token) if credential do - products = Streams.list_products([:active]) + release_channel = + ReleaseChannel.entries() + |> Map.fetch!(credential.release_channel) + + products = Streams.list_products(release_channel.scope) render(conn, :index, %{products: products}) end diff --git a/lib/polar_web/controllers/streams/image_controller.ex b/lib/polar_web/controllers/streams/image_controller.ex index 0501d60..71f1f49 100644 --- a/lib/polar_web/controllers/streams/image_controller.ex +++ b/lib/polar_web/controllers/streams/image_controller.ex @@ -5,15 +5,21 @@ defmodule PolarWeb.Streams.ImageController do alias Polar.Accounts alias Polar.Streams + alias Polar.Streams.ReleaseChannel + action_fallback PolarWeb.FallbackController def index(conn, %{"space_token" => space_token}) do credential = Accounts.get_space_credential(token: space_token) if credential do + release_channel = + ReleaseChannel.entries() + |> Map.fetch!(credential.release_channel) + products = - Streams.list_products([:active]) - |> Repo.preload(active_versions: [:items]) + Streams.list_products(release_channel.scope) + |> Repo.preload(release_channel.preload) render(conn, :index, %{products: products, credential: credential}) end diff --git a/lib/polar_web/controllers/streams/image_json.ex b/lib/polar_web/controllers/streams/image_json.ex index 6edacbd..096d64d 100644 --- a/lib/polar_web/controllers/streams/image_json.ex +++ b/lib/polar_web/controllers/streams/image_json.ex @@ -20,7 +20,13 @@ defmodule PolarWeb.Streams.ImageJSON do {Product.key(product), product_attributes(product, params)} end - defp product_attributes(product, params) do + defp product_attributes(product, %{credential: credential} = params) do + versions_key = + "#{credential.release_channel}_versions" + |> String.to_existing_atom() + + versions = get_in(product, [Access.key!(versions_key)]) + %{ aliases: Enum.join(product.aliases, ","), arch: product.arch, @@ -30,7 +36,7 @@ defmodule PolarWeb.Streams.ImageJSON do requirements: product.requirements, variant: product.variant, versions: - Enum.map(product.active_versions, &render_version(&1, params)) + Enum.map(versions, &render_version(&1, params)) |> Enum.into(%{}) } end diff --git a/lib/polar_web/live/dashboard/credential/new_live.ex b/lib/polar_web/live/dashboard/credential/new_live.ex index daf8d9b..b0e5706 100644 --- a/lib/polar_web/live/dashboard/credential/new_live.ex +++ b/lib/polar_web/live/dashboard/credential/new_live.ex @@ -6,6 +6,8 @@ defmodule PolarWeb.Dashboard.Credential.NewLive do alias Polar.Accounts alias Polar.Accounts.Space + alias Polar.Streams.ReleaseChannel + def render(assigns) do ~H"""
@@ -64,6 +66,40 @@ defmodule PolarWeb.Dashboard.Credential.NewLive do
+
+
+

+ <%= gettext("Release channel") %> +

+
+
+ <%= gettext("Choose release channel") %> + +
+ +
+
+

@@ -83,7 +119,7 @@ defmodule PolarWeb.Dashboard.Credential.NewLive do for={Phoenix.HTML.Form.input_id(:credential, :type, type)} class={[ "flex items-center justify-center rounded-md py-3 px-3 text-sm font-semibold sm:flex-1 cursor-pointer focus:outline-none", - "#{if @credential_form.source.changes[:type] == type, do: "bg-indigo-600 text-white hover:bg-indigo-500", else: "ring-1 ring-inset ring-slate-300 bg-white text-slate-900 hover:bg-slate-50"}" + "#{if (@credential_form.source.changes[:type] || @credential_form.data.type) == type, do: "bg-indigo-600 text-white hover:bg-indigo-500", else: "ring-1 ring-inset ring-slate-300 bg-white text-slate-900 hover:bg-slate-50"}" ]} > <.input @@ -93,7 +129,7 @@ defmodule PolarWeb.Dashboard.Credential.NewLive do value={type} class="sr-only" /> - <%= type %> + <%= Phoenix.Naming.humanize(type) %>

diff --git a/lib/polar_web/live/dashboard/credential_live.ex b/lib/polar_web/live/dashboard/credential_live.ex index 61f2bec..f09a6ac 100644 --- a/lib/polar_web/live/dashboard/credential_live.ex +++ b/lib/polar_web/live/dashboard/credential_live.ex @@ -34,6 +34,12 @@ defmodule PolarWeb.Dashboard.CredentialLive do <%= @credential.type %>
+
+
<%= gettext("Release Channel") %>
+
+ <%= @credential.release_channel %> +
+
<%= gettext("Expires At") %>
diff --git a/lib/polar_web/params/assessment.ex b/lib/polar_web/params/assessment.ex new file mode 100644 index 0000000..d5b84e9 --- /dev/null +++ b/lib/polar_web/params/assessment.ex @@ -0,0 +1,29 @@ +defmodule PolarWeb.Params.Assessment do + use Ecto.Schema + import Ecto.Changeset + + @required_attrs ~w( + check_id + cluster_id + instance_type + )a + + @primary_key false + embedded_schema do + field :check_id, :integer + field :cluster_id, :integer + field :instance_type, :string + end + + def parse(params) do + %__MODULE__{} + |> changeset(params) + |> apply_action(:insert) + end + + def changeset(assessment, params) do + assessment + |> cast(params, @required_attrs) + |> validate_required(@required_attrs) + end +end diff --git a/lib/polar_web/router.ex b/lib/polar_web/router.ex index 6337b1f..72e710b 100644 --- a/lib/polar_web/router.ex +++ b/lib/polar_web/router.ex @@ -102,7 +102,20 @@ defmodule PolarWeb.Router do resources "/storage", StorageController, only: [:show], singleton: true resources "/products", ProductController, only: [:show] do - resources "/versions", VersionController, only: [:create] + resources "/versions", VersionController, only: [:show, :create] + end + + resources "/versions/:version_id/events", EventController, only: [:create] + + scope "/testing", as: :testing do + resources "/checks", Testing.CheckController, only: [:index] + resources "/clusters", Testing.ClusterController, only: [:index] + + resources "/assessments/:assessment_id/events", EventController, only: [:create] + + scope "/versions/:version_id" do + resources "/assessments", Testing.AssessmentController, only: [:create] + end end end end diff --git a/mix.exs b/mix.exs index 15d080f..4e990bf 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Polar.MixProject do def project do [ app: :polar, - version: "0.1.2", + version: "0.2.0", elixir: "~> 1.14", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, @@ -68,11 +68,24 @@ defmodule Polar.MixProject do {:aws, "~> 0.14.0"}, {:aws_signature, "~> 0.3.1"}, + # Background processing + {:oban, "~> 2.17"}, + + # LXD client + {:lexdee, "~> 2.3"}, + # Cert {:x509, "~> 0.8"}, + # Slug + {:slugify, "~> 1.3"}, + + # Encryption + {:cloak_ecto, "~> 1.3"}, + # Dev / Test - {:dialyxir, "~> 1.0", only: [:dev], runtime: false} + {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, + {:mox, "~> 1.0", only: :test} ] end diff --git a/mix.lock b/mix.lock index a712e81..8d49703 100644 --- a/mix.lock +++ b/mix.lock @@ -3,7 +3,9 @@ "aws_signature": {:hex, :aws_signature, "0.3.1", "67f369094cbd55ffa2bbd8cc713ede14b195fcfb45c86665cd7c5ad010276148", [:rebar3], [], "hexpm", "50fc4dc1d1f7c2d0a8c63f455b3c66ecd74c1cf4c915c768a636f9227704a674"}, "bandit": {:hex, :bandit, "1.2.0", "2b5784909cc25b2514868055ff27458cdc63314514b90d86448ff91d18bece80", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "05688b883d87cc3b32991517a61e8c2ce8ee2dd6aa6eb73635426002a6661491"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"}, - "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, + "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, + "cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"}, + "cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, @@ -23,10 +25,14 @@ "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "lexdee": {:hex, :lexdee, "2.3.9", "f0883bdf572fca7c3e1978f6ec388ea3832c73be1419457c2a452e218f688edc", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mint, "~> 1.5", [hex: :mint, repo: "hexpm", optional: false]}, {:mint_web_socket, "~> 1.0.2", [hex: :mint_web_socket, repo: "hexpm", optional: false]}, {:tesla, "~> 1.7.0", [hex: :tesla, repo: "hexpm", optional: false]}, {:x509, "~> 0.8.1", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm", "9ac4cc802ac6810d1395769cea57124aa255df97af9f745237d0609a052d41ea"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, - "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, - "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, - "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, + "mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"}, + "mint_web_socket": {:hex, :mint_web_socket, "1.0.4", "0b539116dbb3d3f861cdf5e15e269a933cb501c113a14db7001a3157d96ffafd", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "027d4c5529c45a4ba0ce27a01c0f35f284a5468519c045ca15f43decb360a991"}, + "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "oban": {:hex, :oban, "2.17.10", "c3e5bd739b5c3fdc38eba1d43ab270a8c6ca4463bb779b7705c69400b0d87678", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4afd027b8e2bc3c399b54318b4f46ee8c40251fb55a285cb4e38b5363f0ee7c4"}, "phoenix": {:hex, :phoenix, "1.7.11", "1d88fc6b05ab0c735b250932c4e6e33bfa1c186f76dcf623d8dd52f07d6379c7", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "b1ec57f2e40316b306708fe59b92a16b9f6f4bf50ccfa41aa8c7feb79e0ec02a"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, "phoenix_html": {:hex, :phoenix_html, "4.0.0", "4857ec2edaccd0934a923c2b0ba526c44a173c86b847e8db725172e9e51d11d6", [:mix], [], "hexpm", "cee794a052f243291d92fa3ccabcb4c29bb8d236f655fb03bcbdc3a8214b8d13"}, @@ -38,11 +44,13 @@ "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, "postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"}, + "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, "swoosh": {:hex, :swoosh, "1.15.2", "490ea85a98e8fb5178c07039e0d8519839e38127724a58947a668c00db7574ee", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9f7739c02f6c7c0ca82ee397f3bfe0465dbe4c8a65372ac2a5584bf147dd5831"}, "tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, + "tesla": {:hex, :tesla, "1.7.0", "a62dda2f80d4f8a925eb7b8c5b78c461e0eb996672719fe1a63b26321a5f8b4e", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2e64f01ebfdb026209b47bc651a0e65203fcff4ae79c11efb73c4852b00dc313"}, "thousand_island": {:hex, :thousand_island, "1.3.2", "bc27f9afba6e1a676dd36507d42e429935a142cf5ee69b8e3f90bff1383943cd", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e085b93012cd1057b378fce40cbfbf381ff6d957a382bfdd5eca1a98eec2535"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"}, diff --git a/priv/repo/migrations/20240619064429_add_release_channel_to_space_credentials.exs b/priv/repo/migrations/20240619064429_add_release_channel_to_space_credentials.exs new file mode 100644 index 0000000..4d517f6 --- /dev/null +++ b/priv/repo/migrations/20240619064429_add_release_channel_to_space_credentials.exs @@ -0,0 +1,9 @@ +defmodule Polar.Repo.Migrations.AddReleaseChannelToSpaceCredentials do + use Ecto.Migration + + def change do + alter table(:space_credentials) do + add :release_channel, :string, default: "active" + end + end +end diff --git a/priv/repo/migrations/20240619102744_create_clusters.exs b/priv/repo/migrations/20240619102744_create_clusters.exs new file mode 100644 index 0000000..c600cb4 --- /dev/null +++ b/priv/repo/migrations/20240619102744_create_clusters.exs @@ -0,0 +1,17 @@ +defmodule Polar.Repo.Migrations.CreateClusters do + use Ecto.Migration + + def change do + create table(:clusters) do + add :name, :citext, null: false + add :type, :citext, null: false + add :arch, :citext, null: false + add :credential, :binary, null: false + add :current_state, :citext, default: "created" + + timestamps(type: :utc_datetime_usec) + end + + create index(:clusters, [:name], unique: true) + end +end diff --git a/priv/repo/migrations/20240619130455_create_checks.exs b/priv/repo/migrations/20240619130455_create_checks.exs new file mode 100644 index 0000000..c7c587d --- /dev/null +++ b/priv/repo/migrations/20240619130455_create_checks.exs @@ -0,0 +1,14 @@ +defmodule Polar.Repo.Migrations.CreateChecks do + use Ecto.Migration + + def change do + create table(:checks) do + add :slug, :citext, null: false + add :description, :citext, null: false + + timestamps(type: :utc_datetime_usec) + end + + create index(:checks, [:slug], unique: true) + end +end diff --git a/priv/repo/migrations/20240619130844_create_assessments.exs b/priv/repo/migrations/20240619130844_create_assessments.exs new file mode 100644 index 0000000..527afe6 --- /dev/null +++ b/priv/repo/migrations/20240619130844_create_assessments.exs @@ -0,0 +1,21 @@ +defmodule Polar.Repo.Migrations.CreateAssessments do + use Ecto.Migration + + def change do + create table(:assessments) do + add :current_state, :citext, default: "created", null: false + + add :check_id, references(:checks, on_delete: :restrict), null: false + add :version_id, references(:versions, on_delete: :restrict), null: false + add :cluster_id, references(:clusters, on_delete: :restrict), null: false + + timestamps(type: :utc_datetime_usec) + end + + create index(:assessments, [:check_id]) + create index(:assessments, [:version_id]) + create index(:assessments, [:cluster_id]) + + create index(:assessments, [:check_id, :version_id], unique: true) + end +end diff --git a/priv/repo/migrations/20240621094029_add_oban_jobs_table.exs b/priv/repo/migrations/20240621094029_add_oban_jobs_table.exs new file mode 100644 index 0000000..b9caf71 --- /dev/null +++ b/priv/repo/migrations/20240621094029_add_oban_jobs_table.exs @@ -0,0 +1,13 @@ +defmodule Polar.Repo.Migrations.AddObanJobsTable do + use Ecto.Migration + + def up do + Oban.Migration.up(version: 12) + end + + # We specify `version: 1` in `down`, ensuring that we'll roll all the way back down if + # necessary, regardless of which version we've migrated `up` to. + def down do + Oban.Migration.down(version: 1) + end +end diff --git a/priv/repo/migrations/20240621115038_create_cluster_events.exs b/priv/repo/migrations/20240621115038_create_cluster_events.exs new file mode 100644 index 0000000..6d35608 --- /dev/null +++ b/priv/repo/migrations/20240621115038_create_cluster_events.exs @@ -0,0 +1,28 @@ +defmodule Polar.Repo.Migrations.CreateClusterEvents do + use Ecto.Migration + + def change do + create table(:cluster_events) do + add(:name, :citext, null: false) + add(:domain, :citext, null: false) + add(:metadata, :map, default: "{}") + + add( + :cluster_id, + references(:clusters, on_delete: :restrict), + null: false + ) + + add( + :user_id, + references(:users, on_delete: :restrict), + null: false + ) + + timestamps(type: :utc_datetime_usec) + end + + create index(:cluster_events, [:cluster_id]) + create index(:cluster_events, [:user_id]) + end +end diff --git a/priv/repo/migrations/20240624084728_create_assessment_events.exs b/priv/repo/migrations/20240624084728_create_assessment_events.exs new file mode 100644 index 0000000..c69e4a1 --- /dev/null +++ b/priv/repo/migrations/20240624084728_create_assessment_events.exs @@ -0,0 +1,28 @@ +defmodule Polar.Repo.Migrations.CreateAssessmentEvents do + use Ecto.Migration + + def change do + create table(:assessment_events) do + add(:name, :citext, null: false) + add(:domain, :citext, null: false) + add(:metadata, :map, default: "{}") + + add( + :assessment_id, + references(:assessments, on_delete: :restrict), + null: false + ) + + add( + :user_id, + references(:users, on_delete: :restrict), + null: false + ) + + timestamps(type: :utc_datetime_usec) + end + + create index(:assessment_events, [:assessment_id]) + create index(:assessment_events, [:user_id]) + end +end diff --git a/priv/repo/migrations/20240628092615_add_instance_type_to_assessments.exs b/priv/repo/migrations/20240628092615_add_instance_type_to_assessments.exs new file mode 100644 index 0000000..dada87b --- /dev/null +++ b/priv/repo/migrations/20240628092615_add_instance_type_to_assessments.exs @@ -0,0 +1,12 @@ +defmodule Polar.Repo.Migrations.AddInstanceTypeToAssessments do + use Ecto.Migration + + def change do + alter table(:assessments) do + add :instance_type, :string, null: false + end + + drop index(:assessments, [:check_id, :version_id], unique: true) + create index(:assessments, [:check_id, :version_id, :instance_type], unique: true) + end +end diff --git a/priv/repo/migrations/20240704043522_add_instance_wait_time_to_clusters.exs b/priv/repo/migrations/20240704043522_add_instance_wait_time_to_clusters.exs new file mode 100644 index 0000000..f25fe01 --- /dev/null +++ b/priv/repo/migrations/20240704043522_add_instance_wait_time_to_clusters.exs @@ -0,0 +1,9 @@ +defmodule Polar.Repo.Migrations.AddInstanceWaitTimeToClusters do + use Ecto.Migration + + def change do + alter table(:clusters) do + add :instance_wait_times, {:array, :map}, default: [] + end + end +end diff --git a/test/polar/machines/assessment/manager_test.exs b/test/polar/machines/assessment/manager_test.exs new file mode 100644 index 0000000..a38ed69 --- /dev/null +++ b/test/polar/machines/assessment/manager_test.exs @@ -0,0 +1,57 @@ +defmodule Polar.Machines.Assessment.ManagerTest do + use Polar.DataCase, async: true + + alias Polar.Machines + alias Polar.Streams + + import Polar.StreamsFixtures + + setup do + {:ok, check} = + Machines.create_check(%{ + name: "ipv4-issuing", + description: "issue ipv4 correctly" + }) + + {:ok, cluster} = + Machines.create_cluster(%{ + name: "example", + type: "lxd", + arch: "amd64", + credential_endpoint: "some.cluster.com:8443", + credential_password: "sometoken", + credential_password_confirmation: "sometoken" + }) + + {:ok, product} = + Streams.create_product(%{ + aliases: ["alpine/3.19", "alpine/3.19/default"], + arch: "amd64", + os: "Alpine", + release: "3.19", + release_title: "3.19", + variant: "default", + requirements: %{ + secureboot: "false" + } + }) + + {:ok, version} = + Streams.create_version(product, valid_version_attributes(2)) + + {:ok, check: check, cluster: cluster, version: version} + end + + describe "create assessment" do + test "successfully create assessment", %{check: check, cluster: cluster, version: version} do + assert {:ok, assessment} = + Machines.get_or_create_assessment(version, %{ + check_id: check.id, + cluster_id: cluster.id, + instance_type: "container" + }) + + assert assessment.current_state == "created" + end + end +end diff --git a/test/polar/machines/assessment/transitions_test.exs b/test/polar/machines/assessment/transitions_test.exs new file mode 100644 index 0000000..830998f --- /dev/null +++ b/test/polar/machines/assessment/transitions_test.exs @@ -0,0 +1,80 @@ +defmodule Polar.Machines.Assessment.TransitionsTest do + use Polar.DataCase, async: true + + alias Polar.Machines + alias Polar.Streams + + import Polar.StreamsFixtures + import Polar.AccountsFixtures + + setup do + user = user_fixture() + + {:ok, check} = + Machines.create_check(%{ + name: "ipv4-issuing", + description: "issue ipv4 correctly" + }) + + {:ok, cluster} = + Machines.create_cluster(%{ + name: "example", + type: "lxd", + arch: "amd64", + credential_endpoint: "some.cluster.com:8443", + credential_password: "sometoken", + credential_password_confirmation: "sometoken" + }) + + {:ok, product} = + Streams.create_product(%{ + aliases: ["alpine/3.19", "alpine/3.19/default"], + arch: "amd64", + os: "Alpine", + release: "3.19", + release_title: "3.19", + variant: "default", + requirements: %{ + secureboot: "false" + } + }) + + {:ok, version} = + Streams.create_version(product, valid_version_attributes(2)) + + {:ok, assessment} = + Machines.get_or_create_assessment(version, %{ + check_id: check.id, + cluster_id: cluster.id, + instance_type: "container" + }) + + {:ok, assessment: assessment, user: user} + end + + describe "transitions" do + test "run", %{assessment: assessment, user: user} do + assert {:ok, %{resource: resource}} = Eventful.Transit.perform(assessment, user, "run") + + assert resource.current_state == "running" + end + + test "pass", %{assessment: assessment, user: user} do + {:ok, %{resource: running_assessment}} = Eventful.Transit.perform(assessment, user, "run") + + assert {:ok, %{resource: resource}} = + Eventful.Transit.perform(running_assessment, user, "pass") + + assert resource.current_state == "passed" + end + + test "fail", %{assessment: assessment, user: user} do + {:ok, %{resource: running_assessment}} = Eventful.Transit.perform(assessment, user, "run") + + assert {:ok, %{resource: resource}} = + Eventful.Transit.perform(running_assessment, user, "fail") + + assert resource.current_state == "failed" + end + end +end diff --git a/test/polar/machines/check/manager_test.exs b/test/polar/machines/check/manager_test.exs new file mode 100644 index 0000000..d536d9a --- /dev/null +++ b/test/polar/machines/check/manager_test.exs @@ -0,0 +1,17 @@ +defmodule Polar.Machines.Check.ManagerTest do + use Polar.DataCase, async: true + + alias Polar.Machines + + describe "create check" do + test "successfully create check" do + assert {:ok, check} = + Machines.create_check(%{ + name: "ipv4 issuing", + description: "checks that ipv4 can be issued." + }) + + assert check.slug == "ipv4-issuing" + end + end +end diff --git a/test/polar/machines/cluster/connect_test.exs b/test/polar/machines/cluster/connect_test.exs new file mode 100644 index 0000000..66d4834 --- /dev/null +++ b/test/polar/machines/cluster/connect_test.exs @@ -0,0 +1,50 @@ +defmodule Polar.Machines.Cluster.ConnectTest do + use Polar.DataCase, async: true + use Oban.Testing, repo: Polar.Repo + + alias Polar.Machines + alias Polar.Machines.Cluster.Connect + + import Polar.AccountsFixtures + + import Mox + + setup :verify_on_exit! + + setup do + user = user_fixture() + + {:ok, cluster} = + Machines.create_cluster(%{ + name: "example", + type: "lxd", + arch: "amd64", + credential_endpoint: "some.cluster.com:8443", + credential_password: "sometoken", + credential_password_confirmation: "sometoken" + }) + + {:ok, cluster: cluster, user: user} + end + + describe "perform" do + setup %{cluster: cluster, user: user} do + {:ok, %{resource: connecting_cluster}} = + Eventful.Transit.perform(cluster, user, "connect") + + {:ok, cluster: connecting_cluster} + end + + test "connect to the cluster", %{cluster: cluster, user: user} do + Polar.LexdeeMock + |> expect(:create_certificate, fn _, _ -> + {:ok, nil} + end) + + assert {:ok, %{resource: healthy_cluster}} = + perform_job(Connect, %{cluster_id: cluster.id, user_id: user.id}) + + assert healthy_cluster.current_state == "healthy" + end + end +end diff --git a/test/polar/machines/cluster/manager_test.exs b/test/polar/machines/cluster/manager_test.exs new file mode 100644 index 0000000..24cd0bf --- /dev/null +++ b/test/polar/machines/cluster/manager_test.exs @@ -0,0 +1,28 @@ +defmodule Polar.Machines.Cluster.ManagerTest do + use Polar.DataCase, async: true + + alias Polar.Machines + + describe "create cluster" do + test "successfully create cluster" do + assert {:ok, cluster} = + Machines.create_cluster(%{ + name: "example", + type: "lxd", + arch: "amd64", + credential_endpoint: "some.cluster.com:8443", + credential_password: "sometoken", + credential_password_confirmation: "sometoken", + instance_wait_times: [ + %{type: "vm", duration: 10_000}, + %{type: "container", duration: 5_000} + ] + }) + + assert %Machines.Cluster.Credential{private_key: _private_key, certificate: _certificate} = + cluster.credential + + assert Enum.count(cluster.instance_wait_times) == 2 + end + end +end diff --git a/test/polar/machines/cluster/transitions_test.exs b/test/polar/machines/cluster/transitions_test.exs new file mode 100644 index 0000000..3c4638f --- /dev/null +++ b/test/polar/machines/cluster/transitions_test.exs @@ -0,0 +1,34 @@ +defmodule Polar.Machines.Cluster.TransitionsTest do + use Polar.DataCase, async: true + + alias Polar.Machines + + import Polar.AccountsFixtures + + setup do + user = user_fixture() + + {:ok, cluster} = + Machines.create_cluster(%{ + name: "example", + type: "lxd", + arch: "amd64", + credential_endpoint: "some.cluster.com:8443", + credential_password: "sometoken", + credential_password_confirmation: "sometoken" + }) + + {:ok, cluster: cluster, user: user} + end + + describe "connect" do + test "can transition", %{user: user, cluster: cluster} do + assert {:ok, %{resource: connecting_cluster, trigger: trigger}} = + Eventful.Transit.perform(cluster, user, "connect") + + assert %Oban.Job{worker: "Polar.Machines.Cluster.Connect"} = trigger + + assert connecting_cluster.current_state == "connecting" + end + end +end diff --git a/test/polar/streams/product/manager_test.exs b/test/polar/streams/product/manager_test.exs index da1d6a9..6cee99d 100644 --- a/test/polar/streams/product/manager_test.exs +++ b/test/polar/streams/product/manager_test.exs @@ -11,13 +11,13 @@ defmodule Polar.Streams.Product.ManagerTest do setup do password = Accounts.generate_automation_password() - _bot = bot_fixture(%{password: password}) + bot = bot_fixture(%{password: password}) - :ok + {:ok, bot: bot} end describe "filter" do - setup do + setup %{bot: bot} do {:ok, %Product{} = without_active_versions} = Streams.create_product(%{ aliases: ["alpine/3.19", "alpine/3.19/default"], @@ -44,7 +44,7 @@ defmodule Polar.Streams.Product.ManagerTest do } }) - {:ok, _version} = + {:ok, version} = Streams.create_version(with_active_versions, %{ serial: "20240209_13:00", items: [ @@ -65,6 +65,12 @@ defmodule Polar.Streams.Product.ManagerTest do ] }) + {:ok, %{resource: testing_version}} = + Eventful.Transit.perform(version, bot, "test") + + {:ok, %{resource: _active_version}} = + Eventful.Transit.perform(testing_version, bot, "activate") + {:ok, without_active_versions: without_active_versions, with_active_versions: with_active_versions} diff --git a/test/polar/streams/version/manager_test.exs b/test/polar/streams/version/manager_test.exs index bd06003..4aefe1e 100644 --- a/test/polar/streams/version/manager_test.exs +++ b/test/polar/streams/version/manager_test.exs @@ -10,7 +10,7 @@ defmodule Polar.Streams.Version.ManagerTest do setup do password = Accounts.generate_automation_password() - _bot = bot_fixture(%{password: password}) + bot = bot_fixture(%{password: password}) {:ok, product} = Streams.create_product(%{ @@ -25,7 +25,7 @@ defmodule Polar.Streams.Version.ManagerTest do } }) - {:ok, product: product} + {:ok, product: product, bot: bot} end describe "create_version" do @@ -36,20 +36,33 @@ defmodule Polar.Streams.Version.ManagerTest do end describe "deactivate old version on create" do - setup %{product: product} do + setup %{product: product, bot: bot} do {:ok, version} = Streams.create_version(product, valid_version_attributes(2)) - %{existing_version: version} + {:ok, %{resource: testing_version}} = + Eventful.Transit.perform(version, bot, "test") + + {:ok, %{resource: active_version}} = + Eventful.Transit.perform(testing_version, bot, "activate") + + %{existing_version: active_version} end - test "creating a new version deactivates old versions", %{ + test "activating a new version deactivates old versions", %{ product: product, + bot: bot, existing_version: existing_version } do - assert {:ok, _version} = + assert {:ok, version} = Streams.create_version(product, valid_version_attributes(3)) + {:ok, %{resource: testing_version}} = + Eventful.Transit.perform(version, bot, "test") + + {:ok, %{resource: _active_version}} = + Eventful.Transit.perform(testing_version, bot, "activate") + existing_version = Repo.reload(existing_version) assert existing_version.current_state == "inactive" @@ -57,26 +70,45 @@ defmodule Polar.Streams.Version.ManagerTest do end describe "keep 2 previous version" do - setup %{product: product} do + setup %{product: product, bot: bot} do Polar.Globals.save("basic", %{versions_per_product: 2}) {:ok, version3} = Streams.create_version(product, valid_version_attributes(3)) + {:ok, %{resource: testing_version3}} = + Eventful.Transit.perform(version3, bot, "test") + + {:ok, %{resource: active_version3}} = + Eventful.Transit.perform(testing_version3, bot, "activate") + {:ok, version4} = Streams.create_version(product, valid_version_attributes(4)) - %{existing_version: version4, to_be_inactive: version3} + {:ok, %{resource: testing_version4}} = + Eventful.Transit.perform(version4, bot, "test") + + {:ok, %{resource: active_version4}} = + Eventful.Transit.perform(testing_version4, bot, "activate") + + %{existing_version: active_version4, to_be_inactive: active_version3} end - test "create new version deactivate version 3", %{ + test "create and activate new version deactivate version 3", %{ product: product, + bot: bot, existing_version: existing_version, to_be_inactive: to_be_inactive } do - assert {:ok, _version} = + assert {:ok, version} = Streams.create_version(product, valid_version_attributes(5)) + {:ok, %{resource: testing_version}} = + Eventful.Transit.perform(version, bot, "test") + + {:ok, %{resource: _active_version}} = + Eventful.Transit.perform(testing_version, bot, "activate") + to_be_inactive = Repo.reload(to_be_inactive) assert to_be_inactive.current_state == "inactive" diff --git a/test/polar/streams/version/pruning_test.exs b/test/polar/streams/version/pruning_test.exs new file mode 100644 index 0000000..71e29d0 --- /dev/null +++ b/test/polar/streams/version/pruning_test.exs @@ -0,0 +1,111 @@ +defmodule Polar.Streams.Version.PruningTest do + use Polar.DataCase, async: true + use Oban.Testing, repo: Polar.Repo + + import Polar.AccountsFixtures + + alias Polar.Repo + alias Polar.Accounts + + alias Polar.Streams + alias Polar.Streams.Product + alias Polar.Streams.Version + alias Polar.Streams.Version.Pruning + + setup do + password = Accounts.generate_automation_password() + + bot = bot_fixture(%{password: password}) + + {:ok, bot: bot} + end + + setup %{bot: bot} do + {:ok, %Product{} = with_active_versions} = + Streams.create_product(%{ + aliases: ["alpine/3.18", "alpine/3.18/default"], + arch: "amd64", + os: "Alpine", + release: "3.18", + release_title: "3.18", + variant: "default", + requirements: %{ + secureboot: "false" + } + }) + + {:ok, version} = + Streams.create_version(with_active_versions, %{ + serial: "20240209-36", + items: [ + %{ + name: "lxd.tar.gz", + file_type: "lxd.tar.gz", + hash: "35363f3d086271ed5402d61ab18ec03987bed51758c00079b8c9d372ff6d62dd", + size: 876, + path: "images/alpine/edge/amd64/default/20240209_13:00/incus.tar.xz" + }, + %{ + name: "root.squashfs", + file_type: "squashfs", + hash: "47cc4070da1bf17d8364c390…3603f4ed7e9e46582e690d2", + size: 2_982_800, + path: "images/alpine/edge/amd64/default/20240209_13:00/rootfs.tar.xz" + } + ] + }) + + {:ok, another_version} = + Streams.create_version(with_active_versions, %{ + serial: "20240209-37", + items: [ + %{ + name: "lxd.tar.gz", + file_type: "lxd.tar.gz", + hash: "35363f3d086271ed5402d61ab18ec03987bed51758c00079b8c9d372ff6d62aa", + size: 876, + path: "images/alpine/edge/amd64/default/20240209_13:00/incus.tar.xz" + }, + %{ + name: "root.squashfs", + file_type: "squashfs", + hash: "47cc4070da1bf17d8364c390…3603f4ed7e9e46582e690aa", + size: 2_982_800, + path: "images/alpine/edge/amd64/default/20240209_13:00/rootfs.tar.xz" + } + ] + }) + + {:ok, %{resource: testing_version}} = Eventful.Transit.perform(version, bot, "test") + + {:ok, %{resource: another_testing_version}} = + Eventful.Transit.perform(another_version, bot, "test") + + expired = + DateTime.utc_now() + |> DateTime.add(-4, :day) + + query = from(v in Version, where: v.id == ^version.id) + + Repo.update_all(query, set: [inserted_at: expired]) + + {:ok, version: testing_version, another_version: another_testing_version} + end + + describe "perform" do + test "successfully deactivate expired version", %{ + version: version, + another_version: another_version + } do + assert version.current_state == "testing" + assert another_version.current_state == "testing" + + assert :ok = perform_job(Pruning, %{}) + + version = Repo.reload(version) + + assert version.current_state == "inactive" + assert another_version.current_state == "testing" + end + end +end diff --git a/test/polar/streams/version/transitions_test.exs b/test/polar/streams/version/transitions_test.exs index e973675..4aa5a43 100644 --- a/test/polar/streams/version/transitions_test.exs +++ b/test/polar/streams/version/transitions_test.exs @@ -11,7 +11,7 @@ defmodule Polar.Streams.Version.TransitionsTest do password = Accounts.generate_automation_password() - _bot = bot_fixture(%{password: password}) + bot = bot_fixture(%{password: password}) {:ok, product} = Streams.create_product(%{ @@ -47,7 +47,12 @@ defmodule Polar.Streams.Version.TransitionsTest do ] }) - {:ok, user: user, version: version} + {:ok, %{resource: testing_version}} = Eventful.Transit.perform(version, bot, "test") + + {:ok, %{resource: active_version}} = + Eventful.Transit.perform(testing_version, bot, "activate") + + {:ok, user: user, version: active_version} end describe "deactivate" do diff --git a/test/polar_web/controllers/publish/event_controller_test.exs b/test/polar_web/controllers/publish/event_controller_test.exs new file mode 100644 index 0000000..3f17d83 --- /dev/null +++ b/test/polar_web/controllers/publish/event_controller_test.exs @@ -0,0 +1,91 @@ +defmodule PolarWeb.Publish.EventControllerTest do + use PolarWeb.ConnCase + + import Polar.AccountsFixtures + import Polar.StreamsFixtures + + alias Polar.Accounts + alias Polar.Streams + alias Polar.Machines + + setup do + password = Accounts.generate_automation_password() + + bot = bot_fixture(%{password: password}) + + user = Accounts.get_user_by_email_and_password(bot.email, password) + + session_token = + Accounts.generate_user_session_token(user) + |> Base.encode64() + + conn = + build_conn() + |> put_req_header("authorization", session_token) + |> put_req_header("content-type", "application/json") + + product_attributes = valid_product_attributes("alpine:3.19:amd64:default") + + {:ok, product} = Streams.create_product(product_attributes) + + {:ok, version} = + Streams.create_version(product, valid_version_attributes(2)) + + {:ok, conn: conn, version: version} + end + + describe "POST /publish/versions/:version_id/events" do + test "can create transition event", %{conn: conn, version: version} do + conn = + post(conn, "/publish/versions/#{version.id}/events", %{ + event: %{ + name: "test" + } + }) + + assert %{"data" => data} = json_response(conn, 201) + + assert %{"id" => _id, "name" => "test"} = data + end + end + + describe "POST /publish/testing/assessments/:assessment_id/events" do + setup %{version: version} do + {:ok, check} = + Machines.create_check(%{ + name: "ipv4-issuing", + description: "issue ipv4 correctly" + }) + + {:ok, cluster} = + Machines.create_cluster(%{ + name: "example", + type: "lxd", + arch: "amd64", + credential_endpoint: "some.cluster.com:8443", + credential_password: "sometoken", + credential_password_confirmation: "sometoken" + }) + + {:ok, assessment} = + Machines.get_or_create_assessment(version, %{ + check_id: check.id, + cluster_id: cluster.id, + instance_type: "container" + }) + + {:ok, assessment: assessment} + end + + test "can transition assessment to running", %{conn: conn, assessment: assessment} do + conn = + post(conn, ~p"/publish/testing/assessments/#{assessment.id}/events", %{ + "event" => %{"name" => "run"} + }) + + assert %{"data" => data} = json_response(conn, 201) + + assert %{"id" => _id, "name" => _name} = data + end + end +end diff --git a/test/polar_web/controllers/publish/product_controller_test.exs b/test/polar_web/controllers/publish/product_controller_test.exs index 8133a8f..e7caac3 100644 --- a/test/polar_web/controllers/publish/product_controller_test.exs +++ b/test/polar_web/controllers/publish/product_controller_test.exs @@ -57,7 +57,9 @@ defmodule PolarWeb.Publish.ProductControllerTest do assert %{"data" => data} = json_response(conn, 200) - assert %{"id" => _id, "key" => _key} = data + assert %{"id" => _id, "key" => _key, "requirements" => requirements} = data + + assert %{"secureboot" => "false"} = requirements end end end diff --git a/test/polar_web/controllers/publish/testing/assessment_controller_test.exs b/test/polar_web/controllers/publish/testing/assessment_controller_test.exs new file mode 100644 index 0000000..3b2b071 --- /dev/null +++ b/test/polar_web/controllers/publish/testing/assessment_controller_test.exs @@ -0,0 +1,124 @@ +defmodule PolarWeb.Publish.Testing.AssessmentControllerTest do + use PolarWeb.ConnCase + + alias Polar.Accounts + alias Polar.Machines + alias Polar.Streams + + import Polar.AccountsFixtures + import Polar.StreamsFixtures + + setup do + password = Accounts.generate_automation_password() + + bot = bot_fixture(%{password: password}) + + user = Accounts.get_user_by_email_and_password(bot.email, password) + + session_token = + Accounts.generate_user_session_token(user) + |> Base.encode64() + + conn = + build_conn() + |> put_req_header("authorization", session_token) + + {:ok, check} = + Machines.create_check(%{ + name: "ipv4-issuing", + description: "issue ipv4 correctly" + }) + + {:ok, cluster} = + Machines.create_cluster(%{ + name: "example", + type: "lxd", + arch: "amd64", + credential_endpoint: "some.cluster.com:8443", + credential_password: "sometoken", + credential_password_confirmation: "sometoken" + }) + + {:ok, product} = + Streams.create_product(%{ + aliases: ["alpine/3.19", "alpine/3.19/default"], + arch: "amd64", + os: "Alpine", + release: "3.19", + release_title: "3.19", + variant: "default", + requirements: %{ + secureboot: "false" + } + }) + + {:ok, version} = + Streams.create_version(product, valid_version_attributes(2)) + + {:ok, version: version, cluster: cluster, check: check, conn: conn} + end + + describe "POST /publish/testing/versions/:version_id/assessments" do + test "successfully create assessment", %{ + version: version, + conn: conn, + check: check, + cluster: cluster + } do + conn = + post(conn, ~p"/publish/testing/versions/#{version.id}/assessments", %{ + "assessment" => %{ + "check_id" => check.id, + "cluster_id" => cluster.id, + "instance_type" => "container" + } + }) + + assert %{"data" => data} = json_response(conn, 200) + + assert %{"id" => _id, "current_state" => "created", "check" => _check} = data + end + + test "when assessment already exists", %{ + version: version, + conn: conn, + check: check, + cluster: cluster + } do + {:ok, _assessment} = + Machines.get_or_create_assessment(version, %{ + check_id: check.id, + cluster_id: cluster.id, + instance_type: "container" + }) + + conn = + post(conn, ~p"/publish/testing/versions/#{version.id}/assessments", %{ + "assessment" => %{ + "check_id" => check.id, + "cluster_id" => cluster.id, + "instance_type" => "container" + } + }) + + assert %{"data" => _data} = json_response(conn, 200) + end + + test "invalid parameter passed in", %{ + version: version, + conn: conn, + check: check, + cluster: cluster + } do + conn = + post(conn, ~p"/publish/testing/versions/#{version.id}/assessments", %{ + "assessment" => %{ + "check_id" => check.id, + "cluster_id" => cluster.id + } + }) + + assert %{"errors" => _errors} = json_response(conn, 422) + end + end +end diff --git a/test/polar_web/controllers/publish/testing/check_controller_test.exs b/test/polar_web/controllers/publish/testing/check_controller_test.exs new file mode 100644 index 0000000..93c5ce7 --- /dev/null +++ b/test/polar_web/controllers/publish/testing/check_controller_test.exs @@ -0,0 +1,42 @@ +defmodule PolarWeb.Publish.Testing.CheckControllerTest do + use PolarWeb.ConnCase + + alias Polar.Accounts + alias Polar.Machines + + import Polar.AccountsFixtures + + setup do + password = Accounts.generate_automation_password() + + bot = bot_fixture(%{password: password}) + + user = Accounts.get_user_by_email_and_password(bot.email, password) + + session_token = + Accounts.generate_user_session_token(user) + |> Base.encode64() + + conn = + build_conn() + |> put_req_header("authorization", session_token) + + {:ok, _check} = + Machines.create_check(%{ + name: "ipv4-issuing", + description: "checks that ipv4 is correctly issued" + }) + + {:ok, conn: conn} + end + + describe "GET /publish/testing/checks" do + test "get list of available checks", %{conn: conn} do + conn = get(conn, "/publish/testing/checks") + + assert %{"data" => data} = json_response(conn, 200) + + assert Enum.count(data) == 1 + end + end +end diff --git a/test/polar_web/controllers/publish/testing/cluster_controller_test.exs b/test/polar_web/controllers/publish/testing/cluster_controller_test.exs new file mode 100644 index 0000000..9c473eb --- /dev/null +++ b/test/polar_web/controllers/publish/testing/cluster_controller_test.exs @@ -0,0 +1,81 @@ +defmodule PolarWeb.Publish.Testing.ClusterControllerTest do + use PolarWeb.ConnCase + + alias Polar.Accounts + alias Polar.Machines + + import Polar.AccountsFixtures + + setup do + password = Accounts.generate_automation_password() + + bot = bot_fixture(%{password: password}) + + user = Accounts.get_user_by_email_and_password(bot.email, password) + + session_token = + Accounts.generate_user_session_token(user) + |> Base.encode64() + + conn = + build_conn() + |> put_req_header("authorization", session_token) + + {:ok, cluster} = + Machines.create_cluster(%{ + name: "example", + type: "lxd", + arch: "amd64", + credential_endpoint: "some.cluster.com:8443", + credential_password: "sometoken", + credential_password_confirmation: "sometoken", + instance_wait_times: [ + %{type: "vm", duration: 10_000}, + %{type: "container", duration: 5_000} + ] + }) + + {:ok, _created_cluster} = + Machines.create_cluster(%{ + name: "example2", + type: "lxd", + arch: "amd64", + credential_endpoint: "some.cluster.com:8443", + credential_password: "sometoken", + credential_password_confirmation: "sometoken", + instance_wait_times: [ + %{type: "vm", duration: 10_000}, + %{type: "container", duration: 5_000} + ] + }) + + {:ok, conn: conn, cluster: cluster, user: user} + end + + describe "GET /publish/testing/clusters" do + setup %{cluster: cluster, user: user} do + {:ok, %{resource: connecting_cluster}} = + Eventful.Transit.perform(cluster, user, "connect") + + {:ok, %{resource: _healthy_cluster}} = + Eventful.Transit.perform(connecting_cluster, user, "healthy") + + {:ok, cluster: cluster} + end + + test "list healthy clusters", %{conn: conn, cluster: cluster} do + conn = + get(conn, "/publish/testing/clusters") + + assert %{"data" => data} = json_response(conn, 200) + + assert cluster.id in Enum.map(data, & &1["id"]) + + cluster = List.first(data) + + assert %{"instance_wait_times" => instance_wait_times} = cluster + + assert Enum.count(instance_wait_times) == 2 + end + end +end diff --git a/test/polar_web/controllers/publish/version_controller_test.exs b/test/polar_web/controllers/publish/version_controller_test.exs index dbb237a..b091ffc 100644 --- a/test/polar_web/controllers/publish/version_controller_test.exs +++ b/test/polar_web/controllers/publish/version_controller_test.exs @@ -7,6 +7,8 @@ defmodule PolarWeb.Publish.VersionControllerTest do alias Polar.Accounts alias Polar.Streams + import Polar.StreamsFixtures + setup do password = Accounts.generate_automation_password() @@ -25,6 +27,27 @@ defmodule PolarWeb.Publish.VersionControllerTest do {:ok, conn: conn} end + describe "GET /publish/products/:product_id/versions/:id" do + setup do + product_attributes = valid_product_attributes("alpine:3.19:amd64:default") + + {:ok, product} = Streams.create_product(product_attributes) + + {:ok, version} = + Streams.create_version(product, valid_version_attributes(2)) + + {:ok, product: product, version: version} + end + + test "can fetch existing version", %{conn: conn, product: product, version: version} do + conn = get(conn, "/publish/products/#{product.id}/versions/#{version.serial}") + + assert %{"data" => data} = json_response(conn, 200) + + assert %{"id" => _id, "serial" => _serial} = data + end + end + describe "POST /publish/products/:product_id/versions" do setup do product_attributes = valid_product_attributes("alpine:3.19:amd64:default") @@ -67,7 +90,7 @@ defmodule PolarWeb.Publish.VersionControllerTest do assert %{"data" => data} = json_response(conn, 201) - assert %{"id" => _id} = data + assert %{"id" => _id, "serial" => _serial} = data end end end diff --git a/test/polar_web/controllers/stream_controller_test.exs b/test/polar_web/controllers/stream_controller_test.exs index d141fb3..bfbd282 100644 --- a/test/polar_web/controllers/stream_controller_test.exs +++ b/test/polar_web/controllers/stream_controller_test.exs @@ -12,7 +12,7 @@ defmodule PolarWeb.StreamControllerTest do password = Accounts.generate_automation_password() - _bot = bot_fixture(%{password: password}) + bot = bot_fixture(%{password: password}) {:ok, space} = Accounts.create_space(user, %{name: "some-test-123"}) @@ -36,7 +36,7 @@ defmodule PolarWeb.StreamControllerTest do } }) - {:ok, _version} = + {:ok, version} = Streams.create_version(product, %{ serial: "20240209_13:00", items: [ @@ -57,6 +57,11 @@ defmodule PolarWeb.StreamControllerTest do ] }) + {:ok, %{resource: testing_version}} = Eventful.Transit.perform(version, bot, "test") + + {:ok, %{resource: _active_version}} = + Eventful.Transit.perform(testing_version, bot, "activate") + {:ok, product: product, credential: credential} end diff --git a/test/polar_web/controllers/streams/image_controller_test.exs b/test/polar_web/controllers/streams/image_controller_test.exs new file mode 100644 index 0000000..ab53692 --- /dev/null +++ b/test/polar_web/controllers/streams/image_controller_test.exs @@ -0,0 +1,159 @@ +defmodule PolarWeb.Streams.ImageControllerTest do + use PolarWeb.ConnCase + + alias Polar.Accounts + alias Polar.Streams + + alias Polar.Streams.Product + + import Polar.AccountsFixtures + + setup do + user = user_fixture() + + password = Accounts.generate_automation_password() + + _bot = bot_fixture(%{password: password}) + + {:ok, space} = Accounts.create_space(user, %{name: "some-test-item"}) + + {:ok, %Product{} = product_with_testing} = + Streams.create_product(%{ + aliases: ["alpine/3.19", "alpine/3.19/default"], + arch: "amd64", + os: "Alpine", + release: "3.19", + release_title: "3.19", + variant: "default", + requirements: %{ + secureboot: false + } + }) + + {:ok, version} = + Streams.create_version(product_with_testing, %{ + serial: "20240209_13:00", + items: [ + %{ + name: "lxd.tar.gz", + file_type: "lxd.tar.gz", + hash: "35363f3d086271ed5402d61ab18ec03987bed51758c00079b8c9d372ff6d62dd", + size: 876, + is_metadata: true, + path: "images/alpine/edge/amd64/default/20240209_13:00/incus.tar.xz" + }, + %{ + name: "root.squashfs", + file_type: "squashfs", + hash: "47cc4070da1bf17d8364c390…3603f4ed7e9e46582e690d2", + size: 2_982_800, + path: "images/alpine/edge/amd64/default/20240209_13:00/rootfs.tar.xz" + } + ] + }) + + {:ok, %{resource: _testing_version}} = + Eventful.Transit.perform(version, user, "test") + + {:ok, %Product{} = product_without_testing} = + Streams.create_product(%{ + aliases: ["alpine/3.20", "alpine/3.20/default"], + arch: "amd64", + os: "Alpine", + release: "3.20", + release_title: "3.20", + variant: "default", + requirements: %{ + secureboot: false + } + }) + + {:ok, version} = + Streams.create_version(product_without_testing, %{ + serial: "20240209_13:00", + items: [ + %{ + name: "lxd.tar.gz", + file_type: "lxd.tar.gz", + hash: "35363f3d086271ed5402d61ab18ec03987bed51758c00079b8c9d372ff6d62aa", + size: 876, + is_metadata: true, + path: "images/alpine/edge/amd64/default/20240209_13:00/incus.tar.xz" + }, + %{ + name: "root.squashfs", + file_type: "squashfs", + hash: "67cc4070da1bf17d8364c390…3603f4ed7e9e46582e690d1", + size: 2_982_800, + path: "images/alpine/edge/amd64/default/20240209_13:00/rootfs.tar.xz" + } + ] + }) + + {:ok, %{resource: testing_version}} = + Eventful.Transit.perform(version, user, "test") + + {:ok, %{resource: _active_version}} = + Eventful.Transit.perform(testing_version, user, "activate") + + {:ok, + space: space, + user: user, + product_with_testing: product_with_testing, + product_without_testing: product_without_testing} + end + + describe "product with testing version" do + setup %{space: space, user: user} do + {:ok, credential} = + Accounts.create_space_credential(space, user, %{ + expires_in: 1_296_000, + name: "test-02", + type: "lxd", + release_channel: "testing" + }) + + {:ok, credential: credential} + end + + test "GET /spaces/:space_token/images.json", %{ + credential: credential, + product_without_testing: product_without_testing + } do + conn = get(build_conn(), ~s"/spaces/#{credential.token}/streams/v1/images.json") + + assert %{"products" => products} = json_response(conn, 200) + + product_keys = Map.keys(products) + + assert Product.key(product_without_testing) not in product_keys + end + end + + describe "product with active version" do + setup %{space: space, user: user} do + {:ok, credential} = + Accounts.create_space_credential(space, user, %{ + expires_in: 1_296_000, + name: "test-02", + type: "lxd", + release_channel: "active" + }) + + {:ok, credential: credential} + end + + test "GET /spaces/:space_token/images.json", %{ + credential: credential, + product_with_testing: product_with_testing + } do + conn = get(build_conn(), ~s"/spaces/#{credential.token}/streams/v1/images.json") + + assert %{"products" => products} = json_response(conn, 200) + + product_keys = Map.keys(products) + + assert Product.key(product_with_testing) not in product_keys + end + end +end diff --git a/test/polar_web/live/dashboard/credential/new_live_test.exs b/test/polar_web/live/dashboard/credential/new_live_test.exs index 6361780..88e58a0 100644 --- a/test/polar_web/live/dashboard/credential/new_live_test.exs +++ b/test/polar_web/live/dashboard/credential/new_live_test.exs @@ -44,5 +44,29 @@ defmodule PolarWeb.Dashboard.Credential.NewLiveTest do assert render(lv) =~ "can't be blank" end + + test "create new credential with testing release channel", %{conn: conn, space: space} do + {:ok, lv, _html} = live(conn, ~p"/dashboard/spaces/#{space.id}/credentials/new") + + lv + |> form("#new-credential-form", %{ + "credential" => %{ + "name" => "new-cred-test-testing", + "type" => "lxd", + "release_channel" => "testing", + "expires_in" => "2592000" + } + }) + |> render_submit() + + credential = Repo.get_by!(Space.Credential, name: "new-cred-test-testing") + + assert credential.release_channel == "testing" + + {:ok, lv, _html} = + live(conn, ~p"/dashboard/spaces/#{space.id}/credentials/#{credential.id}") + + assert render(lv) =~ "testing" + end end end diff --git a/test/polar_web/live/root_live_test.exs b/test/polar_web/live/root_live_test.exs index b8e16f9..4db8960 100644 --- a/test/polar_web/live/root_live_test.exs +++ b/test/polar_web/live/root_live_test.exs @@ -13,7 +13,7 @@ defmodule PolarWeb.RootLiveTest do password = Accounts.generate_automation_password() - _bot = bot_fixture(%{password: password}) + bot = bot_fixture(%{password: password}) {:ok, space} = Accounts.create_space(user, %{name: "some-test-123"}) @@ -58,7 +58,7 @@ defmodule PolarWeb.RootLiveTest do ] }) - {:ok, _version} = + {:ok, version} = Streams.create_version(product, %{ serial: "20240209-13", items: [ @@ -79,6 +79,12 @@ defmodule PolarWeb.RootLiveTest do ] }) + {:ok, %{resource: testing_version}} = + Eventful.Transit.perform(version, bot, "test") + + {:ok, %{resource: _active_version}} = + Eventful.Transit.perform(testing_version, bot, "activate") + {:ok, product: product, credential: credential} end diff --git a/test/support/mocks.ex b/test/support/mocks.ex new file mode 100644 index 0000000..575bd64 --- /dev/null +++ b/test/support/mocks.ex @@ -0,0 +1 @@ +Mox.defmock(Polar.LexdeeMock, for: Lexdee.Behaviour)