Refuse startup with no server admin set up (#2389)

feat: refuse startup with no server admin set up

includes an admin party assert escape hatch for tests

adds a log message every 5 minutes, if escape hatch is enabled.

should play nice with systemd restart policies

Co-authored-by: Joan Touzet <wohali@users.noreply.github.com>
diff --git a/Makefile b/Makefile
index 1b64998..9bcd389 100644
--- a/Makefile
+++ b/Makefile
@@ -169,6 +169,7 @@
 eunit: export BUILDDIR = $(shell pwd)
 eunit: export ERL_AFLAGS = -config $(shell pwd)/rel/files/eunit.config
 eunit: export COUCHDB_QUERY_SERVER_JAVASCRIPT = $(shell pwd)/bin/couchjs $(shell pwd)/share/server/main.js
+eunit: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
 eunit: couch
 	@COUCHDB_VERSION=$(COUCHDB_VERSION) COUCHDB_GIT_SHA=$(COUCHDB_GIT_SHA) $(REBAR) setup_eunit 2> /dev/null
 	@for dir in $(subdirs); do \
@@ -223,7 +224,7 @@
 	@python3 -c "import sys; exit(1 if sys.version_info >= (3,6) else 0)" || \
 		LC_ALL=C.UTF-8 LANG=C.UTF-8 .venv/bin/black --check \
 		--exclude="build/|buck-out/|dist/|_build/|\.git/|\.hg/|\.mypy_cache/|\.nox/|\.tox/|\.venv/|src/rebar/pr2relnotes.py|src/fauxton" \
-		. dev/run "$(TEST_OPTS)" rel/overlay/bin/couchup test/javascript/run
+		. dev/run rel/overlay/bin/couchup test/javascript/run
 
 python-black-update: .venv/bin/black
 	@python3 -c "import sys; exit(1 if sys.version_info < (3,6) else 0)" || \
@@ -235,6 +236,7 @@
 
 .PHONY: elixir
 elixir: export MIX_ENV=integration
+elixir: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
 elixir: elixir-init elixir-check-formatted elixir-credo devclean
 	@dev/run "$(TEST_OPTS)" -a adm:pass -n 1 --no-eval 'mix test --trace --exclude without_quorum_test --exclude with_quorum_test $(EXUNIT_OPTS)'
 
@@ -269,6 +271,7 @@
 
 .PHONY: javascript
 # target: javascript - Run JavaScript test suites or specific ones defined by suites option
+javascript: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
 javascript: devclean
 	@mkdir -p share/www/script/test
 ifeq ($(IN_RELEASE), true)
@@ -284,6 +287,7 @@
             --ignore "$(ignore_js_suites)"'
 
 .PHONY: test-cluster-with-quorum
+test-cluster-with-quorum: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
 test-cluster-with-quorum: devclean
 	@mkdir -p share/www/script/test
 ifeq ($(IN_RELEASE), true)
@@ -300,6 +304,7 @@
 	    --path test/javascript/tests-cluster/with-quorum'
 
 .PHONY: test-cluster-without-quorum
+test-cluster-without-quorum: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
 test-cluster-without-quorum: devclean
 	@mkdir -p share/www/script/test
 ifeq ($(IN_RELEASE), true)
@@ -316,6 +321,7 @@
             --path test/javascript/tests-cluster/without-quorum'
 
 .PHONY: soak-javascript
+soak-javascript: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
 soak-javascript:
 	@mkdir -p share/www/script/test
 ifeq ($(IN_RELEASE), true)
@@ -370,6 +376,7 @@
 
 .PHONY: mango-test
 # target: mango-test - Run Mango tests
+mango-test: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
 mango-test: devclean all
 	@cd src/mango && \
 		python3 -m venv .venv && \
diff --git a/Makefile.win b/Makefile.win
index 77cbcbf..c74a546 100644
--- a/Makefile.win
+++ b/Makefile.win
@@ -140,6 +140,7 @@
 eunit: export ERL_AFLAGS = $(shell echo "-config rel/files/eunit.config")
 eunit: export BUILDDIR = $(shell echo %cd%)
 eunit: export COUCHDB_QUERY_SERVER_JAVASCRIPT = $(shell echo %cd%)/bin/couchjs $(shell echo %cd%)/share/server/main.js
+eunit: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
 eunit: couch
 	@set COUCHDB_VERSION=$(COUCHDB_VERSION) && set COUCHDB_GIT_SHA=$(COUCHDB_GIT_SHA) && $(REBAR) setup_eunit 2> nul
 	@set COUCHDB_VERSION=$(COUCHDB_VERSION) && set COUCHDB_GIT_SHA=$(COUCHDB_GIT_SHA) && $(REBAR) -r eunit $(EUNIT_OPTS)
@@ -186,6 +187,7 @@
 		. dev\run rel\overlay\bin\couchup test\javascript\run
 
 .PHONY: elixir
+elixir: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
 elixir: elixir-init elixir-check-formatted elixir-credo devclean
 	@dev\run -a adm:pass --no-eval 'mix test --trace --exclude without_quorum_test --exclude with_quorum_test $(EXUNIT_OPTS)'
 
@@ -216,6 +218,7 @@
 	@mix credo
 
 .PHONY: test-cluster-with-quorum
+test-cluster-with-quorum: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
 test-cluster-with-quorum: devclean
 	-@mkdir share\www\script\test
 ifeq ($(IN_RELEASE), true)
@@ -232,6 +235,7 @@
 		--path test\javascript\tests-cluster\with-quorum"
 
 .PHONY: test-cluster-without-quorum
+test-cluster-without-quorum: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
 test-cluster-without-quorum: devclean
 	-@mkdir share\www\script\test
 ifeq ($(IN_RELEASE), true)
@@ -250,6 +254,7 @@
 
 .PHONY: javascript
 # target: javascript - Run JavaScript test suites or specific ones defined by suites option
+javascript: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
 javascript:
 	-@mkdir share\www\script\test
 ifeq ($(IN_RELEASE), true)
@@ -266,6 +271,7 @@
 
 
 .PHONY: mango-test
+mango-test: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
 mango-test: devclean all
 	@cd src\mango && \
 		python.exe -m venv .venv && \
diff --git a/build-aux/show-test-results.py b/build-aux/show-test-results.py
index eecfc2e..0503965 100755
--- a/build-aux/show-test-results.py
+++ b/build-aux/show-test-results.py
@@ -11,11 +11,10 @@
 TEST_COLLECTIONS = {
     "EUnit": "src/**/.eunit/*.xml",
     "EXUnit": "_build/integration/lib/couchdbtest/*.xml",
-    "Mango": "src/mango/*.xml"
+    "Mango": "src/mango/*.xml",
 }
 
 
-
 def _attrs(elem):
     ret = {}
     for (k, v) in elem.attributes.items():
@@ -30,7 +29,7 @@
             rc.append(node.data)
         else:
             rc.append(self._text(node))
-    return ''.join(rc)
+    return "".join(rc)
 
 
 class TestCase(object):
@@ -79,17 +78,14 @@
     def _name(self, attrs):
         klass = attrs.get("classname", "")
         if klass.startswith("Elixir."):
-            klass = klass[len("Elixir."):]
+            klass = klass[len("Elixir.") :]
         if klass:
             return "%s - %s" % (klass, attrs["name"])
         return attrs["name"]
 
 
 class TestSuite(object):
-    SUITE_NAME_PATTERNS = [
-        re.compile("module '([^']+)'"),
-        re.compile("Elixir\.(.+)")
-    ]
+    SUITE_NAME_PATTERNS = [re.compile("module '([^']+)'"), re.compile("Elixir\.(.+)")]
 
     def __init__(self, elem):
         self.elem = elem
@@ -151,53 +147,42 @@
 def parse_args():
     parser = argparse.ArgumentParser(description="Show test result summaries")
     parser.add_argument(
-            "--ignore-failures",
-            action="store_true",
-            default=False,
-            help="Don't display test failures"
-        )
+        "--ignore-failures",
+        action="store_true",
+        default=False,
+        help="Don't display test failures",
+    )
     parser.add_argument(
-            "--ignore-errors",
-            action="store_true",
-            default=False,
-            help="Don't display test errors"
-        )
+        "--ignore-errors",
+        action="store_true",
+        default=False,
+        help="Don't display test errors",
+    )
     parser.add_argument(
-            "--ignore-skipped",
-            action="store_true",
-            default=False,
-            help="Don't display skipped tests"
-        )
+        "--ignore-skipped",
+        action="store_true",
+        default=False,
+        help="Don't display skipped tests",
+    )
     parser.add_argument(
-            "--all",
-            type=int,
-            default=0,
-            help="Number of rows to show for all groups"
-        )
+        "--all", type=int, default=0, help="Number of rows to show for all groups"
+    )
     parser.add_argument(
-            "--collection",
-            action="append",
-            default=[],
-            help="Which collection to display. May be repeated."
-        )
+        "--collection",
+        action="append",
+        default=[],
+        help="Which collection to display. May be repeated.",
+    )
     parser.add_argument(
-            "--suites",
-            type=int,
-            default=0,
-            help="Number of suites to show"
-        )
+        "--suites", type=int, default=0, help="Number of suites to show"
+    )
+    parser.add_argument("--tests", type=int, default=0, help="Number of tests to show")
     parser.add_argument(
-            "--tests",
-            type=int,
-            default=0,
-            help="Number of tests to show"
-        )
-    parser.add_argument(
-            "--sort",
-            default="total",
-            choices=["test", "fixture", "total"],
-            help="Timing column to sort on"
-        )
+        "--sort",
+        default="total",
+        choices=["test", "fixture", "total"],
+        help="Timing column to sort on",
+    )
     return parser.parse_args()
 
 
