Skip to content

Commit

Permalink
skip: add integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
DnPlas committed Oct 16, 2023
1 parent 18eee08 commit 639db0f
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 191 deletions.
38 changes: 25 additions & 13 deletions charms/istio-pilot/lib/charms/observability_libs/v0/cert_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@

LIBID = "b5cd5cd580f3428fa5f59a8876dcbe6a"
LIBAPI = 0
LIBPATCH = 8
LIBPATCH = 9


def is_ip_address(value: str) -> bool:
Expand Down Expand Up @@ -181,33 +181,40 @@ def _peer_relation(self) -> Optional[Relation]:
return self.charm.model.get_relation(self.peer_relation_name, None)

def _on_peer_relation_created(self, _):
"""Generate the private key and store it in a peer relation."""
# We're in "relation-created", so the relation should be there
"""Generate the CSR if the certificates relation is ready."""
self._generate_privkey()

# Just in case we already have a private key, do not overwrite it.
# Not sure how this could happen.
# TODO figure out how to go about key rotation.
if not self._private_key:
private_key = generate_private_key()
self._private_key = private_key.decode()

# Generate CSR here, in case peer events fired after tls-certificate relation events
# check cert relation is ready
if not (self.charm.model.get_relation(self.certificates_relation_name)):
# peer relation event happened to fire before tls-certificates events.
# Abort, and let the "certificates joined" observer create the CSR.
logger.info("certhandler waiting on certificates relation")
return

logger.debug("certhandler has peer and certs relation: proceeding to generate csr")
self._generate_csr()

def _on_certificates_relation_joined(self, _) -> None:
"""Generate the CSR and request the certificate creation."""
"""Generate the CSR if the peer relation is ready."""
self._generate_privkey()

# check peer relation is there
if not self._peer_relation:
# tls-certificates relation event happened to fire before peer events.
# Abort, and let the "peer joined" relation create the CSR.
logger.info("certhandler waiting on peer relation")
return

logger.debug("certhandler has peer and certs relation: proceeding to generate csr")
self._generate_csr()

def _generate_privkey(self):
# Generate priv key unless done already
# TODO figure out how to go about key rotation.
if not self._private_key:
private_key = generate_private_key()
self._private_key = private_key.decode()

