Skip to content

Commit

Permalink
feat: replace traefik with istio-ingress for public ingress
Browse files Browse the repository at this point in the history
  • Loading branch information
wood-push-melon committed Nov 27, 2024
1 parent 0217633 commit 2aae8c0
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 57 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ jobs:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
with:
detached: true

- name: Setup operator environment
uses: charmed-kubernetes/actions-operator@main
with:
provider: microk8s
channel: 1.28-strict/stable
juju-channel: 3.4
juju-channel: 3.4/stable
microk8s-addons: "hostpath-storage metallb:10.64.140.43-10.64.140.49"

- name: Run integration tests
# set a predictable model name so it can be consumed by charm-logdump-action
Expand Down
107 changes: 67 additions & 40 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

import asyncio
import functools
import re
import ssl
from contextlib import asynccontextmanager
from pathlib import Path
from typing import AsyncGenerator, Awaitable, Callable, Optional
Expand All @@ -30,13 +30,17 @@
DB_APP = "postgresql-k8s"
CA_APP = "self-signed-certificates"
LOGIN_UI_APP = "identity-platform-login-ui-operator"
PUBLIC_INGRESS_APP = "public-ingress"
INTERNAL_INGRESS_APP = "internal-ingress"
TRAEFIK_CHARM = "traefik-k8s"
TRAEFIK_ADMIN_APP = "traefik-admin"
TRAEFIK_PUBLIC_APP = "traefik-public"
ISTIO_INGRESS_CHARM = "istio-ingress-k8s"
ISTIO_CONTROL_PLANE_CHARM = "istio-k8s"
CLIENT_SECRET = "secret"
CLIENT_REDIRECT_URIS = ["https://example.com"]
PUBLIC_INGRESS_DOMAIN = "public"
ADMIN_INGRESS_DOMAIN = "admin"
INTERNAL_INGRESS_DOMAIN = "internal"
PUBLIC_LOAD_BALANCER = f"{PUBLIC_INGRESS_APP}-istio"
INTERNAL_LOAD_BALANCER = f"{INTERNAL_INGRESS_APP}-lb"


async def integrate_dependencies(ops_test: OpsTest) -> None:
Expand All @@ -45,10 +49,10 @@ async def integrate_dependencies(ops_test: OpsTest) -> None:
f"{HYDRA_APP}:{LOGIN_UI_INTEGRATION_NAME}", f"{LOGIN_UI_APP}:{LOGIN_UI_INTEGRATION_NAME}"
)
await ops_test.model.integrate(
f"{HYDRA_APP}:{INTERNAL_INGRESS_INTEGRATION_NAME}", TRAEFIK_ADMIN_APP
f"{HYDRA_APP}:{INTERNAL_INGRESS_INTEGRATION_NAME}", INTERNAL_INGRESS_APP
)
await ops_test.model.integrate(
f"{HYDRA_APP}:{PUBLIC_INGRESS_INTEGRATION_NAME}", TRAEFIK_PUBLIC_APP
f"{HYDRA_APP}:{PUBLIC_INGRESS_INTEGRATION_NAME}", PUBLIC_INGRESS_APP
)


Expand Down Expand Up @@ -110,19 +114,34 @@ async def leader_peer_integration_data(app_integration_data: Callable) -> Option
return await app_integration_data(HYDRA_APP, HYDRA_APP)


async def unit_address(ops_test: OpsTest, *, app_name: str, unit_num: int = 0) -> str:
status = await ops_test.model.get_status()
return status["applications"][app_name]["units"][f"{app_name}/{unit_num}"]["address"]
async def get_k8s_service_address(namespace: str, service_name: str) -> str:
cmd = [
"kubectl",
"-n",
namespace,
"get",
f"service/{service_name}",
"-o=jsonpath={.status.loadBalancer.ingress[0].ip}",
]

process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, _ = await process.communicate()

return stdout.decode().strip() if not process.returncode else ""


