16
16
"""
17
17
18
18
import logging
19
- from typing import Iterable
20
19
21
- from django .conf import settings
22
20
from django .db import transaction
23
- from kessel .relations .v1beta1 import common_pb2
24
21
from management .group .relation_api_dual_write_group_handler import RelationApiDualWriteGroupHandler
25
- from management .models import Workspace
22
+ from management .models import Group
26
23
from management .principal .model import Principal
27
24
from management .relation_replicator .logging_replicator import LoggingReplicator
28
25
from management .relation_replicator .outbox_replicator import OutboxReplicator
29
26
from management .relation_replicator .relation_replicator import (
30
- PartitionKey ,
31
27
RelationReplicator ,
32
- ReplicationEvent ,
33
28
ReplicationEventType ,
34
29
)
35
30
from management .relation_replicator .relations_api_replicator import RelationsApiReplicator
36
- from management .role .model import BindingMapping , Role
37
- from migration_tool .models import V2rolebinding
38
- from migration_tool .sharedSystemRolesReplicatedRoleBindings import v1_role_to_v2_bindings
39
- from migration_tool .utils import create_relationship
31
+ from management .role .model import Role
32
+ from management .role .relation_api_dual_write_handler import RelationApiDualWriteHandler
40
33
41
34
from api .cross_access .relation_api_dual_write_cross_access_handler import RelationApiDualWriteCrossAccessHandler
42
35
from api .models import CrossAccountRequest , Tenant
43
36
44
37
logger = logging .getLogger (__name__ ) # pylint: disable=invalid-name
45
38
46
39
47
- def get_kessel_relation_tuples (
48
- v2_role_bindings : Iterable [V2rolebinding ],
49
- default_workspace : Workspace ,
50
- ) -> list [common_pb2 .Relationship ]:
51
- """Generate a set of relationships and BindingMappings for the given set of v2 role bindings."""
52
- relationships : list [common_pb2 .Relationship ] = list ()
53
-
54
- for v2_role_binding in v2_role_bindings :
55
- relationships .extend (v2_role_binding .as_tuples ())
56
-
57
- bound_resource = v2_role_binding .resource
58
-
59
- # Is this a workspace binding, but not to the root workspace?
60
- # If so, ensure this workspace is a child of the root workspace.
61
- # All other resource-resource or resource-workspace relations
62
- # which may be implied or necessary are intentionally ignored.
63
- # These should come from the apps that own the resource.
64
- if bound_resource .resource_type == ("rbac" , "workspace" ) and not bound_resource .resource_id == str (
65
- default_workspace .id
66
- ):
67
- # This is not strictly necessary here and the relation may be a duplicate.
68
- # Once we have more Workspace API / Inventory Group migration progress,
69
- # this block can and probably should be removed.
70
- # One of those APIs will add it themselves.
71
- relationships .append (
72
- create_relationship (
73
- bound_resource .resource_type ,
74
- bound_resource .resource_id ,
75
- ("rbac" , "workspace" ),
76
- str (default_workspace .id ),
77
- "parent" ,
78
- )
79
- )
80
-
81
- return relationships
82
-
83
-
84
- def migrate_role (
85
- role : Role ,
86
- default_workspace : Workspace ,
87
- current_bindings : Iterable [BindingMapping ] = [],
88
- ) -> tuple [list [common_pb2 .Relationship ], list [BindingMapping ]]:
89
- """
90
- Migrate a role from v1 to v2, returning the tuples and mappings.
91
-
92
- The mappings are returned so that we can reconstitute the corresponding tuples for a given role.
93
- This is needed so we can remove those tuples when the role changes if needed.
94
- """
95
- v2_role_bindings = v1_role_to_v2_bindings (role , default_workspace , current_bindings )
96
- relationships = get_kessel_relation_tuples ([m .get_role_binding () for m in v2_role_bindings ], default_workspace )
97
- return relationships , v2_role_bindings
98
-
99
-
100
40
def migrate_groups_for_tenant (tenant : Tenant , replicator : RelationReplicator ):
101
41
"""Generate user relationships and system role assignments for groups in a tenant."""
102
- groups = tenant .group_set .all ( )
42
+ groups = tenant .group_set .only ( "pk" ). values ( "pk" )
103
43
for group in groups :
104
- principals : list [Principal ] = []
105
- system_roles : list [Role ] = []
106
- if not group .platform_default :
107
- principals = group .principals .all ()
108
- if group .system is False and group .admin_default is False :
109
- system_roles = group .roles ().public_tenant_only ()
110
- if any (True for _ in system_roles ) or any (True for _ in principals ):
111
- # The migrator does not generally deal with concurrency control,
112
- # but we require an atomic block due to use of select_for_update in the dual write handler.
113
- with transaction .atomic ():
44
+ # The migrator deals with concurrency control.
45
+ # We need an atomic block because the select_for_update is used in the dual write handler,
46
+ # and the group must be locked to add principals to the groups.
47
+ # NOTE: The lock on the group is not necessary when adding system roles to the group,
48
+ # as the binding mappings are locked during this process to ensure concurrency control.
49
+ # Start of transaction for group operations
50
+ with transaction .atomic ():
51
+ # Requery the group with a lock
52
+ group = Group .objects .select_for_update ().get (pk = group ["pk" ])
53
+ principals : list [Principal ] = []
54
+ system_roles : list [Role ] = []
55
+ if not group .platform_default :
56
+ principals = group .principals .all ()
57
+ if group .system is False and group .admin_default is False :
58
+ system_roles = group .roles ().public_tenant_only ()
59
+ if any (True for _ in system_roles ) or any (True for _ in principals ):
114
60
dual_write_handler = RelationApiDualWriteGroupHandler (
115
61
group , ReplicationEventType .MIGRATE_TENANT_GROUPS , replicator = replicator
116
62
)
63
+ # this operation requires lock on group as well as in view,
64
+ # more details in GroupViewSet#get_queryset method which is used to add principals.
117
65
dual_write_handler .generate_relations_to_add_principals (principals )
66
+ # lock on group is not required to add system role, only binding mappings which is included in
67
+ # dual_write_handler
118
68
dual_write_handler .generate_relations_to_add_roles (system_roles )
119
69
dual_write_handler .replicate ()
70
+ # End of transaction for group operations, locks are released
120
71
121
72
122
73
def migrate_roles_for_tenant (tenant , exclude_apps , replicator ):
123
74
"""Migrate all roles for a given tenant."""
124
- default_workspace = Workspace .objects .get (type = Workspace .Types .DEFAULT , tenant = tenant )
125
-
126
- roles = tenant .role_set .all ()
75
+ roles = tenant .role_set .only ("pk" )
127
76
if exclude_apps :
128
77
roles = roles .exclude (access__permission__application__in = exclude_apps )
129
-
130
- for role in roles :
131
- logger .info (f"Migrating role: { role .name } with UUID { role .uuid } ." )
132
-
133
- tuples , mappings = migrate_role (role , default_workspace )
134
-
135
- # Conflicts are not ignored in order to prevent this from
136
- # accidentally running concurrently with dual-writes.
137
- # If migration should be rerun, then the bindings table should be dropped.
138
- # If changing this to allow updates,
139
- # always ensure writes are paused before running.
140
- # This must always be the case, but this should at least start failing you if you forget.
141
- BindingMapping .objects .bulk_create (mappings , ignore_conflicts = False )
142
-
143
- replicator .replicate (
144
- ReplicationEvent (
145
- event_type = ReplicationEventType .MIGRATE_CUSTOM_ROLE ,
146
- info = {"role_uuid" : str (role .uuid )},
147
- partition_key = PartitionKey .byEnvironment (),
148
- add = tuples ,
78
+ role_pks = roles .values_list ("pk" , flat = True )
79
+ for role in role_pks :
80
+ # The migrator deals with concurrency control and roles needs to be locked.
81
+ with transaction .atomic ():
82
+ # Requery and lock role
83
+ role = Role .objects .select_for_update ().get (pk = role )
84
+ logger .info (f"Migrating role: { role .name } with UUID { role .uuid } ." )
85
+ dual_write_handler = RelationApiDualWriteHandler (
86
+ role , ReplicationEventType .MIGRATE_CUSTOM_ROLE , replicator
149
87
)
150
- )
151
-
88
+ dual_write_handler .prepare_for_update ()
89
+ dual_write_handler .replicate_new_or_updated_role (role )
90
+ # End of transaction, locks on role is released.
152
91
logger .info (f"Migration completed for role: { role .name } with UUID { role .uuid } ." )
92
+
153
93
logger .info (f"Migrated { roles .count ()} roles for tenant: { tenant .org_id } " )
154
94
155
95
@@ -171,32 +111,38 @@ def migrate_data_for_tenant(tenant: Tenant, exclude_apps: list, replicator: Rela
171
111
logger .info ("Finished relations of cross account requests." )
172
112
173
113
174
- # The migrator does not generally deal with concurrency control,
175
- # but we require an atomic block due to use of select_for_update in the dual write handler.
176
114
def migrate_cross_account_requests (tenant : Tenant , replicator : RelationReplicator ):
177
115
"""Migrate approved account requests."""
178
116
cross_account_requests = CrossAccountRequest .objects .filter (status = "approved" , target_org = tenant .org_id )
179
117
for cross_account_request in cross_account_requests :
118
+ # The migrator deals with concurrency control.
119
+ # We need an atomic block because the select_for_update is used in the dual write handler,
120
+ # and cross account request must be locked to add roles.
121
+ # Start of transaction for approved cross account request and "add roles" operation
180
122
with transaction .atomic ():
123
+ # Lock cross account request
124
+ cross_account_request = CrossAccountRequest .objects .select_for_update ().get (pk = cross_account_request .pk )
181
125
cross_account_roles = cross_account_request .roles .all ()
182
126
if any (True for _ in cross_account_roles ):
183
127
dual_write_handler = RelationApiDualWriteCrossAccessHandler (
184
128
cross_account_request , ReplicationEventType .MIGRATE_CROSS_ACCOUNT_REQUEST , replicator
185
129
)
130
+ # This operation requires lock on cross account request as is done
131
+ # in CrossAccountRequestViewSet#get_queryset
132
+ # This also locks binding mapping if exists for passed system roles.
186
133
dual_write_handler .generate_relations_to_add_roles (cross_account_request .roles .all ())
187
134
dual_write_handler .replicate ()
135
+ # End of transaction for approved cross account request and its add role operation
136
+ # Locks on cross account request and eventually on default workspace are released.
137
+ # Default workspace is locked when related binding mapping did not exist yet
138
+ # (Considering the position of this algorithm,the binding mappings for system roles should already exist,
139
+ # as they are tied to the system roles.)
188
140
189
141
190
142
def migrate_data (
191
143
exclude_apps : list = [], orgs : list = [], write_relationships : str = "False" , skip_roles : bool = False
192
144
):
193
145
"""Migrate all data for all tenants."""
194
- # Only run this in maintanence mode or
195
- # if we don't write relationships (testing out the migration and clean up the created bindingmappings)
196
- if not settings .READ_ONLY_API_MODE and write_relationships != "False" :
197
- logger .fatal ("Read-only API mode is required. READ_ONLY_API_MODE must be set to true." )
198
- return
199
-
200
146
count = 0
201
147
tenants = Tenant .objects .filter (ready = True ).exclude (tenant_name = "public" )
202
148
replicator = _get_replicator (write_relationships )
0 commit comments