|
| 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