@redpanda-data/docs-extensions-and-macros 4.8.0 → 4.9.0

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.
Files changed (30) hide show
  1. package/bin/doc-tools.js +236 -54
  2. package/package.json +1 -1
  3. package/tools/property-extractor/Makefile +68 -50
  4. package/tools/property-extractor/cloud_config.py +594 -0
  5. package/tools/property-extractor/compare-properties.js +378 -0
  6. package/tools/property-extractor/generate-handlebars-docs.js +444 -0
  7. package/tools/property-extractor/helpers/and.js +10 -0
  8. package/tools/property-extractor/helpers/eq.js +9 -0
  9. package/tools/property-extractor/helpers/formatPropertyValue.js +128 -0
  10. package/tools/property-extractor/helpers/formatUnits.js +26 -0
  11. package/tools/property-extractor/helpers/index.js +13 -0
  12. package/tools/property-extractor/helpers/join.js +18 -0
  13. package/tools/property-extractor/helpers/ne.js +9 -0
  14. package/tools/property-extractor/helpers/not.js +8 -0
  15. package/tools/property-extractor/helpers/or.js +10 -0
  16. package/tools/property-extractor/helpers/renderPropertyExample.js +42 -0
  17. package/tools/property-extractor/package-lock.json +77 -0
  18. package/tools/property-extractor/package.json +6 -0
  19. package/tools/property-extractor/parser.py +27 -1
  20. package/tools/property-extractor/property_extractor.py +1428 -49
  21. package/tools/property-extractor/requirements.txt +2 -0
  22. package/tools/property-extractor/templates/deprecated-properties.hbs +25 -0
  23. package/tools/property-extractor/templates/deprecated-property.hbs +7 -0
  24. package/tools/property-extractor/templates/property-cloud.hbs +105 -0
  25. package/tools/property-extractor/templates/property-page.hbs +22 -0
  26. package/tools/property-extractor/templates/property.hbs +85 -0
  27. package/tools/property-extractor/templates/topic-property-cloud.hbs +97 -0
  28. package/tools/property-extractor/templates/topic-property.hbs +73 -0
  29. package/tools/property-extractor/transformers.py +178 -6
  30. package/tools/property-extractor/json-to-asciidoc/generate_docs.py +0 -491