@pytest_asyncio.fixture
async def public_address() -> Callable[[OpsTest, int], Awaitable[str]]:
return functools.partial(unit_address, app_name=TRAEFIK_PUBLIC_APP)
async def public_ingress_address(ops_test: OpsTest) -> str:
return await get_k8s_service_address(ops_test.model_name, PUBLIC_LOAD_BALANCER)


@pytest_asyncio.fixture
async def admin_address() -> Callable[[OpsTest, int], Awaitable[str]]:
return functools.partial(unit_address, app_name=TRAEFIK_ADMIN_APP)
async def internal_ingress_address(ops_test: OpsTest) -> str:
return await get_k8s_service_address(ops_test.model_name, INTERNAL_LOAD_BALANCER)


@pytest_asyncio.fixture
Expand All @@ -135,47 +154,59 @@ async def http_client() -> AsyncGenerator[httpx.AsyncClient, None]:
async def get_hydra_jwks(
ops_test: OpsTest,
request: pytest.FixtureRequest,
public_address: Callable,
admin_address: Callable,
public_ingress_address: str,
internal_ingress_address: str,
http_client: AsyncClient,
) -> Callable[[], Awaitable[Response]]:
address_func = admin_address if request.param == "admin" else public_address
scheme = "http" if request.param == "admin" else "https"
scheme = "http" if request.param == "internal" else "https"
host = INTERNAL_INGRESS_DOMAIN if request.param == "internal" else PUBLIC_INGRESS_DOMAIN
address = internal_ingress_address if request.param == "internal" else public_ingress_address
url = f"{scheme}://{address}/{ops_test.model_name}-{HYDRA_APP}/.well-known/jwks.json"

async def wrapper() -> Response:
address = await address_func(ops_test)
url = f"{scheme}://{address}/{ops_test.model_name}-{HYDRA_APP}/.well-known/jwks.json"
return await http_client.get(url)
return await http_client.get(
url, headers={"Host": host}, extensions={"sni_hostname": host}
)

return wrapper


@pytest_asyncio.fixture
async def get_openid_configuration(
ops_test: OpsTest, public_address: Callable, http_client: AsyncClient
ops_test: OpsTest,
public_ingress_address: str,
http_client: AsyncClient,
) -> Response:
address = await public_address(ops_test)
url = f"https://{address}/{ops_test.model_name}-{HYDRA_APP}/.well-known/openid-configuration"
return await http_client.get(url)
url = f"https://{public_ingress_address}/{ops_test.model_name}-{HYDRA_APP}/.well-known/openid-configuration"
return await http_client.get(
url,
headers={"Host": PUBLIC_INGRESS_DOMAIN},
extensions={"sni_hostname": PUBLIC_INGRESS_DOMAIN},
)


@pytest_asyncio.fixture
async def get_admin_clients(
ops_test: OpsTest, admin_address: Callable, http_client: AsyncClient
ops_test: OpsTest,
internal_ingress_address: str,
http_client: AsyncClient,
) -> Response:
address = await admin_address(ops_test)
url = f"http://{address}/{ops_test.model_name}-{HYDRA_APP}/admin/clients"
return await http_client.get(url)
url = f"http://{internal_ingress_address}/{ops_test.model_name}-{HYDRA_APP}/admin/clients"
return await http_client.get(url, headers={"Host": INTERNAL_INGRESS_DOMAIN})


@pytest_asyncio.fixture
async def client_credential_request(
ops_test: OpsTest, public_address: Callable, http_client: AsyncClient
ops_test: OpsTest,
public_ingress_address: str,
http_client: AsyncClient,
) -> Callable[[str, str], Awaitable[Response]]:
async def wrapper(client_id: str, client_secret: str) -> Response:
address = await public_address(ops_test)
url = f"https://{address}/{ops_test.model_name}-{HYDRA_APP}/oauth2/token"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
url = f"https://{public_ingress_address}/{ops_test.model_name}-{HYDRA_APP}/oauth2/token"
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Host": PUBLIC_INGRESS_DOMAIN,
}
return await http_client.post(
url,
headers=headers,
Expand All @@ -184,6 +215,7 @@ async def wrapper(client_id: str, client_secret: str) -> Response:
"grant_type": "client_credentials",
"scope": "openid profile",
},
extensions={"sni_hostname": PUBLIC_INGRESS_DOMAIN},
)

