From a7344435a5706e532e3d442f91a505fee933e9ce Mon Sep 17 00:00:00 2001 From: cthuang Date: Mon, 8 Mar 2021 14:09:10 +0000 Subject: [PATCH] TUN-4062: Read component tests config from yaml file --- cfsetup.yaml | 2 + component-tests/README.md | 10 ++++ component-tests/config.py | 97 ++++++++++++++++++++++++++++++++++ component-tests/conftest.py | 24 +++++++++ component-tests/constants.py | 1 + component-tests/test_config.py | 25 ++++----- component-tests/util.py | 17 +++--- 7 files changed, 155 insertions(+), 21 deletions(-) create mode 100644 component-tests/config.py create mode 100644 component-tests/conftest.py create mode 100644 component-tests/constants.py diff --git a/cfsetup.yaml b/cfsetup.yaml index 12092fc0..55b90f87 100644 --- a/cfsetup.yaml +++ b/cfsetup.yaml @@ -215,6 +215,8 @@ 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 - pytest component-tests update-homebrew: builddeps: diff --git a/component-tests/README.md b/component-tests/README.md index 81a4980d..c4c70ca0 100644 --- a/component-tests/README.md +++ b/component-tests/README.md @@ -5,7 +5,17 @@ - `conda activate component-tests` - `pip3 install -r requirements.txt` +2. Create a config yaml file, for example: +``` +cloudflared_binary: "cloudflared" +tunnel: "3d539f97-cd3a-4d8e-c33b-65e9099c7a8d" +credentials_file: "/Users/tunnel/.cloudflared/3d539f97-cd3a-4d8e-c33b-65e9099c7a8d.json" +classic_hostname: "classic-tunnel-component-tests.example.com" +origincert: "/Users/tunnel/.cloudflared/cert.pem" +``` + # How to run +Specify path to config file via env var `COMPONENT_TESTS_CONFIG`. This is required. ## All tests Run `pytest` inside this(component-tests) folder diff --git a/component-tests/config.py b/component-tests/config.py new file mode 100644 index 00000000..2ebbdacf --- /dev/null +++ b/component-tests/config.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +import base64 +import copy +import os +import yaml + +from dataclasses import dataclass + +from constants import METRICS_PORT + +# frozen=True raises exception when assigning to fields. This emulates immutability +@dataclass(frozen=True) +class TunnelBaseConfig: + no_autoupdate: bool = True + metrics: str = f'localhost:{METRICS_PORT}' + + def merge_config(self, additional): + config = copy.copy(additional) + config['no-autoupdate'] = self.no_autoupdate + config['metrics'] = self.metrics + return config + + +@dataclass(frozen=True) +class NamedTunnelBaseConfig(TunnelBaseConfig): + # The attributes of the parent class are ordered before attributes in this class, + # so we have to use default values here and check if they are set in __post_init__ + tunnel: str = None + credentials_file: str = None + + def __post_init__(self): + if self.tunnel is None: + raise TypeError("Field tunnel is not set") + if self.credentials_file is None: + raise TypeError("Field credentials_file is not set") + + def merge_config(self, additional): + config = super(NamedTunnelBaseConfig, self).merge_config(additional) + config['tunnel'] = self.tunnel + config['credentials-file'] = self.credentials_file + return config + + +@dataclass(frozen=True) +class ClassicTunnelBaseConfig(TunnelBaseConfig): + hostname: str = None + origincert: str = None + + def __post_init__(self): + if self.hostname is None: + raise TypeError("Field tunnel is not set") + if self.origincert is None: + raise TypeError("Field credentials_file is not set") + + def merge_config(self, additional): + config = super(ClassicTunnelBaseConfig, self).merge_config(additional) + config['hostnamel'] = self.hostname + config['origincert'] = self.origincert + return config + + +@dataclass +class ComponentTestConfig: + cloudflared_binary: str + named_tunnel_config: dict + classic_tunnel_config: dict + + +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() \ No newline at end of file diff --git a/component-tests/conftest.py b/component-tests/conftest.py new file mode 100644 index 00000000..204212f4 --- /dev/null +++ b/component-tests/conftest.py @@ -0,0 +1,24 @@ +import os +import pytest +import yaml + +from config import ComponentTestConfig, NamedTunnelBaseConfig, ClassicTunnelBaseConfig +from util import LOGGER + +@pytest.fixture(scope="session") +def component_tests_config(): + config_file = os.getenv("COMPONENT_TESTS_CONFIG") + if config_file is None: + raise Exception("Need to provide path to config file in COMPONENT_TESTS_CONFIG") + with open(config_file, 'r') as stream: + config = yaml.safe_load(stream) + LOGGER.info(f"component tests base config {config}") + base_named_tunnel_config = NamedTunnelBaseConfig(tunnel=config['tunnel'], credentials_file=config['credentials_file']) + base_classic_tunnel_config = ClassicTunnelBaseConfig(hostname=config['classic_hostname'], origincert=config['origincert']) + + def _component_tests_config(extra_named_tunnel_config={}, extra_classic_tunnel_config={}): + named_tunnel_config = base_named_tunnel_config.merge_config(extra_named_tunnel_config) + classic_tunnel_config = base_classic_tunnel_config.merge_config(extra_classic_tunnel_config) + return ComponentTestConfig(config['cloudflared_binary'], named_tunnel_config, classic_tunnel_config) + + return _component_tests_config \ No newline at end of file diff --git a/component-tests/constants.py b/component-tests/constants.py new file mode 100644 index 00000000..c2c759c5 --- /dev/null +++ b/component-tests/constants.py @@ -0,0 +1 @@ +METRICS_PORT = 51000 \ No newline at end of file diff --git a/component-tests/test_config.py b/component-tests/test_config.py index 5aa94e9d..814cb201 100644 --- a/component-tests/test_config.py +++ b/component-tests/test_config.py @@ -3,9 +3,8 @@ from util import start_cloudflared class TestConfig: # tmp_path is a fixture provides a temporary directory unique to the test invocation - def test_validate_ingress_rules(self, tmp_path): - config = { - 'metrics': 'localhost:50000', + def test_validate_ingress_rules(self, tmp_path, component_tests_config): + extra_config = { 'ingress': [ { "hostname": "example.com", @@ -31,23 +30,25 @@ class TestConfig: {"service": "http_status:404"} ], } + component_tests_config = component_tests_config(extra_config) validate_args = ["ingress", "validate"] pre_args = ["tunnel"] - validate = start_cloudflared(tmp_path, config, validate_args, pre_args) + validate = start_cloudflared(tmp_path, component_tests_config, validate_args, pre_args) assert validate.returncode == 0, "failed to validate ingress" + validate.stderr.decode("utf-8") - self.match_rule(tmp_path, config, "http://example.com/index.html", 1) - self.match_rule(tmp_path, config, "https://example.com/index.html", 1) - self.match_rule(tmp_path, config, "https://api.example.com/login", 2) - self.match_rule(tmp_path, config, "https://wss.example.com", 3) - self.match_rule(tmp_path, config, "https://ssh.example.com", 4) - self.match_rule(tmp_path, config, "https://api.example.com", 5) + self.match_rule(tmp_path, component_tests_config, "http://example.com/index.html", 1) + self.match_rule(tmp_path, component_tests_config, "https://example.com/index.html", 1) + self.match_rule(tmp_path, component_tests_config, "https://api.example.com/login", 2) + self.match_rule(tmp_path, component_tests_config, "https://wss.example.com", 3) + self.match_rule(tmp_path, component_tests_config, "https://ssh.example.com", 4) + self.match_rule(tmp_path, component_tests_config, "https://api.example.com", 5) + # This is used to check that the command tunnel ingress url matches rule number . Note that rule number uses 1-based indexing - def match_rule(self, tmp_path, config, url, rule_num): + def match_rule(self, tmp_path, component_tests_config, url, rule_num): args = ["ingress", "rule", url] pre_args = ["tunnel"] - match_rule = start_cloudflared(tmp_path, config, args, pre_args) + match_rule = start_cloudflared(tmp_path, component_tests_config, args, pre_args) assert match_rule.returncode == 0, "failed to check rule" + match_rule.stderr.decode("utf-8") assert f"Matched rule #{rule_num}" .encode() in match_rule.stdout \ No newline at end of file diff --git a/component-tests/util.py b/component-tests/util.py index aed3d82b..41c54ac7 100644 --- a/component-tests/util.py +++ b/component-tests/util.py @@ -5,11 +5,6 @@ import yaml LOGGER = logging.getLogger(__name__) -def get_cloudflared(): - cfd_binary = os.getenv('CFD_BINARY') - return "cloudflared" if cfd_binary is None else cfd_binary - - def write_config(path, config): config_path = path / "config.yaml" with open(config_path, 'w') as outfile: @@ -17,11 +12,15 @@ def write_config(path, config): return config_path -def start_cloudflared(path, config, args, pre_args=[]): +def start_cloudflared(path, component_test_config, cfd_args, cfd_pre_args=[], classic=False): + if classic: + config = component_test_config.classic_tunnel_config + else: + config = component_test_config.named_tunnel_config config_path = write_config(path, config) - cmd = [get_cloudflared()] - cmd += pre_args + cmd = [component_test_config.cloudflared_binary] + cmd += cfd_pre_args cmd += ["--config", config_path] - cmd += args + cmd += cfd_args LOGGER.info(f"Run cmd {cmd} with config {config}") return subprocess.run(cmd, capture_output=True) \ No newline at end of file