Files
zulip/tools/oneclickapps/prepare_digital_ocean_one_click_app_release.py
2024-07-13 22:28:22 -07:00

171 lines
5.3 KiB
Python

import os
import subprocess
import time
from pathlib import Path
import digitalocean
import zulip
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
manager = digitalocean.Manager(token=os.environ["DIGITALOCEAN_API_KEY"])
# We just temporarily create the client now, to validate that we can
# auth to the server; reusing it after the whole install fails because
# the connection has been half-closed in a way that breaks it.
zulip.Client()
TEST_DROPLET_SUBDOMAIN = "do"
def generate_ssh_keys() -> None:
subprocess.check_call(
["ssh-keygen", "-f", str(Path.home()) + "/.ssh/id_ed25519", "-P", "", "-t", "ed25519"]
)
def get_public_ssh_key() -> str:
try:
with open(str(Path.home()) + "/.ssh/id_ed25519.pub") as f:
return f.read()
except FileNotFoundError:
return ""
def sleep_until_droplet_action_is_completed(
droplet: digitalocean.Droplet, action_type: str
) -> None:
incomplete = True
while incomplete:
for action in droplet.get_actions():
action.load()
print(f"...[{action.type}]: {action.status}")
if action.type == action_type and action.status == "completed":
incomplete = False
break
if incomplete:
time.sleep(5)
# Sometimes the droplet does not yet have an .ip_address value
# (the attribute is None) after .load()ing the droplet. We cannot
# proceed without the IP, so we wait in a loop until the IP is
# returned to us.
while True:
droplet.load()
if droplet.ip_address:
break
time.sleep(5)
def set_api_request_retry_limits(api_object: digitalocean.baseapi.BaseAPI) -> None:
retry = Retry(connect=5, read=5, backoff_factor=0.1)
adapter = HTTPAdapter(max_retries=retry)
api_object._session.mount("https://", adapter)
def create_droplet(
name: str, ssh_keys: list[str], image: str = "ubuntu-22-04-x64"
) -> digitalocean.Droplet:
droplet = digitalocean.Droplet(
token=manager.token,
name=name,
region="nyc3",
size_slug="s-1vcpu-2gb",
image=image,
backups=False,
ssh_keys=ssh_keys,
tags=["github-action", "temporary"],
)
set_api_request_retry_limits(droplet)
droplet.create()
sleep_until_droplet_action_is_completed(droplet, "create")
return droplet
def create_ssh_key(name: str, public_key: str) -> digitalocean.SSHKey:
action_public_ssh_key_object = digitalocean.SSHKey(
name=name, public_key=public_key, token=manager.token
)
set_api_request_retry_limits(action_public_ssh_key_object)
action_public_ssh_key_object.create()
return action_public_ssh_key_object
def create_snapshot(droplet: digitalocean.Droplet, snapshot_name: str) -> None:
droplet.take_snapshot(snapshot_name, power_off=True)
droplet.load()
sleep_until_droplet_action_is_completed(droplet, "snapshot")
def create_dns_records(droplet: digitalocean.Droplet) -> None:
domain = digitalocean.Domain(token=manager.token, name="oneclick.zulip.dev")
set_api_request_retry_limits(domain)
domain.load()
oneclick_test_app_record_names = [TEST_DROPLET_SUBDOMAIN, f"*.{TEST_DROPLET_SUBDOMAIN}"]
for record in domain.get_records():
if (
record.name in oneclick_test_app_record_names
and record.domain == "oneclick.zulip.dev"
and record.type == "A"
):
record.destroy()
domain.load()
for record_name in oneclick_test_app_record_names:
domain.create_new_domain_record(type="A", name=record_name, data=droplet.ip_address)
def setup_one_click_app_installer(droplet: digitalocean.Droplet) -> None:
subprocess.check_call(
[
"fab",
"build_image",
"-H",
droplet.ip_address,
"--keepalive",
"5",
"--connection-attempts",
"10",
],
cwd="marketplace-partners/marketplace_docs/templates/Fabric",
)
def send_message(content: str) -> None:
request = {
"type": "stream",
"to": os.environ["ONE_CLICK_ACTION_STREAM"],
"topic": "digitalocean installer",
"content": content,
}
zulip.Client().send_message(request)
if __name__ == "__main__":
release_version = os.environ["RELEASE_VERSION"]
generate_ssh_keys()
action_public_ssh_key_object = create_ssh_key(
f"oneclickapp-{release_version}-image-generator-public-key", get_public_ssh_key()
)
image_generator_droplet = create_droplet(
f"oneclickapp-{release_version}-image-generator", [action_public_ssh_key_object]
)
setup_one_click_app_installer(image_generator_droplet)
oneclick_image_name = f"oneclickapp-{release_version}"
create_snapshot(image_generator_droplet, oneclick_image_name)
snapshot = image_generator_droplet.get_snapshots()[0]
send_message(f"One click app image `{oneclick_image_name}` created.")
image_generator_droplet.destroy()
action_public_ssh_key_object.destroy()
test_droplet_name = f"oneclickapp-{release_version}-test"
test_droplet = create_droplet(test_droplet_name, manager.get_all_sshkeys(), image=snapshot.id)
create_dns_records(test_droplet)
send_message(
f"Test droplet `{test_droplet_name}` created. SSH as root to {TEST_DROPLET_SUBDOMAIN}.oneclick.zulip.dev for testing."
)