return wrapper
Expand Down Expand Up @@ -225,15 +257,10 @@ async def oauth_clients(hydra_unit: Unit) -> dict[str, str]:


@pytest_asyncio.fixture
async def jwks_client(ops_test: OpsTest, public_address: Callable) -> PyJWKClient:
address = await public_address(ops_test)

ssl_ctx = ssl.create_default_context()
ssl_ctx.check_hostname = False
ssl_ctx.verify_mode = ssl.CERT_NONE
async def jwks_client(ops_test: OpsTest, internal_ingress_address: str) -> PyJWKClient:
return PyJWKClient(
f"https://{address}/{ops_test.model.name}-{HYDRA_APP}/.well-known/jwks.json",
ssl_context=ssl_ctx,
f"http://{internal_ingress_address}/{ops_test.model.name}-{HYDRA_APP}/.well-known/jwks.json",
headers={"Host": INTERNAL_INGRESS_DOMAIN},
)


Expand Down
49 changes: 35 additions & 14 deletions tests/integration/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@
import jwt
import pytest
from conftest import (
ADMIN_INGRESS_DOMAIN,
CA_APP,
CLIENT_REDIRECT_URIS,
CLIENT_SECRET,
DB_APP,
HYDRA_APP,
HYDRA_IMAGE,
INTERNAL_INGRESS_APP,
INTERNAL_INGRESS_DOMAIN,
ISTIO_CONTROL_PLANE_CHARM,
ISTIO_INGRESS_CHARM,
LOGIN_UI_APP,
PUBLIC_INGRESS_APP,
PUBLIC_INGRESS_DOMAIN,
TRAEFIK_ADMIN_APP,
TRAEFIK_CHARM,
TRAEFIK_PUBLIC_APP,
integrate_dependencies,
remove_integration,
)
Expand All @@ -40,6 +42,25 @@
@pytest.mark.skip_if_deployed
@pytest.mark.abort_on_fail
async def test_build_and_deploy(ops_test: OpsTest, local_charm: Path) -> None:
await ops_test.track_model(
alias="istio-system",
model_name="istio-system",
destroy_storage=True,
)
istio_system = ops_test.models.get("istio-system")

await istio_system.model.deploy(
application_name=ISTIO_CONTROL_PLANE_CHARM,
entity_url=ISTIO_CONTROL_PLANE_CHARM,
channel="latest/edge",
trust=True,
)
await istio_system.model.wait_for_idle(
[ISTIO_CONTROL_PLANE_CHARM],
status="active",
timeout=5 * 60,
)

await ops_test.model.deploy(
application_name=HYDRA_APP,
entity_url=str(local_charm),
Expand All @@ -56,17 +77,17 @@ async def test_build_and_deploy(ops_test: OpsTest, local_charm: Path) -> None:
trust=True,
)
await ops_test.model.deploy(
TRAEFIK_CHARM,
application_name=TRAEFIK_PUBLIC_APP,
ISTIO_INGRESS_CHARM,
application_name=PUBLIC_INGRESS_APP,
channel="latest/edge",
config={"external_hostname": PUBLIC_INGRESS_DOMAIN},
trust=True,
)
await ops_test.model.deploy(
TRAEFIK_CHARM,
application_name=TRAEFIK_ADMIN_APP,
application_name=INTERNAL_INGRESS_APP,
channel="latest/edge",
config={"external_hostname": ADMIN_INGRESS_DOMAIN},
config={"external_hostname": INTERNAL_INGRESS_DOMAIN},
trust=True,
)
await ops_test.model.deploy(
Expand All @@ -79,14 +100,14 @@ async def test_build_and_deploy(ops_test: OpsTest, local_charm: Path) -> None:
channel="latest/edge",
trust=True,
)
await ops_test.model.integrate(f"{TRAEFIK_PUBLIC_APP}:certificates", f"{CA_APP}:certificates")
await ops_test.model.integrate(TRAEFIK_PUBLIC_APP, f"{LOGIN_UI_APP}:ingress")
await ops_test.model.integrate(f"{PUBLIC_INGRESS_APP}:certificates", f"{CA_APP}:certificates")
await ops_test.model.integrate(PUBLIC_INGRESS_APP, f"{LOGIN_UI_APP}:ingress")

