src-auth-perms-sync 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- src_auth_perms_sync/__init__.py +1 -0
- src_auth_perms_sync/__main__.py +6 -0
- src_auth_perms_sync/cli.py +646 -0
- src_auth_perms_sync/orgs/__init__.py +1 -0
- src_auth_perms_sync/orgs/command.py +7 -0
- src_auth_perms_sync/orgs/queries.py +44 -0
- src_auth_perms_sync/orgs/sync.py +1167 -0
- src_auth_perms_sync/orgs/types.py +103 -0
- src_auth_perms_sync/permissions/__init__.py +1 -0
- src_auth_perms_sync/permissions/apply.py +420 -0
- src_auth_perms_sync/permissions/command.py +918 -0
- src_auth_perms_sync/permissions/full_set.py +880 -0
- src_auth_perms_sync/permissions/mapping.py +627 -0
- src_auth_perms_sync/permissions/maps.py +291 -0
- src_auth_perms_sync/permissions/queries.py +180 -0
- src_auth_perms_sync/permissions/restore.py +913 -0
- src_auth_perms_sync/permissions/snapshot.py +1502 -0
- src_auth_perms_sync/permissions/sourcegraph.py +392 -0
- src_auth_perms_sync/permissions/types.py +116 -0
- src_auth_perms_sync/permissions/workflow.py +526 -0
- src_auth_perms_sync/shared/__init__.py +1 -0
- src_auth_perms_sync/shared/backups.py +119 -0
- src_auth_perms_sync/shared/id_codec.py +67 -0
- src_auth_perms_sync/shared/queries.py +65 -0
- src_auth_perms_sync/shared/run_context.py +34 -0
- src_auth_perms_sync/shared/saml_groups.py +267 -0
- src_auth_perms_sync/shared/site_config.py +366 -0
- src_auth_perms_sync/shared/sourcegraph.py +69 -0
- src_auth_perms_sync/shared/types.py +69 -0
- src_auth_perms_sync-0.2.1.dist-info/METADATA +256 -0
- src_auth_perms_sync-0.2.1.dist-info/RECORD +34 -0
- src_auth_perms_sync-0.2.1.dist-info/WHEEL +4 -0
- src_auth_perms_sync-0.2.1.dist-info/entry_points.txt +2 -0
- src_auth_perms_sync-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""TypedDict shapes for Sourcegraph organization sync."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Literal, TypeAlias, TypedDict
|
|
7
|
+
|
|
8
|
+
from ..shared import types as shared_types
|
|
9
|
+
|
|
10
|
+
OrganizationChangeKind: TypeAlias = Literal["add", "remove"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OrgMember(TypedDict):
|
|
14
|
+
id: str
|
|
15
|
+
username: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CreatedOrganization(TypedDict):
|
|
19
|
+
id: str
|
|
20
|
+
name: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OrganizationSnapshotStats(TypedDict):
|
|
24
|
+
target_organizations: int
|
|
25
|
+
existing_organizations: int
|
|
26
|
+
total_current_members: int
|
|
27
|
+
total_desired_members: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OrganizationSnapshotEntry(TypedDict):
|
|
31
|
+
id: str | None
|
|
32
|
+
provider_config_id: str
|
|
33
|
+
saml_group: str
|
|
34
|
+
members: list[OrgMember]
|
|
35
|
+
desired_members: list[OrgMember]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OrganizationSnapshot(TypedDict):
|
|
39
|
+
schema_version: int
|
|
40
|
+
captured_at: str
|
|
41
|
+
endpoint: str
|
|
42
|
+
stats: OrganizationSnapshotStats
|
|
43
|
+
organizations: dict[str, OrganizationSnapshotEntry]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class OrganizationSnapshotDiffSummary(TypedDict):
|
|
47
|
+
organizations_changed: int
|
|
48
|
+
organizations_created: int
|
|
49
|
+
members_added: int
|
|
50
|
+
members_removed: int
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class OrganizationSnapshotDiffEntry(TypedDict):
|
|
54
|
+
name: str
|
|
55
|
+
id: str | None
|
|
56
|
+
provider_config_id: str
|
|
57
|
+
saml_group: str
|
|
58
|
+
created: bool
|
|
59
|
+
before_count: int
|
|
60
|
+
after_count: int
|
|
61
|
+
added: list[str]
|
|
62
|
+
removed: list[str]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class OrganizationSnapshotDiff(TypedDict):
|
|
66
|
+
schema_version: int
|
|
67
|
+
diff_kind: Literal["saml_organizations"]
|
|
68
|
+
before_captured_at: str
|
|
69
|
+
after_captured_at: str
|
|
70
|
+
endpoint: str
|
|
71
|
+
summary: OrganizationSnapshotDiffSummary
|
|
72
|
+
organizations: list[OrganizationSnapshotDiffEntry]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class TargetOrganization:
|
|
77
|
+
name: str
|
|
78
|
+
provider_config_id: str
|
|
79
|
+
saml_group: str
|
|
80
|
+
desired_members_by_id: dict[str, OrgMember] = field(default_factory=dict[str, OrgMember])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class OrganizationState:
|
|
85
|
+
id: str | None
|
|
86
|
+
name: str
|
|
87
|
+
members_by_id: dict[str, OrgMember]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class OrganizationUserChange(shared_types.UserIdentity):
|
|
92
|
+
organization_name: str
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class OrganizationBatchLookup(TypedDict):
|
|
96
|
+
current_user: OrgMember
|
|
97
|
+
states: dict[str, OrganizationState]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class OrganizationPlan(TypedDict):
|
|
101
|
+
create_names: list[str]
|
|
102
|
+
additions: list[OrganizationUserChange]
|
|
103
|
+
removals: list[OrganizationUserChange]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Repo-permission sync workflow."""
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""Repo-permission mutation application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
from collections import deque
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from concurrent.futures import (
|
|
10
|
+
FIRST_COMPLETED,
|
|
11
|
+
CancelledError,
|
|
12
|
+
Future,
|
|
13
|
+
ThreadPoolExecutor,
|
|
14
|
+
as_completed,
|
|
15
|
+
wait,
|
|
16
|
+
)
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import TypeAlias, cast
|
|
19
|
+
|
|
20
|
+
import src_py_lib as src
|
|
21
|
+
|
|
22
|
+
from ..shared import id_codec, run_context
|
|
23
|
+
from ..shared import types as shared_types
|
|
24
|
+
from . import queries
|
|
25
|
+
from . import types as permission_types
|
|
26
|
+
|
|
27
|
+
log = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class CircuitBreaker:
|
|
32
|
+
"""Sliding-window circuit breaker for the apply phase.
|
|
33
|
+
|
|
34
|
+
Tracks the most recent `window_size` mutation outcomes (success or
|
|
35
|
+
failure). Once `failure_rate` over that window exceeds
|
|
36
|
+
`failure_threshold` AND we have at least `min_samples` outcomes
|
|
37
|
+
recorded, the breaker opens and `is_open()` returns True for the rest
|
|
38
|
+
of the run (no half-open / reset logic — once we decide the backend
|
|
39
|
+
is too unhealthy, we stay tripped).
|
|
40
|
+
|
|
41
|
+
Designed to bail out of a hopeless run (e.g., backend down or
|
|
42
|
+
severely rate-limiting) instead of grinding through every remaining
|
|
43
|
+
mutation, retrying each request repeatedly, and burning hours of
|
|
44
|
+
wall-clock in retries while making things worse for the server.
|
|
45
|
+
|
|
46
|
+
Used by the apply helpers: each completed mutation calls
|
|
47
|
+
`breaker.record(success=...)`, then `is_open()` is checked between
|
|
48
|
+
completions; once open, the remaining queued futures are cancelled
|
|
49
|
+
and the loop exits, leaving the operator a clear ERROR log + a
|
|
50
|
+
non-zero exit code.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
window_size: int = 50
|
|
54
|
+
failure_threshold: float = 0.25 # fraction; 0.25 == 25%
|
|
55
|
+
min_samples: int = 20
|
|
56
|
+
_outcomes: deque[bool] = field(init=False)
|
|
57
|
+
_lock: threading.Lock = field(init=False, default_factory=threading.Lock)
|
|
58
|
+
_opened: bool = field(init=False, default=False)
|
|
59
|
+
total_successes: int = field(init=False, default=0)
|
|
60
|
+
total_failures: int = field(init=False, default=0)
|
|
61
|
+
|
|
62
|
+
def __post_init__(self) -> None:
|
|
63
|
+
self._outcomes = deque(maxlen=self.window_size)
|
|
64
|
+
|
|
65
|
+
def record(self, success: bool) -> None:
|
|
66
|
+
with self._lock:
|
|
67
|
+
self._outcomes.append(success)
|
|
68
|
+
if success:
|
|
69
|
+
self.total_successes += 1
|
|
70
|
+
else:
|
|
71
|
+
self.total_failures += 1
|
|
72
|
+
if self._opened:
|
|
73
|
+
return
|
|
74
|
+
if len(self._outcomes) < self.min_samples:
|
|
75
|
+
return
|
|
76
|
+
failures = sum(1 for outcome in self._outcomes if not outcome)
|
|
77
|
+
rate = failures / len(self._outcomes)
|
|
78
|
+
if rate >= self.failure_threshold:
|
|
79
|
+
self._opened = True
|
|
80
|
+
src.info(
|
|
81
|
+
"circuit_breaker_open",
|
|
82
|
+
window_size=len(self._outcomes),
|
|
83
|
+
recent_failures=failures,
|
|
84
|
+
failure_rate=round(rate, 3),
|
|
85
|
+
total_successes=self.total_successes,
|
|
86
|
+
total_failures=self.total_failures,
|
|
87
|
+
)
|
|
88
|
+
log.error(
|
|
89
|
+
"Circuit breaker OPEN: %d/%d (%.0f%%) of last %d "
|
|
90
|
+
"mutations failed; halting apply to avoid hammering "
|
|
91
|
+
"a struggling instance. Remaining work will be "
|
|
92
|
+
"cancelled; the run will continue with the after-"
|
|
93
|
+
"snapshot+validation, then exit 1.",
|
|
94
|
+
failures,
|
|
95
|
+
len(self._outcomes),
|
|
96
|
+
100 * rate,
|
|
97
|
+
len(self._outcomes),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def is_open(self) -> bool:
|
|
101
|
+
with self._lock:
|
|
102
|
+
return self._opened
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True, slots=True)
|
|
106
|
+
class PermissionAddition(shared_types.UserIdentity):
|
|
107
|
+
"""One explicit repository permission to add for one user."""
|
|
108
|
+
|
|
109
|
+
repo_id: str
|
|
110
|
+
repo_name: str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True, slots=True)
|
|
114
|
+
class PermissionRemoval(shared_types.UserIdentity):
|
|
115
|
+
"""One explicit repository permission to remove for one user."""
|
|
116
|
+
|
|
117
|
+
repo_id: str
|
|
118
|
+
repo_name: str
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
PermissionChange: TypeAlias = PermissionAddition | PermissionRemoval
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def set_repo_permissions(
|
|
125
|
+
client: src.SourcegraphClient,
|
|
126
|
+
repo_id: str,
|
|
127
|
+
user_perms: list[dict[str, str]],
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Overwrite a repo's explicit permissions with `user_perms` in one call.
|
|
130
|
+
|
|
131
|
+
`user_perms` is a list of `{"bindID": <username>, "permission": "READ"}`.
|
|
132
|
+
bindID is always the Sourcegraph username — validate_site_config()
|
|
133
|
+
enforces that the site is configured with `bindID: "username"`.
|
|
134
|
+
|
|
135
|
+
Retries on transient transport failures (network errors, HTTP 408/429/5xx)
|
|
136
|
+
happen inside the shared Sourcegraph client — every GraphQL call goes through the
|
|
137
|
+
same retry plumbing. Application-level GraphQL errors (auth, validation,
|
|
138
|
+
schema) are NOT retried — they propagate on the first attempt.
|
|
139
|
+
"""
|
|
140
|
+
with src.event(
|
|
141
|
+
"set_repo_perms",
|
|
142
|
+
repo_id=repo_id,
|
|
143
|
+
user_count=len(user_perms),
|
|
144
|
+
):
|
|
145
|
+
client.graphql(
|
|
146
|
+
queries.MUTATION_SET_REPO_PERMISSIONS,
|
|
147
|
+
cast(src.JSONDict, {"repo": repo_id, "userPerms": user_perms}),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def set_repo_permissions_for_usernames(
|
|
152
|
+
client: src.SourcegraphClient,
|
|
153
|
+
repo_id: str,
|
|
154
|
+
usernames: Sequence[str],
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Overwrite a repo's explicit permissions, building GraphQL input lazily."""
|
|
157
|
+
set_repo_permissions(
|
|
158
|
+
client,
|
|
159
|
+
repo_id,
|
|
160
|
+
[{"bindID": username, "permission": "READ"} for username in usernames],
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _mutate_repo_permission_for_user(
|
|
165
|
+
client: src.SourcegraphClient,
|
|
166
|
+
change: PermissionChange,
|
|
167
|
+
mutation: str,
|
|
168
|
+
event_name: str,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Apply one additive repo-permission edge mutation."""
|
|
171
|
+
with src.event(
|
|
172
|
+
event_name,
|
|
173
|
+
repo_id=change.repo_id,
|
|
174
|
+
username=change.username,
|
|
175
|
+
):
|
|
176
|
+
client.graphql(
|
|
177
|
+
mutation,
|
|
178
|
+
cast(src.JSONDict, {"repo": change.repo_id, "user": change.user_id}),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _apply_permission_changes(
|
|
183
|
+
client: src.SourcegraphClient,
|
|
184
|
+
changes: Sequence[PermissionChange],
|
|
185
|
+
*,
|
|
186
|
+
parallelism: int,
|
|
187
|
+
worker_pool: ThreadPoolExecutor | None = None,
|
|
188
|
+
mutation: str,
|
|
189
|
+
event_name: str,
|
|
190
|
+
action: str,
|
|
191
|
+
) -> shared_types.MutationCounts:
|
|
192
|
+
"""Dispatch additive edge mutations across a thread pool."""
|
|
193
|
+
with src.event(
|
|
194
|
+
f"apply_{action}_payloads",
|
|
195
|
+
payload_count=len(changes),
|
|
196
|
+
parallelism=parallelism,
|
|
197
|
+
) as batch_event:
|
|
198
|
+
succeeded = 0
|
|
199
|
+
failed = 0
|
|
200
|
+
canceled = 0
|
|
201
|
+
breaker = CircuitBreaker()
|
|
202
|
+
with run_context.thread_pool(parallelism, worker_pool) as executor:
|
|
203
|
+
futures = {
|
|
204
|
+
src.submit_with_log_context(
|
|
205
|
+
executor,
|
|
206
|
+
_mutate_repo_permission_for_user,
|
|
207
|
+
client,
|
|
208
|
+
change,
|
|
209
|
+
mutation,
|
|
210
|
+
event_name,
|
|
211
|
+
): change
|
|
212
|
+
for change in changes
|
|
213
|
+
}
|
|
214
|
+
for future in as_completed(futures):
|
|
215
|
+
change = futures[future]
|
|
216
|
+
try:
|
|
217
|
+
future.result()
|
|
218
|
+
succeeded += 1
|
|
219
|
+
breaker.record(success=True)
|
|
220
|
+
log.info(
|
|
221
|
+
" OK %s %s → %s (id=%d).",
|
|
222
|
+
action,
|
|
223
|
+
change.username,
|
|
224
|
+
change.repo_name,
|
|
225
|
+
id_codec.decode_repository_id(change.repo_id),
|
|
226
|
+
)
|
|
227
|
+
except CancelledError:
|
|
228
|
+
canceled += 1
|
|
229
|
+
continue
|
|
230
|
+
except Exception as exception:
|
|
231
|
+
failed += 1
|
|
232
|
+
breaker.record(success=False)
|
|
233
|
+
log.error(
|
|
234
|
+
" FAIL %s %s → %s (id=%d): %s",
|
|
235
|
+
action,
|
|
236
|
+
change.username,
|
|
237
|
+
change.repo_name,
|
|
238
|
+
id_codec.decode_repository_id(change.repo_id),
|
|
239
|
+
exception,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if breaker.is_open():
|
|
243
|
+
for pending_future in futures:
|
|
244
|
+
if not pending_future.done():
|
|
245
|
+
pending_future.cancel()
|
|
246
|
+
batch_event["succeeded"] = succeeded
|
|
247
|
+
batch_event["failed"] = failed
|
|
248
|
+
batch_event["canceled"] = canceled
|
|
249
|
+
batch_event["circuit_broken"] = breaker.is_open()
|
|
250
|
+
return shared_types.MutationCounts(
|
|
251
|
+
succeeded=succeeded,
|
|
252
|
+
failed=failed,
|
|
253
|
+
canceled=canceled,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def apply_additions(
|
|
258
|
+
client: src.SourcegraphClient,
|
|
259
|
+
additions: Sequence[PermissionAddition],
|
|
260
|
+
*,
|
|
261
|
+
parallelism: int,
|
|
262
|
+
worker_pool: ThreadPoolExecutor | None = None,
|
|
263
|
+
) -> shared_types.MutationCounts:
|
|
264
|
+
"""Add explicit repo permissions while preserving existing grants."""
|
|
265
|
+
return _apply_permission_changes(
|
|
266
|
+
client,
|
|
267
|
+
additions,
|
|
268
|
+
parallelism=parallelism,
|
|
269
|
+
worker_pool=worker_pool,
|
|
270
|
+
mutation=queries.MUTATION_ADD_REPO_PERMISSION,
|
|
271
|
+
event_name="add_repo_permission",
|
|
272
|
+
action="add",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def apply_removals(
|
|
277
|
+
client: src.SourcegraphClient,
|
|
278
|
+
removals: Sequence[PermissionRemoval],
|
|
279
|
+
*,
|
|
280
|
+
parallelism: int,
|
|
281
|
+
worker_pool: ThreadPoolExecutor | None = None,
|
|
282
|
+
) -> shared_types.MutationCounts:
|
|
283
|
+
"""Remove explicit repo permissions while preserving other grants."""
|
|
284
|
+
return _apply_permission_changes(
|
|
285
|
+
client,
|
|
286
|
+
removals,
|
|
287
|
+
parallelism=parallelism,
|
|
288
|
+
worker_pool=worker_pool,
|
|
289
|
+
mutation=queries.MUTATION_REMOVE_REPO_PERMISSION,
|
|
290
|
+
event_name="remove_repo_permission",
|
|
291
|
+
action="remove",
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _apply_repo_overwrite_plans(
|
|
296
|
+
client: src.SourcegraphClient,
|
|
297
|
+
overwrites: Sequence[permission_types.RepositoryUsernameOverwrite],
|
|
298
|
+
*,
|
|
299
|
+
parallelism: int,
|
|
300
|
+
worker_pool: ThreadPoolExecutor | None = None,
|
|
301
|
+
) -> shared_types.MutationCounts:
|
|
302
|
+
"""Dispatch per-repo overwrite mutations with bounded in-flight work."""
|
|
303
|
+
max_pending_futures = max(1, parallelism * 2)
|
|
304
|
+
total_users = sum(len(overwrite.usernames) for overwrite in overwrites)
|
|
305
|
+
with src.event(
|
|
306
|
+
"apply_username_overwrites",
|
|
307
|
+
payload_count=len(overwrites),
|
|
308
|
+
parallelism=parallelism,
|
|
309
|
+
total_users=total_users,
|
|
310
|
+
max_pending_futures=max_pending_futures,
|
|
311
|
+
) as batch_event:
|
|
312
|
+
succeeded = 0
|
|
313
|
+
failed = 0
|
|
314
|
+
canceled = 0
|
|
315
|
+
submitted_count = 0
|
|
316
|
+
submissions_stopped = False
|
|
317
|
+
breaker = CircuitBreaker()
|
|
318
|
+
overwrite_iterator = iter(overwrites)
|
|
319
|
+
futures: dict[Future[None], permission_types.RepositoryUsernameOverwrite] = {}
|
|
320
|
+
|
|
321
|
+
def _submit_next(executor: ThreadPoolExecutor) -> bool:
|
|
322
|
+
nonlocal submitted_count
|
|
323
|
+
try:
|
|
324
|
+
overwrite = next(overwrite_iterator)
|
|
325
|
+
except StopIteration:
|
|
326
|
+
return False
|
|
327
|
+
future = cast(
|
|
328
|
+
Future[None],
|
|
329
|
+
src.submit_with_log_context(
|
|
330
|
+
executor,
|
|
331
|
+
set_repo_permissions_for_usernames,
|
|
332
|
+
client,
|
|
333
|
+
overwrite.repository_id,
|
|
334
|
+
overwrite.usernames,
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
futures[future] = overwrite
|
|
338
|
+
submitted_count += 1
|
|
339
|
+
return True
|
|
340
|
+
|
|
341
|
+
def _stop_submissions() -> None:
|
|
342
|
+
nonlocal submissions_stopped
|
|
343
|
+
if submissions_stopped:
|
|
344
|
+
return
|
|
345
|
+
submissions_stopped = True
|
|
346
|
+
for pending_future in futures:
|
|
347
|
+
if not pending_future.done():
|
|
348
|
+
pending_future.cancel()
|
|
349
|
+
|
|
350
|
+
with run_context.thread_pool(parallelism, worker_pool) as executor:
|
|
351
|
+
while len(futures) < max_pending_futures and _submit_next(executor):
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
while futures:
|
|
355
|
+
done_futures, _ = wait(futures, return_when=FIRST_COMPLETED)
|
|
356
|
+
for future in done_futures:
|
|
357
|
+
overwrite = futures.pop(future)
|
|
358
|
+
try:
|
|
359
|
+
future.result()
|
|
360
|
+
succeeded += 1
|
|
361
|
+
breaker.record(success=True)
|
|
362
|
+
log.info(
|
|
363
|
+
" OK %s (id=%d) — %d users.",
|
|
364
|
+
overwrite.repository_name,
|
|
365
|
+
id_codec.decode_repository_id(overwrite.repository_id),
|
|
366
|
+
len(overwrite.usernames),
|
|
367
|
+
)
|
|
368
|
+
except CancelledError:
|
|
369
|
+
# Cancelled by the breaker; not counted as a failure
|
|
370
|
+
# because we never gave the server a chance to apply it.
|
|
371
|
+
canceled += 1
|
|
372
|
+
continue
|
|
373
|
+
except Exception as exception:
|
|
374
|
+
failed += 1
|
|
375
|
+
breaker.record(success=False)
|
|
376
|
+
log.error(
|
|
377
|
+
" FAIL %s (id=%d): %s",
|
|
378
|
+
overwrite.repository_name,
|
|
379
|
+
id_codec.decode_repository_id(overwrite.repository_id),
|
|
380
|
+
exception,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
if breaker.is_open():
|
|
384
|
+
_stop_submissions()
|
|
385
|
+
|
|
386
|
+
while (
|
|
387
|
+
not submissions_stopped
|
|
388
|
+
and len(futures) < max_pending_futures
|
|
389
|
+
and _submit_next(executor)
|
|
390
|
+
):
|
|
391
|
+
pass
|
|
392
|
+
|
|
393
|
+
if submissions_stopped:
|
|
394
|
+
canceled += len(overwrites) - submitted_count
|
|
395
|
+
batch_event["succeeded"] = succeeded
|
|
396
|
+
batch_event["failed"] = failed
|
|
397
|
+
batch_event["canceled"] = canceled
|
|
398
|
+
batch_event["circuit_broken"] = breaker.is_open()
|
|
399
|
+
batch_event["submitted"] = submitted_count
|
|
400
|
+
return shared_types.MutationCounts(
|
|
401
|
+
succeeded=succeeded,
|
|
402
|
+
failed=failed,
|
|
403
|
+
canceled=canceled,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def apply_username_overwrites(
|
|
408
|
+
client: src.SourcegraphClient,
|
|
409
|
+
overwrites: Sequence[permission_types.RepositoryUsernameOverwrite],
|
|
410
|
+
*,
|
|
411
|
+
parallelism: int,
|
|
412
|
+
worker_pool: ThreadPoolExecutor | None = None,
|
|
413
|
+
) -> shared_types.MutationCounts:
|
|
414
|
+
"""Dispatch repo overwrite mutations, building GraphQL dicts in workers."""
|
|
415
|
+
return _apply_repo_overwrite_plans(
|
|
416
|
+
client,
|
|
417
|
+
overwrites,
|
|
418
|
+
parallelism=parallelism,
|
|
419
|
+
worker_pool=worker_pool,
|
|
420
|
+
)
|