Skip to content

Commit cfdd37f

Browse files
authored
Merge pull request #294 from yma96/main
Feat: Support to accept the response of signing result from RADAS
2 parents 23a4dc8 + 83a92f9 commit cfdd37f

File tree

7 files changed

+341
-17
lines changed

7 files changed

+341
-17
lines changed

charon/cmd/cmd_upload.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,16 @@
136136
default=False
137137
)
138138
@option("--dryrun", "-n", is_flag=True, default=False)
139+
@option(
140+
"--sign_result_loc",
141+
"-l",
142+
default="/tmp/sign",
143+
help="""
144+
The local save path for oras to pull the radas signature result.
145+
Sign request will use this path to download the signature result,
146+
Upload will use the file on this path to generate the corresponding .asc files
147+
""",
148+
)
139149
@command()
140150
def upload(
141151
repo: str,
@@ -150,7 +160,8 @@ def upload(
150160
sign_key: str = "redhatdevel",
151161
debug=False,
152162
quiet=False,
153-
dryrun=False
163+
dryrun=False,
164+
sign_result_loc="/tmp/sign"
154165
):
155166
"""Upload all files from a released product REPO to Ronda
156167
Service. The REPO points to a product released tarball which
@@ -221,7 +232,8 @@ def upload(
221232
key=sign_key,
222233
dry_run=dryrun,
223234
manifest_bucket_name=manifest_bucket_name,
224-
config=config
235+
config=config,
236+
sign_result_loc=sign_result_loc
225237
)
226238
if not succeeded:
227239
sys.exit(1)

charon/config.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
"""
16+
1617
import logging
1718
import os
1819
from typing import Dict, List, Optional
@@ -34,13 +35,20 @@ def __init__(self, data: Dict):
3435
self.__client_key: str = data.get("client_key", None)
3536
self.__client_key_pass_file: str = data.get("client_key_pass_file", None)
3637
self.__root_ca: str = data.get("root_ca", "/etc/pki/tls/certs/ca-bundle.crt")
38+
self.__quay_radas_registry_config: Optional[str] = data.get(
39+
"quay_radas_registry_config", None
40+
)
41+
self.__radas_sign_timeout_retry_count: int = data.get("radas_sign_timeout_retry_count", 10)
42+
self.__radas_sign_timeout_retry_interval: int = data.get(
43+
"radas_sign_timeout_retry_interval", 60
44+
)
3745

3846
def validate(self) -> bool:
3947
if not self.__umb_host:
4048
logger.error("Missing host name setting for UMB!")
4149
return False
4250
if not self.__result_queue:
43-
logger.error("Missing the queue setting to receive siging result in UMB!")
51+
logger.error("Missing the queue setting to receive signing result in UMB!")
4452
return False
4553
if not self.__request_queue:
4654
logger.error("Missing the queue setting to send signing request in UMB!")
@@ -57,10 +65,17 @@ def validate(self) -> bool:
5765
if self.__root_ca and not os.access(self.__root_ca, os.R_OK):
5866
logger.error("The root ca file is not valid!")
5967
return False
68+
if self.__quay_radas_registry_config and not os.access(
69+
self.__quay_radas_registry_config, os.R_OK
70+
):
71+
self.__quay_radas_registry_config = None
72+
logger.warning(
73+
"The quay registry config for oras is not valid, will ignore the registry config!"
74+
)
6075
return True
6176

6277
def umb_target(self) -> str:
63-
return f'amqps://{self.__umb_host}:{self.__umb_host_port}'
78+
return f"amqps://{self.__umb_host}:{self.__umb_host_port}"
6479

6580
def result_queue(self) -> str:
6681
return self.__result_queue
@@ -77,7 +92,7 @@ def client_key(self) -> str:
7792
def client_key_password(self) -> str:
7893
pass_file = self.__client_key_pass_file
7994
if os.access(pass_file, os.R_OK):
80-
with open(pass_file, 'r') as f:
95+
with open(pass_file, "r") as f:
8196
return f.read()
8297
elif pass_file:
8398
logger.warning("The key password file is not accessible. Will ignore the password.")
@@ -86,6 +101,15 @@ def client_key_password(self) -> str:
86101
def root_ca(self) -> str:
87102
return self.__root_ca
88103

104+
def quay_radas_registry_config(self) -> Optional[str]:
105+
return self.__quay_radas_registry_config
106+
107+
def radas_sign_timeout_retry_count(self) -> int:
108+
return self.__radas_sign_timeout_retry_count
109+
110+
def radas_sign_timeout_retry_interval(self) -> int:
111+
return self.__radas_sign_timeout_retry_interval
112+
89113

90114
class CharonConfig(object):
91115
"""CharonConfig is used to store all configurations for charon
@@ -102,9 +126,10 @@ def __init__(self, data: Dict):
102126
self.__ignore_signature_suffix: Dict = data.get("ignore_signature_suffix", None)
103127
self.__signature_command: str = data.get("detach_signature_command", None)
104128
self.__aws_cf_enable: bool = data.get("aws_cf_enable", False)
129+
self.__radas_config__: Optional[RadasConfig] = None
105130
radas_config: Dict = data.get("radas", None)
106131
if radas_config:
107-
self.__radas_config__: RadasConfig = RadasConfig(radas_config)
132+
self.__radas_config__ = RadasConfig(radas_config)
108133

109134
def get_ignore_patterns(self) -> List[str]:
110135
return self.__ignore_patterns
@@ -133,22 +158,23 @@ def get_detach_signature_command(self) -> str:
133158
def is_aws_cf_enable(self) -> bool:
134159
return self.__aws_cf_enable
135160

136-
def get_radas_config(self) -> RadasConfig:
161+
def is_radas_enabled(self) -> bool:
162+
return bool(self.__radas_config__ and self.__radas_config__.validate())
163+
164+
def get_radas_config(self) -> Optional[RadasConfig]:
137165
return self.__radas_config__
138166

139167

140168
def get_config(cfgPath=None) -> CharonConfig:
141169
config_file_path = cfgPath
142170
if not config_file_path or not os.path.isfile(config_file_path):
143171
config_file_path = os.path.join(os.getenv("HOME", ""), ".charon", CONFIG_FILE)
144-
data = read_yaml_from_file_path(config_file_path, 'schemas/charon.json')
172+
data = read_yaml_from_file_path(config_file_path, "schemas/charon.json")
145173
return CharonConfig(data)
146174

147175

148176
def get_template(template_file: str) -> str:
149-
template = os.path.join(
150-
os.getenv("HOME", ''), ".charon/template", template_file
151-
)
177+
template = os.path.join(os.getenv("HOME", ""), ".charon/template", template_file)
152178
if os.path.isfile(template):
153179
with open(template, encoding="utf-8") as file_:
154180
return file_.read()

charon/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,5 @@
175175
DEFAULT_ERRORS_LOG = "errors.log"
176176

177177
DEFAULT_REGISTRY = "localhost"
178+
DEFAULT_RADAS_SIGN_TIMEOUT_RETRY_COUNT = 10
179+
DEFAULT_RADAS_SIGN_TIMEOUT_RETRY_INTERVAL = 60

charon/pkgs/maven.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from charon.utils.files import HashType
1717
import charon.pkgs.indexing as indexing
1818
import charon.pkgs.signature as signature
19+
import charon.pkgs.radas_signature_handler as radas_signature
1920
from charon.utils.files import overwrite_file, digest, write_manifest
2021
from charon.utils.archive import extract_zip_all
2122
from charon.utils.strings import remove_prefix
@@ -274,7 +275,8 @@ def handle_maven_uploading(
274275
key=None,
275276
dry_run=False,
276277
manifest_bucket_name=None,
277-
config=None
278+
config=None,
279+
sign_result_loc="/tmp/sign"
278280
) -> Tuple[str, bool]:
279281
""" Handle the maven product release tarball uploading process.
280282
* repo is the location of the tarball in filesystem
@@ -408,11 +410,38 @@ def handle_maven_uploading(
408410
if cf_enable:
409411
cf_invalidate_paths.extend(archetype_files)
410412

411-
# 10. Generate signature file if contain_signature is set to True
412-
if gen_sign:
413-
conf = get_config(config)
414-
if not conf:
415-
sys.exit(1)
413+
# 10. Generate signature file if radas sign is enabled,
414+
# or do detached sign if contain_signature is set to True
415+
conf = get_config(config)
416+
if not conf:
417+
sys.exit(1)
418+
419+
if conf.is_radas_enabled():
420+
logger.info("Start generating radas signature files for s3 bucket %s\n", bucket_name)
421+
(_failed_metas, _generated_signs) = radas_signature.generate_radas_sign(
422+
top_level=top_level, sign_result_loc=sign_result_loc
423+
)
424+
if not _generated_signs:
425+
logger.error(
426+
"No sign result files were downloaded, "
427+
"please make sure the sign process is already done and without timeout")
428+
return (tmp_root, False)
429+
430+
failed_metas.extend(_failed_metas)
431+
generated_signs.extend(_generated_signs)
432+
logger.info("Radas signature files generation done.\n")
433+
434+
logger.info("Start upload radas signature files to s3 bucket %s\n", bucket_name)
435+
_failed_metas = s3_client.upload_signatures(
436+
meta_file_paths=generated_signs,
437+
target=(bucket_name, prefix),
438+
product=None,
439+
root=top_level
440+
)
441+
failed_metas.extend(_failed_metas)
442+
logger.info("Radas signature files uploading done.\n")
443+
444+
elif gen_sign:
416445
suffix_list = __get_suffix(PACKAGE_TYPE_MAVEN, conf)
417446
command = conf.get_detach_signature_command()
418447
artifacts = [s for s in valid_mvn_paths if not s.endswith(tuple(suffix_list))]

charon/pkgs/oras_client.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
Copyright (C) 2022 Red Hat, Inc. (https://github.com/Commonjava/charon)
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
import oras.client
18+
import logging
19+
from charon.config import get_config
20+
from typing import List
21+
from urllib.parse import urlparse
22+
23+
logger = logging.getLogger(__name__)
24+
25+
26+
class OrasClient:
27+
"""
28+
Wrapper for oras‑py’s OrasClient, deciding whether to login based on config.
29+
"""
30+
31+
def __init__(self):
32+
self.conf = get_config()
33+
self.client = oras.client.OrasClient()
34+
35+
def login_if_needed(self, registry: str) -> None:
36+
"""
37+
If quay_radas_registry_config is provided, call login to authenticate.
38+
"""
39+
if not registry.startswith("http://") and not registry.startswith("https://"):
40+
registry = "https://" + registry
41+
registry = urlparse(registry).netloc
42+
43+
rconf = self.conf.get_radas_config() if self.conf else None
44+
if rconf and rconf.quay_radas_registry_config():
45+
logger.info("Logging in to registry: %s", registry)
46+
res = self.client.login(
47+
hostname=registry,
48+
config_path=rconf.quay_radas_registry_config(),
49+
)
50+
logger.info(res)
51+
else:
52+
logger.info("Registry config is not provided, skip login.")
53+
54+
def pull(self, result_reference_url: str, sign_result_loc: str) -> List[str]:
55+
"""
56+
Call oras‑py’s pull method to pull the remote file to local.
57+
Args:
58+
result_reference_url (str):
59+
Reference of the remote file (e.g. “quay.io/repository/signing/radas@hash”).
60+
sign_result_loc (str):
61+
Local save path (e.g. “/tmp/sign”).
62+
"""
63+
files = []
64+
try:
65+
self.login_if_needed(registry=result_reference_url)
66+
files = self.client.pull(target=result_reference_url, outdir=sign_result_loc)
67+
logger.info("Pull file from %s to %s", result_reference_url, sign_result_loc)
68+
except Exception as e:
69+
logger.error(
70+
"Failed to pull file from %s to %s: %s", result_reference_url, sign_result_loc, e
71+
)
72+
return files

0 commit comments

Comments
 (0)