Exemplo n.º 1
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,
    )
Exemplo n.º 2
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.
    config: pykube.KubeConfig
    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,
    )
Exemplo n.º 3
0
    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.")
Exemplo n.º 4
0
def login_with_kubeconfig(**_: Any) -> Optional[credentials.ConnectionInfo]:
    """
    A minimalistic login handler that can get raw data from a kubeconfig file.

    Authentication capabilities can be limited to keep the code short & simple.
    No parsing or sophisticated multi-step token retrieval is performed.

    This login function is intended to make Kopf runnable in trivial cases
    when neither pykube-ng nor the official client library are installed.
    """

    # As per https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/
    kubeconfig = os.environ.get('KUBECONFIG')
    if not kubeconfig and os.path.exists(os.path.expanduser('~/.kube/config')):
        kubeconfig = '~/.kube/config'
    if not kubeconfig:
        return None

    paths = [path.strip() for path in kubeconfig.split(os.pathsep)]
    paths = [os.path.expanduser(path) for path in paths if path]

    # As prescribed: if the file is absent or non-deserialisable, then fail. The first value wins.
    current_context: Optional[str] = None
    contexts: Dict[Any, Any] = {}
    clusters: Dict[Any, Any] = {}
    users: Dict[Any, Any] = {}
    for path in paths:

        with open(path, 'rt', encoding='utf-8') as f:
            config = yaml.safe_load(f.read()) or {}

        if current_context is None:
            current_context = config.get('current-context')
        for item in config.get('contexts', []):
            if item['name'] not in contexts:
                contexts[item['name']] = item.get('context') or {}
        for item in config.get('clusters', []):
            if item['name'] not in clusters:
                clusters[item['name']] = item.get('cluster') or {}
        for item in config.get('users', []):
            if item['name'] not in users:
                users[item['name']] = item.get('user') or {}

    # Once fully parsed, use the current context only.
    if current_context is None:
        raise credentials.LoginError('Current context is not set in kubeconfigs.')
    context = contexts[current_context]
    cluster = clusters[context['cluster']]
    user = users[context['user']]

    # Unlike pykube's login, we do not make a fake API request to refresh the token.
    provider_token = user.get('auth-provider', {}).get('config', {}).get('access-token')

    # Map the retrieved fields into the credentials object.
    return credentials.ConnectionInfo(
        server=cluster.get('server'),
        ca_path=cluster.get('certificate-authority'),
        ca_data=cluster.get('certificate-authority-data'),
        insecure=cluster.get('insecure-skip-tls-verify'),
        certificate_path=user.get('client-certificate'),
        certificate_data=user.get('client-certificate-data'),
        private_key_path=user.get('client-key'),
        private_key_data=user.get('client-key-data'),
        username=user.get('username'),
        password=user.get('password'),
        token=user.get('token') or provider_token,
        default_namespace=context.get('namespace'),
        priority=PRIORITY_OF_KUBECONFIG,
    )
Exemplo n.º 5
0
    def __init__(
            self,
            info: credentials.ConnectionInfo,
    ) -> None:
        super().__init__()

        # 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.
        self.session = aiohttp.ClientSession(
            connector=aiohttp.TCPConnector(
                limit=0,
                ssl=context,
            ),
            headers=headers,
            auth=auth,
        )

        # Add the extra payload information. We avoid overriding the constructor.
        self.server = info.server
        self.default_namespace = info.default_namespace

        # For purging on garbage collection.
        self._tempfiles = tempfiles