Merge pull request #3210 from quay/project/kube-tls
Add ability to add extra certs for config tool
This commit is contained in:
commit
8a83bf7c81
9 changed files with 165 additions and 24 deletions
71
conf/init/02_get_kube_certs.py
Normal file
71
conf/init/02_get_kube_certs.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from requests import Request, Session
|
||||||
|
|
||||||
|
QUAYPATH = os.environ.get('QUAYPATH', '.')
|
||||||
|
KUBE_EXTRA_CA_CERTDIR = os.environ.get('KUBE_EXTRA_CA_CERTDIR', '%s/conf/kube_extra_certs' % QUAYPATH)
|
||||||
|
|
||||||
|
KUBERNETES_API_HOST = os.environ.get('KUBERNETES_SERVICE_HOST', '')
|
||||||
|
port = os.environ.get('KUBERNETES_SERVICE_PORT')
|
||||||
|
if port:
|
||||||
|
KUBERNETES_API_HOST += ':' + port
|
||||||
|
|
||||||
|
SERVICE_ACCOUNT_TOKEN_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/token'
|
||||||
|
|
||||||
|
QE_NAMESPACE = os.environ.get('QE_K8S_NAMESPACE', 'quay-enterprise')
|
||||||
|
QE_CONFIG_SECRET = os.environ.get('QE_K8S_CONFIG_SECRET', 'quay-enterprise-config-secret')
|
||||||
|
EXTRA_CA_DIRECTORY_PREFIX = 'extra_ca_certs_'
|
||||||
|
|
||||||
|
|
||||||
|
def _lookup_secret(service_token):
|
||||||
|
secret_url = 'namespaces/%s/secrets/%s' % (QE_NAMESPACE, QE_CONFIG_SECRET)
|
||||||
|
response = _execute_k8s_api(service_token, 'GET', secret_url)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise Exception('Cannot get the config secret')
|
||||||
|
return json.loads(response.text)
|
||||||
|
|
||||||
|
def _execute_k8s_api(service_account_token, method, relative_url, data=None, api_prefix='api/v1', content_type='application/json'):
|
||||||
|
headers = {
|
||||||
|
'Authorization': 'Bearer ' + service_account_token
|
||||||
|
}
|
||||||
|
|
||||||
|
if data:
|
||||||
|
headers['Content-Type'] = content_type
|
||||||
|
|
||||||
|
data = json.dumps(data) if data else None
|
||||||
|
session = Session()
|
||||||
|
url = 'https://%s/%s/%s' % (KUBERNETES_API_HOST, api_prefix, relative_url)
|
||||||
|
|
||||||
|
request = Request(method, url, data=data, headers=headers)
|
||||||
|
return session.send(request.prepare(), verify=False, timeout=2)
|
||||||
|
|
||||||
|
def is_extra_cert(key):
|
||||||
|
return key.find(EXTRA_CA_DIRECTORY_PREFIX) == 0
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Load the service account token from the local store.
|
||||||
|
if not os.path.exists(SERVICE_ACCOUNT_TOKEN_PATH):
|
||||||
|
raise Exception('Cannot load Kubernetes service account token')
|
||||||
|
|
||||||
|
with open(SERVICE_ACCOUNT_TOKEN_PATH, 'r') as f:
|
||||||
|
service_token = f.read()
|
||||||
|
|
||||||
|
secret_data = _lookup_secret(service_token).get('data', {})
|
||||||
|
cert_keys = filter(is_extra_cert, secret_data.keys())
|
||||||
|
|
||||||
|
for cert_key in cert_keys:
|
||||||
|
if not os.path.exists(KUBE_EXTRA_CA_CERTDIR):
|
||||||
|
os.mkdir(KUBE_EXTRA_CA_CERTDIR)
|
||||||
|
|
||||||
|
cert_value = base64.b64decode(secret_data[cert_key])
|
||||||
|
cert_filename = cert_key.replace(EXTRA_CA_DIRECTORY_PREFIX, '')
|
||||||
|
print "Found an extra cert %s in config-secret, copying to kube ca dir"
|
||||||
|
|
||||||
|
with open(os.path.join(KUBE_EXTRA_CA_CERTDIR, cert_filename), 'w') as f:
|
||||||
|
f.write(cert_value)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
12
conf/init/02_get_kube_certs.sh
Executable file
12
conf/init/02_get_kube_certs.sh
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
QUAYDIR=${QUAYDIR:-"/"}
|
||||||
|
QUAYPATH=${QUAYPATH:-"."}
|
||||||
|
QUAYCONF=${QUAYCONF:-"$QUAYPATH/conf"}
|
||||||
|
|
||||||
|
cd $QUAYDIR
|
||||||
|
|
||||||
|
if [[ "$KUBERNETES_SERVICE_HOST" != "" ]];then
|
||||||
|
echo "Running on kubernetes, attempting to retrieve extra certs from secret"
|
||||||
|
venv/bin/python $QUAYCONF/init/02_get_kube_certs.py
|
||||||
|
fi
|
|
@ -3,6 +3,12 @@ set -e
|
||||||
QUAYPATH=${QUAYPATH:-"."}
|
QUAYPATH=${QUAYPATH:-"."}
|
||||||
QUAYCONF=${QUAYCONF:-"$QUAYPATH/conf/stack"}
|
QUAYCONF=${QUAYCONF:-"$QUAYPATH/conf/stack"}
|
||||||
QUAYCONFIG=${QUAYCONFIG:-"$QUAYCONF/stack"}
|
QUAYCONFIG=${QUAYCONFIG:-"$QUAYCONF/stack"}
|
||||||
|
CERTDIR=${QUAYCONFIG/extra_ca_certs}
|
||||||
|
|
||||||
|
# If we're running under kube, the previous script (02_get_kube_certs.sh) will put the certs in a different location
|
||||||
|
if [[ "$KUBERNETES_SERVICE_HOST" != "" ]];then
|
||||||
|
CERTDIR=${KUBE_EXTRA_CA_CERTDIR:-"$QUAYPATH/conf/kube_extra_certs"}
|
||||||
|
fi
|
||||||
|
|
||||||
cd ${QUAYDIR:-"/quay-registry"}
|
cd ${QUAYDIR:-"/quay-registry"}
|
||||||
|
|
||||||
|
@ -13,25 +19,25 @@ then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Add extra trusted certificates (as a directory)
|
# Add extra trusted certificates (as a directory)
|
||||||
if [ -d $QUAYCONFIG/extra_ca_certs ]; then
|
if [ -d $CERTDIR ]; then
|
||||||
if test "$(ls -A "$QUAYCONFIG/extra_ca_certs")"; then
|
if test "$(ls -A "$CERTDIR")"; then
|
||||||
echo "Installing extra certificates found in $QUAYCONFIG/extra_ca_certs directory"
|
echo "Installing extra certificates found in $CERTDIR directory"
|
||||||
cp $QUAYCONFIG/extra_ca_certs/* /usr/local/share/ca-certificates/
|
cp $CERTDIR/* /usr/local/share/ca-certificates/
|
||||||
cat $QUAYCONFIG/extra_ca_certs/* >> venv/lib/python2.7/site-packages/requests/cacert.pem
|
cat $CERTDIR/* >> venv/lib/python2.7/site-packages/requests/cacert.pem
|
||||||
cat $QUAYCONFIG/extra_ca_certs/* >> venv/lib/python2.7/site-packages/certifi/cacert.pem
|
cat $CERTDIR/* >> venv/lib/python2.7/site-packages/certifi/cacert.pem
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Add extra trusted certificates (as a file)
|
# Add extra trusted certificates (as a file)
|
||||||
if [ -f $QUAYCONFIG/extra_ca_certs ]; then
|
if [ -f $CERTDIR ]; then
|
||||||
echo "Installing extra certificates found in $QUAYCONFIG/extra_ca_certs file"
|
echo "Installing extra certificates found in $CERTDIR file"
|
||||||
csplit -z -f /usr/local/share/ca-certificates/extra-ca- $QUAYCONFIG/extra_ca_certs '/-----BEGIN CERTIFICATE-----/' '{*}'
|
csplit -z -f /usr/local/share/ca-certificates/extra-ca- $CERTDIR '/-----BEGIN CERTIFICATE-----/' '{*}'
|
||||||
cat $QUAYCONFIG/extra_ca_certs >> venv/lib/python2.7/site-packages/requests/cacert.pem
|
cat $CERTDIR >> venv/lib/python2.7/site-packages/requests/cacert.pem
|
||||||
cat $QUAYCONFIG/extra_ca_certs >> venv/lib/python2.7/site-packages/certifi/cacert.pem
|
cat $CERTDIR >> venv/lib/python2.7/site-packages/certifi/cacert.pem
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Add extra trusted certificates (prefixed)
|
# Add extra trusted certificates (prefixed)
|
||||||
for f in $(find $QUAYCONFIG/ -maxdepth 1 -type f -name "extra_ca*")
|
for f in $(find $CERTDIR/ -maxdepth 1 -type f -name "extra_ca*")
|
||||||
do
|
do
|
||||||
echo "Installing extra cert $f"
|
echo "Installing extra cert $f"
|
||||||
cp "$f" /usr/local/share/ca-certificates/
|
cp "$f" /usr/local/share/ca-certificates/
|
||||||
|
|
|
@ -10,6 +10,9 @@ proxy_redirect off;
|
||||||
|
|
||||||
proxy_set_header Transfer-Encoding $http_transfer_encoding;
|
proxy_set_header Transfer-Encoding $http_transfer_encoding;
|
||||||
|
|
||||||
|
# The DB migrations sometimes take a while, so increase timeoutso we don't report an error
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://web_app_server;
|
proxy_pass http://web_app_server;
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,8 @@ class SuperUserCustomCertificate(ApiResource):
|
||||||
return '', 204
|
return '', 204
|
||||||
|
|
||||||
# Call the update script with config dir location to install the certificate immediately.
|
# Call the update script with config dir location to install the certificate immediately.
|
||||||
if subprocess.call([os.path.join(INIT_SCRIPTS_LOCATION, 'certs_install.sh')],
|
cert_dir = os.path.join(config_provider.get_config_dir_path(), EXTRA_CA_DIRECTORY)
|
||||||
env={ 'QUAYCONFIG': config_provider.get_config_dir_path() }) != 0:
|
if subprocess.call([os.path.join(INIT_SCRIPTS_LOCATION, 'certs_install.sh')], env={ 'CERTDIR': cert_dir }) != 0:
|
||||||
raise Exception('Could not install certificates')
|
raise Exception('Could not install certificates')
|
||||||
|
|
||||||
return '', 204
|
return '', 204
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import os
|
import os
|
||||||
|
import base64
|
||||||
|
|
||||||
from backports.tempfile import TemporaryDirectory
|
from backports.tempfile import TemporaryDirectory
|
||||||
|
|
||||||
from config_app.config_util.config.fileprovider import FileConfigProvider
|
from config_app.config_util.config.fileprovider import FileConfigProvider
|
||||||
from config_app.config_util.k8saccessor import KubernetesAccessorSingleton
|
from config_app.config_util.k8saccessor import KubernetesAccessorSingleton
|
||||||
|
from util.config.validator import EXTRA_CA_DIRECTORY, EXTRA_CA_DIRECTORY_PREFIX
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TransientDirectoryProvider(FileConfigProvider):
|
class TransientDirectoryProvider(FileConfigProvider):
|
||||||
|
@ -38,10 +42,25 @@ class TransientDirectoryProvider(FileConfigProvider):
|
||||||
return self.config_volume
|
return self.config_volume
|
||||||
|
|
||||||
def save_configuration_to_kubernetes(self):
|
def save_configuration_to_kubernetes(self):
|
||||||
config_path = self.get_config_dir_path()
|
data = {}
|
||||||
|
|
||||||
for name in os.listdir(config_path):
|
# Kubernetes secrets don't have sub-directories, so for the extra_ca_certs dir
|
||||||
|
# we have to put the extra certs in with a prefix, and then one of our init scripts
|
||||||
|
# (02_get_kube_certs.sh) will expand the prefixed certs into the equivalent directory
|
||||||
|
# so that they'll be installed correctly on startup by the certs_install script
|
||||||
|
certs_dir = os.path.join(self.config_volume, EXTRA_CA_DIRECTORY)
|
||||||
|
if os.path.exists(certs_dir):
|
||||||
|
for extra_cert in os.listdir(certs_dir):
|
||||||
|
with open(os.path.join(certs_dir, extra_cert)) as f:
|
||||||
|
data[EXTRA_CA_DIRECTORY_PREFIX + extra_cert] = base64.b64encode(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
for name in os.listdir(self.config_volume):
|
||||||
file_path = os.path.join(self.config_volume, name)
|
file_path = os.path.join(self.config_volume, name)
|
||||||
KubernetesAccessorSingleton.get_instance().save_file_as_secret(name, file_path)
|
if not os.path.isdir(file_path):
|
||||||
|
with open(file_path) as f:
|
||||||
|
data[name] = base64.b64encode(f.read())
|
||||||
|
|
||||||
|
KubernetesAccessorSingleton.get_instance().replace_qe_secret(data)
|
||||||
|
|
||||||
return 200
|
return 200
|
||||||
|
|
|
@ -35,11 +35,41 @@ class KubernetesAccessorSingleton(object):
|
||||||
|
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def save_file_as_secret(self, name, file_path):
|
def save_file_as_secret(self, name, file_pointer):
|
||||||
with open(file_path) as f:
|
value = file_pointer.read()
|
||||||
value = f.read()
|
|
||||||
self._update_secret_file(name, value)
|
self._update_secret_file(name, value)
|
||||||
|
|
||||||
|
def replace_qe_secret(self, new_secret_data):
|
||||||
|
"""
|
||||||
|
Removes the old config and replaces it with the new_secret_data as one action
|
||||||
|
"""
|
||||||
|
# Check first that the namespace for Quay Enterprise exists. If it does not, report that
|
||||||
|
# as an error, as it seems to be a common issue.
|
||||||
|
namespace_url = 'namespaces/%s' % (self.kube_config.qe_namespace)
|
||||||
|
response = self._execute_k8s_api('GET', namespace_url)
|
||||||
|
if response.status_code // 100 != 2:
|
||||||
|
msg = 'A Kubernetes namespace with name `%s` must be created to save config' % self.kube_config.qe_namespace
|
||||||
|
raise Exception(msg)
|
||||||
|
|
||||||
|
# Check if the secret exists. If not, then we create an empty secret and then update the file
|
||||||
|
# inside.
|
||||||
|
secret_url = 'namespaces/%s/secrets/%s' % (self.kube_config.qe_namespace, self.kube_config.qe_config_secret)
|
||||||
|
secret = self._lookup_secret()
|
||||||
|
if secret is None:
|
||||||
|
self._assert_success(self._execute_k8s_api('POST', secret_url, {
|
||||||
|
"kind": "Secret",
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"metadata": {
|
||||||
|
"name": self.kube_config.qe_config_secret
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Update the secret to reflect the file change.
|
||||||
|
secret['data'] = new_secret_data
|
||||||
|
|
||||||
|
self._assert_success(self._execute_k8s_api('PUT', secret_url, secret))
|
||||||
|
|
||||||
def get_qe_deployments(self):
|
def get_qe_deployments(self):
|
||||||
""""
|
""""
|
||||||
Returns all deployments matching the label selector provided in the KubeConfig
|
Returns all deployments matching the label selector provided in the KubeConfig
|
||||||
|
|
|
@ -24,7 +24,7 @@ export class KubeDeployModalComponent {
|
||||||
this.state = 'loadingDeployments';
|
this.state = 'loadingDeployments';
|
||||||
|
|
||||||
ApiService.scGetNumDeployments().then(resp => {
|
ApiService.scGetNumDeployments().then(resp => {
|
||||||
this.deploymentsStatus = resp.items.map(dep => ({ name: dep.metadata.name, numPods: dep.status.replicas }));
|
this.deploymentsStatus = resp.items.map(dep => ({ name: dep.metadata.name, numPods: dep.spec.replicas }));
|
||||||
this.state = 'readyToDeploy';
|
this.state = 'readyToDeploy';
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
this.state = 'error';
|
this.state = 'error';
|
||||||
|
@ -46,7 +46,6 @@ export class KubeDeployModalComponent {
|
||||||
this.errorMessage = `Could cycle the deployments with the new configuration. Error: ${err.toString()}`;
|
this.errorMessage = `Could cycle the deployments with the new configuration. Error: ${err.toString()}`;
|
||||||
})
|
})
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.log(err)
|
|
||||||
this.state = 'error';
|
this.state = 'error';
|
||||||
this.errorMessage = `Could not deploy the configuration. Error: ${err.toString()}`;
|
this.errorMessage = `Could not deploy the configuration. Error: ${err.toString()}`;
|
||||||
})
|
})
|
||||||
|
|
|
@ -41,6 +41,7 @@ CONFIG_FILENAMES = (SSL_FILENAMES + DB_SSL_FILENAMES + JWT_FILENAMES + ACI_CERT_
|
||||||
LDAP_FILENAMES)
|
LDAP_FILENAMES)
|
||||||
CONFIG_FILE_SUFFIXES = ['-cloudfront-signing-key.pem']
|
CONFIG_FILE_SUFFIXES = ['-cloudfront-signing-key.pem']
|
||||||
EXTRA_CA_DIRECTORY = 'extra_ca_certs'
|
EXTRA_CA_DIRECTORY = 'extra_ca_certs'
|
||||||
|
EXTRA_CA_DIRECTORY_PREFIX = 'extra_ca_certs_'
|
||||||
|
|
||||||
VALIDATORS = {
|
VALIDATORS = {
|
||||||
DatabaseValidator.name: DatabaseValidator.validate,
|
DatabaseValidator.name: DatabaseValidator.validate,
|
||||||
|
|
Reference in a new issue