Skip to content

Commit 054d4c8

Browse files
authored
Merge pull request #764 from averevki/coredns-multicluster-tests
Add CoreDNS multicluster tests
2 parents bf7e292 + 678c7ee commit 054d4c8

22 files changed

+671
-50
lines changed

config/settings.local.yaml.tpl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,17 @@
5454
# api_url: "https://api.kubernetes2.com"
5555
# token: "KUADRANT_RULEZ"
5656
# kubeconfig_path: "~/.kube/config2"
57+
# cluster3: # Third cluster for the multicluster tests
58+
# api_url: "https://api.kubernetes3.com"
59+
# token: "KUADRANT_RULEZ"
60+
# kubeconfig_path: "~/.kube/config3"
5761
# slow_loadbalancers: false # For use in Openshift on AWS: If true, causes all Gateways and LoadBalancer Services to wait longer to become ready
5862
# provider_secret: "aws-credentials" # Name of the Secret resource that contains DNS provider credentials
5963
# issuer: # Issuer object for testing TLSPolicy
6064
# name: "selfsigned-cluster-issuer" # Name of Issuer CR
6165
# kind: "ClusterIssuer" # Kind of Issuer, can be "Issuer" or "ClusterIssuer"
6266
# dns:
67+
# coredns_zone: "coredns.kuadrant-qe.net" # hosted zone for coredns delegation tests
6368
# dns_server:
6469
# geo_code: "DE" # dns provider geo code of the dns server
6570
# address: "ns1.seolizer.de" # dns nameserver hostname or ip

testsuite/capabilities.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ def has_kuadrant():
1414
clusters = [settings["control_plane"]["cluster"]]
1515
if cluster2 := settings["control_plane"]["cluster2"]:
1616
clusters.append(cluster2)
17+
if cluster3 := settings["control_plane"]["cluster3"]:
18+
clusters.append(cluster3)
19+
1720
for cluster in clusters:
1821
system_project = cluster.change_project(project)
1922
if not system_project.connected:
@@ -32,6 +35,8 @@ def kuadrant_version():
3235
clusters = [settings["control_plane"]["cluster"]]
3336
if cluster2 := settings["control_plane"]["cluster2"]:
3437
clusters.append(cluster2)
38+
if cluster3 := settings["control_plane"]["cluster3"]:
39+
clusters.append(cluster3)
3540
versions = []
3641
for cluster in clusters:
3742
project = cluster.change_project(settings["service_protection"]["system_project"])

testsuite/config/openshift_loader.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,8 @@ def load(obj, env=None, silent=True, key=None, filename=None):
4343
obj["control_plane"]["cluster2"] = KubernetesClient(
4444
cluster2.get("project"), cluster2.get("api_url"), cluster2.get("token"), cluster2.get("kubeconfig_path")
4545
)
46+
47+
if cluster3 := control_plane.setdefault("cluster3", {}):
48+
obj["control_plane"]["cluster3"] = KubernetesClient(
49+
cluster3.get("project"), cluster3.get("api_url"), cluster3.get("token"), cluster3.get("kubeconfig_path")
50+
)

testsuite/kuadrant/policy/dns.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,55 @@ def wait_for_ready(self):
6666
assert success, "DNSHealthCheckProbe status wasn't ready in time"
6767

6868

69+
@dataclass
70+
class DNSRecordEndpoint: # pylint: disable=invalid-name
71+
"""Spec for DNSRecord endpoint"""
72+
73+
dnsName: str
74+
recordTTL: int
75+
recordType: str
76+
targets: list[str]
77+
78+
79+
class DNSRecord(KubernetesObject):
80+
"""DNSRecord object"""
81+
82+
@classmethod
83+
def create_instance(
84+
cls,
85+
cluster: KubernetesClient,
86+
name: str,
87+
root_host: str,
88+
endpoints: list[DNSRecordEndpoint] = None,
89+
delegate: bool = None,
90+
labels: dict[str, str] = None,
91+
):
92+
"""Creates new instance of DNSRecord"""
93+
94+
model: dict = {
95+
"apiVersion": "kuadrant.io/v1alpha1",
96+
"kind": "DNSRecord",
97+
"metadata": {"name": name, "labels": labels},
98+
"spec": {
99+
"rootHost": root_host,
100+
"endpoints": [asdict(ep) for ep in endpoints] if endpoints else None,
101+
},
102+
}
103+
104+
if delegate is not None:
105+
model["spec"]["delegate"] = delegate
106+
107+
return cls(model, context=cluster.context)
108+
109+
def wait_for_ready(self):
110+
"""Waits until DNSRecord is ready"""
111+
success = self.wait_until(
112+
lambda obj: len(obj.model.status.conditions) > 0
113+
and all(condition.status == "True" for condition in obj.model.status.conditions)
114+
)
115+
assert success, f"DNSRecord {self.name()} did not get ready in time"
116+
117+
69118
class DNSPolicy(Policy):
70119
"""DNSPolicy object"""
71120

