diff --git a/.commitlintrc.yaml b/.commitlintrc.yaml new file mode 100644 index 00000000..522602e5 --- /dev/null +++ b/.commitlintrc.yaml @@ -0,0 +1,28 @@ +--- +# The rules below have been manually copied from @commitlint/config-conventional +# and match the v1.0.0 specification: +# https://www.conventionalcommits.org/en/v1.0.0/#specification +# +# You can remove them and uncomment the config below when the following issue is +# fixed: https://github.com/conventional-changelog/commitlint/issues/613 +# +# extends: +# - '@commitlint/config-conventional' +rules: + body-leading-blank: [1, always] + body-max-line-length: [2, always, 100] + footer-leading-blank: [1, always] + footer-max-line-length: [2, always, 100] + header-max-length: [2, always, 100] + subject-case: + - 2 + - never + - [sentence-case, start-case, pascal-case, upper-case] + subject-empty: [2, never] + subject-full-stop: [2, never, "."] + type-case: [2, always, lower-case] + type-empty: [2, never] + type-enum: + - 2 + - always + - [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] diff --git a/flake.nix b/flake.nix index 47e3d070..8a9c6193 100644 --- a/flake.nix +++ b/flake.nix @@ -106,7 +106,51 @@ } ]; }; + + test = pkgs.nixosTest { + name = "penguin_memories"; + nodes.machine = {...}: { + imports = [ + self.nixosModules.default + ]; + services.penguin_memories = { + enable = true; + http_url = "http://localhost:4000"; + port = 4000; + secrets = pkgs.writeText "secrets.txt" '' + export RELEASE_COOKIE="12345678901234567890123456789012345678901234567890123456" + export DATABASE_URL="postgres://penguin_memories:your_secure_password_here@localhost/penguin_memories" + export GUARDIAN_SECRET="1234567890123456789012345678901234567890123456789012345678901234" + export SECRET_KEY_BASE="1234567890123456789012345678901234567890123456789012345678901234" + export SIGNING_SALT="12345678901234567890123456789012" + export OIDC_DISCOVERY_URL="http://localhost" + export OIDC_CLIENT_ID="photos" + export OIDC_CLIENT_SECRET="12345678901234567890123456789012" + export OIDC_AUTH_SCOPE="openid profile groups" + ''; + }; + system.stateVersion = "24.05"; + + services.postgresql = { + enable = true; + extraPlugins = ps: [ps.postgis]; + initialScript = pkgs.writeText "init.psql" '' + CREATE DATABASE penguin_memories; + CREATE USER penguin_memories with encrypted password 'your_secure_password_here'; + ALTER DATABASE penguin_memories OWNER TO penguin_memories; + ALTER USER penguin_memories WITH SUPERUSER; + ''; + }; + }; + + testScript = '' + machine.wait_for_unit("penguin_memories.service") + machine.wait_for_open_port(4000) + machine.succeed("${pkgs.curl}/bin/curl --fail -v http://localhost:4000/_health") + ''; + }; in { + checks.nixosModules = test; packages = { devenv-up = devShell.config.procfileScript; default = pkg; diff --git a/lib/penguin_memories/release.ex b/lib/penguin_memories/release.ex new file mode 100644 index 00000000..f69404ba --- /dev/null +++ b/lib/penguin_memories/release.ex @@ -0,0 +1,70 @@ +defmodule PenguinMemories.Release do + @app :penguin_memories + + import Ecto.Query + alias PenguinMemories.Repo + + def migrate do + for repo <- repos() do + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) + end + end + + def rollback(repo, version) do + for r <- repos(), r == repo do + {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) + end + end + + def seconds_since_last_migration do + Repo.one( + from m in "schema_migrations", + select: fragment("EXTRACT(EPOCH FROM age(NOW(), ?::timestamp))::BIGINT", m.inserted_at), + order_by: [desc: m.inserted_at], + limit: 1 + ) + end + + def health_check do + repos = Application.fetch_env!(@app, :ecto_repos) + + migrations = + repos + |> Enum.map(&Ecto.Migrator.migrations/1) + |> List.flatten() + |> Enum.filter(fn + {:up, _, _} -> false + {_, _, _} -> true + end) + + migrations = + if Enum.empty?(migrations) do + :ok + else + {:error, "Migrations pending: #{inspect(migrations)}"} + end + + database = + Enum.reduce_while(repos, :ok, fn item, _acc -> + case Ecto.Adapters.SQL.query(item, "SELECT 1") do + {:ok, %{num_rows: 1, rows: [[1]]}} -> + {:cont, :ok} + + {:error, reason} -> + {:halt, {:error, inspect(reason)}} + end + end) + + case {migrations, database} do + {:ok, :ok} -> :ok + {{:error, reason}, _} -> {:error, reason} + {_, {:error, reason}} -> {:error, reason} + end + end + + defp repos do + Application.ensure_all_started(:ssl) + Application.load(@app) + Application.fetch_env!(@app, :ecto_repos) + end +end diff --git a/lib/penguin_memories_web/controllers/healh_check_controller.ex b/lib/penguin_memories_web/controllers/healh_check_controller.ex new file mode 100644 index 00000000..ff91fce9 --- /dev/null +++ b/lib/penguin_memories_web/controllers/healh_check_controller.ex @@ -0,0 +1,17 @@ +defmodule PenguinMemoriesWeb.HealthCheckController do + use PenguinMemoriesWeb, :controller + + alias PenguinMemories.Release + require Logger + + def index(conn, _params) do + case Release.health_check() do + :ok -> + text(conn, "HEALTHY") + + {:error, reason} -> + Logger.error("health check error: #{reason}") + conn |> put_status(500) |> text("ERROR") + end + end +end diff --git a/lib/penguin_memories_web/router.ex b/lib/penguin_memories_web/router.ex index fbf99088..29ce1f1f 100644 --- a/lib/penguin_memories_web/router.ex +++ b/lib/penguin_memories_web/router.ex @@ -76,6 +76,7 @@ defmodule PenguinMemoriesWeb.Router do pipe_through [:browser, :auth] PenguinMemoriesWeb live "/", PageLive, :index + get "/_health", HealthCheckController, :index post "/logout", PageController, :logout get "/file/:id/size/:size/", RedirectController, :photo live "/:type/", MainLive, :index diff --git a/module.nix b/module.nix index 71be3e48..9ca69cde 100644 --- a/module.nix +++ b/module.nix @@ -1,13 +1,18 @@ -{ self }: -{ lib, pkgs, config, ... }: -with lib; -let +{self}: { + lib, + pkgs, + config, + ... +}: let + inherit (lib) mkOption types mkEnableOption mkIf; + cfg = config.services.penguin_memories; system = pkgs.stdenv.system; penguin_memories_pkg = self.packages.${system}.default; - private_locations = lib.concatMapStringsSep ";" + private_locations = + lib.concatMapStringsSep ";" (l: "${toString l.longitude},${toString l.latitude},${toString l.distance}") cfg.private_locations; @@ -27,20 +32,28 @@ let locations = types.submodule { options = { - longitude = mkOption { type = types.float; }; - latitude = mkOption { type = types.float; }; - distance = mkOption { type = types.int; }; + longitude = mkOption {type = types.float;}; + latitude = mkOption {type = types.float;}; + distance = mkOption {type = types.int;}; }; }; - in { options.services.penguin_memories = { enable = mkEnableOption "penguin_memories service"; - secrets = mkOption { type = types.path; }; - http_url = mkOption { type = types.str; }; - image_dir = mkOption { type = types.path; }; - private_locations = mkOption { type = types.listOf locations; }; - port = mkOption { type = types.int; }; + secrets = mkOption {type = types.path;}; + http_url = mkOption {type = types.str;}; + image_dir = mkOption { + type = types.path; + default = "/var/lib/penguin_memories"; + }; + private_locations = mkOption { + type = types.listOf locations; + default = []; + }; + port = mkOption { + type = types.int; + default = 4000; + }; data_dir = mkOption { type = types.str; default = "/var/lib/penguin_memories"; @@ -56,13 +69,14 @@ in { home = "${cfg.data_dir}"; }; - users.groups.penguin_memories = { }; + users.groups.penguin_memories = {}; systemd.services.penguin_memories = { wantedBy = ["multi-user.target"]; after = ["network.target" "postgresql.service"]; serviceConfig = { User = "penguin_memories"; + ExecStartPre = ''${wrapper}/bin/penguin_memories eval "PenguinMemories.Release.migrate"''; ExecStart = "${wrapper}/bin/penguin_memories start"; ExecStop = "${wrapper}/bin/penguin_memories stop"; ExecReload = "${wrapper}/bin/penguin_memories reload";