TUN-4062: Read component tests config from yaml file
This commit is contained in:
		
							parent
							
								
									206523344f
								
							
						
					
					
						commit
						a7344435a5
					
				|  | @ -215,6 +215,8 @@ stretch: &stretch | ||||||
|     pre-cache: |     pre-cache: | ||||||
|       - sudo pip3 install --upgrade -r component-tests/requirements.txt |       - sudo pip3 install --upgrade -r component-tests/requirements.txt | ||||||
|     post-cache: |     post-cache: | ||||||
|  |       # Constructs config file from env vars | ||||||
|  |       - python3 component-tests/config.py | ||||||
|       - pytest component-tests |       - pytest component-tests | ||||||
|   update-homebrew: |   update-homebrew: | ||||||
|     builddeps: |     builddeps: | ||||||
|  |  | ||||||
|  | @ -5,7 +5,17 @@ | ||||||
|    - `conda activate component-tests` |    - `conda activate component-tests` | ||||||
|    - `pip3 install -r requirements.txt` |    - `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 | # How to run | ||||||
|  | Specify path to config file via env var `COMPONENT_TESTS_CONFIG`. This is required. | ||||||
| ## All tests | ## All tests | ||||||
| Run `pytest` inside this(component-tests) folder | Run `pytest` inside this(component-tests) folder | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
|  | @ -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 | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | METRICS_PORT = 51000 | ||||||
|  | @ -3,9 +3,8 @@ from util import start_cloudflared | ||||||
| 
 | 
 | ||||||
| class TestConfig: | class TestConfig: | ||||||
|     # tmp_path is a fixture provides a temporary directory unique to the test invocation |     # tmp_path is a fixture provides a temporary directory unique to the test invocation | ||||||
|     def test_validate_ingress_rules(self, tmp_path): |     def test_validate_ingress_rules(self, tmp_path, component_tests_config): | ||||||
|         config = { |         extra_config = { | ||||||
|             'metrics': 'localhost:50000', |  | ||||||
|             'ingress': [ |             'ingress': [ | ||||||
|                 { |                 { | ||||||
|                     "hostname": "example.com", |                     "hostname": "example.com", | ||||||
|  | @ -31,23 +30,25 @@ class TestConfig: | ||||||
|                 {"service": "http_status:404"} |                 {"service": "http_status:404"} | ||||||
|             ], |             ], | ||||||
|         } |         } | ||||||
|  |         component_tests_config = component_tests_config(extra_config) | ||||||
|         validate_args = ["ingress", "validate"] |         validate_args = ["ingress", "validate"] | ||||||
|         pre_args = ["tunnel"] |         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") |         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, component_tests_config, "http://example.com/index.html", 1) | ||||||
|         self.match_rule(tmp_path, config, "https://example.com/index.html", 1) |         self.match_rule(tmp_path, component_tests_config, "https://example.com/index.html", 1) | ||||||
|         self.match_rule(tmp_path, config, "https://api.example.com/login", 2) |         self.match_rule(tmp_path, component_tests_config, "https://api.example.com/login", 2) | ||||||
|         self.match_rule(tmp_path, config, "https://wss.example.com", 3) |         self.match_rule(tmp_path, component_tests_config, "https://wss.example.com", 3) | ||||||
|         self.match_rule(tmp_path, config, "https://ssh.example.com", 4) |         self.match_rule(tmp_path, component_tests_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, "https://api.example.com", 5) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|     # This is used to check that the command tunnel ingress url <url> matches rule number <rule_num>. Note that rule number uses 1-based indexing |     # This is used to check that the command tunnel ingress url <url> matches rule number <rule_num>. 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] |         args = ["ingress", "rule", url] | ||||||
|         pre_args = ["tunnel"] |         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 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 |         assert f"Matched rule #{rule_num}" .encode() in match_rule.stdout | ||||||
|  | @ -5,11 +5,6 @@ import yaml | ||||||
| 
 | 
 | ||||||
| LOGGER = logging.getLogger(__name__) | 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): | def write_config(path, config): | ||||||
|     config_path = path / "config.yaml" |     config_path = path / "config.yaml" | ||||||
|     with open(config_path, 'w') as outfile: |     with open(config_path, 'w') as outfile: | ||||||
|  | @ -17,11 +12,15 @@ def write_config(path, config): | ||||||
|     return config_path |     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) |     config_path = write_config(path, config) | ||||||
|     cmd = [get_cloudflared()] |     cmd = [component_test_config.cloudflared_binary] | ||||||
|     cmd += pre_args |     cmd += cfd_pre_args | ||||||
|     cmd += ["--config", config_path] |     cmd += ["--config", config_path] | ||||||
|     cmd += args |     cmd += cfd_args | ||||||
|     LOGGER.info(f"Run cmd {cmd} with config {config}") |     LOGGER.info(f"Run cmd {cmd} with config {config}") | ||||||
|     return subprocess.run(cmd, capture_output=True) |     return subprocess.run(cmd, capture_output=True) | ||||||
		Loading…
	
		Reference in New Issue