@@ -76,6 +125,7 @@ def create_instance(
76125
name: str,
77126
parent: Referencable,
78127
provider_secret_name: str,
128+
delegate: bool = None,
79129
load_balancing: LoadBalancing = None,
80130
labels: dict[str, str] = None,
81131
):
@@ -91,6 +141,9 @@ def create_instance(
91141
},
92142
}
93143

144+
if delegate is not None:
145+
model["spec"]["delegate"] = delegate
146+
94147
if load_balancing:
95148
model["spec"]["loadBalancing"] = asdict(load_balancing)
96149

testsuite/kubernetes/client.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from testsuite.kubernetes.openshift.route import OpenshiftRoute
1010
from testsuite.kubernetes.service import Service
11+
from .service_account import ServiceAccount
1112
from .deployment import Deployment
1213
from .secret import Secret
1314

@@ -54,6 +55,11 @@ def token(self):
5455
"""Returns real Kubernetes token"""
5556
return self._token or self.inspect_context(jsonpath="{.users[*].user.token}", raw=True)
5657

58+
def get_service_account(self, name: str):
59+
"""Select service account by the name and return testsuite ServiceAccount object wrapping it"""
60+
with self.context:
61+
return oc.selector(f"sa/{name}").object(cls=ServiceAccount)
62+
5763
@cached_property
5864
def apps_url(self):
5965
"""Return URL under which all routes are routed"""
@@ -113,14 +119,12 @@ def do_action(self, verb: str, *args, stdin_str=None, auto_raise: bool = True, p
113119
return oc.APIObject(string_to_model=result.out())
114120
return result
115121

116-
def inspect_context(self, jsonpath, raw=False):
122+
def inspect_context(self, jsonpath=None, raw=False):
117123
"""Returns jsonpath from the current context"""
118-
return (
119-
self.do_action("config", "view", f'--output=jsonpath="{jsonpath}"', f"--raw={raw}", "--minify=true")
120-
.out()
121-
.replace('"', "")
122-
.strip()
123-
)
124+
action = ["config", "view", "--minify=true", f"--raw={raw}"]
125+
if jsonpath:
126+
action.append(f'--output=jsonpath="{jsonpath}"')
127+
return self.do_action(*action).out().replace('"', "").strip()
124128

125129
@property
126130
def project_exists(self):

testsuite/kubernetes/deployment.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,10 @@ def wait_for_ready(self, timeout=90):
152152

153153
def wait_for_replicas(self, replicas: int, timeout=90):
154154
"""Waits until Deployment has at least the given number of replicas"""
155-
success = self.wait_until(lambda obj: obj.model.status["readyReplicas"] >= replicas, timelimit=timeout)
155+
success = self.wait_until(
156+
lambda obj: "readyReplicas" in obj.model.status and obj.model.status["readyReplicas"] >= replicas,
157+
timelimit=timeout,
158+
)
156159
assert success, f"Deployment {self.name()} did not get {replicas} replicas in time"
157160

158161
@property
@@ -187,6 +190,13 @@ def add_volume(self, volume: Volume):
187190
mounts = self.template.setdefault("volumes", [])
188191
mounts.append(asdict(volume))
189192

193+
def restart(self):
194+
"""Restarts the deployment by scaling replicas to 0 and back to original"""
195+
original_replicas = self.replicas
196+
self.set_replicas(0)
197+
self.set_replicas(original_replicas)
198+
self.wait_for_replicas(original_replicas)
199+
190200
def rollout(self, hard=False, timeout=90):
191201
"""
192202
Performs rollout on the Deployment and waits until complete.

testsuite/kubernetes/secret.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def create_instance(
1616
cluster,
1717
name,
1818
data: dict[str, str],
19-
secret_type: Literal["kubernetes.io/tls", "kuadrant.io/aws", "Opaque"] = "Opaque",
19+
secret_type: Literal["kubernetes.io/tls", "kuadrant.io/aws", "kuadrant.io/coredns", "Opaque"] = "Opaque",
2020
labels: dict[str, str] = None,
2121
):
2222
"""Creates new Secret"""
Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
"""Service Account object for Kubernetes"""
22

3+
import yaml
4+
5+
import openshift_client as oc
6+
37
from testsuite.kubernetes import KubernetesObject
4-
from testsuite.kubernetes.client import KubernetesClient
58

69

710
class ServiceAccount(KubernetesObject):
811
"""Kubernetest ServiceAccount"""
912

10-
def __init__(self, cluster: KubernetesClient, model: dict):
11-
self.cluster = cluster
12-
super().__init__(model, context=cluster.context)
13-
1413
@classmethod
15-
def create_instance(cls, openshift: KubernetesClient, name: str, labels: dict[str, str] = None):
14+
def create_instance(cls, cluster, name: str, labels: dict[str, str] = None):
1615
"""Creates new instance of service account"""
1716
model = {
1817
"kind": "ServiceAccount",
@@ -23,9 +22,38 @@ def create_instance(cls, openshift: KubernetesClient, name: str, labels: dict[st
2322
},
2423
}
2524

26-
return cls(openshift, model)
25+
return cls(model, context=cluster.context)
2726

28-
def get_auth_token(self, audiences: list[str] = None) -> str:
27+
def get_auth_token(self, audiences: list[str] = None, duration: str = None) -> str:
2928
"""Requests and returns bound token for service account"""
30-
audiences_args = [f"--audience={a}" for a in audiences or []]
31-
return self.cluster.do_action("create", "token", self.name(), *audiences_args).out().strip()
29+
args = ["token", self.name()]
30+
if audiences:
31+
args.extend([f"--audience={a}" for a in audiences])
32+
if duration:
33+
args.append(f"--duration={duration}")
34+
with self.context:
35+
return oc.invoke("create", args).out().strip()
36+
37+
def get_kubeconfig(self, context_name, user_name, cluster_name, api_url) -> str:
38+
"""Assembles and returns kubeconfig with service account token"""
39+
kubeconfig = {
40+
"apiVersion": "v1",
41+
"kind": "Config",
42+
"clusters": [
43+
{
44+
"cluster": {"insecure-skip-tls-verify": True, "server": api_url}, # insecure clusters only for now
45+
"name": cluster_name,
46+
}
47+
],
48+
"contexts": [
49+
{
50+
"context": {"cluster": cluster_name, "namespace": self.context.project_name, "user": user_name},
51+
"name": context_name,
52+
}
53+
],
54+
"current-context": context_name,
55+
"preferences": {},
56+
"users": [{"name": user_name, "user": {"token": self.get_auth_token(duration="1h")}}],
57+
}
58+
59+
return yaml.dump(kubeconfig)

testsuite/tests/conftest.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import signal
44
from urllib.parse import urlparse
5+
import yaml
56

67
import pytest
78
from pytest_metadata.plugin import metadata_key # type: ignore
9+
from openshift_client import selector
810
from dynaconf import ValidationError
911
from keycloak import KeycloakAuthenticationError
1012

@@ -16,8 +18,10 @@
1618
from testsuite.mockserver import Mockserver
1719
from testsuite.oidc import OIDCProvider
1820
from testsuite.oidc.auth0 import Auth0Provider
21+
from testsuite.prometheus import Prometheus
1922
from testsuite.oidc.keycloak import Keycloak
2023
from testsuite.tracing.jaeger import JaegerClient
24+
from testsuite.kubernetes.config_map import ConfigMap
2125
from testsuite.tracing.tempo import RemoteTempoClient
2226
from testsuite.utils import randomize, _whoami
2327

@@ -128,6 +132,33 @@ def testconfig():
128132
return settings
129133

130134

135+
@pytest.fixture(scope="session")
136+
def prometheus(cluster):
137+
"""
138+
Return an instance of Thanos metrics client
139+
Skip tests if query route is not properly configured
140+
"""
141+
openshift_monitoring = cluster.change_project("openshift-monitoring")
142+
# Check if metrics are enabled
143+
try:
144+
with openshift_monitoring.context:
145+
cm = selector("cm/cluster-monitoring-config").object(cls=ConfigMap)
146+
assert yaml.safe_load(cm["config.yaml"])["enableUserWorkload"]
147+
except Exception: # pylint: disable=broad-exception-caught
148+
pytest.skip("User workload monitoring is disabled")
149+
150+
# find thanos-querier route in the openshift-monitoring project
151+
# this route allows to query metrics
152+
153+
routes = openshift_monitoring.get_routes_for_service("thanos-querier")
154+
if len(routes) == 0:
155+
pytest.skip("Skipping metrics tests as query route is not properly configured")
156+
157+
url = ("https://" if "tls" in routes[0].model.spec else "http://") + routes[0].model.spec.host
158+
with KuadrantClient(headers={"Authorization": f"Bearer {cluster.token}"}, base_url=url, verify=False) as client:
159+
yield Prometheus(client)
160+
161+
131162
@pytest.fixture(scope="session")
132163
def keycloak(request, testconfig, blame, skip_or_fail):
133164
"""Keycloak OIDC Provider fixture"""

testsuite/tests/multicluster/coredns/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)