Add cycling deployments and updating config

Add kube config with refactor to kube accessor

Add tests for k8s accessor, some styling changes
This commit is contained in:
Sam Chow 2018-08-09 16:43:11 -04:00
parent d387ba171f
commit eea5fe3391
23 changed files with 830 additions and 560 deletions

View file

@ -129,6 +129,7 @@ def validate_json_request(schema_name, optional=False):
return wrapper
def kubernetes_only(f):
""" Aborts the request with a 400 if the app is not running on kubernetes """
@wraps(f)
def abort_if_not_kube(*args, **kwargs):
if not IS_KUBERNETES:

View file

@ -5,8 +5,8 @@ from flask import abort, request
from config_app.config_endpoints.api.suconfig_models_pre_oci import pre_oci_model as model
from config_app.config_endpoints.api import resource, ApiResource, nickname, validate_json_request, kubernetes_only
from config_app.c_app import (app, config_provider, superusers, ip_resolver,
instance_keys, INIT_SCRIPTS_LOCATION, IS_KUBERNETES)
from config_app.config_util.k8sinterface import KubernetesAccessInterface, kubernetes_access_instance
instance_keys, INIT_SCRIPTS_LOCATION)
from config_app.config_util.k8saccessor import KubernetesAccessorSingleton
from data.database import configure
from data.runmigration import run_alembic_migration
@ -262,15 +262,38 @@ class SuperUserConfigValidate(ApiResource):
@resource('/v1/kubernetes/deployments/')
class SuperUserKubernetesDeployment(ApiResource):
""" Resource for fetching the status of config files and overriding them. """
""" Resource for the getting the status of Quay Enterprise deployments and cycling them """
schemas = {
'ValidateDeploymentNames': {
'type': 'object',
'description': 'Validates deployment names for cycling',
'required': [
'deploymentNames'
],
'properties': {
'deploymentNames': {
'type': 'array',
'description': 'The names of the deployments to cycle'
},
},
}
}
@kubernetes_only
@nickname('scGetNumDeployments')
def get(self):
return kubernetes_access_instance.get_qe_deployments()
return KubernetesAccessorSingleton.get_instance().get_qe_deployments()
@kubernetes_only
@nickname('scCycleQEDeployments')
def put(self):
deployment_names = request.get_json()['deploymentNames']
return KubernetesAccessorSingleton.get_instance().cycle_qe_deployments(deployment_names)
@resource('/v1/superuser/config/kubernetes')
class SuperUserKubernetesConfiguration(ApiResource):
""" Resource for fetching the status of config files and overriding them. """
""" Resource for saving the config files to kubernetes secrets. """
@kubernetes_only
@nickname('scDeployConfiguration')
def post(self):

View file

@ -9,6 +9,7 @@ from config import frontend_visible_config
from config_app.c_app import app, IS_KUBERNETES
from config_app._init_config import ROOT_DIR
from config_app.config_util.k8sconfig import get_k8s_namespace
def truthy_bool(param):
@ -49,7 +50,7 @@ def render_page_template(name, route_data=None, js_bundle_name=DEFAULT_JS_BUNDLE
route_data=route_data,
main_scripts=main_scripts,
config_set=frontend_visible_config(app.config),
is_kubernetes=IS_KUBERNETES,
kubernetes_namespace=IS_KUBERNETES and get_k8s_namespace(),
**kwargs)
resp = make_response(contents)

View file

@ -2,7 +2,7 @@ import os
from backports.tempfile import TemporaryDirectory
from config_app.config_util.config.fileprovider import FileConfigProvider
from config_app.config_util.k8sinterface import kubernetes_access_instance
from config_app.config_util.k8saccessor import KubernetesAccessorSingleton
class TransientDirectoryProvider(FileConfigProvider):
@ -41,6 +41,6 @@ class TransientDirectoryProvider(FileConfigProvider):
for name in os.listdir(config_path):
file_path = os.path.join(self.config_volume, name)
kubernetes_access_instance.save_file_as_secret(name, file_path)
KubernetesAccessorSingleton.get_instance().save_file_as_secret(name, file_path)
return 200

View file