@@ -304,7 +289,7 @@
             num_failures,
             num_errors,
             num_skipped,
-            collection.name + "        "
+            collection.name + "        ",
         )
         rows.append(cols)
 
@@ -313,22 +298,16 @@
         scol = 1
     elif sort == "test":
         scol = 2
+
     def skey(row):
         return (-1.0 * row[scol], row[-1])
+
     rows.sort(key=skey)
 
     print "Collections"
     print "==========="
     print
-    headers = [
-        "Total",
-        "Fixture",
-        "Test",
-        "Count",
-        "Failed",
-        "Errors",
-        "Skipped"
-    ]
+    headers = ["Total", "Fixture", "Test", "Count", "Failed", "Errors", "Skipped"]
     display_table([headers] + rows)
     print
 
@@ -345,7 +324,7 @@
                 suite.num_failures,
                 suite.num_errors,
                 suite.num_skipped,
-                collection.name + " - " + suite.name
+                collection.name + " - " + suite.name,
             ]
             rows.append(cols)
 
@@ -354,8 +333,10 @@
         scol = 1
     elif sort == "test":
         scol = 2
+
     def skey(row):
         return (-1.0 * row[scol], row[-1])
+
     rows.sort(key=skey)
 
     rows = rows[:count]
@@ -363,15 +344,7 @@
     print "Suites"
     print "======"
     print
