Skip to content

Commit 10efcb7

Browse files
committed
(Re)add support to work with S3 buckets
Signed-off-by: Tobias Wolf <wolf@b1-systems.de>
1 parent 6278da3 commit 10efcb7

File tree

9 files changed

+304
-4
lines changed

9 files changed

+304
-4
lines changed

.github/actions/features_parse/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ outputs:
1111
runs:
1212
using: composite
1313
steps:
14-
- uses: gardenlinux/python-gardenlinux-lib/.github/actions/setup@0.7.3
14+
- uses: gardenlinux/python-gardenlinux-lib/.github/actions/setup@0.8.0
1515
- id: result
1616
shell: bash
1717
run: |

.github/actions/flavors_parse/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ outputs:
1313
runs:
1414
using: composite
1515
steps:
16-
- uses: gardenlinux/python-gardenlinux-lib/.github/actions/setup@0.7.3
16+
- uses: gardenlinux/python-gardenlinux-lib/.github/actions/setup@0.8.0
1717
- id: matrix
1818
shell: bash
1919
run: |

.github/actions/setup/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Installs the given GardenLinux Python library
33
inputs:
44
version:
55
description: GardenLinux Python library version
6-
default: "0.7.3"
6+
default: "0.8.0"
77
runs:
88
using: composite
99
steps:

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "gardenlinux"
3-
version = "0.7.3"
3+
version = "0.8.0"
44
description = "Contains tools to work with the features directory of gardenlinux, for example deducting dependencies from feature sets or validating cnames"
55
authors = ["Garden Linux Maintainers <contact@gardenlinux.io>"]
66
license = "Apache-2.0"
@@ -34,6 +34,7 @@ gl-cname = "gardenlinux.features.cname_main:main"
3434
gl-features-parse = "gardenlinux.features.__main__:main"
3535
gl-flavors-parse = "gardenlinux.flavors.__main__:main"
3636
gl-oci = "gardenlinux.oci.__main__:main"
37+
gl-s3 = "gardenlinux.s3.__main__:main"
3738

3839
[tool.pytest.ini_options]
3940
pythonpath = ["src"]

src/gardenlinux/features/cname.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@ def feature_set(self) -> str:
128128

129129
return Parser().filter_as_string(self.flavor)
130130

131+
@property
132+
def platform(self) -> str:
133+
"""
134+
Returns the platform for the cname parsed.
135+
136+
:return: (str) Flavor
137+
"""
138+
139+
return re.split("[_-]", self._flavor, 1)[0]
140+
131141
@property
132142
def version(self) -> Optional[str]:
133143
"""

src/gardenlinux/s3/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
S3 module
5+
"""
6+
7+
from .bucket import Bucket
8+
from .s3_artifacts import S3Artifacts
9+
10+
__all__ = ["Bucket", "S3Artifacts"]

src/gardenlinux/s3/__main__.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
"""
5+
gl-s3 main entrypoint
6+
"""
7+
8+
import argparse
9+
import os
10+
import re
11+
import sys
12+
from functools import reduce
13+
from pathlib import Path
14+
from typing import Any, List, Set
15+
16+
from .s3_artifacts import S3Artifacts
17+
18+
19+
_ARGS_ACTION_ALLOWED = [
20+
"push-artifacts-to-bucket",
21+
]
22+
23+
def main() -> None:
24+
"""
25+
gl-s3 main()
26+
27+
:since: 0.7.0
28+
"""
29+
30+
parser = argparse.ArgumentParser()
31+
32+
parser.add_argument("--bucket", dest="bucket")
33+
parser.add_argument("--cname", required=False, dest="cname")
34+
parser.add_argument("--path", required=False, dest="path")
35+
36+
parser.add_argument("action", nargs="?", choices=_ARGS_ACTION_ALLOWED)
37+
38+
args = parser.parse_args()
39+
40+
if args.action == "push-artifacts-to-bucket":
41+
S3Artifacts(args.bucket, "http://127.0.0.1:8000").push_from_directory(args.cname, args.path)

