Skip to content

Commit 29594e5

Browse files
committed
Feat: Accept multiple maven zips with non-RADAS signing way
1 parent cd42d1f commit 29594e5

File tree

7 files changed

+320
-32
lines changed

7 files changed

+320
-32
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,10 @@
3535

3636

3737
@argument(
38-
"repo",
38+
"repos",
3939
type=str,
40+
nargs='+', # This allows multiple arguments for zip urls
41+
required=True
4042
)
4143
@option(
4244
"--product",
@@ -138,7 +140,7 @@
138140
@option("--dryrun", "-n", is_flag=True, default=False)
139141
@command()
140142
def upload(
141-
repo: str,
143+
repos: List[str],
142144
product: str,
143145
version: str,
144146
targets: List[str],
@@ -152,9 +154,9 @@ def upload(
152154
quiet=False,
153155
dryrun=False
154156
):
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.
157+
"""Upload all files from released product REPOs to Ronda
158+
Service. The REPOs point to a product released tarballs which
159+
are hosted in remote urls or local paths.
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: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@
3232
META_FILE_FAILED, MAVEN_METADATA_TEMPLATE,
3333
ARCHETYPE_CATALOG_TEMPLATE, ARCHETYPE_CATALOG_FILENAME,
3434
PACKAGE_TYPE_MAVEN)
35-
from typing import Dict, List, Tuple
35+
from typing import Dict, List, Tuple, Union
3636
from jinja2 import Template
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: Union[str, List[str]],
265266
prod_key: str,
266267
ignore_patterns=None,
267268
root="maven-repository",
@@ -294,8 +295,10 @@ 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+
if isinstance(repos, str):
299+
repos = [repos]
300+
# 1. extract tarballs
301+
tmp_root = _extract_tarballs(repos, root, prod_key, dir__=dir_)
299302

300303
# 2. scan for paths and filter out the ignored paths,
301304
# and also collect poms for later metadata generation
@@ -673,6 +676,135 @@ def _extract_tarball(repo: str, prefix="", dir__=None) -> str:
673676
sys.exit(1)
674677

675678

679+
def _extract_tarballs(repos: List[str], root: str, prefix="", dir__=None) -> str:
680+
""" Extract multiple zip archives to a temporary directory.
681+
* repos are the list of repo paths to extract
682+
* root is a prefix in the tarball to identify which path is
683+
the beginning of the maven GAV path
684+
* prefix is the prefix for temporary directory name
685+
* dir__ is the directory where temporary directories will be created.
686+
687+
Returns the path to the merged temporary directory containing all extracted files
688+
"""
689+
# Create final merge directory
690+
final_tmp_root = mkdtemp(prefix=f"charon-{prefix}-final-", dir=dir__)
691+
692+
total_copied = 0
693+
total_overwritten = 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: %s", 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+
# Get top-level directory names for merged from all repos
718+
top_level_merged_name_dirs = []
719+
for extracted_dir in extracted_dirs:
720+
for item in os.listdir(extracted_dir):
721+
item_path = os.path.join(extracted_dir, item)
722+
# Check the root maven-repository subdirectory existence
723+
maven_repo_path = os.path.join(item_path, root)
724+
if os.path.isdir(item_path) and os.path.exists(maven_repo_path):
725+
top_level_merged_name_dirs.append(item)
726+
break
727+
728+
# Create merged directory name
729+
merged_dir_name = (
730+
"_".join(top_level_merged_name_dirs) if top_level_merged_name_dirs else "merged"
731+
)
732+
merged_dest_dir = os.path.join(final_tmp_root, merged_dir_name)
733+
734+
# Merge content from all extracted directories
735+
for extracted_dir in extracted_dirs:
736+
copied, overwritten, processed = _merge_directories_with_rename(
737+
extracted_dir, merged_dest_dir, root
738+
)
739+
total_copied += copied
740+
total_overwritten += overwritten
741+
total_processed += processed
742+
743+
# Clean up temporary extraction directory
744+
rmtree(extracted_dir)
745+
746+
logger.info(
747+
"All zips merged! Total copied: %s, Total overwritten: %s, Total processed: %s",
748+
total_copied,
749+
total_overwritten,
750+
total_processed,
751+
)
752+
return final_tmp_root
753+
754+
755+
def _merge_directories_with_rename(src_dir: str, dest_dir: str, root: str):
756+
""" Recursively copy files from src_dir to dest_dir, overwriting existing files.
757+
* src_dir is the source directory to copy from
758+
* dest_dir is the destination directory to copy to.
759+
760+
Returns Tuple of (copied_count, overwritten_count, processed_count)
761+
"""
762+
copied_count = 0
763+
overwritten_count = 0
764+
processed_count = 0
765+
766+
# Find the actual content directory
767+
content_root = src_dir
768+
for item in os.listdir(src_dir):
769+
item_path = os.path.join(src_dir, item)
770+
# Check the root maven-repository subdirectory existence
771+
maven_repo_path = os.path.join(item_path, root)
772+
if os.path.isdir(item_path) and os.path.exists(maven_repo_path):
773+
content_root = item_path
774+
break
775+
776+
# pylint: disable=unused-variable
777+
for root_dir, dirs, files in os.walk(content_root):
778+
# Calculate relative path from content root
779+
rel_path = os.path.relpath(root_dir, content_root)
780+
dest_root = os.path.join(dest_dir, rel_path) if rel_path != '.' else dest_dir
781+
782+
# Create destination directory if it doesn't exist
783+
os.makedirs(dest_root, exist_ok=True)
784+
785+
# Copy all files, overwriting existing ones
786+
for file in files:
787+
src_file = os.path.join(root_dir, file)
788+
dest_file = os.path.join(dest_root, file)
789+
if os.path.exists(dest_file):
790+
overwritten_count += 1
791+
logger.debug("Overwritten: %s -> %s", src_file, dest_file)
792+
else:
793+
copied_count += 1
794+
logger.debug("Copied: %s -> %s", src_file, dest_file)
795+
796+
processed_count += 1
797+
copy2(src_file, dest_file)
798+
799+
logger.info(
800+
"One zip merged! Files copied: %s, Files overwritten: %s, Total files processed: %s",
801+
copied_count,
802+
overwritten_count,
803+
processed_count,
804+
)
805+
return copied_count, overwritten_count, processed_count
806+
807+
676808
def _scan_paths(files_root: str, ignore_patterns: List[str],
677809
root: str) -> Tuple[str, List[str], List[str], List[str]]:
678810
# 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_):

tests/test_archive.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from tests.base import BaseTest
2-
from charon.utils.archive import NpmArchiveType, detect_npm_archive
2+
from charon.utils.archive import NpmArchiveType, detect_npm_archive, detect_npm_archives
33
import os
44

55
from tests.constants import INPUTS
@@ -12,5 +12,36 @@ def test_detect_package(self):
1212
npm_tarball = os.path.join(INPUTS, "code-frame-7.14.5.tgz")
1313
self.assertEqual(NpmArchiveType.TAR_FILE, detect_npm_archive(npm_tarball))
1414

15+
def test_detect_packages(self):
16+
mvn_tarballs = [
17+
os.path.join(INPUTS, "commons-client-4.5.6.zip"),
18+
os.path.join(INPUTS, "commons-client-4.5.9.zip")
19+
]
20+
archive_types = detect_npm_archives(mvn_tarballs)
21+
self.assertEqual(2, archive_types.count(NpmArchiveType.NOT_NPM))
22+
23+
npm_tarball = [
24+
os.path.join(INPUTS, "code-frame-7.14.5.tgz")
25+
]
26+
archive_types = detect_npm_archives(npm_tarball)
27+
self.assertEqual(1, archive_types.count(NpmArchiveType.TAR_FILE))
28+
29+
npm_tarballs = [
30+
os.path.join(INPUTS, "code-frame-7.14.5.tgz"),
31+
os.path.join(INPUTS, "code-frame-7.15.8.tgz")
32+
]
33+
archive_types = detect_npm_archives(npm_tarballs)
34+
self.assertEqual(2, archive_types.count(NpmArchiveType.TAR_FILE))
35+
36+
tarballs = [
37+
os.path.join(INPUTS, "commons-client-4.5.6.zip"),
38+
os.path.join(INPUTS, "commons-client-4.5.9.zip"),
39+
os.path.join(INPUTS, "code-frame-7.14.5.tgz"),
40+
os.path.join(INPUTS, "code-frame-7.15.8.tgz")
41+
]
42+
archive_types = detect_npm_archives(tarballs)
43+
self.assertEqual(2, archive_types.count(NpmArchiveType.NOT_NPM))
44+
self.assertEqual(2, archive_types.count(NpmArchiveType.TAR_FILE))
45+
1546
def test_download_archive(self):
1647
pass

0 commit comments

Comments
 (0)