-    headers = [
-        "Total",
-        "Fixture",
-        "Test",
-        "Count",
-        "Failed",
-        "Errors",
-        "Skipped"
-    ]
+    headers = ["Total", "Fixture", "Test", "Count", "Failed", "Errors", "Skipped"]
     display_table([headers] + rows)
     print
 
@@ -389,6 +362,7 @@
 
     def skey(row):
         return (-1.0 * row[0], row[-1])
+
     rows.sort(key=skey)
     rows = rows[:count]
 
diff --git a/src/couch/src/couch_sup.erl b/src/couch/src/couch_sup.erl
index 8dcaf1d..ac117ea 100644
--- a/src/couch/src/couch_sup.erl
+++ b/src/couch/src/couch_sup.erl
@@ -28,6 +28,8 @@
 
 
 start_link() ->
+    assert_admins(),
+    maybe_launch_admin_annoyance_reporter(),
     write_pidfile(),
     notify_starting(),
 
@@ -87,6 +89,34 @@
 handle_config_terminate(_Server, _Reason, _State) ->
     ok.
 
+assert_admins() ->
+    couch_log:info("Preflight check: Asserting Admin Account~n", []),
+    case {config:get("admins"), os:getenv("COUCHDB_TEST_ADMIN_PARTY_OVERRIDE")} of
+        {[], false} ->
+            couch_log:info("~n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%~n"
+                          ++ "  No Admin Account Found, aborting startup.                  ~n"
+                          ++ "  Please configure an admin account in your local.ini file.  ~n"
+                          ++ "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%~n", []),
+            % Wait a second so the log message can make it to the log
+            timer:sleep(500),
+            throw(admin_account_required);
+        _ -> ok
+    end.
+
+send_no_admin_account_error_message() ->
+    couch_log:error("No Admin Account configured."
+        ++ " Please configure an Admin Account in your local.ini file and restart CouchDB.~n", []),
+    FiveMinutes = 5 * 1000 * 60,
+    timer:sleep(FiveMinutes),
+    send_no_admin_account_error_message().
+    
+maybe_launch_admin_annoyance_reporter() ->
+    case os:getenv("COUCHDB_TEST_ADMIN_PARTY_OVERRIDE") of
+        false -> ok;
+        _ -> spawn_link(fun send_no_admin_account_error_message/0)
+    end.
+
+
 notify_starting() ->
     couch_log:info("Apache CouchDB ~s is starting.~n", [
         couch_server:get_version()