diff --git a/poetry.lock b/poetry.lock index 620cac7..b252f96 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1195,6 +1195,23 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-mock" +version = "3.12.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, + {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -1641,4 +1658,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "3.10.13" -content-hash = "585a26db846050bc04174c540b5264ef702246021141f10aeff6eaa254d869c5" +content-hash = "bfb0f22a29590bf0be73db5441869ace570ee4a9839694d2382c19cfbb2c18b9" diff --git a/pyproject.toml b/pyproject.toml index b81f15f..34ee629 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,8 @@ vulture = "2.10" [tool.poetry.group.dev.dependencies] types-pyyaml = "^6.0.12.12" types-setuptools = "^69.0.0.20240115" +pytest-mock = "^3.12.0" +isort = "^5.13.2" [tool.stew.ci] black = true @@ -40,6 +42,7 @@ pylint = { args = ["src/"] } vulture = { args = ["src/", "tests/"] } pydocstyle = {args = ["src/", "--ignore=D104,D213,D203,D107,D202"]} pycodestyle = { args = ["src/", "--max-line-length=120","--ignore=E203,W503,W504"] } +isort = { check-args = ["--check", "."], autofix-args = ["."] } [tool.stew.ci.custom-runners.pytest] check-args = ["--cov", "src", "--cov-report", "term", "--cov-report", "html", "--cov-report", "xml", "--junit-xml=reports/test_results.xml"] @@ -70,6 +73,10 @@ min_confidence = 61 [tool.black] line-length = 120 +[tool.isort] +profile = "black" +line_length = 120 + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/src/kubernetes_operator/iam_mapping.py b/src/kubernetes_operator/iam_mapping.py index e55ead1..aa4ccad 100644 --- a/src/kubernetes_operator/iam_mapping.py +++ b/src/kubernetes_operator/iam_mapping.py @@ -4,13 +4,14 @@ from copy import deepcopy from os import environ from pathlib import Path -from typing import List, Any +from typing import Any, List import kopf import yaml -from kubernetes import client, config # type: ignore from kubernetes.client.models.v1_config_map import V1ConfigMap +from kubernetes import client, config # type: ignore + logger = logging.getLogger("operator") try: diff --git a/src/mypy.ini b/src/mypy.ini deleted file mode 100644 index d6fcc7a..0000000 --- a/src/mypy.ini +++ /dev/null @@ -1,8 +0,0 @@ -[mypy] -python_version = 3.10 - -[mypy-kubernetes.*] -ignore_missing_imports = True - -[mypy-kubernetes.client.*] -ignore_missing_imports = True diff --git a/tests/.vulture_whitelist.py b/tests/.vulture_whitelist.py new file mode 100644 index 0000000..e2b009c --- /dev/null +++ b/tests/.vulture_whitelist.py @@ -0,0 +1 @@ +login_mocks diff --git a/tests/kubernetes_operator/conftest.py b/tests/kubernetes_operator/conftest.py new file mode 100644 index 0000000..a97def2 --- /dev/null +++ b/tests/kubernetes_operator/conftest.py @@ -0,0 +1,49 @@ +import dataclasses +from unittest.mock import Mock + +import pytest + + +@dataclasses.dataclass(frozen=True, eq=False, order=False) +class LoginMocks: + pykube_in_cluster: Mock = None + pykube_from_file: Mock = None + pykube_from_env: Mock = None + client_in_cluster: Mock = None + client_from_file: Mock = None + + +@pytest.fixture() +def login_mocks(mocker): + """ + Make all client libraries potentially optional, but do not skip the tests: + skipping the tests is the tests' decision, not this mocking fixture's one. + """ + kwargs = {} + try: + import pykube + except ImportError: + pass + else: + cfg = pykube.KubeConfig( + { + "current-context": "self", + "clusters": [{"name": "self", "cluster": {"server": "localhost"}}], + "contexts": [{"name": "self", "context": {"cluster": "self", "namespace": "default"}}], + } + ) + kwargs.update( + pykube_in_cluster=mocker.patch.object(pykube.KubeConfig, "from_service_account", return_value=cfg), + pykube_from_file=mocker.patch.object(pykube.KubeConfig, "from_file", return_value=cfg), + pykube_from_env=mocker.patch.object(pykube.KubeConfig, "from_env", return_value=cfg), + ) + try: + import kubernetes + except ImportError: + pass + else: + kwargs.update( + client_in_cluster=mocker.patch.object(kubernetes.config, "load_incluster_config"), + client_from_file=mocker.patch.object(kubernetes.config, "load_kube_config"), + ) + return LoginMocks(**kwargs) diff --git a/tests/kubernetes_operator/test_iam_mapping.py b/tests/kubernetes_operator/test_iam_mapping.py index e7dc55c..e644cd1 100644 --- a/tests/kubernetes_operator/test_iam_mapping.py +++ b/tests/kubernetes_operator/test_iam_mapping.py @@ -5,9 +5,8 @@ import yaml from pytest import fixture, raises -from kubernetes import client -import src.kubernetes_operator.iam_mapping as iam_mapping +from kubernetes import client BASE_PATH = "src.kubernetes_operator.iam_mapping" @@ -95,6 +94,11 @@ PLURAL = "iamidentitymappings" +@fixture(autouse=True) +def no_config_needed(login_mocks): + pass + + @fixture def api_client(): with patch(f"{BASE_PATH}.API") as client_mock: @@ -136,6 +140,8 @@ def run_sync(coroutine): def test_create_mapping_userarn(mock_apply_identity_mappings, api_client): + import src.kubernetes_operator.iam_mapping as iam_mapping + run_sync(iam_mapping.create_mapping(spec=SPEC_USER_MARK, diff=DIFF_NEW_USER_MARK)) mock_apply_identity_mappings.assert_called_with(CONFIGMAP, [SPEC_USER_JOHNDOE, SPEC_CSEC_ADMIN, SPEC_USER_MARK]) @@ -143,6 +149,8 @@ def test_create_mapping_userarn(mock_apply_identity_mappings, api_client): def test_create_mapping_rolearn(mock_apply_identity_mappings, api_client): + import src.kubernetes_operator.iam_mapping as iam_mapping + run_sync(iam_mapping.create_mapping(spec=SPEC_CSEC_MAINTENANCE, diff=DIFF_NEW_ROLE_CSEC_MAINTENANCE)) mock_apply_identity_mappings.assert_called_with( @@ -152,6 +160,8 @@ def test_create_mapping_rolearn(mock_apply_identity_mappings, api_client): def test_update_mapping_userarn(mock_apply_identity_mappings, api_client): + import src.kubernetes_operator.iam_mapping as iam_mapping + spec_user_johndoe_updated = { "groups": ["system:masters", "some-other-group-namespace-admin", "new-group-to-update"], "userarn": "arn:aws:iam::000000000000:user/johndoe", @@ -166,6 +176,8 @@ def test_update_mapping_userarn(mock_apply_identity_mappings, api_client): def test_delete_mapping_userarn(mock_apply_identity_mappings, api_client): + import src.kubernetes_operator.iam_mapping as iam_mapping + run_sync(iam_mapping.delete_mapping(spec=SPEC_USER_JOHNDOE)) mock_apply_identity_mappings.assert_called_with(CONFIGMAP, [SPEC_CSEC_ADMIN]) @@ -173,6 +185,8 @@ def test_delete_mapping_userarn(mock_apply_identity_mappings, api_client): def test_delete_mapping_rolearn(mock_apply_identity_mappings, api_client): + import src.kubernetes_operator.iam_mapping as iam_mapping + run_sync(iam_mapping.delete_mapping(spec=SPEC_CSEC_ADMIN)) mock_apply_identity_mappings.assert_called_with(CONFIGMAP, [SPEC_USER_JOHNDOE]) @@ -180,12 +194,16 @@ def test_delete_mapping_rolearn(mock_apply_identity_mappings, api_client): def test_check_synchronization_no_diff(api_client, custom_objects_api): + import src.kubernetes_operator.iam_mapping as iam_mapping + assert iam_mapping.check_synchronization() api_client.read_namespaced_config_map.assert_called_with("aws-auth", "kube-system") custom_objects_api.list_cluster_custom_object.assert_called_with(GROUP, VERSION, PLURAL) def test_check_synchronization_no_diff_with_ignored_identity(api_client, custom_objects_api): + import src.kubernetes_operator.iam_mapping as iam_mapping + data = { "mapRoles": yaml.safe_dump([SPEC_CSEC_ADMIN, SPEC_USER_SYSTEM_NODE_TO_IGNORE]), "mapUsers": yaml.safe_dump([SPEC_USER_JOHNDOE]), @@ -204,6 +222,8 @@ def test_check_synchronization_no_diff_with_ignored_identity(api_client, custom_ environ, {"IGNORED_CM_IDENTITIES": f"{SPEC_USER_MARK.get('username')},{SPEC_CSEC_MAINTENANCE.get('username')}"} ) def test_check_synchronization_no_diff_with_ignored_identity_env(api_client, custom_objects_api): + import src.kubernetes_operator.iam_mapping as iam_mapping + data = { "mapRoles": yaml.safe_dump([SPEC_CSEC_MAINTENANCE, SPEC_CSEC_ADMIN, SPEC_USER_SYSTEM_NODE_TO_IGNORE]), "mapUsers": yaml.safe_dump([SPEC_USER_JOHNDOE, SPEC_USER_MARK]), @@ -219,6 +239,8 @@ def test_check_synchronization_no_diff_with_ignored_identity_env(api_client, cus def test_check_synchronization_with_diff(api_client, custom_objects_api): + import src.kubernetes_operator.iam_mapping as iam_mapping + modified_iam_identity_mapping = custom_objects_api.list_cluster_custom_object.return_value modified_iam_identity_mapping["items"].pop() custom_objects_api.list_cluster_custom_object.return_value = modified_iam_identity_mapping @@ -230,6 +252,8 @@ def test_check_synchronization_with_diff(api_client, custom_objects_api): def test_full_synchronize(mock_apply_identity_mappings, api_client, custom_objects_api): + import src.kubernetes_operator.iam_mapping as iam_mapping + iam_mapping.full_synchronize() mock_apply_identity_mappings.assert_called_with(CONFIGMAP, [SPEC_USER_JOHNDOE, SPEC_CSEC_ADMIN]) @@ -238,6 +262,8 @@ def test_full_synchronize(mock_apply_identity_mappings, api_client, custom_objec def test_apply_cm_identity_mappings_with_userarn(api_client): + import src.kubernetes_operator.iam_mapping as iam_mapping + run_sync(iam_mapping.apply_cm_identity_mappings(CONFIGMAP, [SPEC_USER_JOHNDOE])) expected_cm_data = {"mapRoles": yaml.safe_dump([]), "mapUsers": yaml.safe_dump([SPEC_USER_JOHNDOE])} @@ -248,6 +274,8 @@ def test_apply_cm_identity_mappings_with_userarn(api_client): def test_apply_cm_identity_mappings_with_rolearn(api_client): + import src.kubernetes_operator.iam_mapping as iam_mapping + run_sync(iam_mapping.apply_cm_identity_mappings(CONFIGMAP, [SPEC_CSEC_ADMIN])) expected_cm_data = {"mapRoles": yaml.safe_dump([SPEC_CSEC_ADMIN]), "mapUsers": yaml.safe_dump([])} @@ -258,6 +286,8 @@ def test_apply_cm_identity_mappings_with_rolearn(api_client): def test_apply_cm_identity_mappings_with_unknown_mapping(api_client, caplog): + import src.kubernetes_operator.iam_mapping as iam_mapping + caplog.set_level(logging.WARNING) some_unknown_spec = {"groups": ["system:masters"], "arn": "arn:aws:iam::000000000000:user/bob", "username": "bob"} @@ -275,6 +305,8 @@ def test_apply_cm_identity_mappings_with_unknown_mapping(api_client, caplog): def test_apply_cm_identity_mappings_with_userarn_and_rolearn_and_unknown_mapping(api_client, caplog): + import src.kubernetes_operator.iam_mapping as iam_mapping + caplog.set_level(logging.WARNING) some_unknown_spec = {"groups": ["system:masters"], "arn": "arn:aws:iam::000000000000:user/bob", "username": "bob"} @@ -291,6 +323,8 @@ def test_apply_cm_identity_mappings_with_userarn_and_rolearn_and_unknown_mapping def test_apply_cm_identity_mappings_with_no_mapping(api_client): + import src.kubernetes_operator.iam_mapping as iam_mapping + run_sync(iam_mapping.apply_cm_identity_mappings(CONFIGMAP, [])) expected_cm_data = {"mapRoles": yaml.safe_dump([]), "mapUsers": yaml.safe_dump([])} @@ -301,6 +335,8 @@ def test_apply_cm_identity_mappings_with_no_mapping(api_client): def test_get_cm_identity_mappings_with_empty_mapusers_skips(): + import src.kubernetes_operator.iam_mapping as iam_mapping + ret = iam_mapping.get_cm_identity_mappings(CONFIGMAP_MISSING_MAPUSERS) assert len(ret) == 1 @@ -308,6 +344,8 @@ def test_get_cm_identity_mappings_with_empty_mapusers_skips(): def test_get_cm_identity_mappings_with_empty_maproles_skips(): + import src.kubernetes_operator.iam_mapping as iam_mapping + ret = iam_mapping.get_cm_identity_mappings(CONFIGMAP_MISSING_MAPROLES) assert len(ret) == 1 @@ -315,6 +353,8 @@ def test_get_cm_identity_mappings_with_empty_maproles_skips(): def test_get_cm_identity_mappings_with_empty_configmap_returns_no_identity(): + import src.kubernetes_operator.iam_mapping as iam_mapping + ret = iam_mapping.get_cm_identity_mappings(CONFIGMAP_MISSING_DATA) assert len(ret) == 0