|
1 | 1 | import logging |
| 2 | +import re |
2 | 3 | import time |
3 | 4 | from collections import defaultdict |
4 | 5 | from collections.abc import Container as ContainerT |
5 | 6 | from collections.abc import Generator, Iterable, Sequence |
6 | 7 | from contextlib import contextmanager |
7 | 8 | from tempfile import NamedTemporaryFile |
8 | | -from typing import Optional |
| 9 | +from typing import Dict, Optional |
9 | 10 |
|
10 | 11 | from nebius.aio.authorization.options import options_to_metadata |
11 | 12 | from nebius.aio.operation import Operation as SDKOperation |
@@ -249,13 +250,14 @@ def get_default_subnet(sdk: SDK, project_id: str) -> Subnet: |
249 | 250 |
|
250 | 251 |
|
251 | 252 | def create_disk( |
252 | | - sdk: SDK, name: str, project_id: str, size_mib: int, image_family: str |
| 253 | + sdk: SDK, name: str, project_id: str, size_mib: int, image_family: str, labels: Dict[str, str] |
253 | 254 | ) -> SDKOperation[Operation]: |
254 | 255 | client = DiskServiceClient(sdk) |
255 | 256 | request = CreateDiskRequest( |
256 | 257 | metadata=ResourceMetadata( |
257 | 258 | name=name, |
258 | 259 | parent_id=project_id, |
| 260 | + labels=labels, |
259 | 261 | ), |
260 | 262 | spec=DiskSpec( |
261 | 263 | size_mebibytes=size_mib, |
@@ -288,12 +290,14 @@ def create_instance( |
288 | 290 | disk_id: str, |
289 | 291 | subnet_id: str, |
290 | 292 | preemptible: bool, |
| 293 | + labels: Dict[str, str], |
291 | 294 | ) -> SDKOperation[Operation]: |
292 | 295 | client = InstanceServiceClient(sdk) |
293 | 296 | request = CreateInstanceRequest( |
294 | 297 | metadata=ResourceMetadata( |
295 | 298 | name=name, |
296 | 299 | parent_id=project_id, |
| 300 | + labels=labels, |
297 | 301 | ), |
298 | 302 | spec=InstanceSpec( |
299 | 303 | cloud_init_user_data=user_data, |
@@ -367,3 +371,42 @@ def delete_cluster(sdk: SDK, cluster_id: str) -> None: |
367 | 371 | metadata=REQUEST_MD, |
368 | 372 | ) |
369 | 373 | ) |
| 374 | + |
| 375 | + |
| 376 | +def filter_invalid_labels(labels: Dict[str, str]) -> Dict[str, str]: |
| 377 | + filtered_labels = {} |
| 378 | + for k, v in labels.items(): |
| 379 | + if not _is_valid_label(k, v): |
| 380 | + logger.warning("Skipping invalid label '%s: %s'", k, v) |
| 381 | + continue |
| 382 | + filtered_labels[k] = v |
| 383 | + return filtered_labels |
| 384 | + |
| 385 | + |
| 386 | +def validate_labels(labels: Dict[str, str]): |
| 387 | + for k, v in labels.items(): |
| 388 | + if not _is_valid_label(k, v): |
| 389 | + raise BackendError("Invalid resource labels") |
| 390 | + |
| 391 | + |
| 392 | +def _is_valid_label(key: str, value: str) -> bool: |
| 393 | + # TODO: [Nebius] current validation logic reuses GCP's approach. |
| 394 | + # There is no public information on Nebius labels restrictions. |
| 395 | + return is_valid_resource_name(key) and is_valid_label_value(value) |
| 396 | + |
| 397 | + |
| 398 | +MAX_RESOURCE_NAME_LEN = 63 |
| 399 | +NAME_PATTERN = re.compile(r"^[a-z][_\-a-z0-9]{0,62}$") |
| 400 | +LABEL_VALUE_PATTERN = re.compile(r"^[_\-a-z0-9]{0,63}$") |
| 401 | + |
| 402 | + |
| 403 | +def is_valid_resource_name(name: str) -> bool: |
| 404 | + if len(name) < 1 or len(name) > MAX_RESOURCE_NAME_LEN: |
| 405 | + return False |
| 406 | + match = re.match(NAME_PATTERN, name) |
| 407 | + return match is not None |
| 408 | + |
| 409 | + |
| 410 | +def is_valid_label_value(value: str) -> bool: |
| 411 | + match = re.match(LABEL_VALUE_PATTERN, value) |
| 412 | + return match is not None |
0 commit comments