Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

also support logging in via kubernetes_asyncio #1010

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions kopf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
from kopf._core.intents.piggybacking import (
login_via_pykube,
login_via_client,
login_via_async_client,
login_with_kubeconfig,
login_with_service_account,
)
Expand Down Expand Up @@ -184,6 +185,7 @@
'configure', 'LogFormat',
'login_via_pykube',
'login_via_client',
'login_via_async_client',
'login_with_kubeconfig',
'login_with_service_account',
'LoginError',
Expand Down
71 changes: 71 additions & 0 deletions kopf/_core/intents/piggybacking.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

# Keep as constants to make them patchable. Higher priority is more preferred.
PRIORITY_OF_CLIENT: int = 10
PRIORITY_OF_ASYNC_CLIENT: int = 15
PRIORITY_OF_PYKUBE: int = 20

# Rudimentary logins are added only if the clients are absent, so the priorities can overlap.
Expand All @@ -28,6 +29,10 @@


def has_client() -> bool:
return has_sync_client() or has_async_client()


def has_sync_client() -> bool:
try:
import kubernetes
except ImportError:
Expand All @@ -36,6 +41,15 @@ def has_client() -> bool:
return True


def has_async_client() -> bool:
try:
import kubernetes_asyncio
except ImportError:
return False
else:
return True


def has_pykube() -> bool:
try:
import pykube
Expand Down Expand Up @@ -102,6 +116,63 @@ def login_via_client(
)


# This is basically a copy of `login_via_client` changed to use the
# kubernetes_asyncio client library.
async def login_via_async_client(
*,
logger: typedefs.Logger,
**_: Any,
) -> Optional[credentials.ConnectionInfo]:

# Keep imports in the function, as module imports are mocked in some tests.
try:
import kubernetes_asyncio.config
except ImportError:
return None

try:
kubernetes_asyncio.config.load_incluster_config() # cluster env vars
logger.debug("Async client is configured in cluster with service account.")
except kubernetes_asyncio.config.ConfigException as e1:
try:
await kubernetes_asyncio.config.load_kube_config() # developer's config files
logger.debug("Async client is configured via kubeconfig file.")
except kubernetes_asyncio.config.ConfigException as e2:
raise credentials.LoginError("Cannot authenticate the async client library "
"neither in-cluster, nor via kubeconfig.")

# We do not even try to understand how it works and why. Just load it, and extract the results.
# For kubernetes client >= 12.0.0 use the new 'get_default_copy' method
if callable(getattr(kubernetes_asyncio.client.Configuration, 'get_default_copy', None)):
config = kubernetes_asyncio.client.Configuration.get_default_copy()
else:
config = kubernetes_asyncio.client.Configuration()

# For auth-providers, this method is monkey-patched with the auth-provider's one.
# We need the actual auth-provider's token, so we call it instead of accessing api_key.
# Other keys (token, tokenFile) also end up being retrieved via this method.
header: Optional[str] = config.get_api_key_with_prefix('BearerToken')
parts: Sequence[str] = header.split(' ', 1) if header else []
scheme, token = ((None, None) if len(parts) == 0 else
(None, parts[0]) if len(parts) == 1 else
(parts[0], parts[1])) # RFC-7235, Appendix C.

# Interpret the config object for our own minimalistic credentials.
# Note: kubernetes client has no concept of a "current" context's namespace.
return credentials.ConnectionInfo(
server=config.host,
ca_path=config.ssl_ca_cert, # can be a temporary file
insecure=not config.verify_ssl,
username=config.username or None, # an empty string when not defined
password=config.password or None, # an empty string when not defined
scheme=scheme,
token=token,
certificate_path=config.cert_file, # can be a temporary file
private_key_path=config.key_file, # can be a temporary file
priority=PRIORITY_OF_ASYNC_CLIENT,
)
asteven marked this conversation as resolved.
Show resolved Hide resolved


def login_via_pykube(
*,
logger: typedefs.Logger,
Expand Down
26 changes: 18 additions & 8 deletions kopf/_core/intents/registries.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,14 +279,24 @@ def __init__(self) -> None:
_fallback=True,
))
if piggybacking.has_client():
self._activities.append(handlers.ActivityHandler(
id=ids.HandlerId('login_via_client'),
fn=piggybacking.login_via_client,
activity=causes.Activity.AUTHENTICATION,
errors=execution.ErrorsMode.IGNORED,
param=None, timeout=None, retries=None, backoff=None,
_fallback=True,
))
if piggybacking.has_sync_client():
self._activities.append(handlers.ActivityHandler(
id=ids.HandlerId('login_via_client'),
fn=piggybacking.login_via_client,
activity=causes.Activity.AUTHENTICATION,
errors=execution.ErrorsMode.IGNORED,
param=None, timeout=None, retries=None, backoff=None,
_fallback=True,
))
elif piggybacking.has_async_client():
self._activities.append(handlers.ActivityHandler(
id=ids.HandlerId('login_via_async_client'),
fn=piggybacking.login_via_async_client,
activity=causes.Activity.AUTHENTICATION,
errors=execution.ErrorsMode.IGNORED,
param=None, timeout=None, retries=None, backoff=None,
_fallback=True,
))

# As a last resort, fall back to rudimentary logins if no advanced ones are available.
thirdparties_present = piggybacking.has_pykube() or piggybacking.has_client()
Expand Down