Skip to content

Commit 1e71202

Browse files
TUN-6054: Create and upload deb packages to R2
This PR does the following: 1. Creates packages.gz, signed InRelease files for debs in built_artifacts for configured debian releases. 2. Uploads them to Cloudflare R2. 3. Adds a Workers KV entry that talks about where these assets are uploaded.
1 parent 8250708 commit 1e71202

File tree

1 file changed

+229
-0
lines changed

1 file changed

+229
-0
lines changed

release_pkgs.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
"""
2+
This is a utility for creating deb and rpm packages, signing them
3+
and uploading them to a storage and adding metadata to workers KV.
4+
5+
It has two over-arching responsiblities:
6+
1. Create deb and yum repositories from .deb and .rpm files.
7+
This is also responsible for signing the packages and generally preparing
8+
them to be in an uploadable state.
9+
2. Upload these packages to a storage and add metadata cross reference
10+
for these to be accessed.
11+
"""
12+
import requests
13+
import subprocess
14+
import os
15+
import io
16+
import shutil
17+
import logging
18+
from hashlib import sha256
19+
20+
import boto3
21+
from botocore.client import Config
22+
from botocore.exceptions import ClientError
23+
24+
BASE_KV_URL = 'https://api.cloudflare.com/client/v4/accounts/'
25+
# The front facing R2 URL to access assets from.
26+
R2_ASSET_URL = 'https://demo-r2-worker.cloudflare-tunnel.workers.dev/'
27+
28+
class PkgUploader:
29+
def __init__(self, kv_api_token, namespace, account_id, bucket_name, client_id, client_secret):
30+
self.kv_api_token = kv_api_token
31+
self.namespace = namespace
32+
self.account_id = account_id
33+
self.bucket_name = bucket_name
34+
self.client_id = client_id
35+
self.client_secret = client_secret
36+
37+
def send_to_kv(self, key, value):
38+
headers = {
39+
"Content-Type": "application/json",
40+
"Authorization": "Bearer " + self.kv_api_token,
41+
}
42+
43+
kv_url = f"{BASE_KV_URL}{self.account_id}/storage/kv/namespaces/{self.namespace}/values/{key}"
44+
response = requests.put(
45+
kv_url,
46+
headers=headers,
47+
data=value
48+
)
49+
50+
if response.status_code != 200:
51+
jsonResponse = response.json()
52+
errors = jsonResponse["errors"]
53+
if len(errors) > 0:
54+
raise Exception("failed to send to workers kv: {0}", errors[0])
55+
else:
56+
raise Exception("recieved error code: {0}", response.status_code)
57+
58+
59+
def send_pkg_info(self, binary, flavor, asset_name, arch, uploaded_package_location):
60+
key = f"pkg_{binary}_{flavor}_{arch}_{asset_name}"
61+
print(f"writing key:{key} , value: {uploaded_package_location}")
62+
self.send_to_kv(key, uploaded_package_location)
63+
64+
65+
def upload_pkg_to_r2(self, filename, upload_file_path):
66+
endpoint_url = f"https://{self.account_id}.r2.cloudflarestorage.com"
67+
token_secret_hash = sha256(self.client_secret.encode()).hexdigest()
68+
69+
config = Config(
70+
s3={
71+
"addressing_style": "path",
72+
}
73+
)
74+
75+
r2 = boto3.client(
76+
"s3",
77+
endpoint_url=endpoint_url,
78+
aws_access_key_id=self.client_id,
79+
aws_secret_access_key=token_secret_hash,
80+
config=config,
81+
)
82+
83+
print(f"uploading asset: {filename} to {upload_file_path}...")
84+
try:
85+
r2.upload_file(filename, self.bucket_name, upload_file_path)
86+
except ClientError as e:
87+
raise e
88+
89+
90+
class PkgCreator:
91+
"""
92+
The distribution conf is what dictates to reprepro, the debian packaging building
93+
and signing tool we use, what distros to support, what GPG key to use for signing
94+
and what to call the debian binary etc. This function creates it "./conf/distributions".
95+
96+
origin - name of your package (String)
97+
label - label of your package (could be same as the name) (String)
98+
flavor - flavor you want this to be distributed for (List of Strings)
99+
components - could be a channel like main/stable/beta
100+
archs - Architecture (List of Strings)
101+
description - (String)
102+
gpg_key_id - gpg key id of what you want to use to sign the packages.(String)
103+
"""
104+
def create_distribution_conf(self,
105+
file_path,
106+
origin,
107+
label,
108+
flavors,
109+
archs,
110+
components,
111+
description,
112+
gpg_key_id ):
113+
with open(file_path, "w") as distributions_file:
114+
for flavor in flavors:
115+
distributions_file.write(f"Origin: {origin}\n")
116+
distributions_file.write(f"Label: {label}\n")
117+
distributions_file.write(f"Codename: {flavor}\n")
118+
archs_list = " ".join(archs)
119+
distributions_file.write(f"Architectures: {archs_list}\n")
120+
distributions_file.write(f"Components: {components}\n")
121+
distributions_file.write(f"Description: {description} - {flavor}\n")
122+
distributions_file.write(f"SignWith: {gpg_key_id}\n")
123+
distributions_file.write("\n")
124+
return distributions_file
125+
126+
"""
127+
Uses the reprepro tool to generate packages, sign them and create the InRelease as specified
128+
by the distribution_conf file.
129+
130+
This function creates three folders db, pool and dist.
131+
db and pool contain information and metadata about builds. We can ignore these.
132+
dist: contains all the pkgs and signed releases that are necessary for an apt download.
133+
"""
134+
def create_deb_pkgs(self, flavor, deb_file):
135+
self._clean_build_resources()
136+
subprocess.call(("reprepro", "includedeb", flavor, deb_file))
137+
138+
"""
139+
This is mostly useful to clear previously built db, dist and pool resources.
140+
"""
141+
def _clean_build_resources(self):
142+
subprocess.call(("reprepro", "clearvanished"))
143+
144+
def upload_from_directories(pkg_uploader, directory, arch, release):
145+
for root, _ , files in os.walk(directory):
146+
for file in files:
147+
root_elements = root.split("/")
148+
upload_file_name = os.path.join(root, arch, release, file)
149+
flavor_prefix = root_elements[1]
150+
if root_elements[0] == "pool":
151+
upload_file_name = os.path.join(root, file)
152+
flavor_prefix = "deb"
153+
filename = os.path.join(root,file)
154+
try:
155+
pkg_uploader.upload_pkg_to_r2(filename, upload_file_name)
156+
except ClientError as e:
157+
logging.error(e)
158+
return
159+
160+
# save to workers kv in the following formats
161+
# Example:
162+
# key : pkg_cloudflared_bullseye_InRelease,
163+
# value: https://r2.cloudflarestorage.com/dists/bullseye/amd64/2022_3_4/InRelease
164+
r2_asset_url = f"{R2_ASSET_URL}{upload_file_name}"
165+
pkg_uploader.send_pkg_info("cloudflared", flavor_prefix, upload_file_name, arch, r2_asset_url)
166+
167+
# TODO https://jira.cfops.it/browse/TUN-6163: Add a key for latest version.
168+
169+
"""
170+
1. looks into a built_artifacts folder for cloudflared debs
171+
2. creates Packages.gz, InRelease (signed) files
172+
3. uploads them to Cloudflare R2 and
173+
4. adds a Workers KV reference
174+
175+
pkg_creator, pkg_uploader: are instantiations of the two classes above.
176+
177+
gpg_key_id: is an id indicating the key the package should be signed with. The public key of this id will be
178+
uploaded to R2 so it can be presented to apt downloaders.
179+
180+
release_version: is the cloudflared release version.
181+
"""
182+
def create_deb_packaging(pkg_creator, pkg_uploader, flavors, gpg_key_id, binary_name, arch, package_component, release_version):
183+
# set configuration for package creation.
184+
print(f"initialising configuration for {binary_name} , {arch}")
185+
pkg_creator.create_distribution_conf(
186+
"./conf/distributions",
187+
binary_name,
188+
binary_name,
189+
flavors,
190+
[arch],
191+
package_component,
192+
f"apt repository for {binary_name}",
193+
gpg_key_id)
194+
195+
# create deb pkgs
196+
for flavor in flavors:
197+
print(f"creating deb pkgs for {flavor} and {arch}...")
198+
pkg_creator.create_deb_pkgs(flavor, f"./built_artifacts/cloudflared-linux-{arch}.deb")
199+
200+
print("uploading to r2...")
201+
upload_from_directories(pkg_uploader, "dists", arch, release_version)
202+
upload_from_directories(pkg_uploader, "pool", arch, release_version)
203+
204+
print("cleaning up directories...")
205+
shutil.rmtree("./dists")
206+
shutil.rmtree("./pool")
207+
shutil.rmtree("./db")
208+
209+
#TODO: https://jira.cfops.it/browse/TUN-6146 will extract this into it's own command line script.
210+
if __name__ == "__main__":
211+
# initialise pkg creator
212+
pkg_creator = PkgCreator()
213+
214+
# initialise pkg uploader
215+
bucket_name = os.getenv('R2_BUCKET_NAME')
216+
client_id = os.getenv('R2_CLIENT_ID')
217+
client_secret = os.getenv('R2_CLIENT_SECRET')
218+
tunnel_account_id = '5ab4e9dfbd435d24068829fda0077963'
219+
kv_namespace = os.getenv('KV_NAMESPACE')
220+
kv_api_token = os.getenv('KV_API_TOKEN')
221+
release_version = os.getenv('RELEASE_VERSION')
222+
gpg_key_id = os.getenv('GPG_KEY_ID')
223+
224+
pkg_uploader = PkgUploader(kv_api_token, kv_namespace, tunnel_account_id, bucket_name, client_id, client_secret)
225+
226+
archs = ["amd64", "386", "arm64"]
227+
flavors = ["bullseye", "buster", "bionic"]
228+
for arch in archs:
229+
create_deb_packaging(pkg_creator, pkg_uploader, flavors, gpg_key_id, "cloudflared", arch, "main", release_version)

0 commit comments

Comments
 (0)