@ -0,0 +1,145 @@
import logging
import json
import base64
import datetime
from requests import Request, Session
from config_app.config_util.k8sconfig import KubernetesConfig
logger = logging.getLogger(__name__)
QE_DEPLOYMENT_LABEL = 'quay-enterprise-component'
class KubernetesAccessorSingleton(object):
""" Singleton allowing access to kubernetes operations """
_instance = None
def __init__(self, kube_config=None):
self.kube_config = kube_config
if kube_config is None:
self.kube_config = KubernetesConfig.from_env()
KubernetesAccessorSingleton._instance = self
@classmethod
def get_instance(cls, kube_config=None):
"""
Singleton getter implementation, returns the instance if one exists, otherwise creates the
instance and ties it to the class.
:return: KubernetesAccessorSingleton
"""
if cls._instance is None:
return cls(kube_config)
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 get_qe_deployments(self):
""""
Returns all deployments matching the label selector provided in the KubeConfig
"""
deployment_selector_url = 'namespaces/%s/deployments?labelSelector=%s%%3D%s' % (
self.kube_config.qe_namespace, QE_DEPLOYMENT_LABEL, self.kube_config.qe_deployment_selector
)
response = self._execute_k8s_api('GET', deployment_selector_url, api_prefix='apis/extensions/v1beta1')
if response.status_code != 200:
return None
return json.loads(response.text)
def cycle_qe_deployments(self, deployment_names):
""""
Triggers a rollout of all desired deployments in the qe namespace
"""
for name in deployment_names:
logger.debug('Cycling deployment %s', name)
deployment_url = 'namespaces/%s/deployments/%s' % (self.kube_config.qe_namespace, name)
# There is currently no command to simply rolling restart all the pods: https://github.com/kubernetes/kubernetes/issues/13488
# Instead, we modify the template of the deployment with a dummy env variable to trigger a cycle of the pods
# (based off this comment: https://github.com/kubernetes/kubernetes/issues/13488#issuecomment-240393845)
self._assert_success(self._execute_k8s_api('PATCH', deployment_url, {
'spec': {
'template': {
'spec': {
'containers': [{
'name': 'quay-enterprise-app', 'env': [{
'name': 'RESTART_TIME',
'value': str(datetime.datetime.now())
}]
}]
}
}
}
}, api_prefix='apis/extensions/v1beta1', content_type='application/strategic-merge-patch+json'))
def _assert_success(self, response):
if response.status_code != 200:
logger.error('Kubernetes API call failed with response: %s => %s', response.status_code,
response.text)
raise Exception('Kubernetes API call failed: %s' % response.text)
def _update_secret_file(self, relative_file_path, value=None):
if '/' in relative_file_path:
raise Exception('Expected path from get_volume_path, but found slashes')
# 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'] = secret.get('data', {})
if value is not None:
secret['data'][relative_file_path] = base64.b64encode(value)
else:
secret['data'].pop(relative_file_path)
self._assert_success(self._execute_k8s_api('PUT', secret_url, secret))
def _lookup_secret(self):
secret_url = 'namespaces/%s/secrets/%s' % (self.kube_config.qe_namespace, self.kube_config.qe_config_secret)
response = self._execute_k8s_api('GET', secret_url)
if response.status_code != 200:
return None
return json.loads(response.text)
def _execute_k8s_api(self, method, relative_url, data=None, api_prefix='api/v1', content_type='application/json'):
headers = {
'Authorization': 'Bearer ' + self.kube_config.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' % (self.kube_config.api_host, api_prefix, relative_url)
request = Request(method, url, data=data, headers=headers)
return session.send(request.prepare(), verify=False, timeout=2)

View file

@ -0,0 +1,47 @@
import os
SERVICE_ACCOUNT_TOKEN_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/token'
DEFAULT_QE_NAMESPACE = 'quay-enterprise'
DEFAULT_QE_CONFIG_SECRET = 'quay-enterprise-config-secret'
# The name of the quay enterprise deployment (not config app) that is used to query & rollout
DEFAULT_QE_DEPLOYMENT_SELECTOR = 'app'
def get_k8s_namespace():
return os.environ.get('QE_K8S_NAMESPACE', DEFAULT_QE_NAMESPACE)
class KubernetesConfig(object):
def __init__(self, api_host='', service_account_token=SERVICE_ACCOUNT_TOKEN_PATH,
qe_namespace=DEFAULT_QE_NAMESPACE,
qe_config_secret=DEFAULT_QE_CONFIG_SECRET,
qe_deployment_selector=DEFAULT_QE_DEPLOYMENT_SELECTOR):
self.api_host = api_host
self.qe_namespace = qe_namespace
self.qe_config_secret = qe_config_secret
self.qe_deployment_selector = qe_deployment_selector
self.service_account_token = service_account_token
@classmethod
def from_env(cls):
# 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()
api_host=os.environ.get('KUBERNETES_SERVICE_HOST', '')
port = os.environ.get('KUBERNETES_SERVICE_PORT')
if port:
api_host += ':' + port
qe_namespace = get_k8s_namespace()
qe_config_secret = os.environ.get('QE_K8S_CONFIG_SECRET', DEFAULT_QE_CONFIG_SECRET)
qe_deployment_selector = os.environ.get('QE_DEPLOYMENT_SELECTOR', DEFAULT_QE_DEPLOYMENT_SELECTOR)
return cls(api_host=api_host, service_account_token=service_token, qe_namespace=qe_namespace,
qe_config_secret=qe_config_secret, qe_deployment_selector=qe_deployment_selector)

View file

@ -1,189 +0,0 @@
import os
import logging
import json
import base64
import time
from cStringIO import StringIO
from requests import Request, Session
logger = logging.getLogger(__name__)
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')
# The name of the quay enterprise deployment (not config app) that is used to query & rollout
QE_DEPLOYMENT_SELECTOR = os.environ.get('QE_DEPLOYMENT_SELECTOR', 'app')
class KubernetesAccessInterface:
""" Implementation of the config provider that reads and writes configuration
data from a Kubernetes Secret. """
def __init__(self, api_host=None, service_account_token_path=None):
service_account_token_path = service_account_token_path or SERVICE_ACCOUNT_TOKEN_PATH
api_host = api_host or KUBERNETES_API_HOST
# 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:
self._service_token = f.read()
self._api_host = api_host
# def volume_file_exists(self, relative_file_path):
# if '/' in relative_file_path:
# raise Exception('Expected path from get_volume_path, but found slashes')
#
# # NOTE: Overridden because we don't have subdirectories, which aren't supported
# # in Kubernetes secrets.
# secret = self._lookup_secret()
# if not secret or not secret.get('data'):
# return False
# return relative_file_path in secret['data']
#
# def list_volume_directory(self, path):
# # NOTE: Overridden because we don't have subdirectories, which aren't supported
# # in Kubernetes secrets.
# secret = self._lookup_secret()
#
# if not secret:
# return []
#
# paths = []
# for filename in secret.get('data', {}):
# if filename.startswith(path):
# paths.append(filename[len(path) + 1:])
# return paths
#
# def save_config(self, config_obj):
# self._update_secret_file(self.yaml_filename, get_yaml(config_obj))
#
# def remove_volume_file(self, relative_file_path):
# try:
# self._update_secret_file(relative_file_path, None)
# except IOError as ioe:
# raise CannotWriteConfigException(str(ioe))
#
# def save_volume_file(self, flask_file, relative_file_path):
# # Write the file to a temp location.
# buf = StringIO()
# try:
# try:
# flask_file.save(buf)
# except IOError as ioe:
# raise Exception(str(ioe))
#
# self._update_secret_file(relative_file_path, buf.getvalue())
# finally:
# buf.close()
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 get_qe_deployments(self):
pod_selector_url = 'namespaces/%s/deployments?labelSelector=quay-enterprise-component%%3D%s' % (QE_NAMESPACE, QE_DEPLOYMENT_SELECTOR)
response = self._execute_k8s_api('GET', pod_selector_url, api_prefix='apis/extensions/v1beta1')
if response.status_code != 200:
return None
return json.loads(response.text)
def _assert_success(self, response):
if response.status_code != 200:
logger.error('Kubernetes API call failed with response: %s => %s', response.status_code,
response.text)
raise Exception('Kubernetes API call failed: %s' % response.text)
def _update_secret_file(self, relative_file_path, value=None):
if '/' in relative_file_path:
raise Exception('Expected path from get_volume_path, but found slashes')
# 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' % (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' % 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' % (QE_NAMESPACE, 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": QE_CONFIG_SECRET
},
"data": {}
}))
# Update the secret to reflect the file change.
secret['data'] = secret.get('data', {})
if value is not None:
secret['data'][relative_file_path] = base64.b64encode(value)
else:
secret['data'].pop(relative_file_path)
self._assert_success(self._execute_k8s_api('PUT', secret_url, secret))
# Wait until the local mounted copy of the secret has been updated, as
# this is an eventual consistency operation, but the caller expects immediate
# consistency.
# while True:
# matching_files = set()
# for secret_filename, encoded_value in secret['data'].iteritems():
# expected_value = base64.b64decode(encoded_value)
# try:
# with self.get_volume_file(secret_filename) as f:
# contents = f.read()
#
# if contents == expected_value:
# matching_files.add(secret_filename)
#
# except IOError:
# continue
#
# if matching_files == set(secret['data'].keys()):
# break
#
# # Sleep for a second and then try again.
# time.sleep(1)
def _lookup_secret(self):
secret_url = 'namespaces/%s/secrets/%s' % (QE_NAMESPACE, QE_CONFIG_SECRET)
response = self._execute_k8s_api('GET', secret_url)
if response.status_code != 200:
return None
return json.loads(response.text)
def _execute_k8s_api(self, method, relative_url, data=None, api_prefix='api/v1'):
headers = {
'Authorization': 'Bearer ' + self._service_token
}
if data:
headers['Content-Type'] = 'application/json'
data = json.dumps(data) if data else None
session = Session()
url = 'https://%s/%s/%s' % (self._api_host, api_prefix, relative_url)
request = Request(method, url, data=data, headers=headers)
return session.send(request.prepare(), verify=False, timeout=2)
kubernetes_access_instance = KubernetesAccessInterface()

