TUN-4062: Read component tests config from yaml file

This commit is contained in:
cthuang 2021-03-08 14:09:10 +00:00 committed by Chung Ting Huang
parent 206523344f
commit a7344435a5
7 changed files with 155 additions and 21 deletions

View File

@ -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:

View File

@ -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

97
component-tests/config.py Normal file
View File

@ -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()

View File

@ -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

View File

@ -0,0 +1 @@
METRICS_PORT = 51000

View File

@ -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

View File

@ -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)