33This module handles copying configuration files to the Helm staging area
44and synchronizing settings between the application config and Helm values.
55
6- Note on YAML editing:
7- This module uses regex-based line editing to preserve comments and structure
8- in values.yaml. If more complex YAML manipulation is needed in the future,
9- consider using `ruamel.yaml` instead of `pyyaml`. ruamel.yaml preserves
10- comments, ordering, and formatting when round-tripping YAML files:
11-
12- from ruamel.yaml import YAML
13- yaml = YAML()
14- yaml.preserve_quotes = True
15- with open(path) as f:
16- data = yaml.load(f)
17- # modify data...
18- with open(path, 'w') as f:
19- yaml.dump(data, f)
6+ Uses ruamel.yaml for YAML editing to preserve comments, ordering, and formatting
7+ when round-tripping YAML files.
208"""
219
2210from __future__ import annotations
2311
24- import re
2512import shutil
2613from pathlib import Path
2714from typing import TYPE_CHECKING
2815
29- import yaml # type: ignore[ import-untyped]
16+ from ruamel . yaml import YAML
3017
3118from src .app .runtime .config .config_loader import load_config
3219
@@ -100,7 +87,7 @@ def _compute_config_changes(
10087 ) -> list [str ]:
10188 """Compare and update values.yaml with config.yaml settings.
10289
103- Uses line-based editing to preserve comments and formatting in values.yaml.
90+ Uses ruamel.yaml to preserve comments and formatting in values.yaml.
10491
10592 Args:
10693 config_path: Path to config.yaml
@@ -112,78 +99,44 @@ def _compute_config_changes(
11299 config_raw = load_config (config_path , processed = False )
113100 config_data = config_raw if isinstance (config_raw , dict ) else {}
114101
102+ # Set up ruamel.yaml for round-trip (comment-preserving) mode
103+ yaml = YAML ()
104+ yaml .preserve_quotes = True
105+
115106 # Read values.yaml for comparison (to detect what needs changing)
116107 with open (values_path ) as f :
117- values_data = yaml .safe_load (f )
108+ values_data = yaml .load (f )
118109
119110 changes = []
120- updates : dict [ str , tuple [ bool , bool ]] = {} # key -> (old_val, new_val)
111+ modified = False
121112
122113 # Check redis.enabled
123114 if redis_config := config_data .get ("config" , {}).get ("redis" ):
124115 if "redis" in values_data :
125116 old_val = values_data ["redis" ].get ("enabled" , True )
126117 new_val = redis_config .get ("enabled" , True )
127118 if old_val != new_val :
128- updates ["redis" ] = ( old_val , new_val )
119+ values_data ["redis" ][ "enabled" ] = new_val
129120 changes .append (f"redis.enabled: { old_val } → { new_val } " )
121+ modified = True
130122
131123 # Check temporal.enabled
132124 if temporal_config := config_data .get ("config" , {}).get ("temporal" ):
133125 if "temporal" in values_data :
134126 old_val = values_data ["temporal" ].get ("enabled" , True )
135127 new_val = temporal_config .get ("enabled" , True )
136128 if old_val != new_val :
137- updates ["temporal" ] = ( old_val , new_val )
129+ values_data ["temporal" ][ "enabled" ] = new_val
138130 changes .append (f"temporal.enabled: { old_val } → { new_val } " )
131+ modified = True
139132
140- if updates :
141- self ._update_values_preserving_format (values_path , updates )
133+ # Write back with preserved formatting if any changes were made
134+ if modified :
135+ with open (values_path , "w" ) as f :
136+ yaml .dump (values_data , f )
142137
143138 return changes
144139
145- def _update_values_preserving_format (
146- self ,
147- values_path : Path ,
148- updates : dict [str , tuple [bool , bool ]],
149- ) -> None :
150- """Update values.yaml while preserving comments and formatting.
151-
152- Uses line-based editing to find and update specific values without
153- rewriting the entire file.
154-
155- Args:
156- values_path: Path to values.yaml
157- updates: Dict of section_name -> (old_value, new_value) to update
158- """
159- with open (values_path ) as f :
160- lines = f .readlines ()
161-
162- current_section : str | None = None
163- modified_lines = []
164-
165- for line in lines :
166- # Detect top-level section (no leading whitespace, ends with :)
167- stripped = line .rstrip ()
168- if stripped and not line [0 ].isspace () and stripped .endswith (":" ):
169- # Extract section name (e.g., "redis:" -> "redis")
170- current_section = stripped [:- 1 ].split ("#" )[0 ].strip ()
171-
172- # Check if this line is an 'enabled:' setting in a section we want to update
173- if current_section in updates :
174- # Match lines like " enabled: true" or " enabled: false"
175- match = re .match (r"^(\s+enabled:\s*)(true|false)(.*)$" , line )
176- if match :
177- indent , old_bool , rest = match .groups ()
178- _ , new_val = updates [current_section ]
179- new_bool = "true" if new_val else "false"
180- line = f"{ indent } { new_bool } { rest } \n "
181-
182- modified_lines .append (line )
183-
184- with open (values_path , "w" ) as f :
185- f .writelines (modified_lines )
186-
187140 def copy_config_files (self , progress_factory : type [Progress ]) -> None :
188141 """Copy configuration files to Helm staging area.
189142
0 commit comments