Skip to content

Commit 32ddde4

Browse files
committed
Merge branch 'main' into release
2 parents 9ac6c10 + 44d173b commit 32ddde4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+2911
-654
lines changed

.github/workflows/linters.yaml

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,16 @@ jobs:
2929
- name: Run flake8 on python${{ matrix.python-version }}
3030
run: python -m tox -e flake8
3131

32-
markdownlint:
33-
name: Markdownlint
34-
runs-on: ubuntu-latest
32+
# markdownlint:
33+
# name: Markdownlint
34+
# runs-on: ubuntu-latest
3535

36-
steps:
37-
- name: Check out repo
38-
uses: actions/checkout@v2
36+
# steps:
37+
# - name: Check out repo
38+
# uses: actions/checkout@v2
3939

40-
- name: Run markdownlint
41-
uses: containerbuildsystem/actions/markdownlint@master
40+
# - name: Run markdownlint
41+
# uses: containerbuildsystem/actions/markdownlint@master
4242

4343
pylint:
4444
name: Pylint analyzer for Python ${{ matrix.python-version }}
@@ -91,22 +91,22 @@ jobs:
9191
# - name: Run mypy on python${{ matrix.python-version }}
9292
# run: python -m tox -e mypy
9393

94-
bandit:
95-
name: Bandit analyzer for Python ${{ matrix.python-version }}
96-
runs-on: ubuntu-latest
97-
98-
strategy:
99-
matrix:
100-
python-version: [ "3.8" ]
101-
102-
steps:
103-
- uses: actions/checkout@v1
104-
- uses: actions/setup-python@v4
105-
with:
106-
python-version: ${{ matrix.python-version }}
107-
- name: Install dependencies
108-
run: |
109-
python -m pip install --upgrade pip setuptools tox
110-
111-
- name: Run bandit analyzer on python${{ matrix.python-version }}
112-
run: python -m tox -e bandit
94+
# bandit:
95+
# name: Bandit analyzer for Python ${{ matrix.python-version }}
96+
# runs-on: ubuntu-latest
97+
98+
# strategy:
99+
# matrix:
100+
# python-version: [ "3.8" ]
101+
102+
# steps:
103+
# - uses: actions/checkout@v1
104+
# - uses: actions/setup-python@v4
105+
# with:
106+
# python-version: ${{ matrix.python-version }}
107+
# - name: Install dependencies
108+
# run: |
109+
# python -m pip install --upgrade pip setuptools tox
110+
111+
# - name: Run bandit analyzer on python${{ matrix.python-version }}
112+
# run: python -m tox -e bandit

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ coverage
1111
.vscode
1212
package/
1313
.local
14+
local
1415
.DS_Store
1516

1617
# Unit test
1718
__pytest_reports
1819
htmlcov
20+
21+
# Generated when local run
22+
*.log

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,21 @@ This command will delete some paths from repo in S3.
9696
but not delete the artifacts themselves.
9797
* During or after the paths' deletion, regenerate the
9898
metadata files and index files for both types.
99+
100+
### charon-index: refresh the index.html for the specified path
101+
102+
```bash
103+
usage: charon index $PATH [-t, --target] [-D, --debug] [-q, --quiet]
104+
```
105+
106+
This command will refresh the index.html for the specified path.
107+
108+
* Note that if the path is a NPM metadata path which contains package.json, this refreshment will not work because this type of folder will display the package.json instead of the index.html in http request.
109+
110+
### charon-validate: validate the checksum of files in specified path in a maven repository
111+
112+
```bash
113+
usage: charon validate $path [-t, --target] [-f, --report_file_path] [-i, --includes] [-r, --recursive] [-D, --debug] [-q, --quiet]
114+
```
115+
116+
This command will validate the checksum of the specified path for the maven repository. It will calculate the sha1 checksum of all artifact files in the specified path and compare with the companied .sha1 files of the artifacts, then record all mismatched artifacts in the report file. If some artifact files misses the companied .sha1 files, they will also be recorded.

charon.spec

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ Requires: python%{python3_pkgversion}-zipp
5151
Requires: python%{python3_pkgversion}-attrs
5252
Requires: python%{python3_pkgversion}-pyrsistent
5353

54-
5554
%description
5655
Simple Python tool with command line interface for charon init,
5756
upload, delete, gen and ls functions.
@@ -81,6 +80,15 @@ export LANG=en_US.UTF-8 LANGUAGE=en_US.en LC_ALL=en_US.UTF-8
8180

8281

8382
%changelog
83+
* Fri Apr 12 2024 Gang Li <[email protected]>
84+
- 1.3.0 release
85+
- Add validate command: validate the checksum for maven artifacts
86+
- Add index command: support to re-index of the speicified folder
87+
- Add CF invalidating features:
88+
- Invalidate generated metadata files (maven-metadata*/package.json/index.html) after product uploading/deleting in CloudFront
89+
- Add command to do CF invalidating and checking
90+
- Fix bug: picking the root package.json as the first priority one to generate npm package path
91+
8492
* Mon Sep 18 2023 Harsh Modi <[email protected]>
8593
- 1.2.2 release
8694
- hot fix for "dist_tags" derived issue

