release_pkgs.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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. It has two over-arching responsiblities:
  5. 1. Create deb and yum repositories from .deb and .rpm files.
  6. This is also responsible for signing the packages and generally preparing
  7. them to be in an uploadable state.
  8. 2. Upload these packages to a storage in a format that apt and yum expect.
  9. """
  10. import argparse
  11. import base64
  12. import logging
  13. import os
  14. import shutil
  15. from pathlib import Path
  16. from subprocess import Popen, PIPE
  17. import boto3
  18. import gnupg
  19. from botocore.client import Config
  20. from botocore.exceptions import ClientError
  21. # The front facing R2 URL to access assets from.
  22. R2_ASSET_URL = 'https://demo-r2-worker.cloudflare-tunnel.workers.dev/'
  23. class PkgUploader:
  24. def __init__(self, account_id, bucket_name, client_id, client_secret):
  25. self.account_id = account_id
  26. self.bucket_name = bucket_name
  27. self.client_id = client_id
  28. self.client_secret = client_secret
  29. def upload_pkg_to_r2(self, filename, upload_file_path):
  30. endpoint_url = f"https://{self.account_id}.r2.cloudflarestorage.com"
  31. config = Config(
  32. region_name='auto',
  33. s3={
  34. "addressing_style": "path",
  35. }
  36. )
  37. r2 = boto3.client(
  38. "s3",
  39. endpoint_url=endpoint_url,
  40. aws_access_key_id=self.client_id,
  41. aws_secret_access_key=self.client_secret,
  42. config=config,
  43. )
  44. print(f"uploading asset: {filename} to {upload_file_path} in bucket{self.bucket_name}...")
  45. try:
  46. r2.upload_file(filename, self.bucket_name, upload_file_path)
  47. except ClientError as e:
  48. raise e
  49. class PkgCreator:
  50. """
  51. The distribution conf is what dictates to reprepro, the debian packaging building
  52. and signing tool we use, what distros to support, what GPG key to use for signing
  53. and what to call the debian binary etc. This function creates it "./conf/distributions".
  54. origin - name of your package (String)
  55. label - label of your package (could be same as the name) (String)
  56. release - release you want this to be distributed for (List of Strings)
  57. components - could be a channel like main/stable/beta
  58. archs - Architecture (List of Strings)
  59. description - (String)
  60. gpg_key_id - gpg key id of what you want to use to sign the packages.(String)
  61. """
  62. def create_distribution_conf(self,
  63. file_path,
  64. origin,
  65. label,
  66. releases,
  67. archs,
  68. components,
  69. description,
  70. gpg_key_id):
  71. with open(file_path, "w+") as distributions_file:
  72. for release in releases:
  73. distributions_file.write(f"Origin: {origin}\n")
  74. distributions_file.write(f"Label: {label}\n")
  75. distributions_file.write(f"Codename: {release}\n")
  76. archs_list = " ".join(archs)
  77. distributions_file.write(f"Architectures: {archs_list}\n")
  78. distributions_file.write(f"Components: {components}\n")
  79. distributions_file.write(f"Description: {description} - {release}\n")
  80. distributions_file.write(f"SignWith: {gpg_key_id}\n")
  81. distributions_file.write("\n")
  82. return distributions_file
  83. """
  84. Uses the reprepro tool to generate packages, sign them and create the InRelease as specified
  85. by the distribution_conf file.
  86. This function creates three folders db, pool and dist.
  87. db and pool contain information and metadata about builds. We can ignore these.
  88. dist: contains all the pkgs and signed releases that are necessary for an apt download.
  89. """
  90. def create_deb_pkgs(self, release, deb_file):
  91. print(f"creating deb pkgs: {release} : {deb_file}")
  92. p = Popen(["reprepro", "includedeb", release, deb_file], stdout=PIPE, stderr=PIPE)
  93. out, err = p.communicate()
  94. if p.returncode != 0:
  95. print(f"create deb_pkgs result => {out}, {err}")
  96. raise
  97. def create_rpm_pkgs(self, artifacts_path, gpg_key_name):
  98. self._setup_rpm_pkg_directories(artifacts_path, gpg_key_name)
  99. p = Popen(["createrepo", "./rpm"], stdout=PIPE, stderr=PIPE)
  100. out, err = p.communicate()
  101. if p.returncode != 0:
  102. print(f"create rpm_pkgs result => {out}, {err}")
  103. raise
  104. self._sign_repomd()
  105. """
  106. creates a <binary>.repo file with details like so
  107. [cloudflared-stable]
  108. name=cloudflared-stable
  109. baseurl=https://pkg.cloudflare.com/cloudflared/rpm
  110. enabled=1
  111. type=rpm
  112. gpgcheck=1
  113. gpgkey=https://pkg.cloudflare.com/cloudflare-main.gpg
  114. """
  115. def create_repo_file(self, file_path, binary_name, baseurl, gpgkey_url):
  116. with open(os.path.join(file_path, binary_name + '.repo'), "w+") as repo_file:
  117. repo_file.write(f"[{binary_name}-stable]")
  118. repo_file.write(f"{binary_name}-stable")
  119. repo_file.write(f"baseurl={baseurl}/rpm")
  120. repo_file.write("enabled=1")
  121. repo_file.write("type=rpm")
  122. repo_file.write("gpgcheck=1")
  123. repo_file.write(f"gpgkey={gpgkey_url}")
  124. def _sign_rpms(self, file_path):
  125. p = Popen(["rpm", "--define", f"_gpg_name {gpg_key_name}", "--addsign", file_path], stdout=PIPE, stderr=PIPE)
  126. out, err = p.communicate()
  127. if p.returncode != 0:
  128. print(f"rpm sign result result => {out}, {err}")
  129. raise
  130. def _sign_repomd(self):
  131. p = Popen(["gpg", "--batch", "--detach-sign", "--armor", "./rpm/repodata/repomd.xml"], stdout=PIPE, stderr=PIPE)
  132. out, err = p.communicate()
  133. if p.returncode != 0:
  134. print(f"sign repomd result => {out}, {err}")
  135. raise
  136. """
  137. sets up and signs the RPM directories in the following format:
  138. - rpm
  139. - aarch64
  140. - x86_64
  141. - 386
  142. this assumes the assets are in the format <prefix>-<aarch64/x86_64/386>.rpm
  143. """
  144. def _setup_rpm_pkg_directories(self, artifacts_path, gpg_key_name, archs=["aarch64", "x86_64", "386"]):
  145. for arch in archs:
  146. for root, _, files in os.walk(artifacts_path):
  147. for file in files:
  148. if file.endswith(f"{arch}.rpm"):
  149. new_dir = f"./rpm/{arch}"
  150. os.makedirs(new_dir, exist_ok=True)
  151. old_path = os.path.join(root, file)
  152. new_path = os.path.join(new_dir, file)
  153. shutil.copyfile(old_path, new_path)
  154. self._sign_rpms(new_path)
  155. """
  156. imports gpg keys into the system so reprepro and createrepo can use it to sign packages.
  157. it returns the GPG ID after a successful import
  158. """
  159. def import_gpg_keys(self, private_key, public_key):
  160. gpg = gnupg.GPG()
  161. private_key = base64.b64decode(private_key)
  162. gpg.import_keys(private_key)
  163. public_key = base64.b64decode(public_key)
  164. gpg.import_keys(public_key)
  165. data = gpg.list_keys(secret=True)
  166. return (data[0]["fingerprint"], data[0]["uids"][0])
  167. """
  168. basically rpm --import <key_file>
  169. This enables us to sign rpms.
  170. """
  171. def import_rpm_key(self, public_key):
  172. file_name = "pb.key"
  173. with open(file_name, "wb") as f:
  174. public_key = base64.b64decode(public_key)
  175. f.write(public_key)
  176. p = Popen(["rpm", "--import", file_name], stdout=PIPE, stderr=PIPE)
  177. out, err = p.communicate()
  178. if p.returncode != 0:
  179. print(f"create rpm import result => {out}, {err}")
  180. raise
  181. """
  182. Walks through a directory and uploads it's assets to R2.
  183. directory : root directory to walk through (String).
  184. release: release string. If this value is none, a specific release path will not be created
  185. and the release will be uploaded to the default path.
  186. binary: name of the binary to upload
  187. """
  188. def upload_from_directories(pkg_uploader, directory, release, binary):
  189. for root, _, files in os.walk(directory):
  190. for file in files:
  191. upload_file_name = os.path.join(binary, root, file)
  192. if release:
  193. upload_file_name = os.path.join(release, upload_file_name)
  194. filename = os.path.join(root, file)
  195. try:
  196. pkg_uploader.upload_pkg_to_r2(filename, upload_file_name)
  197. except ClientError as e:
  198. logging.error(e)
  199. return
  200. """
  201. 1. looks into a built_artifacts folder for cloudflared debs
  202. 2. creates Packages.gz, InRelease (signed) files
  203. 3. uploads them to Cloudflare R2
  204. pkg_creator, pkg_uploader: are instantiations of the two classes above.
  205. gpg_key_id: is an id indicating the key the package should be signed with. The public key of this id will be
  206. uploaded to R2 so it can be presented to apt downloaders.
  207. release_version: is the cloudflared release version. Only provide this if you want a permanent backup.
  208. """
  209. def create_deb_packaging(pkg_creator, pkg_uploader, releases, gpg_key_id, binary_name, archs, package_component,
  210. release_version):
  211. # set configuration for package creation.
  212. print(f"initialising configuration for {binary_name} , {archs}")
  213. Path("./conf").mkdir(parents=True, exist_ok=True)
  214. pkg_creator.create_distribution_conf(
  215. "./conf/distributions",
  216. binary_name,
  217. binary_name,
  218. releases,
  219. archs,
  220. package_component,
  221. f"apt repository for {binary_name}",
  222. gpg_key_id)
  223. # create deb pkgs
  224. for release in releases:
  225. for arch in archs:
  226. print(f"creating deb pkgs for {release} and {arch}...")
  227. pkg_creator.create_deb_pkgs(release, f"./built_artifacts/cloudflared-linux-{arch}.deb")
  228. print("uploading latest to r2...")
  229. upload_from_directories(pkg_uploader, "dists", None, binary_name)
  230. upload_from_directories(pkg_uploader, "pool", None, binary_name)
  231. if release_version:
  232. print(f"uploading versioned release {release_version} to r2...")
  233. upload_from_directories(pkg_uploader, "dists", release_version, binary_name)
  234. upload_from_directories(pkg_uploader, "pool", release_version, binary_name)
  235. def create_rpm_packaging(
  236. pkg_creator,
  237. pkg_uploader,
  238. artifacts_path,
  239. release_version,
  240. binary_name,
  241. gpg_key_name,
  242. base_url,
  243. gpg_key_url,
  244. ):
  245. print(f"creating rpm pkgs...")
  246. pkg_creator.create_rpm_pkgs(artifacts_path, gpg_key_name)
  247. pkg_creator.create_repo_file(artifacts_path, binary_name, base_url, gpg_key_url)
  248. print("uploading latest to r2...")
  249. upload_from_directories(pkg_uploader, "rpm", None, binary_name)
  250. if release_version:
  251. print(f"uploading versioned release {release_version} to r2...")
  252. upload_from_directories(pkg_uploader, "rpm", release_version, binary_name)
  253. def parse_args():
  254. parser = argparse.ArgumentParser(
  255. description="Creates linux releases and uploads them in a packaged format"
  256. )
  257. parser.add_argument(
  258. "--bucket", default=os.environ.get("R2_BUCKET"), help="R2 Bucket name"
  259. )
  260. parser.add_argument(
  261. "--id", default=os.environ.get("R2_CLIENT_ID"), help="R2 Client ID"
  262. )
  263. parser.add_argument(
  264. "--secret", default=os.environ.get("R2_CLIENT_SECRET"), help="R2 Client Secret"
  265. )
  266. parser.add_argument(
  267. "--account", default=os.environ.get("R2_ACCOUNT_ID"), help="R2 Account Tag"
  268. )
  269. parser.add_argument(
  270. "--release-tag", default=os.environ.get("RELEASE_VERSION"), help="Release version you want your pkgs to be\
  271. prefixed with. Leave empty if you don't want tagged release versions backed up to R2."
  272. )
  273. parser.add_argument(
  274. "--binary", default=os.environ.get("BINARY_NAME"), help="The name of the binary the packages are for"
  275. )
  276. parser.add_argument(
  277. "--gpg-private-key", default=os.environ.get("LINUX_SIGNING_PRIVATE_KEY"), help="GPG private key to sign the\
  278. packages"
  279. )
  280. parser.add_argument(
  281. "--gpg-public-key", default=os.environ.get("LINUX_SIGNING_PUBLIC_KEY"), help="GPG public key used for\
  282. signing packages"
  283. )
  284. parser.add_argument(
  285. "--gpg-public-key-url", default=os.environ.get("GPG_PUBLIC_KEY_URL"), help="GPG public key url that\
  286. downloaders can use to verify signing"
  287. )
  288. parser.add_argument(
  289. "--pkg-upload-url", default=os.environ.get("PKG_URL"), help="URL to be used by downloaders"
  290. )
  291. parser.add_argument(
  292. "--deb-based-releases", default=["bookworm", "bullseye", "buster", "jammy", "impish", "focal", "bionic",
  293. "xenial", "trusty"],
  294. help="list of debian based releases that need to be packaged for"
  295. )
  296. parser.add_argument(
  297. "--archs", default=["amd64", "386", "arm64", "arm", "armhf"], help="list of architectures we want to package for. Note that\
  298. it is the caller's responsiblity to ensure that these debs are already present in a directory. This script\
  299. will not build binaries or create their debs."
  300. )
  301. args = parser.parse_args()
  302. return args
  303. if __name__ == "__main__":
  304. try:
  305. args = parse_args()
  306. except Exception as e:
  307. logging.exception(e)
  308. exit(1)
  309. pkg_creator = PkgCreator()
  310. (gpg_key_id, gpg_key_name) = pkg_creator.import_gpg_keys(args.gpg_private_key, args.gpg_public_key)
  311. pkg_creator.import_rpm_key(args.gpg_public_key)
  312. pkg_uploader = PkgUploader(args.account, args.bucket, args.id, args.secret)
  313. print(f"signing with gpg_key: {gpg_key_id}")
  314. create_deb_packaging(pkg_creator, pkg_uploader, args.deb_based_releases, gpg_key_id, args.binary, args.archs,
  315. "main", args.release_tag)
  316. create_rpm_packaging(
  317. pkg_creator,
  318. pkg_uploader,
  319. "./built_artifacts",
  320. args.release_tag,
  321. args.binary,
  322. gpg_key_name,
  323. args.gpg_public_key_url,
  324. args.pkg_upload_url,
  325. )