# Integrate with dependencies
await integrate_dependencies(ops_test)

await ops_test.model.wait_for_idle(
apps=[HYDRA_APP, DB_APP, TRAEFIK_PUBLIC_APP, TRAEFIK_ADMIN_APP],
apps=[HYDRA_APP, DB_APP, PUBLIC_INGRESS_APP, INTERNAL_INGRESS_APP],
raise_on_blocked=False,
status="active",
timeout=5 * 60,
Expand Down Expand Up @@ -139,14 +160,14 @@ async def test_openid_configuration_endpoint(
assert payload["jwks_uri"] == str(base_path / ".well-known/jwks.json")


@pytest.mark.parametrize("get_hydra_jwks", ["admin"], indirect=True)
@pytest.mark.parametrize("get_hydra_jwks", ["internal"], indirect=True)
async def test_internal_ingress_integration(
leader_internal_ingress_integration_data: Optional[dict],
get_admin_clients: Response,
get_hydra_jwks: Callable[[], Awaitable[Response]],
) -> None:
assert leader_internal_ingress_integration_data
assert leader_internal_ingress_integration_data["external_host"] == ADMIN_INGRESS_DOMAIN
assert leader_internal_ingress_integration_data["external_host"] == INTERNAL_INGRESS_DOMAIN
assert leader_internal_ingress_integration_data["scheme"] == "http"

# examine the admin endpoint
Expand Down Expand Up @@ -354,7 +375,7 @@ async def test_remove_database_integration(
async def test_remove_public_ingress_integration(
ops_test: OpsTest, hydra_application: Application
) -> None:
async with remove_integration(ops_test, TRAEFIK_PUBLIC_APP, PUBLIC_INGRESS_INTEGRATION_NAME):
async with remove_integration(ops_test, PUBLIC_INGRESS_APP, PUBLIC_INGRESS_INTEGRATION_NAME):
assert hydra_application.status == "blocked"


Expand Down Expand Up @@ -393,7 +414,7 @@ async def test_upgrade(
await integrate_dependencies(ops_test)

await ops_test.model.wait_for_idle(
apps=[HYDRA_APP, DB_APP, TRAEFIK_PUBLIC_APP, TRAEFIK_ADMIN_APP],
apps=[HYDRA_APP, DB_APP, PUBLIC_INGRESS_APP, INTERNAL_INGRESS_APP],
raise_on_blocked=False,
status="active",
timeout=5 * 60,
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def database_integration(harness: Harness) -> int:

@pytest.fixture
def public_ingress_integration(harness: Harness) -> int:
return harness.add_relation(PUBLIC_INGRESS_INTEGRATION_NAME, "traefik-public")
return harness.add_relation(PUBLIC_INGRESS_INTEGRATION_NAME, "public-ingress")


@pytest.fixture
Expand All @@ -124,7 +124,7 @@ def database_integration_data(harness: Harness, database_integration: int) -> No
def public_ingress_integration_data(harness: Harness, public_ingress_integration: int) -> None:
harness.update_relation_data(
public_ingress_integration,
"traefik-public",
"public-ingress",
{
"ingress": '{"url": "https://hydra.ory.com"}',
},
Expand Down

0 comments on commit 2aae8c0

Please sign in to comment.