@@ -0,0 +1,594 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Cloud configuration integration for property documentation generation.
4
+
5
+ This module fetches cloud configuration from the cloudv2 repository to determine
6
+ which Redpanda properties are supported, editable, or readonly in cloud deployments.
7
+
8
+ Prerequisites:
9
+ - GITHUB_TOKEN environment variable set with appropriate permissions
10
+ - Internet connection to access GitHub API
11
+
12
+ Usage:
13
+ from cloud_config import fetch_cloud_config, add_cloud_support_metadata
14
+
15
+ # Fetch cloud configuration
16
+ config = fetch_cloud_config()
17
+ if config:
18
+ properties = add_cloud_support_metadata(properties, config)
19
+
20
+ Error Handling:
21
+ - Network errors: Logged with retry suggestions
22
+ - Authentication errors: Clear instructions for token setup
23
+ - Parsing errors: Specific file and line information
24
+ - Missing dependencies: Installation instructions provided
25
+ """
26
+
27
+ import os
28
+ import json
29
+ import logging
30
+ from dataclasses import dataclass
31
+ from typing import Dict, Set, Optional, List
32
+
33
+ # Check for required dependencies early
34
+ try:
35
+ import requests
36
+ except ImportError as e:
37
+ raise ImportError("Missing required dependency 'requests': install with pip install requests") from e
38
+
39
+ try:
40
+ import yaml
41
+ except ImportError as e:
42
+ raise ImportError("Missing required dependency 'PyYAML': install with pip install pyyaml") from e
43
+
44
+ # Set up logging with production-ready configuration
45
+ logger = logging.getLogger(__name__)
46
+
47
+ class CloudConfigError(Exception):
48
+ """Base exception for cloud configuration errors."""
49
+ pass
50
+
51
+ class GitHubAuthError(CloudConfigError):
52
+ """Raised when GitHub authentication fails."""
53
+ pass
54
+
55
+ class CloudConfigParsingError(CloudConfigError):
56
+ """Raised when cloud configuration parsing fails."""
57
+ pass
58
+
59
+ class NetworkError(CloudConfigError):
60
+ """Raised when network operations fail."""
61
+ pass
62
+
63
+ @dataclass
64
+ class CloudConfig:
65
+ """Cloud configuration data for a specific version."""
66
+ version: str
67
+ customer_managed_configs: List[Dict]
68
+ readonly_cluster_config: List[str]
69
+
70
+ def get_editable_properties(self) -> Set[str]:
71
+ """Get set of property names that customers can edit."""
72
+ return {config.get('name') for config in self.customer_managed_configs if config.get('name')}
73
+
74
+ def get_readonly_properties(self) -> Set[str]:
75
+ """Get set of property names that are read-only for customers."""
76
+ return set(self.readonly_cluster_config)
77
+
78
+ def get_all_cloud_properties(self) -> Set[str]:
79
+ """
80
+ Return the set of all property names present in the cloud configuration (union of editable and readonly).
81
+
82
+ Returns:
83
+ Set[str]: Property names that are either customer-editable or readonly in cloud deployments.
84
+ """
85
+ return self.get_editable_properties() | self.get_readonly_properties()
86
+
87
+ def is_byoc_only(self, property_name: str) -> bool:
88
+ """
89
+ Return True if the given property is defined in customer_managed_configs and its `cluster_types` list is exactly ['byoc'].
90
+
91
+ Parameters:
92
+ property_name (str): Name of the property to check.
93
+
94
+ Returns:
95
+ bool: True when a matching config entry exists and its `cluster_types` equals ['byoc']; False otherwise.
96
+ """
97
+ for config in self.customer_managed_configs:
98
+ if config.get('name') == property_name:
99
+ cluster_types = config.get('cluster_types', [])
100
+ return cluster_types == ['byoc']
101
+ return False
102
+
103
+
104
+ def fetch_cloud_config(github_token: Optional[str] = None) -> CloudConfig:
105
+ """
106
+ Fetch the latest cloud configuration from the redpanda-data/cloudv2 repository and return it as a CloudConfig.
107
+
108
+ This function uses a GitHub personal access token for authentication. If `github_token` is not provided, it will read GITHUB_TOKEN or REDPANDA_GITHUB_TOKEN from the environment. It downloads the most recent versioned YAML from the repository's install-pack directory, validates expected sections (`customer_managed_configs` and `readonly_cluster_config`), and constructs a CloudConfig instance.
109
+
110
+ Parameters:
111
+ github_token (Optional[str]): Personal access token for GitHub API. If omitted, the function will try environment variables GITHUB_TOKEN or REDPANDA_GITHUB_TOKEN.
112
+
113
+ Returns:
114
+ CloudConfig: Parsed cloud configuration for the latest available version.
115
+
116
+ Raises:
117
+ GitHubAuthError: Authentication or access problems with the GitHub API (including 401/403 responses).
118
+ NetworkError: Network connectivity or timeout failures when contacting the GitHub API.
119
+ CloudConfigParsingError: Failure to parse or validate the repository YAML files or their expected structure.
120
+ CloudConfigError: Generic configuration error (e.g., missing token) or unexpected internal failures.
121
+ """
122
+ if not github_token:
123
+ github_token = os.environ.get('GITHUB_TOKEN') or os.environ.get('REDPANDA_GITHUB_TOKEN')
124
+
125
+ if not github_token:
126
+ error_msg = (
127
+ "No GitHub token provided.\n"
128
+ "Cloud configuration requires authentication to access private repositories.\n"
129
+ "To fix this:\n"
130
+ "1. Go to https://github.com/settings/tokens\n"
131
+ "2. Generate a personal access token with 'repo' scope\n"
132
+ "3. Set the token: export GITHUB_TOKEN=your_token_here\n"
133
+ "4. Re-run the command with --cloud-support flag"
134
+ )
135
+ logger.error(error_msg)
136
+ raise GitHubAuthError(error_msg)
137
+
138
+ headers = {
139
+ 'Authorization': f'token {github_token}',
140
+ 'Accept': 'application/vnd.github.v3+json',
141
+ 'User-Agent': 'Redpanda-Docs-Property-Extractor/1.0'
142
+ }
143
+
144
+ try:
145
+ # First, list all YAML files in the install-pack directory
146
+ logger.info("Fetching install-pack directory listing from cloudv2 repository...")
147
+ url = 'https://api.github.com/repos/redpanda-data/cloudv2/contents/install-pack'
148
+
149
+ response = requests.get(url, headers=headers, timeout=30)
150
+
151
+ # Handle common HTTP error responses with detailed guidance
152
+ if response.status_code in [401, 403, 404]:
153
+ status_messages = {
154
+ 401: {
155
+ "title": "GitHub authentication failed (HTTP 401)",
156
+ "causes": [
157
+ "Invalid or expired GitHub token",
158
+ "Token lacks 'repo' scope for private repositories",
159
+ "Token user doesn't have access to redpanda-data/cloudv2"
160
+ ],
161
+ "fixes": [
162
+ "Verify token at: https://github.com/settings/tokens",
163
+ "Ensure 'repo' scope is enabled",
164
+ "Contact team lead if access is needed to cloudv2 repository"
165
+ ],
166
+ "exception": GitHubAuthError
167
+ },
168
+ 403: {
169
+ "title": f"GitHub API access denied (HTTP 403)",
170
+ "causes": [
171
+ "API rate limit exceeded (5000 requests/hour for authenticated users)",
172
+ "Repository access denied"
173
+ ],
174
+ "fixes": [
175
+ "Wait for rate limit reset if exceeded",
176
+ "Verify repository access permissions",
177
+ "Contact team lead if repository access is needed"
178
+ ],
179
+ "extra": f"Rate limit remaining: {response.headers.get('X-RateLimit-Remaining', 'unknown')}\n"
180
+ f"Rate limit resets at: {response.headers.get('X-RateLimit-Reset', 'unknown')}",
181
+ "exception": GitHubAuthError
182
+ },
183
+ 404: {
184
+ "title": "Install-pack directory not found (HTTP 404)",
185
+ "causes": [
186
+ "Directory 'install-pack' doesn't exist in cloudv2 repository",
187
+ "Repository 'redpanda-data/cloudv2' not accessible",
188
+ "Directory path has changed"
189
+ ],
190
+ "fixes": [
191
+ "Verify directory exists in repository",
192
+ "Check if directory path has changed",
193
+ "Contact cloud team for current configuration location"
194
+ ],
195
+ "exception": NetworkError
196
+ }
197
+ }
198
+
199
+ msg_config = status_messages[response.status_code]
200
+ error_msg = f"{msg_config['title']}.\n"
201
+ error_msg += f"Possible causes:\n"
202
+ for i, cause in enumerate(msg_config['causes'], 1):
203
+ error_msg += f"{i}. {cause}\n"
204
+ if 'extra' in msg_config:
205
+ error_msg += f"{msg_config['extra']}\n"
206
+ error_msg += f"\nTo fix:\n"
207
+ for i, fix in enumerate(msg_config['fixes'], 1):
208
+ error_msg += f"{i}. {fix}\n"
209
+
210
+ logger.error(error_msg)
211
+ raise msg_config['exception'](error_msg)
212
+
213
+ response.raise_for_status()
214
+
215
+ try:
216
+ files = response.json()
217
+ except ValueError as e:
218
+ error_msg = (
219
+ f"Invalid JSON response from GitHub API.\n"
220
+ "This indicates an API format change or server error.\n"
221
+ "Contact development team to update integration."
222
+ )
223
+ logger.exception(error_msg)
224
+ raise CloudConfigParsingError(error_msg) from e
225
+
226
+ if not isinstance(files, list):
227
+ error_msg = (
228
+ f"Expected list of files, got {type(files)}: {files}\n"
229
+ "This indicates an API format change.\n"
230
+ "Contact development team to update integration."
231
+ )
232
+ logger.error(error_msg)
233
+ raise CloudConfigParsingError(error_msg)
234
+
235
+ # Find YAML files with version numbers
236
+ version_files = []
237
+ for file in files:
238
+ if not isinstance(file, dict):
239
+ logger.warning(f"Skipping non-dictionary file entry: {file}")
240
+ continue
241
+
242
+ file_name = file.get('name', '')
243
+ download_url = file.get('download_url', '')
244
+
245
+ if not file_name or not download_url:
246
+ logger.warning(f"Skipping file with missing name/url: {file}")
247
+ continue
248
+
249
+ # Look for version YAML files (e.g., "25.1.yml", "25.2.yml")
250
+ if file_name.endswith('.yml'):
251
+ version_part = file_name.replace('.yml', '')
252
+ # Check if it looks like a version number (e.g., "25.1", "25.2.1")
253
+ if version_part.replace('.', '').isdigit():
254
+ version_files.append((version_part, download_url))
255
+ logger.debug(f"Found version file: {file_name} -> {version_part}")
256
+
257
+ if not version_files:
258
+ error_msg = (
259
+ "No version YAML files found in cloudv2/install-pack directory.\n"
260
+ "Expected files like '25.1.yml', '25.2.yml', etc.\n"
261
+ "Available files: " + ", ".join([f.get('name', 'unknown') for f in files]) + "\n"
262
+ "Contact cloud team to verify configuration file naming convention."
263
+ )
264
+ logger.error(error_msg)
265
+ raise CloudConfigParsingError(error_msg)
266
+
267
+ # Parse and filter valid version entries before sorting
268
+ valid_versions = []
269
+ for version_str, download_url in version_files:
270
+ try:
271
+ # Parse version string into tuple of integers
272
+ version_tuple = tuple(int(part) for part in version_str.split('.'))
273
+ valid_versions.append((version_tuple, version_str, download_url))
274
+ logger.debug(f"Valid version parsed: {version_str} -> {version_tuple}")
275
+ except ValueError as e:
276
+ logger.warning(f"Skipping invalid version format: {version_str} (error: {e})")
277
+ continue
278
+
279
+ # Check if we have any valid versions
280
+ if not valid_versions:
281
+ error_msg = (
282
+ "No valid version files found in cloudv2/install-pack directory.\n"
283
+ f"Found {len(version_files)} files but none had valid version formats.\n"
284
+ f"Available files: {[v[0] for v in version_files]}\n"
285
+ "Expected version format: 'X.Y' or 'X.Y.Z' (e.g., '25.1', '25.2.1')\n"
286
+ "Contact cloud team to verify configuration file naming convention."
287
+ )
288
+ logger.error(error_msg)
289
+ raise CloudConfigParsingError(error_msg)
290
+
291
+ # Sort by parsed version tuple and get the latest
292
+ valid_versions.sort(key=lambda x: x[0]) # Sort by version tuple
293
+ latest_version_tuple, latest_version, download_url = valid_versions[-1]
294
+
295
+ logger.info(f"Found {len(valid_versions)} valid version files, using latest: {latest_version}")
296
+ logger.info(f"Valid versions: {[v[1] for v in valid_versions]}")
297
+ if len(version_files) > len(valid_versions):
298
+ logger.info(f"Skipped {len(version_files) - len(valid_versions)} invalid version files")
299
+
300
+ # Download the latest version file
301
+ logger.info(f"Downloading configuration file for version {latest_version}...")
302
+ response = requests.get(download_url, headers=headers, timeout=60)
303
+ response.raise_for_status()
304
+
305
+ # Parse YAML content
306
+ try:
307
+ config_data = yaml.safe_load(response.text)
308
+ except yaml.YAMLError as e:
309
+ error_msg = (
310
+ f"Failed to parse cloud configuration YAML for version {latest_version}: {e}\n"
311
+ f"File URL: {download_url}\n"
312
+ "The configuration file contains invalid YAML syntax.\n"
313
+ "Contact cloud team to fix configuration file.\n"
314
+ f"Parse error details: {str(e)}"
315
+ )
316
+ logger.error(error_msg)
317
+ raise CloudConfigParsingError(error_msg)
318
+
319
+ if not isinstance(config_data, dict):
320
+ error_msg = (
321
+ f"Cloud configuration root is not a dictionary: {type(config_data)}\n"
322
+ f"Version: {latest_version}\n"
323
+ "Expected YAML file to contain a dictionary at root level.\n"
324
+ "Contact cloud team to verify configuration file format."
325
+ )
326
+ logger.error(error_msg)
327
+ raise CloudConfigParsingError(error_msg)
328
+
329
+ # Extract and validate the relevant sections
330
+ customer_managed = config_data.get('customer_managed_configs', [])
331
+ readonly_config = config_data.get('readonly_cluster_config', [])
332
+
333
+ if not isinstance(customer_managed, list):
334
+ error_msg = (
335
+ f"'customer_managed_configs' section is not a list: {type(customer_managed)}\n"
336
+ f"Version: {latest_version}\n"
337
+ "Expected format:\n"
338
+ "customer_managed_configs:\n"
339
+ " - name: property_name\n"
340
+ " ...\n"
341
+ "Contact cloud team to verify configuration file format."
342
+ )
343
+ logger.error(error_msg)
344
+ raise CloudConfigParsingError(error_msg)
345
+
346
+ if not isinstance(readonly_config, list):
347
+ error_msg = (
348
+ f"'readonly_cluster_config' section is not a list: {type(readonly_config)}\n"
349
+ f"Version: {latest_version}\n"
350
+ "Expected format:\n"
351
+ "readonly_cluster_config:\n"
352
+ " - property_name_1\n"
353
+ " - property_name_2\n"
354
+ "Contact cloud team to verify configuration file format."
355
+ )
356
+ logger.error(error_msg)
357
+ raise CloudConfigParsingError(error_msg)
358
+
359
+ # Validate customer_managed_configs structure
360
+ for i, config in enumerate(customer_managed):
361
+ if not isinstance(config, dict):
362
+ logger.warning(f"customer_managed_configs[{i}] is not a dictionary: {config}, skipping")
363
+ continue
364
+ if 'name' not in config:
365
+ logger.warning(f"customer_managed_configs[{i}] missing 'name' field: {config}, skipping")
366
+
367
+ # Validate readonly_cluster_config structure
368
+ for i, prop_name in enumerate(readonly_config):
369
+ if not isinstance(prop_name, str):
370
+ logger.warning(f"readonly_cluster_config[{i}] is not a string: {prop_name} ({type(prop_name)}), converting")
371
+ readonly_config[i] = str(prop_name)
372
+
373
+ config = CloudConfig(
374
+ version=latest_version,
375
+ customer_managed_configs=customer_managed,
376
+ readonly_cluster_config=readonly_config
377
+ )
378
+
379
+ # Log summary statistics
380
+ editable_count = len(config.get_editable_properties())
381
+ readonly_count = len(config.get_readonly_properties())
382
+ total_count = len(config.get_all_cloud_properties())
383
+
384
+ logger.info(f"Cloud configuration loaded successfully:")
385
+ logger.info(f" Version: {latest_version}")
386
+ logger.info(f" Editable properties: {editable_count}")
387
+ logger.info(f" Readonly properties: {readonly_count}")
388
+ logger.info(f" Total cloud properties: {total_count}")
389
+
390
+ return config
391
+
392
+ except requests.exceptions.ConnectionError as e:
393
+ error_msg = (
394
+ "Network connection failed.\n"
395
+ "Possible causes:\n"
396
+ "1. No internet connection\n"
397
+ "2. Corporate firewall blocking GitHub\n"
398
+ "3. DNS resolution issues\n"
399
+ "\nTo fix:\n"
400
+ "1. Check internet connectivity\n"
401
+ "2. Try: curl -I https://api.github.com\n"
402
+ "3. Contact IT if behind corporate firewall"
403
+ )
404
+ logger.exception(error_msg)
405
+ raise NetworkError(error_msg) from e
406
+
407
+ except requests.exceptions.Timeout as e:
408
+ error_msg = (
409
+ "Request timeout after 30 seconds.\n"
410
+ "GitHub API may be experiencing issues.\n"
411
+ "To fix:\n"
412
+ "1. Check GitHub status: https://status.github.com/\n"
413
+ "2. Try again in a few minutes\n"
414
+ "3. Check network connectivity"
415
+ )
416
+ logger.exception(error_msg)
417
+ raise NetworkError(error_msg) from e
418
+
419
+ except (GitHubAuthError, NetworkError, CloudConfigParsingError):
420
+ # Re-raise our custom exceptions
421
+ raise
422
+
423
+ except Exception as e:
424
+ error_msg = (
425
+ "Unexpected error fetching cloud configuration.\n"
426
+ "This is likely a bug in the cloud configuration integration.\n"
427
+ "Please report this error to the development team with:\n"
428
+ "1. Full error message above\n"
429
+ "2. Command that triggered the error\n"
430
+ "3. Environment details (OS, Python version)\n"
431
+ "4. GitHub token permissions (without revealing the token)"
432
+ )
433
+ logger.exception(error_msg)
434
+ raise CloudConfigError(error_msg) from e
435
+
436
+
437
+ def add_cloud_support_metadata(properties: Dict, cloud_config: CloudConfig) -> Dict:
438
+ """
439
+ Annotate property definitions with cloud-support metadata derived from a CloudConfig.
440
+
441
+ Mutates the provided properties dictionary in place by adding the boolean fields
442
+ 'cloud_editable', 'cloud_readonly', 'cloud_supported', and 'cloud_byoc_only' for
443
+ each property. Only entries whose value is a dict and whose 'config_scope' is
444
+ one of 'cluster', 'broker', or 'topic' are processed; other entries are skipped.
445
+
446
+ Returns:
447
+ The same properties dictionary, updated with cloud metadata.
448
+
449
+ Raises:
450
+ CloudConfigError: If `properties` is not a dict or if required data cannot be
451
+ extracted from the provided CloudConfig.
452
+ """
453
+ if not isinstance(properties, dict):
454
+ error_msg = f"Properties argument must be a dictionary, got {type(properties)}"
455
+ logger.error(error_msg)
456
+ raise CloudConfigError(error_msg)
457
+
458
+ try:
459
+ editable_props = cloud_config.get_editable_properties()
460
+ readonly_props = cloud_config.get_readonly_properties()
461
+ except (AttributeError, KeyError) as e:
462
+ error_msg = f"Failed to extract property sets from cloud configuration: {e}"
463
+ logger.error(error_msg)
464
+ raise CloudConfigError(error_msg) from e
465
+
466
+ logger.info(f"Applying cloud metadata using configuration version {cloud_config.version}")
467
+ logger.info(f"Cloud properties: {len(editable_props)} editable, {len(readonly_props)} readonly")
468
+
469
+ # Counters for reporting
470
+ processed_count = 0
471
+ cloud_supported_count = 0
472
+ editable_count = 0
473
+ readonly_count = 0
474
+ byoc_only_count = 0
475
+ skipped_count = 0
476
+ errors = []
477
+
478
+ for prop_name, prop_data in properties.items():
479
+ try:
480
+ if not isinstance(prop_data, dict):
481
+ error_msg = f"Property '{prop_name}' data is not a dictionary: {type(prop_data)}"
482
+ logger.warning(error_msg)
483
+ errors.append(error_msg)
484
+ skipped_count += 1
485
+ continue
486
+
487
+ # Only process cluster, broker, and topic properties for cloud support
488
+ config_scope = prop_data.get('config_scope', '')
489
+ if config_scope not in ['cluster', 'broker', 'topic']:
490
+ # Skip node config properties and others without cloud relevance
491
+ skipped_count += 1
492
+ continue
493
+
494
+ processed_count += 1
495
+
496
+ # Initialize cloud metadata with defaults
497
+ prop_data['cloud_editable'] = False
498
+ prop_data['cloud_readonly'] = False
499
+ prop_data['cloud_supported'] = False
500
+ prop_data['cloud_byoc_only'] = False
501
+
502
+ # Determine cloud support status
503
+ if prop_name in editable_props:
504
+ prop_data['cloud_editable'] = True
505
+ prop_data['cloud_readonly'] = False
506
+ prop_data['cloud_supported'] = True
507
+ cloud_supported_count += 1
508
+ editable_count += 1
509
+
510
+ # Check if BYOC only
511
+ if cloud_config.is_byoc_only(prop_name):
512
+ prop_data['cloud_byoc_only'] = True
513
+ byoc_only_count += 1
514
+ else:
515
+ prop_data['cloud_byoc_only'] = False
516
+
517
+ elif prop_name in readonly_props:
518
+ prop_data['cloud_editable'] = False
519
+ prop_data['cloud_readonly'] = True
520
+ prop_data['cloud_supported'] = True
521
+ prop_data['cloud_byoc_only'] = False
522
+ cloud_supported_count += 1
523
+ readonly_count += 1
524
+
525
+ else:
526
+ # Property not supported in cloud
527
+ prop_data['cloud_editable'] = False
528
+ prop_data['cloud_readonly'] = False
529
+ prop_data['cloud_supported'] = False
530
+ prop_data['cloud_byoc_only'] = False
531
+
532
+ except Exception as e:
533
+ error_msg = f"Error processing property '{prop_name}': {e}"
534
+ logger.warning(error_msg)
535
+ errors.append(error_msg)
536
+ continue
537
+
538
+ # Log comprehensive summary
539
+ logger.info(f"Cloud metadata application completed:")
540
+ logger.info(f" Properties processed: {processed_count}")
541
+ logger.info(f" Properties skipped (non-cloud scope): {skipped_count}")
542
+ logger.info(f" Cloud-supported properties: {cloud_supported_count}")
543
+ logger.info(f" - Editable: {editable_count}")
544
+ logger.info(f" - Readonly: {readonly_count}")
545
+ logger.info(f" - BYOC-only: {byoc_only_count}")
546
+ logger.info(f" Self-managed only: {processed_count - cloud_supported_count}")
547
+
548
+ if errors:
549
+ logger.warning(f"Encountered {len(errors)} errors during processing:")
550
+ for error in errors[:10]: # Log first 10 errors
551
+ logger.warning(f" - {error}")
552
+ if len(errors) > 10:
553
+ logger.warning(f" ... and {len(errors) - 10} more errors")
554
+
555
+ # Validation checks
556
+ if processed_count == 0:
557
+ logger.warning("No properties were processed for cloud metadata. This may indicate:")
558
+ logger.warning(" 1. All properties are node-scoped (not cluster/broker/topic)")
559
+ logger.warning(" 2. Properties dictionary is empty")
560
+ logger.warning(" 3. Properties missing 'config_scope' field")
561
+
562
+ if cloud_supported_count == 0:
563
+ logger.warning("No cloud-supported properties found. This may indicate:")
564
+ logger.warning(" 1. Cloud configuration is empty or invalid")
565
+ logger.warning(" 2. Property names don't match between sources")
566
+ logger.warning(" 3. All properties are self-managed only")
567
+
568
+ # Check for potential mismatches
569
+ unmatched_cloud_props = (editable_props | readonly_props) - {
570
+ name for name, data in properties.items()
571
+ if isinstance(data, dict) and data.get('config_scope') in ['cluster', 'broker', 'topic']
572
+ }
573
+
574
+ if unmatched_cloud_props:
575
+ logger.info(f"Cloud configuration contains {len(unmatched_cloud_props)} properties not found in extracted properties:")
576
+ for prop in sorted(list(unmatched_cloud_props)[:10]): # Show first 10
577
+ logger.info(f" - {prop}")
578
+ if len(unmatched_cloud_props) > 10:
579
+ logger.info(f" ... and {len(unmatched_cloud_props) - 10} more")
580
+ logger.info("This is normal if cloud config includes deprecated or future properties.")
581
+
582
+ return properties
583
+
584
+
585
+ if __name__ == "__main__":
586
+ # Test the cloud config fetcher
587
+ logging.basicConfig(level=logging.INFO)
588
+ config = fetch_cloud_config()
589
+ if config:
590
+ print(f"Successfully fetched cloud config for version {config.version}")
591
+ print(f"Editable properties: {len(config.get_editable_properties())}")
592
+ print(f"Readonly properties: {len(config.get_readonly_properties())}")
593
+ else:
594
+ print("Failed to fetch cloud configuration")