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.
This commit is contained in:
parent
8250708b37
commit
1e71202c89
|
@ -0,0 +1,229 @@
|
||||||
|
"""
|
||||||
|
This is a utility for creating deb and rpm packages, signing them
|
||||||
|
and uploading them to a storage and adding metadata to workers KV.
|
||||||
|
|
||||||
|
It has two over-arching responsiblities:
|
||||||
|
1. Create deb and yum repositories from .deb and .rpm files.
|
||||||
|
This is also responsible for signing the packages and generally preparing
|
||||||
|
them to be in an uploadable state.
|
||||||
|
2. Upload these packages to a storage and add metadata cross reference
|
||||||
|
for these to be accessed.
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import io
|
||||||
|
import shutil
|
||||||
|
import logging
|
||||||
|
from hashlib import sha256
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
from botocore.client import Config
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
|
||||||
|
BASE_KV_URL = 'https://api.cloudflare.com/client/v4/accounts/'
|
||||||
|
# The front facing R2 URL to access assets from.
|
||||||
|
R2_ASSET_URL = 'https://demo-r2-worker.cloudflare-tunnel.workers.dev/'
|
||||||
|
|
||||||
|
class PkgUploader:
|
||||||
|
def __init__(self, kv_api_token, namespace, account_id, bucket_name, client_id, client_secret):
|
||||||
|
self.kv_api_token = kv_api_token
|
||||||
|
self.namespace = namespace
|
||||||
|
self.account_id = account_id
|
||||||
|
self.bucket_name = bucket_name
|
||||||
|
self.client_id = client_id
|
||||||
|
self.client_secret = client_secret
|
||||||
|
|
||||||
|
def send_to_kv(self, key, value):
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + self.kv_api_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
kv_url = f"{BASE_KV_URL}{self.account_id}/storage/kv/namespaces/{self.namespace}/values/{key}"
|
||||||
|
response = requests.put(
|
||||||
|
kv_url,
|
||||||
|
headers=headers,
|
||||||
|
data=value
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
jsonResponse = response.json()
|
||||||
|
errors = jsonResponse["errors"]
|
||||||
|
if len(errors) > 0:
|
||||||
|
raise Exception("failed to send to workers kv: {0}", errors[0])
|
||||||
|
else:
|
||||||
|
raise Exception("recieved error code: {0}", response.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
def send_pkg_info(self, binary, flavor, asset_name, arch, uploaded_package_location):
|
||||||
|
key = f"pkg_{binary}_{flavor}_{arch}_{asset_name}"
|
||||||
|
print(f"writing key:{key} , value: {uploaded_package_location}")
|
||||||
|
self.send_to_kv(key, uploaded_package_location)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_pkg_to_r2(self, filename, upload_file_path):
|
||||||
|
endpoint_url = f"https://{self.account_id}.r2.cloudflarestorage.com"
|
||||||
|
token_secret_hash = sha256(self.client_secret.encode()).hexdigest()
|
||||||
|
|
||||||
|
config = Config(
|
||||||
|
s3={
|
||||||
|
"addressing_style": "path",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
r2 = boto3.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url=endpoint_url,
|
||||||
|
aws_access_key_id=self.client_id,
|
||||||
|
aws_secret_access_key=token_secret_hash,
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"uploading asset: {filename} to {upload_file_path}...")
|
||||||
|
try:
|
||||||
|
r2.upload_file(filename, self.bucket_name, upload_file_path)
|
||||||
|
except ClientError as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
class PkgCreator:
|
||||||
|
"""
|
||||||
|
The distribution conf is what dictates to reprepro, the debian packaging building
|
||||||
|
and signing tool we use, what distros to support, what GPG key to use for signing
|
||||||
|
and what to call the debian binary etc. This function creates it "./conf/distributions".
|
||||||
|
|
||||||
|
origin - name of your package (String)
|
||||||
|
label - label of your package (could be same as the name) (String)
|
||||||
|
flavor - flavor you want this to be distributed for (List of Strings)
|
||||||
|
components - could be a channel like main/stable/beta
|
||||||
|
archs - Architecture (List of Strings)
|
||||||
|
description - (String)
|
||||||
|
gpg_key_id - gpg key id of what you want to use to sign the packages.(String)
|
||||||
|
"""
|
||||||
|
def create_distribution_conf(self,
|
||||||
|
file_path,
|
||||||
|
origin,
|
||||||
|
label,
|
||||||
|
flavors,
|
||||||
|
archs,
|
||||||
|
components,
|
||||||
|
description,
|
||||||
|
gpg_key_id ):
|
||||||
|
with open(file_path, "w") as distributions_file:
|
||||||
|
for flavor in flavors:
|
||||||
|
distributions_file.write(f"Origin: {origin}\n")
|
||||||
|
distributions_file.write(f"Label: {label}\n")
|
||||||
|
distributions_file.write(f"Codename: {flavor}\n")
|
||||||
|
archs_list = " ".join(archs)
|
||||||
|
distributions_file.write(f"Architectures: {archs_list}\n")
|
||||||
|
distributions_file.write(f"Components: {components}\n")
|
||||||
|
distributions_file.write(f"Description: {description} - {flavor}\n")
|
||||||
|
distributions_file.write(f"SignWith: {gpg_key_id}\n")
|
||||||
|
distributions_file.write("\n")
|
||||||
|
return distributions_file
|
||||||
|
|
||||||
|
"""
|
||||||
|
Uses the reprepro tool to generate packages, sign them and create the InRelease as specified
|
||||||
|
by the distribution_conf file.
|
||||||
|
|
||||||
|
This function creates three folders db, pool and dist.
|
||||||
|
db and pool contain information and metadata about builds. We can ignore these.
|
||||||
|
dist: contains all the pkgs and signed releases that are necessary for an apt download.
|
||||||
|
"""
|
||||||
|
def create_deb_pkgs(self, flavor, deb_file):
|
||||||
|
self._clean_build_resources()
|
||||||
|
subprocess.call(("reprepro", "includedeb", flavor, deb_file))
|
||||||
|
|
||||||
|
"""
|
||||||
|
This is mostly useful to clear previously built db, dist and pool resources.
|
||||||
|
"""
|
||||||
|
def _clean_build_resources(self):
|
||||||
|
subprocess.call(("reprepro", "clearvanished"))
|
||||||
|
|
||||||
|
def upload_from_directories(pkg_uploader, directory, arch, release):
|
||||||
|
for root, _ , files in os.walk(directory):
|
||||||
|
for file in files:
|
||||||
|
root_elements = root.split("/")
|
||||||
|
upload_file_name = os.path.join(root, arch, release, file)
|
||||||
|
flavor_prefix = root_elements[1]
|
||||||
|
if root_elements[0] == "pool":
|
||||||
|
upload_file_name = os.path.join(root, file)
|
||||||
|
flavor_prefix = "deb"
|
||||||
|
filename = os.path.join(root,file)
|
||||||
|
try:
|
||||||
|
pkg_uploader.upload_pkg_to_r2(filename, upload_file_name)
|
||||||
|
except ClientError as e:
|
||||||
|
logging.error(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
# save to workers kv in the following formats
|
||||||
|
# Example:
|
||||||
|
# key : pkg_cloudflared_bullseye_InRelease,
|
||||||
|
# value: https://r2.cloudflarestorage.com/dists/bullseye/amd64/2022_3_4/InRelease
|
||||||
|
r2_asset_url = f"{R2_ASSET_URL}{upload_file_name}"
|
||||||
|
pkg_uploader.send_pkg_info("cloudflared", flavor_prefix, upload_file_name, arch, r2_asset_url)
|
||||||
|
|
||||||
|
# TODO https://jira.cfops.it/browse/TUN-6163: Add a key for latest version.
|
||||||
|
|
||||||
|
"""
|
||||||
|
1. looks into a built_artifacts folder for cloudflared debs
|
||||||
|
2. creates Packages.gz, InRelease (signed) files
|
||||||
|
3. uploads them to Cloudflare R2 and
|
||||||
|
4. adds a Workers KV reference
|
||||||
|
|
||||||
|
pkg_creator, pkg_uploader: are instantiations of the two classes above.
|
||||||
|
|
||||||
|
gpg_key_id: is an id indicating the key the package should be signed with. The public key of this id will be
|
||||||
|
uploaded to R2 so it can be presented to apt downloaders.
|
||||||
|
|
||||||
|
release_version: is the cloudflared release version.
|
||||||
|
"""
|
||||||
|
def create_deb_packaging(pkg_creator, pkg_uploader, flavors, gpg_key_id, binary_name, arch, package_component, release_version):
|
||||||
|
# set configuration for package creation.
|
||||||
|
print(f"initialising configuration for {binary_name} , {arch}")
|
||||||
|
pkg_creator.create_distribution_conf(
|
||||||
|
"./conf/distributions",
|
||||||
|
binary_name,
|
||||||
|
binary_name,
|
||||||
|
flavors,
|
||||||
|
[arch],
|
||||||
|
package_component,
|
||||||
|
f"apt repository for {binary_name}",
|
||||||
|
gpg_key_id)
|
||||||
|
|
||||||
|
# create deb pkgs
|
||||||
|
for flavor in flavors:
|
||||||
|
print(f"creating deb pkgs for {flavor} and {arch}...")
|
||||||
|
pkg_creator.create_deb_pkgs(flavor, f"./built_artifacts/cloudflared-linux-{arch}.deb")
|
||||||
|
|
||||||
|
print("uploading to r2...")
|
||||||
|
upload_from_directories(pkg_uploader, "dists", arch, release_version)
|
||||||
|
upload_from_directories(pkg_uploader, "pool", arch, release_version)
|
||||||
|
|
||||||
|
print("cleaning up directories...")
|
||||||
|
shutil.rmtree("./dists")
|
||||||
|
shutil.rmtree("./pool")
|
||||||
|
shutil.rmtree("./db")
|
||||||
|
|
||||||
|
#TODO: https://jira.cfops.it/browse/TUN-6146 will extract this into it's own command line script.
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# initialise pkg creator
|
||||||
|
pkg_creator = PkgCreator()
|
||||||
|
|
||||||
|
# initialise pkg uploader
|
||||||
|
bucket_name = os.getenv('R2_BUCKET_NAME')
|
||||||
|
client_id = os.getenv('R2_CLIENT_ID')
|
||||||
|
client_secret = os.getenv('R2_CLIENT_SECRET')
|
||||||
|
tunnel_account_id = '5ab4e9dfbd435d24068829fda0077963'
|
||||||
|
kv_namespace = os.getenv('KV_NAMESPACE')
|
||||||
|
kv_api_token = os.getenv('KV_API_TOKEN')
|
||||||
|
release_version = os.getenv('RELEASE_VERSION')
|
||||||
|
gpg_key_id = os.getenv('GPG_KEY_ID')
|
||||||
|
|
||||||
|
pkg_uploader = PkgUploader(kv_api_token, kv_namespace, tunnel_account_id, bucket_name, client_id, client_secret)
|
||||||
|
|
||||||
|
archs = ["amd64", "386", "arm64"]
|
||||||
|
flavors = ["bullseye", "buster", "bionic"]
|
||||||
|
for arch in archs:
|
||||||
|
create_deb_packaging(pkg_creator, pkg_uploader, flavors, gpg_key_id, "cloudflared", arch, "main", release_version)
|
Loading…
Reference in New Issue