View file

@ -0,0 +1,63 @@
import pytest
import re
from httmock import urlmatch, HTTMock, response
from config_app.config_util.k8saccessor import KubernetesAccessorSingleton
from config_app.config_util.k8sconfig import KubernetesConfig
@pytest.mark.parametrize('kube_config, expected_api, expected_query', [
({'api_host':'www.customhost.com'},
'/apis/extensions/v1beta1/namespaces/quay-enterprise/deployments', 'labelSelector=quay-enterprise-component%3Dapp'),
({'api_host':'www.customhost.com', 'qe_deployment_selector':'custom-selector'},
'/apis/extensions/v1beta1/namespaces/quay-enterprise/deployments', 'labelSelector=quay-enterprise-component%3Dcustom-selector'),
({'api_host':'www.customhost.com', 'qe_namespace':'custom-namespace'},
'/apis/extensions/v1beta1/namespaces/custom-namespace/deployments', 'labelSelector=quay-enterprise-component%3Dapp'),
({'api_host':'www.customhost.com', 'qe_namespace':'custom-namespace', 'qe_deployment_selector':'custom-selector'},
'/apis/extensions/v1beta1/namespaces/custom-namespace/deployments', 'labelSelector=quay-enterprise-component%3Dcustom-selector'),
])
def test_get_qe_deployments(kube_config, expected_api, expected_query):
config = KubernetesConfig(**kube_config)
url_hit = [False]
@urlmatch(netloc=r'www.customhost.com')
def handler(request, _):
assert request.path == expected_api
assert request.query == expected_query
url_hit[0] = True
return response(200, '{}')
with HTTMock(handler):
KubernetesAccessorSingleton._instance = None
assert KubernetesAccessorSingleton.get_instance(config).get_qe_deployments() is not None
assert url_hit[0]
@pytest.mark.parametrize('kube_config, deployment_names, expected_api_hits', [
({'api_host':'www.customhost.com'}, [], []),
({'api_host':'www.customhost.com'}, ['myDeployment'], ['/apis/extensions/v1beta1/namespaces/quay-enterprise/deployments/myDeployment']),
({'api_host':'www.customhost.com', 'qe_namespace':'custom-namespace'},
['myDeployment', 'otherDeployment'],
['/apis/extensions/v1beta1/namespaces/custom-namespace/deployments/myDeployment', '/apis/extensions/v1beta1/namespaces/custom-namespace/deployments/otherDeployment']),
])
def test_cycle_qe_deployments(kube_config, deployment_names, expected_api_hits):
KubernetesAccessorSingleton._instance = None
config = KubernetesConfig(**kube_config)
url_hit = [False] * len(expected_api_hits)
i = [0]
@urlmatch(netloc=r'www.customhost.com', method='PATCH')
def handler(request, _):
assert request.path == expected_api_hits[i[0]]
url_hit[i[0]] = True
i[0] += 1
return response(200, '{}')
with HTTMock(handler):
KubernetesAccessorSingleton.get_instance(config).cycle_qe_deployments(deployment_names)
assert all(url_hit)

