Skip to content

Commit 561404d

Browse files
committed
feat(gooddata-pipelines): runtime validation of provisioning inputs
1 parent 7ba099d commit 561404d

File tree

10 files changed

+236
-45
lines changed

10 files changed

+236
-45
lines changed

gooddata-pipelines/gooddata_pipelines/provisioning/entities/users/permissions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ class PermissionProvisioner(
3535
source_group_incremental: list[PermissionIncrementalLoad]
3636
source_group_full: list[PermissionFullLoad]
3737

38+
FULL_LOAD_TYPE: type[PermissionFullLoad] = PermissionFullLoad
39+
INCREMENTAL_LOAD_TYPE: type[PermissionIncrementalLoad] = (
40+
PermissionIncrementalLoad
41+
)
42+
3843
def _get_ws_declaration(self, ws_id: str) -> PermissionDeclaration:
3944
users: TargetsPermissionDict = {}
4045
user_groups: TargetsPermissionDict = {}

gooddata-pipelines/gooddata_pipelines/provisioning/entities/users/user_groups.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ class UserGroupProvisioner(
2929
source_group_full: list[UserGroupFullLoad]
3030
upstream_user_groups: list[CatalogUserGroup]
3131

32+
FULL_LOAD_TYPE: type[UserGroupFullLoad] = UserGroupFullLoad
33+
INCREMENTAL_LOAD_TYPE: type[UserGroupIncrementalLoad] = (
34+
UserGroupIncrementalLoad
35+
)
36+
3237
@staticmethod
3338
def _is_changed(
3439
group: UserGroupModel, existing_group: CatalogUserGroup

gooddata-pipelines/gooddata_pipelines/provisioning/entities/users/users.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ class UserProvisioner(Provisioning[UserFullLoad, UserIncrementalLoad]):
2424
"""Provisioning class for users in GoodData workspaces.
2525
2626
This class handles the creation, update, and deletion of users
27-
based on the provided source data. Use the `full_load` or `incremental_load`
28-
methods to run the provisioning.
27+
based on the provided source data.
2928
"""
3029

3130
source_group_incremental: list[UserIncrementalLoad]
3231
source_group_full: list[UserFullLoad]
3332

33+
FULL_LOAD_TYPE: type[UserFullLoad] = UserFullLoad
34+
INCREMENTAL_LOAD_TYPE: type[UserIncrementalLoad] = UserIncrementalLoad
35+
3436
def __init__(self, host: str, token: str) -> None:
3537
super().__init__(host, token)
3638
self.upstream_user_cache: dict[UserId, UserModel] = {}

gooddata-pipelines/gooddata_pipelines/provisioning/entities/workspaces/workspace.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ class WorkspaceProvisioner(
3535
source_group_full: list[WorkspaceFullLoad]
3636
source_group_incremental: list[WorkspaceIncrementalLoad]
3737

38+
FULL_LOAD_TYPE: type[WorkspaceFullLoad] = WorkspaceFullLoad
39+
INCREMENTAL_LOAD_TYPE: type[WorkspaceIncrementalLoad] = (
40+
WorkspaceIncrementalLoad
41+
)
42+
3843
def __init__(self, *args: str, **kwargs: str) -> None:
3944
"""Creates an instance of the WorkspaceProvisioner.
4045

gooddata-pipelines/gooddata_pipelines/provisioning/provisioning.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ class Provisioning(Generic[TFullLoadSourceData, TIncrementalSourceData]):
2323
TProvisioning = TypeVar("TProvisioning", bound="Provisioning")
2424
source_group_full: list[TFullLoadSourceData]
2525
source_group_incremental: list[TIncrementalSourceData]
26+
FULL_LOAD_TYPE: type[TFullLoadSourceData]
27+
INCREMENTAL_LOAD_TYPE: type[TIncrementalSourceData]
2628

2729
def __init__(self, host: str, token: str) -> None:
2830
self.source_id: set[str] = set()
@@ -80,6 +82,17 @@ def _create_groups(
8082
ids_to_create=ids_to_create,
8183
)
8284

85+
def _validate_source_data_type(
86+
self,
87+
source_data: list[TFullLoadSourceData] | list[TIncrementalSourceData],
88+
model: type[TFullLoadSourceData] | type[TIncrementalSourceData],
89+
) -> None:
90+
"""Validates data type of the source data."""
91+
if not all(isinstance(record, model) for record in source_data):
92+
raise TypeError(
93+
f"Not all elements in source data are instances of {model.__name__}"
94+
)
95+
8396
def _provision_incremental_load(self) -> None:
8497
raise NotImplementedError(
8598
"Provisioning method to be implemented in the subclass."
@@ -103,9 +116,10 @@ def full_load(self, source_data: list[TFullLoadSourceData]) -> None:
103116
- All child workspaces not declared under the parent workspace in the
104117
source data are deleted
105118
"""
106-
self.source_group_full = source_data
107119

108120
try:
121+
self._validate_source_data_type(source_data, self.FULL_LOAD_TYPE)
122+
self.source_group_full = source_data
109123
self._provision_full_load()
110124
self.logger.info("Provisioning completed.")
111125
except Exception as e:
@@ -120,10 +134,11 @@ def incremental_load(
120134
based on the source data provided. Only changes requested in the source
121135
data will be applied.
122136
"""
123-
# TODO: validate the data type of source group at runtime
124-
self.source_group_incremental = source_data
125-
126137
try:
138+
self._validate_source_data_type(
139+
source_data, self.INCREMENTAL_LOAD_TYPE
140+
)
141+
self.source_group_incremental = source_data
127142
self._provision_incremental_load()
128143
self.logger.info("Provisioning completed.")
129144
except Exception as e:

gooddata-pipelines/tests/conftest.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@
66
import boto3
77
import pytest
88
from moto import mock_aws
9-
9+
from pytest_mock import MockerFixture
10+
11+
from gooddata_pipelines import (
12+
PermissionProvisioner,
13+
UserDataFilterProvisioner,
14+
UserGroupProvisioner,
15+
UserProvisioner,
16+
WorkspaceProvisioner,
17+
)
1018
from gooddata_pipelines.api import GoodDataApi
1119

1220
TEST_DATA_DIR = str((Path(__file__).parent / "data").absolute())
@@ -69,3 +77,53 @@ def error(self, msg: str) -> None:
6977
print(msg)
7078

7179
return MockLogger()
80+
81+
82+
@pytest.fixture
83+
def workspace_provisioner(mocker: MockerFixture):
84+
provisioner_instance = WorkspaceProvisioner.create("host", "token")
85+
86+
# Patch the API
87+
mocker.patch.object(provisioner_instance, "_api", return_value=None)
88+
89+
return provisioner_instance
90+
91+
92+
@pytest.fixture
93+
def user_provisioner(mocker: MockerFixture):
94+
provisioner_instance = UserProvisioner.create("host", "token")
95+
96+
# Patch the API
97+
mocker.patch.object(provisioner_instance, "_api", return_value=None)
98+
99+
return UserProvisioner.create("host", "token")
100+
101+
102+
@pytest.fixture
103+
def user_group_provisioner(mocker: MockerFixture):
104+
provisioner_instance = UserGroupProvisioner.create("host", "token")
105+
106+
# Patch the API
107+
mocker.patch.object(provisioner_instance, "_api", return_value=None)
108+
109+
return provisioner_instance
110+
111+
112+
@pytest.fixture
113+
def permission_provisioner(mocker: MockerFixture) -> PermissionProvisioner:
114+
provisioner_instance = PermissionProvisioner.create("host", "token")
115+
116+
# Patch the API
117+
mocker.patch.object(provisioner_instance, "_api", return_value=None)
118+
119+
return provisioner_instance
120+
121+
122+
@pytest.fixture
123+
def user_data_filter_provisioner(mocker: MockerFixture):
124+
provisioner_instance = UserDataFilterProvisioner.create("host", "token")
125+
126+
# Patch the API
127+
mocker.patch.object(provisioner_instance, "_api", return_value=None)
128+
129+
return provisioner_instance

gooddata-pipelines/tests/provisioning/entities/users/test_permissions.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -402,18 +402,6 @@ def mock_upstream_perms(ws_id: str) -> CatalogDeclarativeWorkspacePermissions:
402402
return UPSTREAM_WS_PERMISSIONS[ws_id]
403403

404404

405-
@pytest.fixture
406-
def permission_provisioner(mocker: MockerFixture) -> PermissionProvisioner:
407-
provisioner_instance = PermissionProvisioner.create(
408-
host="https://localhost:3000", token="token"
409-
)
410-
411-
# Patch the API
412-
mocker.patch.object(provisioner_instance, "_api", return_value=None)
413-
414-
return provisioner_instance
415-
416-
417405
def parse_expected_permissions(
418406
raw_data: dict,
419407
) -> dict[str, list[CatalogDeclarativeSingleWorkspacePermission]]:

gooddata-pipelines/tests/provisioning/entities/workspaces/test_provisioning.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# (C) 2025 GoodData Corporation
22

3+
from gooddata_pipelines import WorkspaceProvisioner
34
from gooddata_pipelines.provisioning.provisioning import Provisioning
45

5-
MOCK_PROVISIONER: Provisioning = Provisioning.create("host", "token")
66

7-
8-
def test_create_groups_base_case() -> None:
9-
provisioner: Provisioning = MOCK_PROVISIONER
7+
def test_create_groups_base_case(
8+
workspace_provisioner: WorkspaceProvisioner,
9+
) -> None:
10+
provisioner: Provisioning = workspace_provisioner
1011
test_source_ids = {"source_id_1", "source_id_2", "source_id_3"}
1112
test_panther_ids = {"source_id_2", "source_id_3", "source_id_4"}
1213

@@ -17,8 +18,10 @@ def test_create_groups_base_case() -> None:
1718
assert id_groups.ids_to_create == {"source_id_1"}
1819

1920

20-
def test_create_groups_empty_sets() -> None:
21-
provisioner: Provisioning = MOCK_PROVISIONER
21+
def test_create_groups_empty_sets(
22+
workspace_provisioner: WorkspaceProvisioner,
23+
) -> None:
24+
provisioner: Provisioning = workspace_provisioner
2225
test_source_ids: set[str] = set()
2326
test_panther_ids: set[str] = set()
2427

@@ -29,8 +32,10 @@ def test_create_groups_empty_sets() -> None:
2932
assert id_groups.ids_to_create == set()
3033

3134

32-
def test_create_groups_no_overlap() -> None:
33-
provisioner: Provisioning = MOCK_PROVISIONER
35+
def test_create_groups_no_overlap(
36+
workspace_provisioner: WorkspaceProvisioner,
37+
) -> None:
38+
provisioner: Provisioning = workspace_provisioner
3439
test_source_ids = {"source_id_1", "source_id_2"}
3540
test_panther_ids = {"source_id_3", "source_id_4"}
3641

@@ -41,8 +46,10 @@ def test_create_groups_no_overlap() -> None:
4146
assert id_groups.ids_to_create == {"source_id_1", "source_id_2"}
4247

4348

44-
def test_create_groups_full_overlap() -> None:
45-
provisioner: Provisioning = MOCK_PROVISIONER
49+
def test_create_groups_full_overlap(
50+
workspace_provisioner: WorkspaceProvisioner,
51+
) -> None:
52+
provisioner: Provisioning = workspace_provisioner
4653
test_source_ids = {"source_id_1", "source_id_2"}
4754
test_panther_ids = {"source_id_1", "source_id_2"}
4855

gooddata-pipelines/tests/provisioning/entities/workspaces/test_workspace.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# (C) 2025 GoodData Corporation
22

3+
import pytest
34
from gooddata_sdk.catalog.workspace.entity_model.workspace import (
45
CatalogWorkspace,
56
)
@@ -9,13 +10,11 @@
910
WorkspaceFullLoad,
1011
)
1112

12-
MOCK_WORKSPACE_PROVISIONER: WorkspaceProvisioner = WorkspaceProvisioner.create(
13-
"host", "token"
14-
)
15-
1613

17-
def test_find_workspaces_to_update_same_ids_and_names() -> None:
18-
provisioner: WorkspaceProvisioner = MOCK_WORKSPACE_PROVISIONER
14+
@pytest.fixture
15+
def test_find_workspaces_to_update_same_ids_and_names(
16+
workspace_provisioner: WorkspaceProvisioner,
17+
) -> None:
1918
ids_in_both_systems = {"workspace_id1", "workspace_id2"}
2019
source_group: list[WorkspaceFullLoad] = [
2120
WorkspaceFullLoad(
@@ -42,15 +41,16 @@ def test_find_workspaces_to_update_same_ids_and_names() -> None:
4241
),
4342
]
4443

45-
workspaces_to_update = provisioner._find_workspaces_to_update(
44+
workspaces_to_update = workspace_provisioner._find_workspaces_to_update(
4645
source_group, panther_group, ids_in_both_systems
4746
)
4847

4948
assert workspaces_to_update == set()
5049

5150

52-
def test_find_workspaces_to_update_different_ids() -> None:
53-
provisioner: WorkspaceProvisioner = MOCK_WORKSPACE_PROVISIONER
51+
def test_find_workspaces_to_update_different_ids(
52+
workspace_provisioner: WorkspaceProvisioner,
53+
) -> None:
5454
ids_in_both_systems = {"workspace_id1", "workspace_id2"}
5555
source_group: list[WorkspaceFullLoad] = [
5656
WorkspaceFullLoad(
@@ -77,15 +77,16 @@ def test_find_workspaces_to_update_different_ids() -> None:
7777
),
7878
]
7979

80-
workspaces_to_update = provisioner._find_workspaces_to_update(
80+
workspaces_to_update = workspace_provisioner._find_workspaces_to_update(
8181
source_group, panther_group, ids_in_both_systems
8282
)
8383

8484
assert workspaces_to_update == set()
8585

8686

87-
def test_find_workspaces_to_update_same_ids_different_names() -> None:
88-
provisioner: WorkspaceProvisioner = MOCK_WORKSPACE_PROVISIONER
87+
def test_find_workspaces_to_update_same_ids_different_names(
88+
workspace_provisioner: WorkspaceProvisioner,
89+
) -> None:
8990
ids_in_both_systems: set[str] = {"workspace_id1", "workspace_id2"}
9091
source_group: list[WorkspaceFullLoad] = [
9192
WorkspaceFullLoad(
@@ -112,15 +113,16 @@ def test_find_workspaces_to_update_same_ids_different_names() -> None:
112113
),
113114
]
114115

115-
workspaces_to_update = provisioner._find_workspaces_to_update(
116+
workspaces_to_update = workspace_provisioner._find_workspaces_to_update(
116117
source_group, panther_group, ids_in_both_systems
117118
)
118119

119120
assert workspaces_to_update == {"workspace_id1", "workspace_id2"}
120121

121122

122-
def test_find_workspaces_to_update_no_panther() -> None:
123-
provisioner: WorkspaceProvisioner = MOCK_WORKSPACE_PROVISIONER
123+
def test_find_workspaces_to_update_no_panther(
124+
workspace_provisioner: WorkspaceProvisioner,
125+
) -> None:
124126
ids_in_both_systems: set[str] = set()
125127
source_group: list[WorkspaceFullLoad] = [
126128
WorkspaceFullLoad(
@@ -136,7 +138,7 @@ def test_find_workspaces_to_update_no_panther() -> None:
136138
]
137139
panther_group: list[CatalogWorkspace] = []
138140

139-
workspaces_to_update = provisioner._find_workspaces_to_update(
141+
workspaces_to_update = workspace_provisioner._find_workspaces_to_update(
140142
source_group, panther_group, ids_in_both_systems
141143
)
142144

0 commit comments

Comments
 (0)