def _on_config_changed(self, _):
# FIXME on config changed, the web_external_url may or may not change. But because every
# call to `generate_csr` appends a uuid, CSRs cannot be easily compared to one another.
Expand Down Expand Up @@ -237,7 +244,12 @@ def _generate_csr(
# In case we already have a csr, do not overwrite it by default.
if overwrite or renew or not self._csr:
private_key = self._private_key
assert private_key is not None # for type checker
if private_key is None:
# FIXME: raise this in a less nested scope by
# generating privkey and csr in the same method.
raise RuntimeError(
"private key unset. call _generate_privkey() before you call this method."
)
csr = generate_csr(
private_key=private_key.encode(),
subject=self.cert_subject,
Expand Down
2 changes: 1 addition & 1 deletion tests/test_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ async def test_deploy_bookinfo_example(ops_test: OpsTest):
"kubectl",
"create",
"namespace",
bookinfo_namespace,
"bookinfo-namespace-2",
)

await ops_test.run(
Expand Down
223 changes: 47 additions & 176 deletions tests/test_bundle_tls.py
Original file line number Diff line number Diff line change
@@ -1,84 +1,43 @@
import json
import logging
from pathlib import Path
from time import sleep

import aiohttp
import lightkube
import pytest
import requests
import tenacity
import yaml
from bs4 import BeautifulSoup
from lightkube import codecs
from lightkube.generic_resource import (
create_namespaced_resource,
load_in_cluster_generic_resources,
)
from lightkube.generic_resource import create_namespaced_resource
from lightkube.resources.core_v1 import Secret
from pytest_operator.plugin import OpsTest

log = logging.getLogger(__name__)

DEX_AUTH = "dex-auth"
OIDC_GATEKEEPER = "oidc-gatekeeper"
ISTIO_PILOT = "istio-pilot"
ISTIO_GATEWAY_APP_NAME = "istio-ingressgateway"
TENSORBOARD_CONTROLLER = "tensorboard-controller"
KUBEFLOW_VOLUMES = "kubeflow-volumes"

USERNAME = "user123"
PASSWORD = "user123"

VIRTUAL_SERVICE_LIGHTKUBE_RESOURCE = create_namespaced_resource(
DEFAULT_GATEWAY_NAME = "test-gateway"
GATEWAY_RESOURCE = create_namespaced_resource(
group="networking.istio.io",
version="v1alpha3",
kind="VirtualService",
plural="virtualservices",
kind="Gateway",
plural="gateways",
)


@pytest.mark.abort_on_fail
async def test_kubectl_access(ops_test: OpsTest):
"""Fails if kubectl not available or if no cluster context exists"""
_, stdout, _ = await ops_test.run(
"kubectl",
"config",
"view",
check=True,
fail_msg="Failed to execute kubectl - is kubectl installed?",
)

# Check if kubectl has a context, failing if it does not
kubectl_config = yaml.safe_load(stdout)
error_message = (
"Found no kubectl contexts - did you populate KUBECONFIG? Ex:"
" 'KUBECONFIG=/home/runner/.kube/config tox ...' or"
" 'KUBECONFIG=/home/runner/.kube/config tox ...'"
)
assert kubectl_config["contexts"] is not None, error_message

await ops_test.run(
"kubectl",
"get",
"pods",
check=True,
fail_msg="Failed to do a simple kubectl task - is KUBECONFIG properly configured?",
)
@pytest.fixture(scope="session")
def lightkube_client() -> lightkube.Client:
client = lightkube.Client(field_manager="kserve")
return client


@pytest.mark.abort_on_fail
async def test_build_and_deploy_istio_charms(ops_test: OpsTest):
# Build, deploy, and relate istio charms
"""Build and deploy istio-operators with TLS configuration."""
charms_path = "./charms/istio"
istio_charms = await ops_test.build_charms(f"{charms_path}-gateway", f"{charms_path}-pilot")

await ops_test.model.deploy(
istio_charms["istio-pilot"], application_name=ISTIO_PILOT, series="focal", trust=True
istio_charms["istio-pilot"],
application_name=ISTIO_PILOT,
config={"default-gateway": DEFAULT_GATEWAY_NAME},
trust=True,
)

await ops_test.model.deploy(
istio_charms["istio-gateway"],
application_name=ISTIO_GATEWAY_APP_NAME,
series="focal",
config={"kind": "ingress"},
trust=True,
)
Expand All @@ -93,138 +52,50 @@ async def test_build_and_deploy_istio_charms(ops_test: OpsTest):
timeout=90 * 10,
)

@pytest.mark.abort_on_fail
async def test_deploy_bookinfo_example(ops_test: OpsTest):
root_url = "https://raw.githubusercontent.com/istio/istio/release-1.11/samples/bookinfo"
bookinfo_namespace = f"{ops_test.model_name}-bookinfo"

await ops_test.run(
"kubectl",
"create",
"namespace",
bookinfo_namespace,
)

await ops_test.run(
"kubectl",
"label",
"namespace",
bookinfo_namespace,
"istio-injection=enabled",
"--overwrite=true",
check=True,
)
await ops_test.run(
"kubectl",
"apply",
"-f",
f"{root_url}/platform/kube/bookinfo.yaml",
"-f",
f"{root_url}/networking/bookinfo-gateway.yaml",
"--namespace",
bookinfo_namespace,
check=True,
)

await ops_test.run(
"kubectl",
"wait",
"--for=condition=available",
"deployment",
"--all",
"--all-namespaces",
"--timeout=5m",
check=True,
)

# Wait for the pods as well, since the Deployment can be considered
# "complete" while the pods are still starting.
await ops_test.run(
"kubectl",
"wait",
"--for=condition=ready",
"pod",
"--all",
f"-n={bookinfo_namespace}",
"--timeout=5m",
check=True,
await ops_test.model.deploy(
"self-signed-certificates",
channel="edge",
)

gateway_ip = await get_gateway_ip(ops_test)
await assert_page_reachable(
url=f"https://{gateway_ip}/productpage", title="Simple Bookstore App"
await ops_test.model.add_relation(
f"{ISTIO_PILOT}:certificates", "self-signed-certificates:certificates"
)

# TODO: Change this to use lightkube
async def get_gateway_ip(ops_test: OpsTest, service_name: str = "istio-ingressgateway-workload"):
gateway_json = await ops_test.run(
"kubectl",
"get",
f"services/{service_name}",
"-n",
ops_test.model_name,
"-ojson",
check=True,
await ops_test.model.wait_for_idle(
status="active",
raise_on_blocked=False,
timeout=90 * 10,
)

gateway_obj = json.loads(gateway_json[1])
return gateway_obj["status"]["loadBalancer"]["ingress"][0]["ip"]


@tenacity.retry(
stop=tenacity.stop_after_delay(60),
stop=tenacity.stop_after_delay(200),
wait=tenacity.wait_exponential(multiplier=1, min=1, max=10),
reraise=True,
)
def assert_url_get(url, allowed_statuses: list, disallowed_statuses: list):
"""Asserts that we receive one of a list of allowed status when we `get` an url, or raises.
Raises after max number of attempts or if you receive a disallowed status code
"""
i = 0
max_attempts = 20
while i < max_attempts:
# Test that traffic over the restricted port (8080, the regular ingress)
# is redirected to dex
r = requests.get(url, allow_redirects=False)
if r.status_code in allowed_statuses:
return
elif r.status_code in disallowed_statuses:
raise ValueError(
f"Got disallowed status code {r.status_code}. Communication not as expected"
)
sleep(5)

raise ValueError(
"Timed out before getting an allowed status code. Communication not as expected"
@pytest.mark.abort_on_fail
def test_tls_configuration(lightkube_client, ops_test: OpsTest):
"""Check the Gateway and Secret are configured with TLS."""
secret = lightkube_client.get(
Secret, f"{DEFAULT_GATEWAY_NAME}-gateway-secret", namespace=ops_test.model_name
)
gateway = lightkube_client.get(
GATEWAY_RESOURCE, DEFAULT_GATEWAY_NAME, namespace=ops_test.model_name
)

# Assert the Secret is not None and has correct values
assert secret is not None
assert secret.data["tls.crt"] is not None
assert secret.data["tls.key"] is not None
assert secret.type == "kubernetes.io/tls"

# Use a long stop_after_delay period because wait_for_idle is not reliable.
@tenacity.retry(
stop=tenacity.stop_after_delay(600),
wait=tenacity.wait_exponential(multiplier=1, min=1, max=10),
reraise=True,
)
async def assert_page_reachable(url, title):
"""Asserts that a page with a specific title is reachable at a given url."""
log.info(f"Attempting to access url '{url}' to assert it has title '{title}'")
async with aiohttp.ClientSession(raise_for_status=True) as client:
results = await client.get(url)
soup = BeautifulSoup(await results.text())

assert soup.title.string == title
log.info(f"url '{url}' exists with title '{title}'.")
# Assert the Gateway is correctly configured
servers_dict = gateway.spec["servers"][0]
servers_dict_port = servers_dict["port"]
servers_dict_tls = servers_dict["tls"]

assert servers_dict_port["name"] == "https"
assert servers_dict_port["protocol"] == "HTTPS"

@tenacity.retry(
stop=tenacity.stop_after_delay(600),
wait=tenacity.wait_exponential(multiplier=1, min=1, max=10),
reraise=True,
)
def assert_virtualservice_exists(name: str, namespace: str):
"""Will raise a ApiError(404) if the virtualservice does not exist."""
log.info(f"Attempting to assert that VirtualService '{name}' exists.")
lightkube_client = lightkube.Client()
lightkube_client.get(VIRTUAL_SERVICE_LIGHTKUBE_RESOURCE, name, namespace=namespace)
log.info(f"VirtualService '{name}' exists.")
assert servers_dict_tls["mode"] == "SIMPLE"
assert servers_dict_tls["credentialName"] == secret.metadata["name"]
10 changes: 9 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ max-line-length = 100

[tox]
skipsdist = True
envlist = {pilot,gateway}-{unit,lint},integration
envlist = {pilot,gateway}-{unit,lint},integration, integration-tls

[vars]
all_path = {[vars]src_path} {[vars]tst_path}
Expand Down Expand Up @@ -46,6 +46,7 @@ deps =
description = Apply coding style standards to code

[testenv:lint]
allowlist_externals = black
commands =
# uncomment the following line if this charm owns a lib
# codespell {[vars]lib_path}
Expand All @@ -68,6 +69,13 @@ commands =
deps =
-r requirements-integration.txt

[testenv:integration-tls]
allowlist_externals = rm
commands =
pytest --show-capture=no --log-cli-level=INFO -vvs --tb=native {posargs} tests/test_bundle_tls.py
deps =
-r requirements-integration.txt

[testenv:cos-integration]
allowlist_externals = rm
deps =
Expand Down

0 comments on commit 639db0f

Please sign in to comment.