charon/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,3 @@
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
"""
16-
17-
from charon.cmd.command import cli, upload, delete
18-
19-
# init group command
20-
cli.add_command(upload)
21-
cli.add_command(delete)

charon/cache.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
from boto3 import session
2+
from botocore.exceptions import ClientError
3+
from typing import Dict, List
4+
import os
5+
import logging
6+
import uuid
7+
import time
8+
9+
logger = logging.getLogger(__name__)
10+
11+
ENDPOINT_ENV = "aws_endpoint_url"
12+
INVALIDATION_BATCH_DEFAULT = 3000
13+
INVALIDATION_BATCH_WILDCARD = 15
14+
15+
INVALIDATION_STATUS_COMPLETED = "Completed"
16+
INVALIDATION_STATUS_INPROGRESS = "InProgress"
17+
18+
DEFAULT_BUCKET_TO_DOMAIN = {
19+
"prod-ga": "maven.repository.redhat.com",
20+
"prod-maven-ga": "maven.repository.redhat.com",
21+
"prod-ea": "maven.repository.redhat.com",
22+
"prod-maven-ea": "maven.repository.redhat.com",
23+
"stage-ga": "maven.stage.repository.redhat.com",
24+
"stage-maven-ga": "maven.stage.repository.redhat.com",
25+
"stage-ea": "maven.stage.repository.redhat.com",
26+
"stage-maven-ea": "maven.stage.repository.redhat.com",
27+
"prod-npm": "npm.registry.redhat.com",
28+
"prod-npm-npmjs": "npm.registry.redhat.com",
29+
"stage-npm": "npm.stage.registry.redhat.com",
30+
"stage-npm-npmjs": "npm.stage.registry.redhat.com"
31+
}
32+
33+
34+
class CFClient(object):
35+
"""The CFClient is a wrapper of the original boto3 clouldfrong client,
36+
which will provide CloudFront functions to be used in the charon.
37+
"""
38+
39+
def __init__(
40+
self,
41+
aws_profile=None,
42+
extra_conf=None
43+
) -> None:
44+
self.__client = self.__init_aws_client(aws_profile, extra_conf)
45+
46+
def __init_aws_client(
47+
self, aws_profile=None, extra_conf=None
48+
):
49+
if aws_profile:
50+
logger.debug("[CloudFront] Using aws profile: %s", aws_profile)
51+
cf_session = session.Session(profile_name=aws_profile)
52+
else:
53+
cf_session = session.Session()
54+
endpoint_url = self.__get_endpoint(extra_conf)
55+
return cf_session.client(
56+
'cloudfront',
57+
endpoint_url=endpoint_url
58+
)
59+
60+
def __get_endpoint(self, extra_conf) -> str:
61+
endpoint_url = os.getenv(ENDPOINT_ENV)
62+
if not endpoint_url or not endpoint_url.strip():
63+
if isinstance(extra_conf, Dict):
64+
endpoint_url = extra_conf.get(ENDPOINT_ENV, None)
65+
if endpoint_url:
66+
logger.info(
67+
"[CloudFront] Using endpoint url for aws CF client: %s",
68+
endpoint_url
69+
)
70+
else:
71+
logger.debug("[CloudFront] No user-specified endpoint url is used.")
72+
return endpoint_url
73+
74+
def invalidate_paths(
75+
self, distr_id: str, paths: List[str],
76+
batch_size=INVALIDATION_BATCH_DEFAULT
77+
) -> List[Dict[str, str]]:
78+
"""Send a invalidating requests for the paths in distribution to CloudFront.
79+
This will invalidate the paths in the distribution to enforce the refreshment
80+
from backend S3 bucket for these paths. For details see:
81+
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html
82+
* The distr_id is the id for the distribution. This id can be get through
83+
get_dist_id_by_domain(domain) function
84+
* Can specify the invalidating paths through paths param.
85+
* Batch size is the number of paths to be invalidated in one request.
86+
The default value is 3000 which is the maximum number in official doc:
87+
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html#InvalidationLimits
88+
"""
89+
INPRO_W_SECS = 5
90+
NEXT_W_SECS = 1
91+
real_paths = [paths]
92+
# Split paths into batches by batch_size
93+
if batch_size:
94+
real_paths = [paths[i:i + batch_size] for i in range(0, len(paths), batch_size)]
95+
total_time_approx = len(real_paths) * (INPRO_W_SECS * 2 + NEXT_W_SECS)
96+
logger.info("There will be %d invalidating requests in total,"
97+
" will take more than %d seconds",
98+
len(real_paths), total_time_approx)
99+
results = []
100+
current_invalidation = {}
101+
processed_count = 0
102+
for batch_paths in real_paths:
103+
while (current_invalidation and
104+
INVALIDATION_STATUS_INPROGRESS == current_invalidation.get('Status', '')):
105+
time.sleep(INPRO_W_SECS)
106+
try:
107+
result = self.check_invalidation(distr_id, current_invalidation.get('Id'))
108+
if result:
109+
current_invalidation = {
110+
'Id': result.get('Id', None),
111+
'Status': result.get('Status', None)
112+
}
113+
logger.debug("Check invalidation: %s", current_invalidation)
114+
except Exception as err:
115+
logger.warning(
116+
"[CloudFront] Error occurred while checking invalidation status during"
117+
" creating invalidation, invalidation: %s, error: %s",
118+
current_invalidation, err
119+
)
120+
break
121+
if current_invalidation:
122+
results.append(current_invalidation)
123+
processed_count += 1
124+
if processed_count % 10 == 0:
125+
logger.info(
126+
"[CloudFront] ######### %d/%d requests finished",
127+
processed_count, len(real_paths))
128+
# To avoid conflict rushing request, we can wait 1s here
129+
# for next invalidation request sending.
130+
time.sleep(NEXT_W_SECS)
131+
caller_ref = str(uuid.uuid4())
132+
logger.debug(
133+
"Processing invalidation for batch with ref %s, size: %s",
134+
caller_ref, len(batch_paths)
135+
)
136+
try:
137+
response = self.__client.create_invalidation(
138+
DistributionId=distr_id,
139+
InvalidationBatch={
140+
'CallerReference': caller_ref,
141+
'Paths': {
142+
'Quantity': len(batch_paths),
143+
'Items': batch_paths
144+
}
145+
}
146+
)
147+
if response:
148+
invalidation = response.get('Invalidation', {})
149+
current_invalidation = {
150+
'Id': invalidation.get('Id', None),
151+
'Status': invalidation.get('Status', None)
152+
}
153+
except Exception as err:
154+
logger.error(
155+
"[CloudFront] Error occurred while creating invalidation"
156+
" for paths %s, error: %s", batch_paths, err
157+
)
158+
if current_invalidation:
159+
results.append(current_invalidation)
160+
return results
161+
162+
def check_invalidation(self, distr_id: str, invalidation_id: str) -> dict:
163+
try:
164+
response = self.__client.get_invalidation(
165+
DistributionId=distr_id,
166+
Id=invalidation_id
167+
)
168+
if response:
169+
invalidation = response.get('Invalidation', {})
170+
return {
171+
'Id': invalidation.get('Id', None),
172+
'CreateTime': str(invalidation.get('CreateTime', None)),
173+
'Status': invalidation.get('Status', None)
174+
}
175+
except Exception as err:
176+
logger.error(
177+
"[CloudFront] Error occurred while check invalidation of id %s, "
178+
"error: %s", invalidation_id, err
179+
)
180+
181+
def get_dist_id_by_domain(self, domain: str) -> str:
182+
"""Get distribution id by a domain name. The id can be used to send invalidating
183+
request through #invalidate_paths function
184+
* Domain are Ronda domains, like "maven.repository.redhat.com"
185+
or "npm.registry.redhat.com"
186+
"""
187+
try:
188+
response = self.__client.list_distributions()
189+
if response:
190+
dist_list_items = response.get("DistributionList", {}).get("Items", [])
191+
for distr in dist_list_items:
192+
aliases_items = distr.get('Aliases', {}).get('Items', [])
193+
if aliases_items and domain in aliases_items:
194+
return distr['Id']
195+
logger.error("[CloudFront]: Distribution not found for domain %s", domain)
196+
except ClientError as err:
197+
logger.error(
198+
"[CloudFront]: Error occurred while get distribution for domain %s: %s",
199+
domain, err
200+
)
201+
return None
202+
203+
def get_domain_by_bucket(self, bucket: str) -> str:
204+
return DEFAULT_BUCKET_TO_DOMAIN.get(bucket, None)

charon/cmd/__init__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,26 @@
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
"""
16+
from click import group
17+
from charon.cmd.cmd_upload import upload
18+
from charon.cmd.cmd_delete import delete
19+
from charon.cmd.cmd_index import index
20+
from charon.cmd.cmd_checksum import checksum_validate
21+
from charon.cmd.cmd_cache import cf_invalidate, cf_check
22+
23+
24+
@group()
25+
def cli():
26+
"""Charon is a tool to synchronize several types of
27+
artifacts repository data to Red Hat Ronda
28+
service (maven.repository.redhat.com).
29+
"""
30+
31+
32+
# init group command
33+
cli.add_command(upload)
34+
cli.add_command(delete)
35+
cli.add_command(index)
36+
cli.add_command(checksum_validate)
37+
cli.add_command(cf_invalidate)
38+
cli.add_command(cf_check)

0 commit comments

Comments
 (0)