sunbeam apply [namespace] builds the full kustomize overlay (preserving all image substitutions and patches) then filters the output to only resources in the given namespace before applying. Cleanup and ConfigMap restart detection are also scoped to the target namespace. - manifests.py: _filter_by_namespace(), scoped pre_apply_cleanup() - cli.py: namespace positional arg for apply; meet added to build choices - tests: 17 new tests covering filter logic and CLI dispatch
437 lines
18 KiB
Python
437 lines
18 KiB
Python
"""Tests for CLI routing and argument validation."""
|
|
import sys
|
|
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
import argparse
|
|
|
|
|
|
class TestArgParsing(unittest.TestCase):
|
|
"""Test that argparse parses arguments correctly."""
|
|
|
|
def _parse(self, argv):
|
|
"""Parse argv using the same parser as main(), return args namespace."""
|
|
parser = argparse.ArgumentParser(prog="sunbeam")
|
|
sub = parser.add_subparsers(dest="verb", metavar="verb")
|
|
sub.add_parser("up")
|
|
sub.add_parser("down")
|
|
p_status = sub.add_parser("status")
|
|
p_status.add_argument("target", nargs="?", default=None)
|
|
p_apply = sub.add_parser("apply")
|
|
p_apply.add_argument("namespace", nargs="?", default="")
|
|
p_apply.add_argument("--domain", default="")
|
|
p_apply.add_argument("--email", default="")
|
|
sub.add_parser("seed")
|
|
sub.add_parser("verify")
|
|
p_logs = sub.add_parser("logs")
|
|
p_logs.add_argument("target")
|
|
p_logs.add_argument("-f", "--follow", action="store_true")
|
|
p_get = sub.add_parser("get")
|
|
p_get.add_argument("target")
|
|
p_get.add_argument("-o", "--output", default="yaml", choices=["yaml", "json", "wide"])
|
|
p_restart = sub.add_parser("restart")
|
|
p_restart.add_argument("target", nargs="?", default=None)
|
|
p_build = sub.add_parser("build")
|
|
p_build.add_argument("what", choices=["proxy", "integration", "kratos-admin", "meet",
|
|
"docs-frontend", "people-frontend"])
|
|
p_build.add_argument("--push", action="store_true")
|
|
p_build.add_argument("--deploy", action="store_true")
|
|
sub.add_parser("mirror")
|
|
sub.add_parser("bootstrap")
|
|
p_check = sub.add_parser("check")
|
|
p_check.add_argument("target", nargs="?", default=None)
|
|
p_user = sub.add_parser("user")
|
|
user_sub = p_user.add_subparsers(dest="user_action")
|
|
p_user_list = user_sub.add_parser("list")
|
|
p_user_list.add_argument("--search", default="")
|
|
p_user_get = user_sub.add_parser("get")
|
|
p_user_get.add_argument("target")
|
|
p_user_create = user_sub.add_parser("create")
|
|
p_user_create.add_argument("email")
|
|
p_user_create.add_argument("--name", default="")
|
|
p_user_create.add_argument("--schema", default="default")
|
|
p_user_delete = user_sub.add_parser("delete")
|
|
p_user_delete.add_argument("target")
|
|
p_user_recover = user_sub.add_parser("recover")
|
|
p_user_recover.add_argument("target")
|
|
p_user_disable = user_sub.add_parser("disable")
|
|
p_user_disable.add_argument("target")
|
|
p_user_enable = user_sub.add_parser("enable")
|
|
p_user_enable.add_argument("target")
|
|
p_user_set_pw = user_sub.add_parser("set-password")
|
|
p_user_set_pw.add_argument("target")
|
|
p_user_set_pw.add_argument("password")
|
|
return parser.parse_args(argv)
|
|
|
|
def test_up(self):
|
|
args = self._parse(["up"])
|
|
self.assertEqual(args.verb, "up")
|
|
|
|
def test_status_no_target(self):
|
|
args = self._parse(["status"])
|
|
self.assertEqual(args.verb, "status")
|
|
self.assertIsNone(args.target)
|
|
|
|
def test_status_with_namespace(self):
|
|
args = self._parse(["status", "ory"])
|
|
self.assertEqual(args.verb, "status")
|
|
self.assertEqual(args.target, "ory")
|
|
|
|
def test_logs_no_follow(self):
|
|
args = self._parse(["logs", "ory/kratos"])
|
|
self.assertEqual(args.verb, "logs")
|
|
self.assertEqual(args.target, "ory/kratos")
|
|
self.assertFalse(args.follow)
|
|
|
|
def test_logs_follow_short(self):
|
|
args = self._parse(["logs", "ory/kratos", "-f"])
|
|
self.assertTrue(args.follow)
|
|
|
|
def test_logs_follow_long(self):
|
|
args = self._parse(["logs", "ory/kratos", "--follow"])
|
|
self.assertTrue(args.follow)
|
|
|
|
def test_build_proxy(self):
|
|
args = self._parse(["build", "proxy"])
|
|
self.assertEqual(args.what, "proxy")
|
|
self.assertFalse(args.push)
|
|
self.assertFalse(args.deploy)
|
|
|
|
def test_build_integration(self):
|
|
args = self._parse(["build", "integration"])
|
|
self.assertEqual(args.what, "integration")
|
|
|
|
def test_build_push_flag(self):
|
|
args = self._parse(["build", "proxy", "--push"])
|
|
self.assertTrue(args.push)
|
|
self.assertFalse(args.deploy)
|
|
|
|
def test_build_deploy_flag(self):
|
|
args = self._parse(["build", "proxy", "--deploy"])
|
|
self.assertFalse(args.push)
|
|
self.assertTrue(args.deploy)
|
|
|
|
def test_build_invalid_target(self):
|
|
with self.assertRaises(SystemExit):
|
|
self._parse(["build", "notavalidtarget"])
|
|
|
|
def test_user_set_password(self):
|
|
args = self._parse(["user", "set-password", "admin@example.com", "hunter2"])
|
|
self.assertEqual(args.verb, "user")
|
|
self.assertEqual(args.user_action, "set-password")
|
|
self.assertEqual(args.target, "admin@example.com")
|
|
self.assertEqual(args.password, "hunter2")
|
|
|
|
def test_user_disable(self):
|
|
args = self._parse(["user", "disable", "admin@example.com"])
|
|
self.assertEqual(args.user_action, "disable")
|
|
self.assertEqual(args.target, "admin@example.com")
|
|
|
|
def test_user_enable(self):
|
|
args = self._parse(["user", "enable", "admin@example.com"])
|
|
self.assertEqual(args.user_action, "enable")
|
|
self.assertEqual(args.target, "admin@example.com")
|
|
|
|
def test_user_list_search(self):
|
|
args = self._parse(["user", "list", "--search", "sienna"])
|
|
self.assertEqual(args.user_action, "list")
|
|
self.assertEqual(args.search, "sienna")
|
|
|
|
def test_user_create(self):
|
|
args = self._parse(["user", "create", "x@example.com", "--name", "X Y"])
|
|
self.assertEqual(args.user_action, "create")
|
|
self.assertEqual(args.email, "x@example.com")
|
|
self.assertEqual(args.name, "X Y")
|
|
|
|
def test_get_with_target(self):
|
|
args = self._parse(["get", "ory/kratos-abc"])
|
|
self.assertEqual(args.verb, "get")
|
|
self.assertEqual(args.target, "ory/kratos-abc")
|
|
self.assertEqual(args.output, "yaml")
|
|
|
|
def test_get_json_output(self):
|
|
args = self._parse(["get", "ory/kratos-abc", "-o", "json"])
|
|
self.assertEqual(args.output, "json")
|
|
|
|
def test_get_invalid_output_format(self):
|
|
with self.assertRaises(SystemExit):
|
|
self._parse(["get", "ory/kratos-abc", "-o", "toml"])
|
|
|
|
def test_check_no_target(self):
|
|
args = self._parse(["check"])
|
|
self.assertEqual(args.verb, "check")
|
|
self.assertIsNone(args.target)
|
|
|
|
def test_check_with_namespace(self):
|
|
args = self._parse(["check", "devtools"])
|
|
self.assertEqual(args.verb, "check")
|
|
self.assertEqual(args.target, "devtools")
|
|
|
|
def test_check_with_service(self):
|
|
args = self._parse(["check", "lasuite/people"])
|
|
self.assertEqual(args.verb, "check")
|
|
self.assertEqual(args.target, "lasuite/people")
|
|
|
|
def test_apply_no_namespace(self):
|
|
args = self._parse(["apply"])
|
|
self.assertEqual(args.verb, "apply")
|
|
self.assertEqual(args.namespace, "")
|
|
|
|
def test_apply_with_namespace(self):
|
|
args = self._parse(["apply", "lasuite"])
|
|
self.assertEqual(args.verb, "apply")
|
|
self.assertEqual(args.namespace, "lasuite")
|
|
|
|
def test_apply_ingress_namespace(self):
|
|
args = self._parse(["apply", "ingress"])
|
|
self.assertEqual(args.namespace, "ingress")
|
|
|
|
def test_build_meet(self):
|
|
args = self._parse(["build", "meet"])
|
|
self.assertEqual(args.what, "meet")
|
|
|
|
def test_no_args_verb_is_none(self):
|
|
args = self._parse([])
|
|
self.assertIsNone(args.verb)
|
|
|
|
|
|
class TestCliDispatch(unittest.TestCase):
|
|
"""Test that main() dispatches to the correct command function."""
|
|
|
|
def test_no_verb_exits_0(self):
|
|
with patch.object(sys, "argv", ["sunbeam"]):
|
|
from sunbeam import cli
|
|
with self.assertRaises(SystemExit) as ctx:
|
|
cli.main()
|
|
self.assertEqual(ctx.exception.code, 0)
|
|
|
|
def test_unknown_verb_exits_nonzero(self):
|
|
with patch.object(sys, "argv", ["sunbeam", "unknown-verb"]):
|
|
from sunbeam import cli
|
|
with self.assertRaises(SystemExit) as ctx:
|
|
cli.main()
|
|
self.assertNotEqual(ctx.exception.code, 0)
|
|
|
|
def test_up_calls_cmd_up(self):
|
|
mock_up = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "up"]):
|
|
with patch.dict("sys.modules", {"sunbeam.cluster": MagicMock(cmd_up=mock_up)}):
|
|
import importlib
|
|
import sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_up.assert_called_once()
|
|
|
|
def test_status_no_target(self):
|
|
mock_status = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "status"]):
|
|
with patch.dict("sys.modules", {"sunbeam.services": MagicMock(cmd_status=mock_status)}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_status.assert_called_once_with(None)
|
|
|
|
def test_status_with_namespace(self):
|
|
mock_status = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "status", "ory"]):
|
|
with patch.dict("sys.modules", {"sunbeam.services": MagicMock(cmd_status=mock_status)}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_status.assert_called_once_with("ory")
|
|
|
|
def test_logs_with_target(self):
|
|
mock_logs = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "logs", "ory/kratos"]):
|
|
with patch.dict("sys.modules", {"sunbeam.services": MagicMock(cmd_logs=mock_logs)}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_logs.assert_called_once_with("ory/kratos", follow=False)
|
|
|
|
def test_logs_follow_flag(self):
|
|
mock_logs = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "logs", "ory/kratos", "-f"]):
|
|
with patch.dict("sys.modules", {"sunbeam.services": MagicMock(cmd_logs=mock_logs)}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_logs.assert_called_once_with("ory/kratos", follow=True)
|
|
|
|
def test_get_dispatches_with_target_and_output(self):
|
|
mock_get = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "get", "ory/kratos-abc"]):
|
|
with patch.dict("sys.modules", {"sunbeam.services": MagicMock(cmd_get=mock_get)}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_get.assert_called_once_with("ory/kratos-abc", output="yaml")
|
|
|
|
def test_build_proxy(self):
|
|
mock_build = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "build", "proxy"]):
|
|
with patch.dict("sys.modules", {"sunbeam.images": MagicMock(cmd_build=mock_build)}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_build.assert_called_once_with("proxy", push=False, deploy=False)
|
|
|
|
def test_build_with_push_flag(self):
|
|
mock_build = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "build", "integration", "--push"]):
|
|
with patch.dict("sys.modules", {"sunbeam.images": MagicMock(cmd_build=mock_build)}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_build.assert_called_once_with("integration", push=True, deploy=False)
|
|
|
|
def test_build_with_deploy_flag_implies_push(self):
|
|
mock_build = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "build", "proxy", "--deploy"]):
|
|
with patch.dict("sys.modules", {"sunbeam.images": MagicMock(cmd_build=mock_build)}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_build.assert_called_once_with("proxy", push=True, deploy=True)
|
|
|
|
def test_user_set_password_dispatches(self):
|
|
mock_set_pw = MagicMock()
|
|
mock_users = MagicMock(
|
|
cmd_user_list=MagicMock(), cmd_user_get=MagicMock(),
|
|
cmd_user_create=MagicMock(), cmd_user_delete=MagicMock(),
|
|
cmd_user_recover=MagicMock(), cmd_user_disable=MagicMock(),
|
|
cmd_user_enable=MagicMock(), cmd_user_set_password=mock_set_pw,
|
|
)
|
|
with patch.object(sys, "argv", ["sunbeam", "user", "set-password",
|
|
"admin@sunbeam.pt", "s3cr3t"]):
|
|
with patch.dict("sys.modules", {"sunbeam.users": mock_users}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_set_pw.assert_called_once_with("admin@sunbeam.pt", "s3cr3t")
|
|
|
|
def test_user_disable_dispatches(self):
|
|
mock_disable = MagicMock()
|
|
mock_users = MagicMock(
|
|
cmd_user_list=MagicMock(), cmd_user_get=MagicMock(),
|
|
cmd_user_create=MagicMock(), cmd_user_delete=MagicMock(),
|
|
cmd_user_recover=MagicMock(), cmd_user_disable=mock_disable,
|
|
cmd_user_enable=MagicMock(), cmd_user_set_password=MagicMock(),
|
|
)
|
|
with patch.object(sys, "argv", ["sunbeam", "user", "disable", "x@sunbeam.pt"]):
|
|
with patch.dict("sys.modules", {"sunbeam.users": mock_users}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_disable.assert_called_once_with("x@sunbeam.pt")
|
|
|
|
def test_user_enable_dispatches(self):
|
|
mock_enable = MagicMock()
|
|
mock_users = MagicMock(
|
|
cmd_user_list=MagicMock(), cmd_user_get=MagicMock(),
|
|
cmd_user_create=MagicMock(), cmd_user_delete=MagicMock(),
|
|
cmd_user_recover=MagicMock(), cmd_user_disable=MagicMock(),
|
|
cmd_user_enable=mock_enable, cmd_user_set_password=MagicMock(),
|
|
)
|
|
with patch.object(sys, "argv", ["sunbeam", "user", "enable", "x@sunbeam.pt"]):
|
|
with patch.dict("sys.modules", {"sunbeam.users": mock_users}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_enable.assert_called_once_with("x@sunbeam.pt")
|
|
|
|
def test_apply_full_dispatches_without_namespace(self):
|
|
mock_apply = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "apply"]):
|
|
with patch.dict("sys.modules", {"sunbeam.manifests": MagicMock(cmd_apply=mock_apply)}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_apply.assert_called_once_with(env="local", domain="", email="", namespace="")
|
|
|
|
def test_apply_partial_passes_namespace(self):
|
|
mock_apply = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "apply", "lasuite"]):
|
|
with patch.dict("sys.modules", {"sunbeam.manifests": MagicMock(cmd_apply=mock_apply)}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_apply.assert_called_once_with(env="local", domain="", email="", namespace="lasuite")
|
|
|
|
def test_build_meet_dispatches(self):
|
|
mock_build = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "build", "meet"]):
|
|
with patch.dict("sys.modules", {"sunbeam.images": MagicMock(cmd_build=mock_build)}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_build.assert_called_once_with("meet", push=False, deploy=False)
|
|
|
|
def test_check_no_target(self):
|
|
mock_check = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "check"]):
|
|
with patch.dict("sys.modules", {"sunbeam.checks": MagicMock(cmd_check=mock_check)}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_check.assert_called_once_with(None)
|
|
|
|
def test_check_with_target(self):
|
|
mock_check = MagicMock()
|
|
with patch.object(sys, "argv", ["sunbeam", "check", "lasuite/people"]):
|
|
with patch.dict("sys.modules", {"sunbeam.checks": MagicMock(cmd_check=mock_check)}):
|
|
import importlib, sunbeam.cli as cli_mod
|
|
importlib.reload(cli_mod)
|
|
try:
|
|
cli_mod.main()
|
|
except SystemExit:
|
|
pass
|
|
mock_check.assert_called_once_with("lasuite/people")
|