1212import configparser
1313import xml .etree .ElementTree as ET
1414
15+ from io import StringIO
16+
1517
1618SPLUNK_APPINSPECT_BASE_URL = "https://appinspect.splunk.com/v1"
1719SPLUNKBASE_BASE_URL = "https://splunkbase.splunk.com/api/account:login"
@@ -77,6 +79,35 @@ def download_file_from_s3(bucket_name: str, object_name: str, file_name: str) ->
7779 except Exception as e :
7880 print (f"Error downloading { object_name } from { bucket_name } : { e } " )
7981
82+ def preprocess_empty_headers (file_path : str ) -> list :
83+ """
84+ Preprocess the file to handle empty section headers by replacing `[]` with a valid section name.
85+ """
86+ valid_lines = []
87+ with open (file_path , 'r' ) as file :
88+ for line in file :
89+ # Replace empty section headers with a placeholder
90+ if line .strip () == "[]" :
91+ valid_lines .append ("[DEFAULT]\n " ) # Or any placeholder section name
92+ else :
93+ valid_lines .append (line )
94+ return valid_lines
95+
96+ def replace_default_with_empty_header (file_path : str ) -> None :
97+ """
98+ Replace '[DEFAULT]' header with '[]' in the specified file.
99+ """
100+ with open (file_path , 'r' ) as file :
101+ lines = file .readlines ()
102+
103+ with open (file_path , 'w' ) as file :
104+ for line in lines :
105+ # Replace '[DEFAULT]' with '[]'
106+ if line .strip () == "[DEFAULT]" :
107+ file .write ("[]\n " )
108+ else :
109+ file .write (line )
110+
80111def merge_or_copy_conf (source_path : str , dest_path : str ) -> None :
81112 # Get the filename from the source path
82113 filename = os .path .basename (source_path )
@@ -111,7 +142,61 @@ def merge_or_copy_conf(source_path: str, dest_path: str) -> None:
111142 dest_config .write (file )
112143 print (f"Merged configuration saved to { dest_file } " )
113144
114- def unpack_merge_conf_and_repack (app : str , path : str ) -> None :
145+ def merge_or_copy_meta (local_meta_file : str , default_dir : str ) -> None :
146+ """Merge local.meta with default.meta"""
147+ filename = os .path .basename (local_meta_file )
148+ dest_file = os .path .join (default_dir , "default.meta" )
149+
150+ # Check if the file exists in the destination directory
151+ if not os .path .exists (dest_file ):
152+ # If the file doesn't exist, copy it
153+ shutil .copy (local_meta_file , dest_file )
154+ print (f"Copied { filename } to { dest_file } " )
155+ else :
156+ # If the file exists, merge the configurations
157+ print (f"Merging { filename } with existing file in { dest_file } " )
158+
159+ # Preprocess the default file
160+ default_preprocessed_lines = preprocess_empty_headers (dest_file )
161+ default_preprocessed_content = StringIO ('' .join (default_preprocessed_lines ))
162+
163+ # Read the default.meta file
164+ default_meta = configparser .ConfigParser ()
165+ default_meta .read_file (default_preprocessed_content )
166+
167+ # Preprocess the local file
168+ local_preprocessed_lines = preprocess_empty_headers (local_meta_file )
169+ local_preprocessed_content = StringIO ('' .join (local_preprocessed_lines ))
170+
171+ # Read the local.meta file
172+ local_meta = configparser .ConfigParser ()
173+ local_meta .read_file (local_preprocessed_content )
174+
175+ # Merge local.meta into default.meta
176+ for section in local_meta .sections ():
177+ if not default_meta .has_section (section ):
178+ default_meta .add_section (section )
179+ for option , value in local_meta .items (section ):
180+ if default_meta .has_option (section , option ):
181+ # Merge logic: Option exists in both, decide whether to overwrite
182+ default_value = default_meta .get (section , option )
183+ if value != default_value :
184+ print (f"Conflict detected: { section } { option } - { default_value } -> { value } " )
185+ # Overwrite the option in default.meta
186+ default_meta .set (section , option , value )
187+ default_meta .set (section , option , value )
188+
189+ # Write the merged configuration back to the output file
190+ with open (dest_file , 'w' ) as file :
191+ default_meta .write (file )
192+
193+ # Replace '[DEFAULT]' with '[]' in the output file
194+ replace_default_with_empty_header (dest_file )
195+
196+ print (f"Merged metadata saved to { dest_file } " )
197+
198+
199+ def unpack_merge_conf_and_meta_repack (app : str , path : str ) -> None :
115200 """Unpack the app, load environment configuration files and repack the app."""
116201 temp_dir = "temp_unpack"
117202 os .makedirs (temp_dir , exist_ok = True )
@@ -120,15 +205,23 @@ def unpack_merge_conf_and_repack(app: str, path: str) -> None:
120205 with tarfile .open (f"{ app } .tgz" , "r:gz" ) as tar :
121206 tar .extractall (path = temp_dir )
122207 # Create default directory for unpacked app
123- default_dir = f"{ temp_dir } /{ app } /default"
124- os .makedirs (default_dir , exist_ok = True )
208+ base_default_dir = f"{ temp_dir } /{ app } "
125209 # Load the environment configuration files
126210 app_dir = path
127211 # Copy all .conf files in app_dir to temp_dir of unpacked app
128212 for file in os .listdir (app_dir ):
129213 if file .endswith (".conf" ):
214+ default_dir = base_default_dir + "/default"
215+ os .makedirs (default_dir , exist_ok = True )
130216 source_path = os .path .join (app_dir , file )
131217 merge_or_copy_conf (source_path , default_dir )
218+ # Copy all metadata files in app_dir to temp_dir of unpacked app
219+ for file in os .listdir (app_dir ):
220+ if file .endswith (".meta" ):
221+ default_dir = base_default_dir + "/metadata"
222+ os .makedirs (default_dir , exist_ok = True )
223+ source_path = os .path .join (app_dir , file )
224+ merge_or_copy_meta (source_path , default_dir )
132225 # Repack the app and place it in the root directory
133226 with tarfile .open (f"{ app } .tgz" , "w:gz" ) as tar :
134227 for root , _ , files in os .walk (f"{ temp_dir } /{ app } " ):
@@ -137,7 +230,6 @@ def unpack_merge_conf_and_repack(app: str, path: str) -> None:
137230 arcname = os .path .relpath (full_path , temp_dir )
138231 tar .add (full_path , arcname = arcname )
139232
140-
141233def get_appinspect_token () -> str :
142234 """
143235 Authenticate to the Splunk Cloud.
0 commit comments