Skip to content

Commit d927796

Browse files
authored
Merge pull request #350 from yma96/1.3.x
Feat: Accept multiple maven zips with non-RADAS signing way
2 parents cd42d1f + 56c5a33 commit d927796

18 files changed

+412
-67
lines changed

charon/cmd/cmd_upload.py

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
from typing import List
1717

1818
from charon.config import get_config
19-
from charon.utils.archive import detect_npm_archive, NpmArchiveType
19+
from charon.utils.archive import detect_npm_archives, NpmArchiveType
2020
from charon.pkgs.maven import handle_maven_uploading
2121
from charon.pkgs.npm import handle_npm_uploading
2222
from charon.cmd.internal import (
2323
_decide_mode, _validate_prod_key,
24-
_get_local_repo, _get_targets,
24+
_get_local_repos, _get_targets,
2525
_get_ignore_patterns, _safe_delete
2626
)
2727
from click import command, option, argument
@@ -35,8 +35,9 @@
3535

3636

3737
@argument(
38-
"repo",
38+
"repos",
3939
type=str,
40+
nargs=-1 # This allows multiple arguments for zip urls
4041
)
4142
@option(
4243
"--product",
@@ -138,7 +139,7 @@
138139
@option("--dryrun", "-n", is_flag=True, default=False)
139140
@command()
140141
def upload(
141-
repo: str,
142+
repos: List[str],
142143
product: str,
143144
version: str,
144145
targets: List[str],
@@ -152,9 +153,10 @@ def upload(
152153
quiet=False,
153154
dryrun=False
154155
):
155-
"""Upload all files from a released product REPO to Ronda
156-
Service. The REPO points to a product released tarball which
157-
is hosted in a remote url or a local path.
156+
"""Upload all files from released product REPOs to Ronda
157+
Service. The REPOs point to a product released tarballs which
158+
are hosted in remote urls or local paths.
159+
Notes: It does not support multiple repos for NPM archives
158160
"""
159161
tmp_dir = work_dir
160162
try:
@@ -173,8 +175,8 @@ def upload(
173175
logger.error("No AWS profile specified!")
174176
sys.exit(1)
175177

176-
archive_path = _get_local_repo(repo)
177-
npm_archive_type = detect_npm_archive(archive_path)
178+
archive_paths = _get_local_repos(repos)
179+
archive_types = detect_npm_archives(archive_paths)
178180
product_key = f"{product}-{version}"
179181
manifest_bucket_name = conf.get_manifest_bucket()
180182
targets_ = _get_targets(targets, conf)
@@ -185,31 +187,18 @@ def upload(
185187
" are set correctly.", targets_
186188
)
187189
sys.exit(1)
188-
if npm_archive_type != NpmArchiveType.NOT_NPM:
189-
logger.info("This is a npm archive")
190-
tmp_dir, succeeded = handle_npm_uploading(
191-
archive_path,
192-
product_key,
193-
targets=targets_,
194-
aws_profile=aws_profile,
195-
dir_=work_dir,
196-
gen_sign=contain_signature,
197-
cf_enable=conf.is_aws_cf_enable(),
198-
key=sign_key,
199-
dry_run=dryrun,
200-
manifest_bucket_name=manifest_bucket_name
201-
)
202-
if not succeeded:
203-
sys.exit(1)
204-
else:
190+
191+
maven_count = archive_types.count(NpmArchiveType.NOT_NPM)
192+
npm_count = len(archive_types) - maven_count
193+
if maven_count == len(archive_types):
205194
ignore_patterns_list = None
206195
if ignore_patterns:
207196
ignore_patterns_list = ignore_patterns
208197
else:
209198
ignore_patterns_list = _get_ignore_patterns(conf)
210199
logger.info("This is a maven archive")
211200
tmp_dir, succeeded = handle_maven_uploading(
212-
archive_path,
201+
archive_paths,
213202
product_key,
214203
ignore_patterns_list,
215204
root=root_path,
@@ -225,6 +214,28 @@ def upload(
225214
)
226215
if not succeeded:
227216
sys.exit(1)
217+
elif npm_count == len(archive_types) and len(archive_types) == 1:
218+
logger.info("This is a npm archive")
219+
tmp_dir, succeeded = handle_npm_uploading(
220+
archive_paths[0],
221+
product_key,
222+
targets=targets_,
223+
aws_profile=aws_profile,
224+
dir_=work_dir,
225+
gen_sign=contain_signature,
226+
cf_enable=conf.is_aws_cf_enable(),
227+
key=sign_key,
228+
dry_run=dryrun,
229+
manifest_bucket_name=manifest_bucket_name
230+
)
231+
if not succeeded:
232+
sys.exit(1)
233+
elif npm_count == len(archive_types) and len(archive_types) > 1:
234+
logger.error("Doesn't support multiple upload for npm")
235+
sys.exit(1)
236+
else:
237+
logger.error("Upload types are not consistent")
238+
sys.exit(1)
228239
except Exception:
229240
print(traceback.format_exc())
230241
sys.exit(2) # distinguish between exception and bad config or bad state

charon/cmd/internal.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ def _get_local_repo(url: str) -> str:
7575
return archive_path
7676

7777

78+
def _get_local_repos(urls: list) -> list:
79+
archive_paths = []
80+
for url in urls:
81+
archive_path = _get_local_repo(url)
82+
archive_paths.append(archive_path)
83+
return archive_paths
84+
85+
7886
def _validate_prod_key(product: str, version: str) -> bool:
7987
if not product or product.strip() == "":
8088
logger.error("Error: product can not be empty!")

charon/pkgs/maven.py

Lines changed: 194 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from datetime import datetime
3838
from zipfile import ZipFile, BadZipFile
3939
from tempfile import mkdtemp
40+
from shutil import rmtree, copy2
4041
from defusedxml import ElementTree
4142

4243
import os
@@ -261,7 +262,7 @@ def __gen_digest_file(hash_file_path, meta_file_path: str, hashtype: HashType) -
261262

262263

263264
def handle_maven_uploading(
264-
repo: str,
265+
repos: List[str],
265266
prod_key: str,
266267
ignore_patterns=None,
267268
root="maven-repository",
@@ -294,8 +295,9 @@ def handle_maven_uploading(
294295
"""
295296
if targets is None:
296297
targets = []
297-
# 1. extract tarball
298-
tmp_root = _extract_tarball(repo, prod_key, dir__=dir_)
298+
299+
# 1. extract tarballs
300+
tmp_root = _extract_tarballs(repos, root, prod_key, dir__=dir_)
299301

300302
# 2. scan for paths and filter out the ignored paths,
301303
# and also collect poms for later metadata generation
@@ -673,6 +675,195 @@ def _extract_tarball(repo: str, prefix="", dir__=None) -> str:
673675
sys.exit(1)
674676

675677

678+
def _extract_tarballs(repos: List[str], root: str, prefix="", dir__=None) -> str:
679+
""" Extract multiple zip archives to a temporary directory.
680+
* repos are the list of repo paths to extract
681+
* root is a prefix in the tarball to identify which path is
682+
the beginning of the maven GAV path
683+
* prefix is the prefix for temporary directory name
684+
* dir__ is the directory where temporary directories will be created.
685+
686+
Returns the path to the merged temporary directory containing all extracted files
687+
"""
688+
# Create final merge directory
689+
final_tmp_root = mkdtemp(prefix=f"charon-{prefix}-final-", dir=dir__)
690+
691+
total_copied = 0
692+
total_duplicated = 0
693+
total_merged = 0
694+
total_processed = 0
695+
696+
# Collect all extracted directories first
697+
extracted_dirs = []
698+
699+
for repo in repos:
700+
if os.path.exists(repo):
701+
try:
702+
logger.info("Extracting tarball %s", repo)
703+
repo_zip = ZipFile(repo)
704+
tmp_root = mkdtemp(prefix=f"charon-{prefix}-", dir=dir__)
705+
extract_zip_all(repo_zip, tmp_root)
706+
extracted_dirs.append(tmp_root)
707+
708+
except BadZipFile as e:
709+
logger.error("Tarball extraction error for repo %s: %s", repo, e)
710+
sys.exit(1)
711+
else:
712+
logger.error("Error: archive %s does not exist", repo)
713+
sys.exit(1)
714+
715+
# Merge all extracted directories
716+
if extracted_dirs:
717+
# Create merged directory name
718+
merged_dir_name = "merged_repositories"
719+
merged_dest_dir = os.path.join(final_tmp_root, merged_dir_name)
720+
721+
# Merge content from all extracted directories
722+
for extracted_dir in extracted_dirs:
723+
copied, duplicated, merged, processed = _merge_directories_with_rename(
724+
extracted_dir, merged_dest_dir, root
725+
)
726+
total_copied += copied
727+
total_duplicated += duplicated
728+
total_merged += merged
729+
total_processed += processed
730+
731+
# Clean up temporary extraction directory
732+
rmtree(extracted_dir)
733+
734+
logger.info(
735+
"All zips merged! Total copied: %s, Total duplicated: %s, "
736+
"Total merged: %s, Total processed: %s",
737+
total_copied,
738+
total_duplicated,
739+
total_merged,
740+
total_processed,
741+
)
742+
return final_tmp_root
743+
744+
745+
def _merge_directories_with_rename(src_dir: str, dest_dir: str, root: str):
746+
""" Recursively copy files from src_dir to dest_dir, overwriting existing files.
747+
* src_dir is the source directory to copy from
748+
* dest_dir is the destination directory to copy to.
749+
750+
Returns Tuple of (copied_count, duplicated_count, merged_count, processed_count)
751+
"""
752+
copied_count = 0
753+
duplicated_count = 0
754+
merged_count = 0
755+
processed_count = 0
756+
757+
# Find the actual content directory
758+
content_root = src_dir
759+
for item in os.listdir(src_dir):
760+
item_path = os.path.join(src_dir, item)
761+
# Check the root maven-repository subdirectory existence
762+
maven_repo_path = os.path.join(item_path, root)
763+
if os.path.isdir(item_path) and os.path.exists(maven_repo_path):
764+
content_root = item_path
765+
break
766+
767+
# pylint: disable=unused-variable
768+
for root_dir, dirs, files in os.walk(content_root):
769+
# Calculate relative path from content root
770+
rel_path = os.path.relpath(root_dir, content_root)
771+
dest_root = os.path.join(dest_dir, rel_path) if rel_path != '.' else dest_dir
772+
773+
# Create destination directory if it doesn't exist
774+
os.makedirs(dest_root, exist_ok=True)
775+
776+
# Copy all files, skip existing ones
777+
for file in files:
778+
src_file = os.path.join(root_dir, file)
779+
dest_file = os.path.join(dest_root, file)
780+
781+
if file == ARCHETYPE_CATALOG_FILENAME:
782+
_handle_archetype_catalog_merge(src_file, dest_file)
783+
merged_count += 1
784+
logger.debug("Merged archetype catalog: %s -> %s", src_file, dest_file)
785+
if os.path.exists(dest_file):
786+
duplicated_count += 1
787+
logger.debug("Duplicated: %s, skipped", dest_file)
788+
else:
789+
copy2(src_file, dest_file)
790+
copied_count += 1
791+
logger.debug("Copied: %s -> %s", src_file, dest_file)
792+
793+
processed_count += 1
794+
795+
logger.info(
796+
"One zip merged! Files copied: %s, Files duplicated: %s, "
797+
"Files merged: %s, Total files processed: %s",
798+
copied_count,
799+
duplicated_count,
800+
merged_count,
801+
processed_count,
802+
)
803+
return copied_count, duplicated_count, merged_count, processed_count
804+
805+
806+
def _handle_archetype_catalog_merge(src_catalog: str, dest_catalog: str):
807+
"""
808+
Handle merging of archetype-catalog.xml files during directory merge.
809+
810+
Args:
811+
src_catalog: Source archetype-catalog.xml file path
812+
dest_catalog: Destination archetype-catalog.xml file path
813+
"""
814+
try:
815+
with open(src_catalog, "rb") as sf:
816+
src_archetypes = _parse_archetypes(sf.read())
817+
except ElementTree.ParseError as e:
818+
logger.warning("Failed to read source archetype catalog %s: %s", src_catalog, e)
819+
return
820+
821+
if len(src_archetypes) < 1:
822+
logger.warning(
823+
"No archetypes found in source archetype-catalog.xml: %s, "
824+
"even though the file exists! Skipping.",
825+
src_catalog
826+
)
827+
return
828+
829+
# Copy directly if dest_catalog doesn't exist
830+
if not os.path.exists(dest_catalog):
831+
copy2(src_catalog, dest_catalog)
832+
return
833+
834+
try:
835+
with open(dest_catalog, "rb") as df:
836+
dest_archetypes = _parse_archetypes(df.read())
837+
except ElementTree.ParseError as e:
838+
logger.warning("Failed to read dest archetype catalog %s: %s", dest_catalog, e)
839+
return
840+
841+
if len(dest_archetypes) < 1:
842+
logger.warning(
843+
"No archetypes found in dest archetype-catalog.xml: %s, "
844+
"even though the file exists! Copy directly from the src_catalog, %s.",
845+
dest_catalog, src_catalog
846+
)
847+
copy2(src_catalog, dest_catalog)
848+
return
849+
850+
else:
851+
original_dest_size = len(dest_archetypes)
852+
for sa in src_archetypes:
853+
if sa not in dest_archetypes:
854+
dest_archetypes.append(sa)
855+
else:
856+
logger.debug("DUPLICATE ARCHETYPE: %s", sa)
857+
858+
if len(dest_archetypes) != original_dest_size:
859+
content = MavenArchetypeCatalog(dest_archetypes).generate_meta_file_content()
860+
try:
861+
overwrite_file(dest_catalog, content)
862+
except Exception as e:
863+
logger.error("Failed to merge archetype catalog: %s", dest_catalog)
864+
raise e
865+
866+
676867
def _scan_paths(files_root: str, ignore_patterns: List[str],
677868
root: str) -> Tuple[str, List[str], List[str], List[str]]:
678869
# 2. scan for paths and filter out the ignored paths,

charon/utils/archive.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,19 @@ def detect_npm_archive(repo):
182182
return NpmArchiveType.NOT_NPM
183183

184184

185+
def detect_npm_archives(repos):
186+
"""Detects, if the archives need to have npm workflow.
187+
:parameter repos list of repository directories
188+
:return list of NpmArchiveType values
189+
"""
190+
results = []
191+
for repo in repos:
192+
result = detect_npm_archive(repo)
193+
results.append(result)
194+
195+
return results
196+
197+
185198
def download_archive(url: str, base_dir=None) -> str:
186199
dir_ = base_dir
187200
if not dir_ or not os.path.isdir(dir_):

0 commit comments

Comments
 (0)