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:-"."} | ||||
| QUAYCONF=${QUAYCONF:-"$QUAYPATH/conf/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"} | ||||
| 
 | ||||
|  | @ -13,25 +19,25 @@ then | |||
| fi | ||||
| 
 | ||||
| # Add extra trusted certificates (as a directory) | ||||
| if [ -d $QUAYCONFIG/extra_ca_certs ]; then | ||||
|   if test "$(ls -A "$QUAYCONFIG/extra_ca_certs")"; then | ||||
|       echo "Installing extra certificates found in $QUAYCONFIG/extra_ca_certs directory" | ||||
|       cp $QUAYCONFIG/extra_ca_certs/* /usr/local/share/ca-certificates/ | ||||
|       cat $QUAYCONFIG/extra_ca_certs/* >> venv/lib/python2.7/site-packages/requests/cacert.pem | ||||
|       cat $QUAYCONFIG/extra_ca_certs/* >> venv/lib/python2.7/site-packages/certifi/cacert.pem | ||||
| if [ -d $CERTDIR ]; then | ||||
|   if test "$(ls -A "$CERTDIR")"; then | ||||
|       echo "Installing extra certificates found in $CERTDIR directory" | ||||
|       cp $CERTDIR/* /usr/local/share/ca-certificates/ | ||||
|       cat $CERTDIR/* >> venv/lib/python2.7/site-packages/requests/cacert.pem | ||||
|       cat $CERTDIR/* >> venv/lib/python2.7/site-packages/certifi/cacert.pem | ||||
|   fi | ||||
| fi | ||||
| 
 | ||||
| # Add extra trusted certificates (as a file) | ||||
| if [ -f $QUAYCONFIG/extra_ca_certs ]; then | ||||
|   echo "Installing extra certificates found in $QUAYCONFIG/extra_ca_certs file" | ||||
|   csplit -z -f /usr/local/share/ca-certificates/extra-ca- $QUAYCONFIG/extra_ca_certs  '/-----BEGIN CERTIFICATE-----/' '{*}' | ||||
|   cat $QUAYCONFIG/extra_ca_certs >> venv/lib/python2.7/site-packages/requests/cacert.pem | ||||
|   cat $QUAYCONFIG/extra_ca_certs >> venv/lib/python2.7/site-packages/certifi/cacert.pem | ||||
| if [ -f $CERTDIR ]; then | ||||
|   echo "Installing extra certificates found in $CERTDIR file" | ||||
|   csplit -z -f /usr/local/share/ca-certificates/extra-ca- $CERTDIR  '/-----BEGIN CERTIFICATE-----/' '{*}' | ||||
|   cat $CERTDIR >> venv/lib/python2.7/site-packages/requests/cacert.pem | ||||
|   cat $CERTDIR >> venv/lib/python2.7/site-packages/certifi/cacert.pem | ||||
| fi | ||||
| 
 | ||||
| # 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 | ||||
|  echo "Installing extra cert $f" | ||||
|  cp "$f" /usr/local/share/ca-certificates/ | ||||
|  |  | |||
|  | @ -10,6 +10,9 @@ proxy_redirect off; | |||
| 
 | ||||
| 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 / { | ||||
|     proxy_pass   http://web_app_server; | ||||
| } | ||||
|  |  | |||
|  | @ -54,8 +54,8 @@ class SuperUserCustomCertificate(ApiResource): | |||
|       return '', 204 | ||||
| 
 | ||||
|     # 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')], | ||||
|                        env={ 'QUAYCONFIG': config_provider.get_config_dir_path() }) != 0: | ||||
|     cert_dir = os.path.join(config_provider.get_config_dir_path(), EXTRA_CA_DIRECTORY) | ||||
|     if subprocess.call([os.path.join(INIT_SCRIPTS_LOCATION, 'certs_install.sh')], env={ 'CERTDIR': cert_dir }) != 0: | ||||
|       raise Exception('Could not install certificates') | ||||
| 
 | ||||
|     return '', 204 | ||||
|  |  | |||
|  | @ -1,8 +1,12 @@ | |||
| import os | ||||
| import base64 | ||||
| 
 | ||||
| from backports.tempfile import TemporaryDirectory | ||||
| 
 | ||||
| from config_app.config_util.config.fileprovider import FileConfigProvider | ||||
| from config_app.config_util.k8saccessor import KubernetesAccessorSingleton | ||||
| from util.config.validator import EXTRA_CA_DIRECTORY, EXTRA_CA_DIRECTORY_PREFIX | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| class TransientDirectoryProvider(FileConfigProvider): | ||||
|  | @ -38,10 +42,25 @@ class TransientDirectoryProvider(FileConfigProvider): | |||
|     return self.config_volume | ||||
| 
 | ||||
|   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) | ||||
|       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 | ||||
|  |  | |||
|  | @ -35,10 +35,40 @@ class KubernetesAccessorSingleton(object): | |||
| 
 | ||||
|     return cls._instance | ||||
| 
 | ||||
|   def save_file_as_secret(self, name, file_path): | ||||
|     with open(file_path) as f: | ||||
|       value = f.read() | ||||
|       self._update_secret_file(name, value) | ||||
|   def save_file_as_secret(self, name, file_pointer): | ||||
|     value = file_pointer.read() | ||||
|     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): | ||||
|     """" | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ export class KubeDeployModalComponent { | |||
|         this.state = 'loadingDeployments'; | ||||
| 
 | ||||
|         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'; | ||||
|         }).catch(err => { | ||||
|             this.state = 'error'; | ||||
|  | @ -37,7 +37,7 @@ export class KubeDeployModalComponent { | |||
| 
 | ||||
|     deployConfiguration(): void { | ||||
|         this.ApiService.scDeployConfiguration().then(() => { | ||||
|             const deploymentNames: string[]= this.deploymentsStatus.map(dep => dep.name); | ||||
|             const deploymentNames: string[] = this.deploymentsStatus.map(dep => dep.name); | ||||
| 
 | ||||
|             this.ApiService.scCycleQEDeployments({ deploymentNames }).then(() => { | ||||
|                 this.state = 'deployed' | ||||
|  | @ -46,7 +46,6 @@ export class KubeDeployModalComponent { | |||
|                 this.errorMessage = `Could cycle the deployments with the new configuration. Error: ${err.toString()}`; | ||||
|             }) | ||||
|         }).catch(err => { | ||||
|             console.log(err) | ||||
|             this.state = 'error'; | ||||
|             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) | ||||
| CONFIG_FILE_SUFFIXES = ['-cloudfront-signing-key.pem'] | ||||
| EXTRA_CA_DIRECTORY = 'extra_ca_certs' | ||||
| EXTRA_CA_DIRECTORY_PREFIX = 'extra_ca_certs_' | ||||
| 
 | ||||
| VALIDATORS = { | ||||
|   DatabaseValidator.name: DatabaseValidator.validate, | ||||
|  |  | |||
		Reference in a new issue