Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:58:35 +08:00
commit 2448fbf2fb
25 changed files with 2940 additions and 0 deletions

View File

@@ -0,0 +1,291 @@
#!/usr/bin/env python3
"""
Detect IoT Edge project structure by scanning for existing patterns.
This script auto-detects:
- Modules base path
- Contracts project path and name
- Deployment manifests location
- Project namespace
- Container registry (from existing manifests)
- NuGet feed URL (from existing Dockerfiles)
Returns JSON with detected configuration or empty fields if not found.
"""
import json
import os
import re
import sys
from pathlib import Path
from typing import Optional, Dict, Any
def find_files_recursive(root_path: Path, pattern: str) -> list[Path]:
"""Find files matching glob pattern recursively, excluding test directories."""
files = list(root_path.rglob(pattern))
# Always filter out test directories
files = [f for f in files if not any(part.lower().startswith('test') for part in f.parts)]
return files
def find_modules_base_path(root_path: Path) -> Optional[str]:
"""
Find modules base path by looking for existing modules.
Expected pattern: **/modules/*/Program.cs
"""
module_programs = find_files_recursive(root_path, "modules/*/Program.cs")
if module_programs:
# Extract the modules directory path
# E.g., /path/to/src/IoTEdgeModules/modules/foo/Program.cs -> src/IoTEdgeModules/modules
first_match = module_programs[0]
modules_dir = first_match.parent.parent # Go up from /foo/Program.cs to /modules
relative_path = modules_dir.relative_to(root_path)
return str(relative_path).replace('\\', '/')
return None
def find_contracts_project(root_path: Path) -> Optional[Dict[str, str]]:
"""
Find contracts project by looking for *Modules.Contracts*.csproj or *Contracts*.csproj
Returns dict with path and name, or None if not found.
"""
# Try specific pattern first
contracts_projects = find_files_recursive(root_path, "*Modules.Contracts*.csproj")
# Fallback to generic contracts pattern
if not contracts_projects:
contracts_projects = find_files_recursive(root_path, "*Contracts*.csproj")
if contracts_projects:
first_match = contracts_projects[0]
project_dir = first_match.parent
project_name = first_match.stem # Filename without .csproj
relative_path = project_dir.relative_to(root_path)
return {
"path": str(relative_path).replace('\\', '/'),
"name": project_name
}
return None
def find_deployment_manifests(root_path: Path) -> list[str]:
"""
Find all deployment manifest files (*.deployment.manifest.json).
Returns list of relative paths.
"""
manifests = find_files_recursive(root_path, "*.deployment.manifest.json")
return [str(m.relative_to(root_path)).replace('\\', '/') for m in manifests]
def extract_namespace_from_csharp(root_path: Path, contracts_path: Optional[str] = None) -> Optional[str]:
"""
Extract project namespace by:
1. Reading a C# file from contracts project (if available)
2. Finding pattern: namespace <Something>.Modules.Contracts.<ModuleName>
3. Extracting base: <Something>
"""
cs_files = []
# Try contracts project first
if contracts_path:
contracts_full_path = root_path / contracts_path
cs_files = list(contracts_full_path.rglob("*.cs"))
# Fallback: scan any .cs file in project
if not cs_files:
cs_files = list(root_path.rglob("modules/*/*.cs"))[:10] # Sample first 10
# Pattern to match: namespace <Something>.Modules.Contracts.<ModuleName> or similar
namespace_pattern = re.compile(r'namespace\s+([A-Za-z0-9.]+?)\.Modules(?:\.Contracts)?(?:\.[A-Za-z0-9]+)?;')
for cs_file in cs_files:
try:
content = cs_file.read_text(encoding='utf-8')
match = namespace_pattern.search(content)
if match:
# Extract the base namespace before .Modules
return match.group(1)
except Exception:
continue
return None
def extract_container_registry(root_path: Path, manifest_paths: list[str]) -> Optional[str]:
"""
Extract container registry URL from existing module.json or deployment manifests.
Looks for "repository": "registry.azurecr.io/modulename" pattern.
"""
registry_pattern = re.compile(r'"repository":\s*"([^/"]+)/[^"]+"')
# First try module.json files (more reliable)
module_jsons = find_files_recursive(root_path, "modules/*/module.json")
for module_json in module_jsons[:5]: # Check first 5
try:
content = module_json.read_text(encoding='utf-8')
match = registry_pattern.search(content)
if match:
return match.group(1)
except Exception:
continue
# Fallback to deployment manifests
for manifest_path in manifest_paths:
try:
full_path = root_path / manifest_path
content = full_path.read_text(encoding='utf-8')
match = registry_pattern.search(content)
if match:
return match.group(1)
except Exception:
continue
return None
def extract_nuget_feed_url(root_path: Path, modules_base_path: Optional[str]) -> Optional[str]:
"""
Extract NuGet feed URL from existing Dockerfiles.
Looks for VSS_NUGET_EXTERNAL_FEED_ENDPOINTS pattern.
"""
dockerfiles = find_files_recursive(root_path, "modules/*/Dockerfile*")
# Match both regular and escaped quotes: "endpoint":"url" or \"endpoint\":\"url\"
nuget_pattern = re.compile(r'endpoint\\?":\\s*\\?"(https://[^"\\]+/nuget/v3/index\.json)\\?"')
for dockerfile in dockerfiles[:10]: # Check first 10
try:
content = dockerfile.read_text(encoding='utf-8')
match = nuget_pattern.search(content)
if match:
return match.group(1)
except Exception:
continue
return None
def load_saved_config(root_path: Path) -> Optional[Dict[str, Any]]:
"""Load saved project configuration if it exists."""
config_path = root_path / ".claude" / ".iot-edge-module-config.json"
if config_path.exists():
try:
return json.loads(config_path.read_text(encoding='utf-8'))
except Exception:
return None
return None
def save_project_config(root_path: Path, config: Dict[str, Any]) -> bool:
"""Save project configuration for future runs."""
config_path = root_path / ".claude" / ".iot-edge-module-config.json"
try:
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(json.dumps(config, indent=2), encoding='utf-8')
return True
except Exception as e:
print(f"Warning: Could not save config: {e}", file=sys.stderr)
return False
def detect_project_structure(root_path: Path, force_detect: bool = False) -> Dict[str, Any]:
"""
Detect project structure by scanning for patterns.
Args:
root_path: Root directory of the project
force_detect: If True, ignore saved config and re-detect
Returns:
Dictionary with detected configuration
"""
# Try to load saved config first
if not force_detect:
saved_config = load_saved_config(root_path)
if saved_config:
saved_config["config_source"] = "saved"
return saved_config
# Perform detection
modules_base_path = find_modules_base_path(root_path)
contracts_project = find_contracts_project(root_path)
manifest_paths = find_deployment_manifests(root_path)
contracts_path = contracts_project["path"] if contracts_project else None
project_namespace = extract_namespace_from_csharp(root_path, contracts_path)
container_registry = extract_container_registry(root_path, manifest_paths)
nuget_feed_url = extract_nuget_feed_url(root_path, modules_base_path)
config = {
"config_source": "detected",
"modules_base_path": modules_base_path,
"contracts_project_path": contracts_path,
"contracts_project_name": contracts_project["name"] if contracts_project else None,
"manifests_found": manifest_paths,
"manifests_base_path": os.path.dirname(manifest_paths[0]) if manifest_paths else None,
"project_namespace": project_namespace,
"container_registry": container_registry,
"nuget_feed_url": nuget_feed_url,
"has_contracts_project": contracts_project is not None,
"has_nuget_feed": nuget_feed_url is not None
}
return config
def main():
"""Main entry point."""
import argparse
parser = argparse.ArgumentParser(
description="Detect IoT Edge project structure"
)
parser.add_argument(
"--root",
type=str,
default=".",
help="Root directory of the project (default: current directory)"
)
parser.add_argument(
"--force",
action="store_true",
help="Force re-detection, ignore saved config"
)
parser.add_argument(
"--save",
action="store_true",
help="Save detected configuration for future runs"
)
args = parser.parse_args()
root_path = Path(args.root).resolve()
if not root_path.exists():
print(json.dumps({"error": f"Path does not exist: {root_path}"}))
sys.exit(1)
# Detect structure
config = detect_project_structure(root_path, force_detect=args.force)
# Save if requested
if args.save and config["config_source"] == "detected":
if save_project_config(root_path, config):
config["config_saved"] = True
# Output as JSON
print(json.dumps(config, indent=2))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,286 @@
#!/usr/bin/env python3
"""
Solution File Manager for IoT Edge Module Scaffolding
Manages adding newly created modules to solution files.
Supports:
- .slnx (XML-based, modern format) - auto-add capability
- .sln (legacy format with GUIDs) - manual instructions only
"""
import argparse
import json
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
def find_solution_file(root_path: Path) -> dict:
"""
Find solution file in project root.
Returns:
dict with keys:
- type: "slnx", "sln", or "none"
- path: Path to solution file or None
- name: Solution file name or None
"""
# Always exclude test directories
def is_test_dir(path: Path) -> bool:
return any(part.lower().startswith('test') for part in path.parts)
# Look for .slnx first (modern format)
slnx_files = [f for f in root_path.rglob("*.slnx") if not is_test_dir(f)]
if slnx_files:
# Use the first one found, preferring root directory
slnx_files.sort(key=lambda p: (len(p.parts), p))
return {
"type": "slnx",
"path": slnx_files[0],
"name": slnx_files[0].name
}
# Fall back to .sln (legacy format)
sln_files = [f for f in root_path.rglob("*.sln") if not is_test_dir(f)]
if sln_files:
sln_files.sort(key=lambda p: (len(p.parts), p))
return {
"type": "sln",
"path": sln_files[0],
"name": sln_files[0].name
}
return {
"type": "none",
"path": None,
"name": None
}
def add_module_to_slnx(slnx_path: Path, module_csproj_path: str) -> dict:
"""
Add module to .slnx solution file.
Args:
slnx_path: Path to .slnx file
module_csproj_path: Relative path to module .csproj (e.g., "src/IoTEdgeModules/modules/mymodule/MyModule.csproj")
Returns:
dict with keys:
- success: bool
- message: str
- action: "added", "already_exists", or "error"
"""
try:
# Parse XML
tree = ET.parse(slnx_path)
root = tree.getroot()
# Find or create /modules/ folder
modules_folder = None
for folder in root.findall("Folder"):
if folder.get("Name") == "/modules/":
modules_folder = folder
break
if modules_folder is None:
# Create /modules/ folder if it doesn't exist
modules_folder = ET.SubElement(root, "Folder")
modules_folder.set("Name", "/modules/")
# Check if module already exists
for project in modules_folder.findall("Project"):
if project.get("Path") == module_csproj_path:
return {
"success": True,
"message": f"Module already exists in solution: {module_csproj_path}",
"action": "already_exists"
}
# Get all existing project paths for alphabetical insertion
existing_projects = [(p.get("Path"), p) for p in modules_folder.findall("Project")]
existing_paths = [path for path, _ in existing_projects]
# Find insertion point (alphabetical order)
insertion_index = 0
for i, existing_path in enumerate(existing_paths):
if module_csproj_path.lower() < existing_path.lower():
insertion_index = i
break
insertion_index = i + 1
# Create new project element
new_project = ET.Element("Project")
new_project.set("Path", module_csproj_path)
# Insert at the calculated position
modules_folder.insert(insertion_index, new_project)
# Write back to file with proper formatting
# Add indentation
indent_xml(root)
tree.write(slnx_path, encoding="utf-8", xml_declaration=False)
return {
"success": True,
"message": f"Added module to solution at position {insertion_index + 1} of {len(existing_paths) + 1}",
"action": "added",
"insertion_index": insertion_index,
"total_modules": len(existing_paths) + 1
}
except ET.ParseError as e:
return {
"success": False,
"message": f"Failed to parse .slnx file: {e}",
"action": "error"
}
except Exception as e:
return {
"success": False,
"message": f"Error adding module to .slnx: {e}",
"action": "error"
}
def indent_xml(elem, level=0):
"""
Recursively add indentation to an XML element and its children for pretty printing.
Args:
elem: xml.etree.ElementTree.Element
The XML element to indent.
level: int, optional
The current indentation level (default is 0).
Returns:
None. The function modifies the XML element in place.
"""
indent = "\n" + " " * level
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = indent + " "
if not elem.tail or not elem.tail.strip():
elem.tail = indent
for child in elem:
indent_xml(child, level + 1)
if not child.tail or not child.tail.strip():
child.tail = indent
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = indent
def get_sln_manual_instructions(module_csproj_path: str, module_name: str) -> str:
"""
Generate manual instructions for adding module to .sln file.
.sln files require GUIDs which are complex to generate correctly,
so we provide manual instructions instead.
"""
instructions = f"""
Manual instructions for adding module to .sln file:
.sln files require project GUIDs which must be generated. The easiest way is to use Visual Studio or the dotnet CLI:
Option 1: Using dotnet CLI (recommended):
dotnet sln add "{module_csproj_path}"
Option 2: Using Visual Studio:
1. Open the solution in Visual Studio
2. Right-click on the solution in Solution Explorer
3. Select "Add""Existing Project"
4. Navigate to and select: {module_csproj_path}
Option 3: Manual editing (advanced):
1. Open the .sln file in a text editor
2. Generate a new GUID (use online generator or PowerShell: [guid]::NewGuid())
3. Add project entry:
Project("{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}") = "{module_name}", "{module_csproj_path}", "{{YOUR-NEW-GUID}}"
EndProject
4. Add to solution configuration platforms section (match existing patterns)
Note: The dotnet CLI approach is recommended as it handles all GUID generation automatically.
"""
return instructions
def main():
parser = argparse.ArgumentParser(
description="Manage solution file updates for IoT Edge modules"
)
parser.add_argument(
"--root",
type=str,
default=".",
help="Root directory to search for solution file (default: current directory)"
)
parser.add_argument(
"--detect",
action="store_true",
help="Detect solution file type and location"
)
parser.add_argument(
"--add-module",
type=str,
help="Relative path to module .csproj to add to solution"
)
parser.add_argument(
"--module-name",
type=str,
help="Module name (for manual instructions)"
)
args = parser.parse_args()
root_path = Path(args.root).resolve()
# Detect solution file
solution_info = find_solution_file(root_path)
if args.detect:
# Just output detection results
print(json.dumps(solution_info, indent=2, default=str))
return 0
if args.add_module:
if solution_info["type"] == "none":
result = {
"success": False,
"message": "No solution file found",
"action": "error"
}
print(json.dumps(result, indent=2))
return 1
if solution_info["type"] == "slnx":
# Auto-add to .slnx
result = add_module_to_slnx(
solution_info["path"],
args.add_module
)
print(json.dumps(result, indent=2))
return 0 if result["success"] else 1
elif solution_info["type"] == "sln":
# Provide manual instructions for .sln
module_name = args.module_name or Path(args.add_module).stem
instructions = get_sln_manual_instructions(args.add_module, module_name)
result = {
"success": True,
"message": "Manual instructions generated for .sln file",
"action": "manual_instructions",
"instructions": instructions,
"solution_path": str(solution_info["path"])
}
print(json.dumps(result, indent=2))
return 0
# No action specified
parser.print_help()
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""
Scan for IoT Edge deployment manifest files.
Finds all *.deployment.manifest.json files in the project and returns
metadata about each manifest.
"""
import json
import sys
from pathlib import Path
from typing import Dict, Any, List
def extract_manifest_metadata(manifest_path: Path) -> Dict[str, Any]:
"""
Extract metadata from a deployment manifest file.
Args:
manifest_path: Path to the manifest file
Returns:
Dictionary with manifest metadata
"""
try:
content = manifest_path.read_text(encoding='utf-8')
manifest_data = json.loads(content)
# Extract module count from $edgeAgent
modules_count = 0
module_names = []
edge_agent = manifest_data.get("modulesContent", {}).get("$edgeAgent", {})
# Navigate the nested JSON structure correctly
for key in edge_agent.keys():
if key.startswith("properties.desired.modules."):
modules_count += 1
# Extract module name from key like "properties.desired.modules.modulename"
module_name = key.replace("properties.desired.modules.", "")
module_names.append(module_name)
# Extract route count from $edgeHub
routes_count = 0
edge_hub = manifest_data.get("modulesContent", {}).get("$edgeHub", {})
for key in edge_hub.keys():
if key.startswith("properties.desired.routes."):
routes_count += 1
return {
"valid": True,
"modules_count": modules_count,
"module_names": module_names,
"routes_count": routes_count
}
except Exception as e:
return {
"valid": False,
"error": str(e)
}
def scan_manifests(root_path: Path) -> List[Dict[str, Any]]:
"""
Scan for all deployment manifest files, excluding test directories.
Args:
root_path: Root directory of the project
Returns:
List of dictionaries with manifest information
"""
manifests = list(root_path.rglob("*.deployment.manifest.json"))
# Always exclude test directories
manifests = [m for m in manifests if not any(part.lower().startswith('test') for part in m.parts)]
results = []
for manifest_path in manifests:
relative_path = manifest_path.relative_to(root_path)
metadata = extract_manifest_metadata(manifest_path)
result = {
"path": str(relative_path).replace('\\', '/'),
"name": manifest_path.name,
"basename": manifest_path.stem.replace('.deployment.manifest', ''),
"absolute_path": str(manifest_path),
**metadata
}
results.append(result)
# Sort by path for consistent ordering
results.sort(key=lambda x: x["path"])
return results
def main():
"""Main entry point."""
import argparse
parser = argparse.ArgumentParser(
description="Scan for IoT Edge deployment manifest files"
)
parser.add_argument(
"--root",
type=str,
default=".",
help="Root directory of the project (default: current directory)"
)
parser.add_argument(
"--verbose",
action="store_true",
help="Include detailed metadata for each manifest"
)
args = parser.parse_args()
root_path = Path(args.root).resolve()
if not root_path.exists():
print(json.dumps({"error": f"Path does not exist: {root_path}"}))
sys.exit(1)
# Scan for manifests
manifests = scan_manifests(root_path)
# Output as JSON
output = {
"manifests_found": len(manifests),
"manifests": manifests
}
print(json.dumps(output, indent=2))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,303 @@
#!/usr/bin/env python3
"""
Update IoT Edge deployment manifest with a new module.
This script:
1. Parses the deployment manifest JSON
2. Finds the highest startupOrder in $edgeAgent.modules
3. Inserts a new module definition with calculated startupOrder
4. Adds a default route to $edgeHub
5. Validates and saves the updated JSON
"""
import json
import sys
from pathlib import Path
from typing import Dict, Any, Optional
def find_highest_startup_order(manifest_data: Dict[str, Any]) -> int:
"""
Find the highest startupOrder value in existing modules.
Args:
manifest_data: Parsed manifest JSON
Returns:
Highest startupOrder value, or 0 if no modules found
"""
edge_agent = manifest_data.get("modulesContent", {}).get("$edgeAgent", {})
# Find all keys that start with "properties.desired.modules."
modules = {}
for key, value in edge_agent.items():
if key.startswith("properties.desired.modules."):
modules[key] = value
highest_order = 0
for module_config in modules.values():
startup_order = module_config.get("startupOrder", 0)
highest_order = max(highest_order, startup_order)
return highest_order
def create_module_definition(
module_name: str,
container_registry: str,
with_volume: bool = True
) -> Dict[str, Any]:
"""
Create a standard module definition for $edgeAgent.
Args:
module_name: Lowercase module name
container_registry: Container registry URL
with_volume: Whether to include volume mount
Returns:
Module definition dictionary
"""
create_options = {
"HostConfig": {
"LogConfig": {
"Type": "json-file",
"Config": {
"max-size": "10m",
"max-file": "10"
}
}
}
}
# Add volume mount if requested
if with_volume:
create_options["HostConfig"]["Mounts"] = [
{
"Type": "volume",
"Target": "/app/data/",
"Source": module_name
}
]
module_def = {
"version": "1.0",
"type": "docker",
"status": "running",
"restartPolicy": "always",
"startupOrder": 1,
"settings": {
"image": f"${{MODULES.{module_name}}}",
"createOptions": create_options
}
}
return module_def
def create_default_route(module_name: str) -> Dict[str, Any]:
"""
Create a default route for the module to IoT Hub.
Args:
module_name: Lowercase module name
Returns:
Route definition dictionary
"""
return {
"route": f"FROM /messages/modules/{module_name}/outputs/* INTO $upstream",
"priority": 0,
"timeToLiveSecs": 86400
}
def module_exists(manifest_data: Dict[str, Any], module_name: str) -> bool:
"""
Check if a module already exists in the manifest.
Args:
manifest_data: Parsed manifest JSON
module_name: Module name to check
Returns:
True if module exists, False otherwise
"""
edge_agent = manifest_data.get("modulesContent", {}).get("$edgeAgent", {})
module_key = f"properties.desired.modules.{module_name}"
return module_key in edge_agent
def add_module_to_manifest(
manifest_data: Dict[str, Any],
module_name: str,
container_registry: str,
with_volume: bool = True
) -> Dict[str, Any]:
"""
Add a new module to the deployment manifest.
Args:
manifest_data: Parsed manifest JSON
module_name: Lowercase module name
container_registry: Container registry URL
with_volume: Whether to include volume mount
Returns:
Updated manifest data with module added
Raises:
ValueError: If module already exists
"""
# Check if module already exists
if module_exists(manifest_data, module_name):
raise ValueError(f"Module '{module_name}' already exists in manifest")
# Create module definition with startupOrder = 1
module_def = create_module_definition(
module_name,
container_registry,
with_volume
)
# Add to $edgeAgent using dotted key (Azure IoT Edge format)
if "modulesContent" not in manifest_data:
manifest_data["modulesContent"] = {}
if "$edgeAgent" not in manifest_data["modulesContent"]:
manifest_data["modulesContent"]["$edgeAgent"] = {}
module_key = f"properties.desired.modules.{module_name}"
manifest_data["modulesContent"]["$edgeAgent"][module_key] = module_def
# Create and add route to $edgeHub using dotted key (Azure IoT Edge format)
route_name = f"{module_name}ToIoTHub"
route_def = create_default_route(module_name)
if "$edgeHub" not in manifest_data["modulesContent"]:
manifest_data["modulesContent"]["$edgeHub"] = {}
route_key = f"properties.desired.routes.{route_name}"
manifest_data["modulesContent"]["$edgeHub"][route_key] = route_def
return manifest_data
def update_manifest_file(
manifest_path: Path,
module_name: str,
container_registry: str,
with_volume: bool = True
) -> Dict[str, Any]:
"""
Update a deployment manifest file with a new module.
Args:
manifest_path: Path to manifest file
module_name: Lowercase module name
container_registry: Container registry URL
with_volume: Whether to include volume mount
Returns:
Dictionary with operation result
Raises:
FileNotFoundError: If manifest file doesn't exist
json.JSONDecodeError: If manifest is invalid JSON
ValueError: If module already exists
"""
if not manifest_path.exists():
raise FileNotFoundError(f"Manifest file not found: {manifest_path}")
# Read and parse manifest
content = manifest_path.read_text(encoding='utf-8')
manifest_data = json.loads(content)
# Store original for comparison
edge_agent = manifest_data.get("modulesContent", {}).get("$edgeAgent", {})
original_module_count = len([k for k in edge_agent.keys() if k.startswith("properties.desired.modules.")])
# Add module
updated_manifest = add_module_to_manifest(
manifest_data,
module_name,
container_registry,
with_volume
)
# Write back to file
updated_content = json.dumps(updated_manifest, indent=2)
manifest_path.write_text(updated_content, encoding='utf-8')
edge_agent_updated = updated_manifest.get("modulesContent", {}).get("$edgeAgent", {})
new_module_count = len([k for k in edge_agent_updated.keys() if k.startswith("properties.desired.modules.")])
return {
"success": True,
"manifest_path": str(manifest_path),
"module_name": module_name,
"startup_order": 1,
"modules_before": original_module_count,
"modules_after": new_module_count,
"route_added": f"{module_name}ToIoTHub"
}
def main():
"""Main entry point."""
import argparse
parser = argparse.ArgumentParser(
description="Update IoT Edge deployment manifest with a new module"
)
parser.add_argument(
"manifest_path",
type=str,
help="Path to deployment manifest file"
)
parser.add_argument(
"module_name",
type=str,
help="Lowercase module name"
)
parser.add_argument(
"--registry",
type=str,
required=True,
help="Container registry URL (e.g., myregistry.azurecr.io)"
)
parser.add_argument(
"--no-volume",
action="store_true",
help="Don't add volume mount to module"
)
args = parser.parse_args()
manifest_path = Path(args.manifest_path)
try:
result = update_manifest_file(
manifest_path,
args.module_name,
args.registry,
with_volume=not args.no_volume
)
print(json.dumps(result, indent=2))
except FileNotFoundError as e:
print(json.dumps({"success": False, "error": str(e)}), file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(json.dumps({"success": False, "error": f"Invalid JSON: {e}"}), file=sys.stderr)
sys.exit(1)
except ValueError as e:
print(json.dumps({"success": False, "error": str(e)}), file=sys.stderr)
sys.exit(1)
except Exception as e:
print(json.dumps({"success": False, "error": f"Unexpected error: {e}"}), file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()