View file

@ -9,6 +9,6 @@ spec:
- protocol: TCP
port: 443
targetPort: 443
nodePort: 30080
nodePort: 30090
selector:
quay-enterprise-component: app # TODO: change me to config tool selectgor
quay-enterprise-component: config-tool

View file

@ -25,8 +25,5 @@ rules:
resources:
- deployments
verbs:
- get
- list
- put
- patch
- update

View file

@ -2,19 +2,19 @@ apiVersion: extensions/v1beta1
kind: Deployment
metadata:
namespace: quay-enterprise
name: quay-enterprise-app
name: quay-enterprise-config-tool
labels:
quay-enterprise-component: app # TODO: change to config tool selector
quay-enterprise-component: config-tool
spec:
replicas: 1
selector:
matchLabels:
quay-enterprise-component: app # TODO: change to config tool selector
quay-enterprise-component: config-tool
template:
metadata:
namespace: quay-enterprise
labels:
quay-enterprise-component: app # TODO: change to config tool selector
quay-enterprise-component: config-tool
spec:
serviceAccountName: qe-config-tool-serviceaccount
volumes:
@ -22,15 +22,9 @@ spec:
secret:
secretName: quay-enterprise-config-secret
containers:
- name: quay-enterprise-app
- name: quay-enterprise-config-tool
image: config-app:latest # TODO: change to reference to quay image?
imagePullPolicy: IfNotPresent # enable when testing with minikube
args: ["config"]
ports:
- containerPort: 80
volumeMounts:
- name: configvolume
readOnly: false
mountPath: /conf/stack
imagePullSecrets:
- name: blueish-pull-secret

