diff --git a/cfsetup.yaml b/cfsetup.yaml index e4e4f679..99e72e23 100644 --- a/cfsetup.yaml +++ b/cfsetup.yaml @@ -220,9 +220,11 @@ stretch: &stretch pre-cache: - sudo pip3 install --upgrade -r component-tests/requirements.txt post-cache: - # Constructs config file from env vars - - python3 component-tests/config.py + # Creates and routes a Named Tunnel for this build. Also constructs config file from env vars. + - python3 component-tests/setup.py --type create - pytest component-tests + # The Named Tunnel is deleted and its route unprovisioned here. + - python3 component-tests/setup.py --type cleanup update-homebrew: builddeps: - openssh-client diff --git a/component-tests/config.py b/component-tests/config.py index 4598450e..7a3248cc 100644 --- a/component-tests/config.py +++ b/component-tests/config.py @@ -1,15 +1,10 @@ #!/usr/bin/env python -import base64 import copy -import os -import yaml from dataclasses import dataclass, InitVar from constants import METRICS_PORT -from util import LOGGER - # frozen=True raises exception when assigning to fields. This emulates immutability @@ -98,35 +93,3 @@ class ClassicTunnelConfig(ClassicTunnelBaseConfig): def get_url(self): return "https://" + self.hostname - - -def build_config_from_env(): - config_path = get_env("COMPONENT_TESTS_CONFIG") - config_content = base64.b64decode( - get_env("COMPONENT_TESTS_CONFIG_CONTENT")).decode('utf-8') - config_yaml = yaml.safe_load(config_content) - - credentials_file = get_env("COMPONENT_TESTS_CREDENTIALS_FILE") - write_file(credentials_file, config_yaml["credentials_file"]) - - origincert = get_env("COMPONENT_TESTS_ORIGINCERT") - write_file(origincert, config_yaml["origincert"]) - - write_file(config_content, config_path) - - -def write_file(content, path): - with open(path, 'w') as outfile: - outfile.write(content) - outfile.close - - -def get_env(env_name): - val = os.getenv(env_name) - if val is None: - raise Exception(f"{env_name} is not set") - return val - - -if __name__ == '__main__': - build_config_from_env() diff --git a/component-tests/requirements.txt b/component-tests/requirements.txt index edd3e1f4..061e960c 100644 --- a/component-tests/requirements.txt +++ b/component-tests/requirements.txt @@ -1,3 +1,4 @@ +cloudflare==2.8.15 flaky==3.7.0 pytest==6.2.2 pyyaml==5.4.1 diff --git a/component-tests/setup.py b/component-tests/setup.py new file mode 100644 index 00000000..61352618 --- /dev/null +++ b/component-tests/setup.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +import argparse +import base64 +import json +import os +import subprocess +import uuid + +import CloudFlare +import yaml +from retrying import retry + +from constants import MAX_RETRIES, BACKOFF_SECS +from util import LOGGER + + +def get_config_from_env(): + config_content = base64.b64decode(get_env("COMPONENT_TESTS_CONFIG_CONTENT")).decode('utf-8') + return yaml.safe_load(config_content) + + +def get_config_from_file(): + config_path = get_env("COMPONENT_TESTS_CONFIG") + with open(config_path, 'r') as infile: + return yaml.safe_load(infile) + + +def persist_config(config): + config_path = get_env("COMPONENT_TESTS_CONFIG") + with open(config_path, 'w') as outfile: + yaml.safe_dump(config, outfile) + + +def persist_origin_cert(config): + origincert = get_env("COMPONENT_TESTS_ORIGINCERT") + path = config["origincert"] + with open(path, 'w') as outfile: + outfile.write(origincert) + return path + + +@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) +def create_tunnel(config, origincert_path, random_uuid): + # Delete any previous existing credentials file. If the agent keeps files around (that's the case in Windows) then + # cloudflared tunnel create will refuse to create the tunnel because it does not want to overwrite credentials + # files. + credentials_path = config["credentials_file"] + try: + os.remove(credentials_path) + except OSError: + pass + + tunnel_name = "cfd_component_test-" + random_uuid + create_cmd = [config["cloudflared_binary"], "tunnel", "--origincert", origincert_path, "create", + "--credentials-file", credentials_path, tunnel_name] + LOGGER.info(f"Creating tunnel with {create_cmd}") + subprocess.run(create_cmd, check=True) + + list_cmd = [config["cloudflared_binary"], "tunnel", "--origincert", origincert_path, "list", "--name", + tunnel_name, "--output", "json"] + LOGGER.info(f"Listing tunnel with {list_cmd}") + cloudflared = subprocess.run(list_cmd, check=True, capture_output=True) + return json.loads(cloudflared.stdout)[0]["id"] + + +@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) +def delete_tunnel(config): + credentials_path = config["credentials_file"] + delete_cmd = [config["cloudflared_binary"], "tunnel", "--origincert", config["origincert"], "delete", + "--credentials-file", credentials_path, "-f", config["tunnel"]] + LOGGER.info(f"Deleting tunnel with {delete_cmd}") + subprocess.run(delete_cmd, check=True) + + +@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) +def create_dns(config, hostname, type, content): + cf = CloudFlare.CloudFlare(debug=True, token=get_env("DNS_API_TOKEN")) + cf.zones.dns_records.post( + config["zone_tag"], + data={'name': hostname, 'type': type, 'content': content, 'proxied': True} + ) + + +def create_classic_dns(config, random_uuid): + classic_hostname = "classic-" + random_uuid + "." + config["zone_domain"] + create_dns(config, classic_hostname, "AAAA", "fd10:aec2:5dae::") + return classic_hostname + + +def create_named_dns(config, random_uuid): + hostname = "named-" + random_uuid + "." + config["zone_domain"] + create_dns(config, hostname, "CNAME", config["tunnel"] + ".cfargotunnel.com") + return hostname + + +@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) +def delete_dns(config, hostname): + cf = CloudFlare.CloudFlare(debug=True, token=get_env("DNS_API_TOKEN")) + zone_tag = config["zone_tag"] + dns_records = cf.zones.dns_records.get(zone_tag, params={'name': hostname}) + if len(dns_records) > 0: + cf.zones.dns_records.delete(zone_tag, dns_records[0]['id']) + + +def write_file(content, path): + with open(path, 'w') as outfile: + outfile.write(content) + + +def get_env(env_name): + val = os.getenv(env_name) + if val is None: + raise Exception(f"{env_name} is not set") + return val + + +def create(): + """ + Creates the necessary resources for the components test to run. + - Creates a named tunnel with a random name. + - Creates a random CNAME DNS entry for that tunnel. + - Creates a random AAAA DNS entry for a classic tunnel. + + Those created resources are added to the config (obtained from an environment variable). + The resulting configuration is persisted for the tests to use. + """ + config = get_config_from_env() + origincert_path = persist_origin_cert(config) + + random_uuid = str(uuid.uuid4()) + config["tunnel"] = create_tunnel(config, origincert_path, random_uuid) + config["classic_hostname"] = create_classic_dns(config, random_uuid) + config["ingress"] = [ + { + "hostname": create_named_dns(config, random_uuid), + "service": "hello_world" + }, + { + "service": "http_status:404" + } + ] + + persist_config(config) + + +def cleanup(): + """ + Reads the persisted configuration that was created previously. + Deletes the resources that were created there. + """ + config = get_config_from_file() + delete_tunnel(config) + delete_dns(config, config["classic_hostname"]) + delete_dns(config, config["ingress"][0]["hostname"]) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='setup component tests') + parser.add_argument('--type', choices=['create', 'cleanup'], default='create') + args = parser.parse_args() + + if args.type == 'create': + create() + else: + cleanup() diff --git a/component-tests/test_logging.py b/component-tests/test_logging.py index 3a025682..f8923425 100644 --- a/component-tests/test_logging.py +++ b/component-tests/test_logging.py @@ -1,7 +1,6 @@ #!/usr/bin/env python import json import os -import subprocess from util import start_cloudflared, wait_tunnel_ready, send_requests