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