示例#1
0
def login_via_pykube(
        *args: Any,
        logger: Union[logging.Logger, logging.LoggerAdapter],
        **kwargs: Any,
) -> Optional[credentials.ConnectionInfo]:

    try:
        import pykube
    except ImportError:
        return None

    # Read the pykube config either way for later interpretation.
    # DEPRECATED: Previously, in some cases, get_pykube_cfg() was monkey-patched
    # to inject custom authentication methods. Support these hacks if possible.
    config: pykube.KubeConfig
    try:
        with warnings.catch_warnings():
            warnings.simplefilter('ignore', DeprecationWarning)
            config = auth.get_pykube_cfg()
        logger.debug("Pykube is configured via monkey-patched get_pykube_cfg().")
    except NotImplementedError:
        try:
            config = pykube.KubeConfig.from_service_account()
            logger.debug("Pykube is configured in cluster with service account.")
        except FileNotFoundError:
            try:
                config = pykube.KubeConfig.from_file()
                logger.debug("Pykube is configured via kubeconfig file.")
            except (pykube.PyKubeError, FileNotFoundError):
                raise credentials.LoginError(f"Cannot authenticate pykube "
                                             f"neither in-cluster, nor via kubeconfig.")

    # We don't know how this token will be retrieved, we just get it afterwards.
    provider_token = None
    if config.user.get('auth-provider'):
        api = pykube.HTTPClient(config)
        api.get(version='', base='/')  # ignore the response status
        provider_token = config.user.get('auth-provider', {}).get('config', {}).get('access-token')

    # Interpret the config object for our own minimalistic credentials.
    ca: Optional[pykube.config.BytesOrFile] = config.cluster.get('certificate-authority')
    cert: Optional[pykube.config.BytesOrFile] = config.user.get('client-certificate')
    pkey: Optional[pykube.config.BytesOrFile] = config.user.get('client-key')
    return credentials.ConnectionInfo(
        server=config.cluster.get('server'),
        ca_path=ca.filename() if ca else None,  # can be a temporary file
        insecure=config.cluster.get('insecure-skip-tls-verify'),
        username=config.user.get('username'),
        password=config.user.get('password'),
        token=config.user.get('token') or provider_token,
        certificate_path=cert.filename() if cert else None,  # can be a temporary file
        private_key_path=pkey.filename() if pkey else None,  # can be a temporary file
        default_namespace=config.namespace,
        priority=PRIORITY_OF_PYKUBE,
    )
示例#2
0
def login_via_client(
    *args: Any,
    logger: Union[logging.Logger, logging.LoggerAdapter],
    **kwargs: Any,
) -> Optional[credentials.ConnectionInfo]:

    try:
        import kubernetes.config
    except ImportError:
        return None

    try:
        kubernetes.config.load_incluster_config()  # cluster env vars
        logger.debug("Client is configured in cluster with service account.")
    except kubernetes.config.ConfigException as e1:
        try:
            kubernetes.config.load_kube_config()  # developer's config files
            logger.debug("Client is configured via kubeconfig file.")
        except kubernetes.config.ConfigException as e2:
            raise credentials.LoginError(
                f"Cannot authenticate client 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.client.Configuration, 'get_default_copy',
                    None)):
        config = kubernetes.client.Configuration.get_default_copy()
    else:
        config = kubernetes.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('authorization')
    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_CLIENT,
    )
示例#3
0
 async def wrapper(*args: Any, **kwargs: Any) -> Any:
     vault: credentials.Vault = vault_var.get()
     async for key, info, session in vault.extended(
             APISession.from_connection_info, 'sessions'):
         try:
             return await fn(*args, **kwargs, session=session)
         except aiohttp.ClientResponseError as e:
             if e.status == 401:
                 await vault.invalidate(key, exc=e)
             else:
                 raise
     else:
         raise credentials.LoginError("Ran out of connection credentials.")
示例#4
0
文件: auth.py 项目: ztaylor54/kopf
    async def wrapper(*args: Any, **kwargs: Any) -> Any:

        # If a context is explicitly passed, make it a simple call without re-auth.
        # Exceptions are escalated to a caller, which is probably wrapped itself.
        if 'context' in kwargs:
            return await fn(*args, **kwargs)

        # Otherwise, attempt the execution with the vault credentials and re-authenticate on 401s.
        vault: credentials.Vault = vault_var.get()
        async for key, info, context in vault.extended(APIContext, 'contexts'):
            try:
                return await fn(*args, **kwargs, context=context)
            except errors.APIUnauthorizedError as e:
                await vault.invalidate(key, exc=e)
        raise credentials.LoginError("Ran out of connection credentials.")