View file

@ -1,9 +1,122 @@
# Configuring Quay on Kubernetes
# Quay Enterprise Installation on Kubernetes
This guide walks through the deployment of [Quay Enterprise][quay-enterprise-tour] onto a Kubernetes cluster.
After completing the steps in this guide, a deployer will have a functioning instance of Quay Enterprise orchestrated as a Kubernetes service on a cluster, and will be able to access the Quay Enterprise Setup tool with a browser to complete configuration of image repositories, builders, and users.
[quay-enterprise-tour]: https://quay.io/tour/enterprise
## Prerequisites
A PostgreSQL database must be available for Quay Enterprise metadata storage.
We currently recommend running this database server outside of the cluster.
## Download Kubernetes Configuration Files
Visit the [RedHat Documentation][RedHat-documentation] and download the pre-formatted pull secret, under "Account Assets". There are several formats of the secret, be sure to download the "dockercfg" format resulting in a `config.json` file. This pull secret is used to download the Quay Enterprise containers.
This will be used later in the guide.
[RedHat-documentation]: https://access.redhat.com/documentation/en-us/
Next, download each of the following files to your workstation, placing them alongside your pull secret:
- [quay-enterprise-namespace.yml](files/quay-enterprise-namespace.yml)
- [quay-enterprise-config-secret.yml](files/quay-enterprise-config-secret.yml)
- [quay-enterprise-redis.yml](files/quay-enterprise-redis.yml)
- [quay-enterprise-app-rc.yml](files/quay-enterprise-app-rc.yml)
- [quay-enterprise-service-nodeport.yml](files/quay-enterprise-service-nodeport.yml)
- [quay-enterprise-service-loadbalancer.yml](files/quay-enterprise-service-loadbalancer.yml)
## Role Based Access Control
Quay Enterprise has native Kubernetes integrations. These integrations require Service Account to have access to Kubernetes API. When Kubernetes RBAC is enabled, Role Based Access Control policy manifests also have to be deployed.
Kubernetes API has minor changes between versions 1.4 and 1.5, Download appropiate versions of Role Based Access Control (RBAC) Policies.
### Kubernetes v1.6.x and later RBAC Policies
- [quay-servicetoken-role.yaml](files/quay-servicetoken-role-k8s1-6.yaml)
- [quay-servicetoken-role-binding.yaml](files/quay-servicetoken-role-binding-k8s1-6.yaml)
... include old setup here, with extra steps:
## Deploy to Kubernetes
# Configuring RBAC for the configuration tool
All Kubernetes objects will be deployed under the "quay-enterprise" namespace.
The first step is to create this namespace:
```sh
kubectl create -f quay-enterprise-namespace.yml
```
Next, add your pull secret to Kubernetes (make sure you specify the correct path to `config.json`):
```sh
kubectl create secret generic coreos-pull-secret --from-file=".dockerconfigjson=config.json" --type='kubernetes.io/dockerconfigjson' --namespace=quay-enterprise
```
### Kubernetes v1.6.x and later : Deploy RBAC Policies
```sh
kubectl create -f quay-servicetoken-role-k8s1-6.yaml
kubectl create -f quay-servicetoken-role-binding-k8s1-6.yaml
```
### Deploy Quay Enterprise objects
Finally, the remaining Kubernetes objects can be deployed onto Kubernetes:
```sh
kubectl create -f quay-enterprise-config-secret.yml -f quay-enterprise-redis.yml -f quay-enterprise-app-rc.yml
```
## Expose via Kubernetes Service
In order to access Quay Enterprise, a user must route to it through a Kubernetes Service.
It is up to the deployer to decide which Service type is appropriate for their use case: a [LoadBalancer](http://kubernetes.io/docs/user-guide/services/#type-loadbalancer) or a [NodePort](http://kubernetes.io/docs/user-guide/services/#type-nodeport).
A LoadBalancer is recommended if the Kubernetes cluster is integrated with a cloud provider, otherwise a NodePort will suffice.
Along with this guide are examples of this service.
### LoadBalancer
Using the sample provided, a LoadBalancer Kubernetes Service can be created like so:
```sh
kubectl create -f quay-enterprise-service-loadbalancer.yml
```
kubectl can be used to find the externally-accessible URL of the quay-enterprise service:
```sh
kubectl describe services quay-enterprise --namespace=quay-enterprise
```
### NodePort
Using the sample provided, a NodePort Kubernetes Service can be created like so:
```sh
kubectl create -f quay-enterprise-service-nodeport.yml
```
By default, the quay-enterprise service will be available on port 30080 on every node in the Kubernetes cluster.
If this port conflicts with an existing Kubernetes Service, simply modify the sample configuration file and change the value of NodePort.
## Continue with Quay Enterprise Setup
All that remains is to configure Quay Enterprise itself through the configuration tool.
Download the following files to your workstation:
- [config-tool-service-nodeport.yml](k8s_templates/config-tool-service-nodeport.yml)
- [config-tool-serviceaccount.yml](k8s_templates/config-tool-serviceaccount.yml)
- [config-tool-servicetoken-role.yml](k8s_templates/config-tool-servicetoken-role.yml)
- [config-tool-servicetoken-role-binding.yml](k8s_templates/config-tool-servicetoken-role-binding.yml)
- [qe-config-tool.yml](k8s_templates/qe-config-tool.yml)
### Configuring RBAC for the configuration tool
Apply the following policies to allow the config tool to make changes to the Q.E. deployment:
```bash
kubectl apply -f config-tool-serviceaccount.yaml
```
@ -13,14 +126,18 @@ kubectl apply -f config-tool-servicetoken-role.yaml
```bash
kubectl apply -f config-tool-servicetoken-role-binding.yaml
```
### Deploy Config Tool
Deploy the configuration tool and route a service to it:
```bash
kubectl apply -f qe-config-tool.yml
kubectl apply -f qe-config-tool.yml -f config-tool-service-nodeport.yml
```
Make a nodeservice for it:
```bash
kubectl apply -f config-tool-service-nodeport.yml
```
By default, the config-tool service will be available on port 30090 on every node in the Kubernetes cluster.
Similar to the Quay application service, if this port conflicts with an existing Kubernetes Service, simply modify the sample configuration file and change the value of NodePort.
Once at the Quay Enterprise setup UI, follow the setup instructions to finalize your installation.
## Using the Configuration Tool
Click on "Start New Configuration for this Cluster", and follow the instructions to create your configuration, downloading and saving it (to load as a backup or if you ever wish to change your settings).
You will also be able to deploy the configuration to all instances by hitting "Deploy". Allow for a minute for the Quay instances to cycle the pods, and your configuration will be enacted once the pods have started.

View file

@ -1,4 +1,4 @@
<div ng-if="$ctrl.state === 'choice' && $ctrl.isKubernetes === false">
<div ng-if="$ctrl.state === 'choice' && $ctrl.kubeNamespace === false">
<div class="co-dialog modal fade initial-setup-modal in" id="setupModal" style="display: block;">
<div class="modal-backdrop fade in" style="height: 1000px;"></div>
<div class="modal-dialog fade in">
@ -22,14 +22,14 @@
</div><!-- /.modal-dialog -->
</div>
</div>
<div ng-if="$ctrl.state === 'choice' && $ctrl.isKubernetes === true">
<div ng-if="$ctrl.state === 'choice' && $ctrl.kubeNamespace !== false">
<div class="co-dialog modal fade initial-setup-modal in" id="kubeSetupModal" style="display: block;">
<div class="modal-backdrop fade in" style="height: 1000px;"></div>
<div class="modal-dialog fade in">
<div class="modal-content">
<!-- Header -->
<div class="modal-header">
<h4 class="modal-title"><span>Choose an option</span></h4>
<h4 class="modal-title"><span>Choose an option for the <code>{{ $ctrl.kubeNamespace }}</code> namespace</span></h4>
</div>
<!-- Body -->
<div class="config-setup-wrapper">
@ -37,13 +37,13 @@
<i class="fas fa-edit fa-2x"></i>
<div>Start new configuration for this cluster</div>
</a>
<a class="config-setup_option" ng-click="$ctrl.chooseLoad()">
<i class="fas fa-upload fa-2x"></i>
<div>Modify configuration for this cluster</div>
</a>
<a class="config-setup_option" ng-click="$ctrl.choosePopulate()">
<i class="fas fa-cloud-download-alt fa-2x"></i>
<div>Populate this cluster with a saved configuration</div>
<div>Modify configuration for this cluster</div>
</a>
<a class="config-setup_option" ng-click="$ctrl.chooseLoad()">
<i class="fas fa-upload fa-2x"></i>
<div>Populate this cluster from a previously created configuration</div>
</a>
</div>
</div><!-- /.modal-content -->
@ -55,7 +55,7 @@
<download-tarball-modal
ng-if="$ctrl.state === 'download'"
loaded-config="$ctrl.loadedConfig"
is-kubernetes="$ctrl.isKubernetes"
is-kubernetes="$ctrl.kubeNamespace !== false"
choose-deploy="$ctrl.chooseDeploy()">
</download-tarball-modal>
<kube-deploy-modal ng-if="$ctrl.state === 'deploy'"></kube-deploy-modal>

View file

@ -20,12 +20,12 @@ export class ConfigSetupAppComponent {
| 'deploy';
private loadedConfig = false;
private isKubernetes: boolean = false;
private kubeNamespace: string | boolean = false;
constructor(@Inject('ApiService') private apiService) {
this.state = 'choice';
if (window.__is_kubernetes) {
this.isKubernetes = true;
if (window.__kubernetes_namespace) {
this.kubeNamespace = window.__kubernetes_namespace;
}
}

View file

@ -23,7 +23,7 @@
see the docs.
</a>
<div class="modal__warning-box">
<div class="fas co-alert co-alert-warning">
<div class="co-alert co-alert-warning">
<strong>Warning: </strong>
Your configuration and certificates are kept <i>unencrypted</i>. Please keep this file secure.
</div>
@ -35,13 +35,13 @@
see the docs.
</a>
<div class="modal__warning-box">
<div class="fas co-alert co-alert-warning">
<div class="co-alert co-alert-warning">
<strong>Warning: </strong>
Your configuration and certificates are kept <i>unencrypted</i>. Please keep this file secure.
</div>
</div>
</div>
<button class="btn btn-lg download-button"
<button class="btn btn-default btn-lg download-button"
ng-click="$ctrl.downloadTarball()">
<i class="fa fa-download" style="margin-right: 10px;"></i>Download Configuration
</button>

View file

@ -11,3 +11,8 @@
.download-tarball-modal .download-button {
align-self: center;
}
.download-tarball-modal .download-button:hover {
background-color: #dddddd;
text-decoration: none;
}

View file

@ -18,17 +18,34 @@
</div>
<!-- Body -->
<div class="modal-body">
<div class="cor-loader" ng-if="$ctrl.loading"></div>
The following deployments will be affected:
<li ng-repeat="deployment in $ctrl.deployments">
name: {{deployment}}
<div class="cor-loader" ng-if="$ctrl.state === 'loadingDeployments'"></div>
<div class="kube-deploy-modal__body" ng-if="$ctrl.deploymentsStatus.length > 0">
<span class="kube-deploy-modal__list-header">The following deployments will be affected:</span>
<ul class="kube-deploy-modal__list">
<li class="kube-deploy-modal__list-item" ng-repeat="deployment in $ctrl.deploymentsStatus">
<i class="fa ci-k8s-logo"></i>
<code>{{deployment.name}}</code> with {{deployment.numPods}} <b> {{ deployment.numPods === 1 ? ' pod' : ' pods' }}</b>
</li>
<button class="btn btn-lg" ng-click="$ctrl.deployConfiguration()">
</ul>
<button ng-if="$ctrl.state === 'readyToDeploy'" class="btn btn-primary btn-lg" ng-click="$ctrl.deployConfiguration()">
<i class="far fa-paper-plane" style="margin-right: 10px;"></i>
Populate configuration to deployments
</button>
<div ng-if="$ctrl.state === 'deployingConfiguration'">
<div class="cor-loader"></div>
Deploying configuration...
</div>
<div ng-if="$ctrl.state === 'cyclingDeployments'">
<div class="cor-loader"></div>
Cycling deployments...
</div>
<div ng-if="$ctrl.state === 'deployed'">
Configuration successfully deployed!
</div>
</div>
<div ng-if="$ctrl.state === 'error'">
{{ $ctrl.errorMessage }}
</div>
<div class="modal-footer">
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->

View file

@ -1,36 +1,53 @@
import {Component, Inject} from 'ng-metadata/core';
const templateUrl = require('./kube-deploy-modal.component.html');
const styleUrl = require('./kube-deploy-modal.css');
@Component({
selector: 'kube-deploy-modal',
templateUrl,
styleUrls: [ styleUrl ],
})
export class KubeDeployModalComponent {
private loading: boolean = true;
private deployments: [string];
private state
: 'loadingDeployments'
| 'readyToDeploy'
| 'deployingConfiguration'
| 'cyclingDeployments'
| 'deployed'
| 'error';
private errorMessage: string;
private deploymentsStatus: { name: string, numPods: number }[] = [];
constructor(@Inject('ApiService') private ApiService) {
this.state = 'loadingDeployments';
ApiService.scGetNumDeployments().then(resp => {
console.log(resp)
this.deployments = resp.items.map(dep => dep.metadata.name);
console.log(this.deployments);
this.loading = false;
this.deploymentsStatus = resp.items.map(dep => ({ name: dep.metadata.name, numPods: dep.status.replicas }));
this.state = 'readyToDeploy';
}).catch(err => {
this.loading = false;
this.state = 'error';
this.errorMessage = `There are no Quay deployments active in this namespace. \
Please check that you are running this \
tool in the same namespace as the Quay Enterprise application\
Associated error message: ${err.toString()}`;
})
}
deployConfiguration(): void {
console.log('calling deploy conf')
this.ApiService.scDeployConfiguration().then(resp => {
console.log('resp from deploy was', resp)
this.ApiService.scCycleQEDeployment().then(() => {
console.log('merp')
this.ApiService.scDeployConfiguration().then(() => {
const deploymentNames: string[]= this.deploymentsStatus.map(dep => dep.name);
this.ApiService.scCycleQEDeployments({ deploymentNames }).then(() => {
this.state = 'deployed'
}).catch(err => {
console.log(err)
this.state = 'error';
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()}`;
})
}
}

View file

@ -0,0 +1,31 @@
.kube-deploy-modal__body {
display: flex;
flex-direction: column;
padding: 15px;
list-style: none;
}
.kube-deploy-modal__list {
padding-top: 10px;
padding-left: 15px;
margin-bottom: 15px;
list-style: none;
}
.kube-deploy-modal__list-header {
font-size: 16px;
}
.kube-deploy-modal__list-item {
padding-top: 5px;
}
.kube-deploy-modal__list-item i {
padding-right: 5px;
}
.kube-deploy-modal__body .btn {
align-self: center;
margin-top: 20px;
margin-bottom: 10px;
}

View file

@ -3,15 +3,15 @@
}
.config-setup_option {
font-size: x-large;
font-size: 22px;
height: 250px;
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 15px;
margin: 15px;
justify-content: space-evenly;
}
.config-setup_option i {
@ -20,6 +20,7 @@
.config-setup_option div {
text-align: center;
min-height: 100px;
}
.config-setup_option:hover {

View file

@ -4,7 +4,7 @@
<script type="text/javascript">
window.__endpoints = {{ route_data|tojson|safe }}.paths;
window.__config = {{ config_set|tojson|safe }};
window.__is_kubernetes = {{ is_kubernetes|tojson|safe }};
window.__kubernetes_namespace = {{ kubernetes_namespace|tojson|safe }};
</script>