-
Notifications
You must be signed in to change notification settings - Fork 70
Add support for authenticating via client certificate for Openshift #696
base: master
Are you sure you want to change the base?
Changes from all commits
bd6fd20
ba16abd
f396a57
cc1fe89
bdc3a2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
import anymarkup | ||
import os | ||
from base64 import b64decode | ||
|
||
from atomicapp.plugin import ProviderFailedException | ||
from atomicapp.constants import (PROVIDER_AUTH_KEY, | ||
|
@@ -8,6 +10,8 @@ | |
PROVIDER_TLS_VERIFY_KEY, | ||
PROVIDER_CA_KEY) | ||
import logging | ||
import atexit | ||
from atomicapp.utils import Utils | ||
logger = logging.getLogger(LOGGER_DEFAULT) | ||
|
||
|
||
|
@@ -68,7 +72,7 @@ def parse_kubeconf_data(kubecfg): | |
dict of parsed values from config | ||
""" | ||
url = None | ||
token = None | ||
auth = None | ||
namespace = None | ||
tls_verify = True | ||
ca = None | ||
|
@@ -103,16 +107,57 @@ def parse_kubeconf_data(kubecfg): | |
logger.debug("user: %s", user) | ||
|
||
url = cluster["cluster"]["server"] | ||
token = user["user"]["token"] | ||
if "namespace" in context["context"]: | ||
namespace = context["context"]["namespace"] | ||
if "insecure-skip-tls-verify" in cluster["cluster"]: | ||
tls_verify = not cluster["cluster"]["insecure-skip-tls-verify"] | ||
elif "certificate-authority" in cluster["cluster"]: | ||
ca = cluster["cluster"]["certificate-authority"] | ||
|
||
auth = user["user"].get("token") | ||
namespace = context["context"].get("namespace") | ||
tls_verify = not cluster["cluster"].get("insecure-skip-tls-verify") | ||
|
||
temporary_files = [] | ||
if tls_verify: | ||
ca_data = cluster["cluster"].get("certificate-authority-data") | ||
if ca_data: | ||
ca = Utils.getTmpFile(b64decode(ca_data)) | ||
temporary_files.append(ca) | ||
else: | ||
if "certificate-authority" in cluster["cluster"]: | ||
# if we are in container translate path to path on host | ||
ca = os.path.join(Utils.getRoot(), | ||
cluster["cluster"].get("certificate-authority").lstrip('/')) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should probably add a comment above the if statement below that states something like: "We will give precedence to an access token if it was provided in the kube-config, if not given an access token then check for client certs." |
||
if not auth: | ||
# If token not specified, check for certificate auth. | ||
|
||
# client-certificate-data and client-key-data options overrides | ||
# client-certificate and client-key | ||
# https://github.com/kubernetes/kubernetes/blob/v1.2.2/pkg/client/unversioned/clientcmd/api/types.go#L78 | ||
# `{client-certificate,client-key}-data` keys in Kubernetes config | ||
# file are inline base64 encoded certificates, requests library | ||
# requires certs in files, this is why we are putting them to tmp | ||
# files. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we add a few more users with examples of these cases to the you can truncate output for brevity, just would like a code reader to be able to reference something so they can better understand this code. |
||
|
||
cert_data = user["user"].get("client-certificate-data") | ||
key_data = user["user"].get("client-key-data") | ||
|
||
if cert_data: | ||
cert = Utils.getTmpFile(b64decode(cert_data)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do these tmp files ever get cleaned up? If not we should probably try to make sure they do. I know this makes it harder :( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be able to use atexit to register these to be deleted. https://docs.python.org/2/library/atexit.html |
||
temporary_files.append(cert) | ||
else: | ||
if "client-certificate" in user["user"]: | ||
cert = os.path.join(Utils.getRoot(), | ||
user["user"].get("client-certificate").lstrip('/')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. really long line |
||
|
||
if key_data: | ||
key = Utils.getTmpFile(b64decode(key_data)) | ||
temporary_files.append(key) | ||
else: | ||
if "client-key" in user["user"]: | ||
key = os.path.join(Utils.getRoot(), | ||
user["user"].get("client-key").lstrip('/')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. really long line |
||
|
||
auth = "{}:{}".format(cert, key) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a comment that explains what this format is.. i.e. certfile:keyfile, because that is the format the provider expects it in. |
||
|
||
atexit.register(Utils.rm_files, *temporary_files) | ||
return {PROVIDER_API_KEY: url, | ||
PROVIDER_AUTH_KEY: token, | ||
PROVIDER_AUTH_KEY: auth, | ||
NAMESPACE_KEY: namespace, | ||
PROVIDER_TLS_VERIFY_KEY: tls_verify, | ||
PROVIDER_CA_KEY: ca} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,11 +50,12 @@ | |
class OpenshiftClient(object): | ||
|
||
def __init__(self, providerapi, access_token, | ||
provider_tls_verify, provider_ca): | ||
provider_tls_verify, provider_ca, certs): | ||
self.providerapi = providerapi | ||
self.access_token = access_token | ||
self.provider_tls_verify = provider_tls_verify | ||
self.provider_ca = provider_ca | ||
self.certs = certs | ||
|
||
# construct full urls for api endpoints | ||
self.kubernetes_api = urljoin(self.providerapi, "api/v1/") | ||
|
@@ -80,7 +81,8 @@ def test_connection(self): | |
(status_code, return_data) = \ | ||
Utils.make_rest_request("get", | ||
self.openshift_api, | ||
verify=self._requests_tls_verify()) | ||
verify=self._requests_tls_verify(), | ||
cert=self.certs) | ||
except SSLError as e: | ||
if self.provider_tls_verify: | ||
msg = "SSL/TLS ERROR: invalid certificate. " \ | ||
|
@@ -100,7 +102,8 @@ def get_oapi_resources(self): | |
(status_code, return_data) = \ | ||
Utils.make_rest_request("get", | ||
self.openshift_api, | ||
verify=self._requests_tls_verify()) | ||
verify=self._requests_tls_verify(), | ||
cert=self.certs) | ||
if status_code == 200: | ||
oapi_resources = return_data["resources"] | ||
else: | ||
|
@@ -121,7 +124,8 @@ def get_kapi_resources(self): | |
(status_code, return_data) = \ | ||
Utils.make_rest_request("get", | ||
self.kubernetes_api, | ||
verify=self._requests_tls_verify()) | ||
verify=self._requests_tls_verify(), | ||
cert=self.certs) | ||
if status_code == 200: | ||
kapi_resources = return_data["resources"] | ||
else: | ||
|
@@ -139,7 +143,8 @@ def deploy(self, url, artifact): | |
Utils.make_rest_request("post", | ||
url, | ||
verify=self._requests_tls_verify(), | ||
data=artifact) | ||
data=artifact, | ||
cert=self.certs) | ||
if status_code == 201: | ||
logger.info("Object %s successfully deployed.", | ||
artifact['metadata']['name']) | ||
|
@@ -162,7 +167,8 @@ def delete(self, url): | |
(status_code, return_data) = \ | ||
Utils.make_rest_request("delete", | ||
url, | ||
verify=self._requests_tls_verify()) | ||
verify=self._requests_tls_verify(), | ||
cert=self.certs) | ||
if status_code == 200: | ||
logger.info("Successfully deleted.") | ||
else: | ||
|
@@ -186,7 +192,8 @@ def scale(self, url, replicas): | |
Utils.make_rest_request("patch", | ||
url, | ||
data=patch, | ||
verify=self._requests_tls_verify()) | ||
verify=self._requests_tls_verify(), | ||
cert=self.certs) | ||
if status_code == 200: | ||
logger.info("Successfully scaled to %s replicas", replicas) | ||
else: | ||
|
@@ -199,7 +206,8 @@ def process_template(self, url, template): | |
Utils.make_rest_request("post", | ||
url, | ||
verify=self._requests_tls_verify(), | ||
data=template) | ||
data=template, | ||
cert=self.certs) | ||
if status_code == 201: | ||
logger.info("template processed %s", template['metadata']['name']) | ||
logger.debug("processed template %s", return_data) | ||
|
@@ -306,7 +314,10 @@ def get_pod_status(self, namespace, pod): | |
'namespaces/{namespace}/pods/{pod}?' | ||
'access_token={access_token}'.format(**args)) | ||
(status_code, return_data) = \ | ||
Utils.make_rest_request("get", url, verify=self._requests_tls_verify()) | ||
Utils.make_rest_request("get", | ||
url, | ||
verify=self._requests_tls_verify(), | ||
cert=self.certs) | ||
|
||
if status_code != 200: | ||
raise ProviderFailedException( | ||
|
@@ -331,6 +342,8 @@ class OpenShiftProvider(Provider): | |
provider_tls_verify = True | ||
# path to file or dir with CA certificates | ||
provider_ca = None | ||
# client certificate and key for authentication | ||
certs = None | ||
|
||
def init(self): | ||
# Parsed artifacts. Key is kind of artifacts. Value is list of artifacts. | ||
|
@@ -341,7 +354,8 @@ def init(self): | |
self.oc = OpenshiftClient(self.providerapi, | ||
self.access_token, | ||
self.provider_tls_verify, | ||
self.provider_ca) | ||
self.provider_ca, | ||
self.certs) | ||
self.openshift_api = self.oc.openshift_api | ||
self.kubernetes_api = self.oc.kubernetes_api | ||
|
||
|
@@ -432,7 +446,10 @@ def stop(self): | |
"replicationcontroller", | ||
params=params) | ||
(status_code, return_data) = \ | ||
Utils.make_rest_request("get", url, verify=self.oc._requests_tls_verify()) | ||
Utils.make_rest_request("get", | ||
url, | ||
verify=self.oc._requests_tls_verify(), | ||
cert=self.certs) | ||
if status_code != 200: | ||
raise ProviderFailedException("Cannot get Replication" | ||
"Controllers for Deployment" | ||
|
@@ -593,10 +610,11 @@ def _get_url(self, namespace, kind, name=None, params=None): | |
if name: | ||
url = urljoin(url, name) | ||
|
||
if params: | ||
params["access_token"] = self.access_token | ||
else: | ||
params = {"access_token": self.access_token} | ||
if not params: | ||
params = {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just set |
||
|
||
if self.access_token: | ||
params["access_token"] = self.access_token | ||
|
||
url = urljoin(url, "?%s" % urlencode(params)) | ||
logger.debug("url: %s", url) | ||
|
@@ -677,15 +695,16 @@ def _set_config_values(self): | |
|
||
# set config values | ||
self.providerapi = result[PROVIDER_API_KEY] | ||
self.access_token = result[PROVIDER_AUTH_KEY] | ||
if ":" in result[PROVIDER_AUTH_KEY]: | ||
self.access_token = None | ||
self.certs = tuple(result[PROVIDER_AUTH_KEY].split(":")) | ||
else: | ||
self.access_token = result[PROVIDER_AUTH_KEY] | ||
self.certs = None | ||
|
||
self.namespace = result[NAMESPACE_KEY] | ||
self.provider_tls_verify = result[PROVIDER_TLS_VERIFY_KEY] | ||
if result[PROVIDER_CA_KEY]: | ||
# if we are in container translate path to path on host | ||
self.provider_ca = os.path.join(Utils.getRoot(), | ||
result[PROVIDER_CA_KEY].lstrip('/')) | ||
else: | ||
self.provider_ca = None | ||
self.provider_ca = result[PROVIDER_CA_KEY] | ||
|
||
def extract(self, image, src, dest, update=True): | ||
""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
really long line.. can we break it up into two? maybe
cluster["cluster"].get("certificate-authority").lstrip('/')
on the first and thejoin
on the 2nd.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"into two" meaning we would need to assignments probably
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now when #705 is merged, I'm going to shorten this by using
get_real_abspath
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hey @kadel and @dustymabe feel free to look at my implementation
def cert_file
under https://github.com/projectatomic/atomicapp/pull/712/files (specifically,kubebase.py
) for how I implemented the base64 encoding / files and such.