示例#5
0
    async def wrapper(*args: Any, **kwargs: Any) -> Any:

        # If a session is explicitly passed, make it a simple call without re-auth.
        # Exceptions are escalated to a caller, which is probably wrapped itself.
        if 'session' in kwargs:
            return await fn(*args, **kwargs)

        # Otherwise, attempt the execution with the vault credentials and re-authenticate on 401s.
        vault: credentials.Vault = vault_var.get()
        async for key, info, session in vault.extended(APISession.from_connection_info, 'sessions'):
            try:
                return await fn(*args, **kwargs, session=session)
            except aiohttp.ClientResponseError as e:
                if e.status == 401:
                    await vault.invalidate(key, exc=e)
                else:
                    raise
        else:
            raise credentials.LoginError("Ran out of connection credentials.")
示例#6
0
文件: auth.py 项目: blaisep/kopf
    async def wrapper(*args: Any, **kwargs: Any) -> Any:

        # If a context is explicitly passed, make it a simple call without re-auth.
        # Exceptions are escalated to a caller, which is probably wrapped itself.
        if 'context' in kwargs:
            async for item in fn(*args, **kwargs):
                yield item
            return

        # Otherwise, attempt the execution with the vault credentials and re-authenticate on 401s.
        vault: credentials.Vault = vault_var.get()
        async for key, info, context in vault.extended(APIContext, 'contexts'):
            try:
                async for item in fn(*args, **kwargs, context=context):
                    yield item
                return
            except aiohttp.ClientResponseError as e:
                if e.status == 401:
                    await vault.invalidate(key, exc=e)
                else:
                    raise
        raise credentials.LoginError("Ran out of connection credentials.")
示例#7
0
    def from_connection_info(
            cls,
            info: credentials.ConnectionInfo,
    ) -> "APISession":

        # Some SSL data are not accepted directly, so we have to use temp files.
        tempfiles = _TempFiles()
        ca_path: Optional[str]
        certificate_path: Optional[str]
        private_key_path: Optional[str]

        if info.ca_path and info.ca_data:
            raise credentials.LoginError("Both CA path & data are set. Need only one.")
        elif info.ca_path:
            ca_path = info.ca_path
        elif info.ca_data:
            ca_path = tempfiles[base64.b64decode(info.ca_data)]
        else:
            ca_path = None

        if info.certificate_path and info.certificate_data:
            raise credentials.LoginError("Both certificate path & data are set. Need only one.")
        elif info.certificate_path:
            certificate_path = info.certificate_path
        elif info.certificate_data:
            certificate_path = tempfiles[base64.b64decode(info.certificate_data)]
        else:
            certificate_path = None

        if info.private_key_path and info.private_key_data:
            raise credentials.LoginError("Both private key path & data are set. Need only one.")
        elif info.private_key_path:
            private_key_path = info.private_key_path
        elif info.private_key_data:
            private_key_path = tempfiles[base64.b64decode(info.private_key_data)]
        else:
            private_key_path = None

        # The SSL part (both client certificate auth and CA verification).
        context: ssl.SSLContext
        if certificate_path and private_key_path:
            context = ssl.create_default_context(
                purpose=ssl.Purpose.CLIENT_AUTH,
                cafile=ca_path)
            context.load_cert_chain(
                certfile=certificate_path,
                keyfile=private_key_path)
        else:
            context = ssl.create_default_context(
                cafile=ca_path)

        if info.insecure:
            context.check_hostname = False
            context.verify_mode = ssl.CERT_NONE

        # The token auth part.
        headers: Dict[str, str] = {}
        if info.scheme and info.token:
            headers['Authorization'] = f'{info.scheme} {info.token}'
        elif info.scheme:
            headers['Authorization'] = f'{info.scheme}'
        elif info.token:
            headers['Authorization'] = f'Bearer {info.token}'

        # The basic auth part.
        auth: Optional[aiohttp.BasicAuth]
        if info.username and info.password:
            auth = aiohttp.BasicAuth(info.username, info.password)
        else:
            auth = None

        # It is a good practice to self-identify a bit.
        headers['User-Agent'] = f'kopf/unknown'  # TODO: add version someday

        # Generic aiohttp session based on the constructed credentials.
        session = cls(
            connector=aiohttp.TCPConnector(
                limit=0,
                ssl=context,
            ),
            headers=headers,
            auth=auth,
        )

        # Add the extra payload information. We avoid overriding the constructor.
        session.server = info.server
        session.default_namespace = info.default_namespace
        session._tempfiles = tempfiles  # for purging on garbage collection
        session._discovery_lock = asyncio.Lock()
        session._discovered_resources = {}

        return session