src/gardenlinux/s3/bucket.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
S3 bucket
5+
"""
6+
7+
import boto3
8+
import logging
9+
from typing import Optional
10+
11+
from ..logger import LoggerSetup
12+
13+
14+
class Bucket(object):
15+
"""
16+
S3 bucket class
17+
18+
:author: Garden Linux Maintainers
19+
:copyright: Copyright 2024 SAP SE
20+
:package: gardenlinux
21+
:subpackage: s3
22+
:since: 0.8.0
23+
:license: https://www.apache.org/licenses/LICENSE-2.0
24+
Apache License, Version 2.0
25+
"""
26+
27+
def __init__(
28+
self,
29+
bucket_name: str,
30+
endpoint_url: Optional[str] = None,
31+
logger: Optional[logging.Logger] = None,
32+
):
33+
"""
34+
Constructor __init__(Bucket)
35+
36+
:param bucket_name: S3 bucket name
37+
:param logger: Logger instance
38+
39+
:since: 0.8.0
40+
"""
41+
42+
if logger is None or not logger.hasHandlers():
43+
logger = LoggerSetup.get_logger("gardenlinux.s3")
44+
45+
config={}
46+
47+
if endpoint_url is not None:
48+
config["endpoint_url"] = endpoint_url
49+
50+
self._s3_resource = boto3.resource("s3", aws_access_key_id="default", aws_secret_access_key="default", **config)
51+
self._bucket = self._s3_resource.Bucket(bucket_name)
52+
self._logger = logger
53+
54+
@property
55+
def objects(self):
56+
"""
57+
Returns a list of all objects in a bucket.
58+
59+
:return: (list) S3 bucket objects
60+
:since: 0.8.0
61+
"""
62+
63+
return self._bucket.objects.all()
64+
65+
def __getattr__(self, name):
66+
"""
67+
python.org: Called when an attribute lookup has not found the attribute in
68+
the usual places (i.e. it is not an instance attribute nor is it found in the
69+
class tree for self).
70+
71+
:param name: Attribute name
72+
73+
:return: (mixed) Attribute
74+
:since: v1.0.0
75+
"""
76+
77+
return getattr(self._bucket, name)

src/gardenlinux/s3/s3_artifacts.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
S3 GardenLinux artifacts
5+
"""
6+
7+
import logging
8+
from configparser import ConfigParser, UNNAMED_SECTION
9+
from datetime import datetime
10+
from hashlib import file_digest
11+
from os import PathLike, stat
12+
from pathlib import Path
13+
from tempfile import TemporaryFile
14+
from typing import Optional
15+
from urllib.parse import urlencode
16+
17+
from .bucket import Bucket
18+
from ..features.cname import CName
19+
from ..logger import LoggerSetup
20+
21+
22+
class S3Artifacts(object):
23+
"""
24+
OCI container instance to provide methods for interaction.
25+
26+
:author: Garden Linux Maintainers
27+
:copyright: Copyright 2024 SAP SE
28+
:package: gardenlinux
29+
:subpackage: s3
30+
:since: 0.8.0
31+
:license: https://www.apache.org/licenses/LICENSE-2.0
32+
Apache License, Version 2.0
33+
"""
34+
35+
def __init__(
36+
self,
37+
bucket_name: str,
38+
endpoint_url: Optional[str] = None,
39+
logger: Optional[logging.Logger] = None,
40+
):
41+
"""
42+
Constructor __init__(Container)
43+
44+
:param bucket_name: S3 bucket name
45+
:param endpoint_url: S3 endpoint URL
46+
:param logger: Logger instance
47+
48+
:since: 0.7.0
49+
"""
50+
51+
"""Get or create an S3 client."""
52+
53+
self._bucket = Bucket(bucket_name, endpoint_url)
54+
55+
def push_from_directory(
56+
self,
57+
cname,
58+
artifacts_dir,
59+
):
60+
"""
61+
Pushes S3 artifacts to the underlying bucket.
62+
63+
Args:
64+
S3Artifacts_name (str): Name of the S3 S3Artifacts
65+
prefix (str): Prefix for S3 objects
66+
cache_file (str): Path to cache file
67+
cache_ttl (int): Cache time-to-live in seconds
68+
logger (logging.Logger): Logger instance to use
69+
70+
Returns:
71+
dict: Dictionary containing 'index' and 'artifacts' keys
72+
"""
73+
74+
if not isinstance(artifacts_dir, PathLike):
75+
artifacts_dir = Path(artifacts_dir)
76+
77+
cname_object = CName(cname)
78+
79+
if cname_object.arch is None:
80+
raise RuntimeError(
81+
"Architecture could not be determined from cname"
82+
)
83+
84+
if not artifacts_dir.is_dir():
85+
raise RuntimeError(f"Artifacts directory given is invalid: {artifacts_dir}")
86+
87+
release_file = artifacts_dir.joinpath(f"{cname}.release")
88+
release_timestamp = datetime.fromtimestamp(stat(release_file).st_ctime).isoformat()
89+
90+
release_config = ConfigParser(allow_unnamed_section=True)
91+
release_config.read(release_file)
92+
93+
if cname_object.version != release_config.get(UNNAMED_SECTION, "GARDENLINUX_VERSION"):
94+
raise RuntimeError(
95+
f"Release file data and given cname conflict detected: Version {cname_object.version}"
96+
)
97+
98+
if cname_object.commit_id != release_config.get(UNNAMED_SECTION, "GARDENLINUX_COMMIT_ID"):
99+
raise RuntimeError(
100+
f"Release file data and given cname conflict detected: Commit ID {cname_object.commit_id}"
101+
)
102+
103+
commit_hash = release_config.get(UNNAMED_SECTION, "GARDENLINUX_COMMIT_ID_LONG")
104+
105+
feature_set = release_config.get(UNNAMED_SECTION, "GARDENLINUX_FEATURES")
106+
feature_list = feature_set.split(",")
107+
108+
metadata = {
109+
"platform": cname_object.platform,
110+
"architecture": cname_object.arch,
111+
"base_image": None,
112+
"build_committish": commit_hash,
113+
"build_timestamp": release_timestamp,
114+
"gardenlinux_epoch": cname_object.version.split(".", 1)[0],
115+
"logs": None,
116+
"modifiers": feature_set,
117+
"require_uefi": "_usi" in feature_list,
118+
"secureboot": "_trustedboot" in feature_list,
119+
"published_image_metadata": None,
120+
"s3_bucket": self._bucket.name,
121+
"s3_key": f"meta/singles/{cname}",
122+
"test_result": None,
123+
"version": cname_object.version,
124+
"paths": []
125+
}
126+
127+
for artifact in artifacts_dir.iterdir():
128+
if not artifact.match(f"{cname}*"):
129+
continue
130+
131+
s3_key = f"objects/{cname}/{artifact.name}"
132+
133+
with artifact.open("rb") as fp:
134+
md5sum = file_digest(fp, "md5").hexdigest()
135+
sha256sum = file_digest(fp, "sha256").hexdigest()
136+
137+
artifact_metadata = {
138+
"name": artifact.name,
139+
"s3_bucket_name": self._bucket.name,
140+
"s3_key": s3_key,
141+
"suffix": "".join(artifact.suffixes),
142+
"md5sum": md5sum,
143+
"sha256sum": sha256sum,
144+
}
145+
146+
s3_tags = {
147+
"architecture": cname_object.arch,
148+
"platform": cname_object.platform,
149+
"version": cname_object.version,
150+
"committish": commit_hash,
151+
"md5sum": md5sum,
152+
"sha256sum": sha256sum,
153+
}
154+
155+
self._bucket.upload_file(artifact, s3_key, ExtraArgs={'Tagging': urlencode(s3_tags)})
156+
157+
metadata["paths"].append(artifact_metadata)
158+
159+
with TemporaryFile(mode='w+') as fp:
160+
fp.write(yaml(metadata))
161+
self._bucket.upload_fileobj(fp, f"meta/singles/{cname}", ExtraArgs={'ContentType': "text/yaml"})

0 commit comments

Comments
 (0)