diff --git a/config_app/_init_config.py b/config_app/_init_config.py index a1628321a..4b5d239d9 100644 --- a/config_app/_init_config.py +++ b/config_app/_init_config.py @@ -12,6 +12,7 @@ STATIC_DIR = os.path.join(ROOT_DIR, 'static/') STATIC_LDN_DIR = os.path.join(STATIC_DIR, 'ldn/') STATIC_FONTS_DIR = os.path.join(STATIC_DIR, 'fonts/') TEMPLATE_DIR = os.path.join(ROOT_DIR, 'templates/') +IS_KUBERNETES = 'KUBERNETES_SERVICE_HOST' in os.environ diff --git a/config_app/c_app.py b/config_app/c_app.py index a1e25304a..3f0b13cf0 100644 --- a/config_app/c_app.py +++ b/config_app/c_app.py @@ -7,7 +7,7 @@ from data import database, model from util.config.superusermanager import SuperUserManager from util.ipresolver import NoopIPResolver -from config_app._init_config import ROOT_DIR +from config_app._init_config import ROOT_DIR, IS_KUBERNETES from config_app.config_util.config import get_config_provider from util.security.instancekeys import InstanceKeys @@ -19,9 +19,12 @@ OVERRIDE_CONFIG_DIRECTORY = os.path.join(ROOT_DIR, 'config_app/conf/stack') INIT_SCRIPTS_LOCATION = '/conf/init/' is_testing = 'TEST' in os.environ +is_kubernetes = IS_KUBERNETES + +logger.debug('Configuration is on a kubernetes deployment: %s' % IS_KUBERNETES) config_provider = get_config_provider(OVERRIDE_CONFIG_DIRECTORY, 'config.yaml', 'config.py', - testing=is_testing) + testing=is_testing, kubernetes=is_kubernetes) if is_testing: from test.testconfig import TestConfig diff --git a/config_app/config_endpoints/api/__init__.py b/config_app/config_endpoints/api/__init__.py index 0adf080be..47e9b690d 100644 --- a/config_app/config_endpoints/api/__init__.py +++ b/config_app/config_endpoints/api/__init__.py @@ -1,6 +1,6 @@ import logging -from flask import Blueprint, request +from flask import Blueprint, request, abort from flask_restful import Resource, Api from flask_restful.utils.cors import crossdomain from email.utils import formatdate @@ -8,7 +8,7 @@ from calendar import timegm from functools import partial, wraps from jsonschema import validate, ValidationError -from config_app.c_app import app +from config_app.c_app import app, IS_KUBERNETES from config_app.config_endpoints.exception import InvalidResponse, InvalidRequest logger = logging.getLogger(__name__) @@ -128,6 +128,14 @@ def validate_json_request(schema_name, optional=False): return wrapped return wrapper +def kubernetes_only(f): + @wraps(f) + def abort_if_not_kube(*args, **kwargs): + if not IS_KUBERNETES: + abort(400) + + return f(*args, **kwargs) + return abort_if_not_kube nickname = partial(add_method_metadata, 'nickname') diff --git a/config_app/config_endpoints/api/suconfig.py b/config_app/config_endpoints/api/suconfig.py index d83ad88eb..9aebe0256 100644 --- a/config_app/config_endpoints/api/suconfig.py +++ b/config_app/config_endpoints/api/suconfig.py @@ -3,9 +3,10 @@ import logging 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 +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) + instance_keys, INIT_SCRIPTS_LOCATION, IS_KUBERNETES) +from config_app.config_util.k8sinterface import KubernetesAccessInterface from data.database import configure from data.runmigration import run_alembic_migration @@ -259,6 +260,24 @@ class SuperUserConfigValidate(ApiResource): return validate_service_for_config(service, validator_context) +@resource('/v1/kubernetes/deployments/') +class SuperUserKubernetesDeployment(ApiResource): + """ Resource for fetching the status of config files and overriding them. """ + @kubernetes_only + @nickname('scGetNumDeployments') + def get(self): + accessor = KubernetesAccessInterface() + res = accessor.get_num_qe_pods() + return res + +@resource('/v1/superuser/config/kubernetes') +class SuperUserKubernetesConfiguration(ApiResource): + """ Resource for fetching the status of config files and overriding them. """ + @kubernetes_only + @nickname('scSaveConfigToKube') + def post(self): + return config_provider.save_configuration_to_kubernetes() + @resource('/v1/superuser/config/file/') class SuperUserConfigFile(ApiResource): diff --git a/config_app/config_endpoints/common.py b/config_app/config_endpoints/common.py index 9551101b4..66a8dc79c 100644 --- a/config_app/config_endpoints/common.py +++ b/config_app/config_endpoints/common.py @@ -7,7 +7,7 @@ from flask_restful import reqparse from config import frontend_visible_config -from config_app.c_app import app +from config_app.c_app import app, IS_KUBERNETES from config_app._init_config import ROOT_DIR @@ -49,6 +49,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, **kwargs) resp = make_response(contents) diff --git a/config_app/config_util/config/TransientDirectoryProvider.py b/config_app/config_util/config/TransientDirectoryProvider.py index 9c96bedf6..69d85081b 100644 --- a/config_app/config_util/config/TransientDirectoryProvider.py +++ b/config_app/config_util/config/TransientDirectoryProvider.py @@ -8,12 +8,13 @@ class TransientDirectoryProvider(FileConfigProvider): from/to the file system, only using temporary directories, deleting old dirs and creating new ones as requested. """ - def __init__(self, config_volume, yaml_filename, py_filename): + def __init__(self, config_volume, yaml_filename, py_filename, kubernetes=False): # Create a temp directory that will be cleaned up when we change the config path # This should ensure we have no "pollution" of different configs: # no uploaded config should ever affect subsequent config modifications/creations temp_dir = TemporaryDirectory() self.temp_dir = temp_dir + self.kubernetes = kubernetes super(TransientDirectoryProvider, self).__init__(temp_dir.name, yaml_filename, py_filename) @property @@ -33,3 +34,9 @@ class TransientDirectoryProvider(FileConfigProvider): def get_config_dir_path(self): return self.config_volume + + def save_configuration_to_kubernetes(self): + if not self.kubernetes: + raise Exception("Not on kubernetes, cannot save configuration.") + + print('do stuf') diff --git a/config_app/config_util/config/__init__.py b/config_app/config_util/config/__init__.py index b9edeba3a..e12d013bf 100644 --- a/config_app/config_util/config/__init__.py +++ b/config_app/config_util/config/__init__.py @@ -3,10 +3,10 @@ from config_app.config_util.config.testprovider import TestConfigProvider from config_app.config_util.config.TransientDirectoryProvider import TransientDirectoryProvider -def get_config_provider(config_volume, yaml_filename, py_filename, testing=False): +def get_config_provider(config_volume, yaml_filename, py_filename, testing=False, kubernetes=False): """ Loads and returns the config provider for the current environment. """ if testing: return TestConfigProvider() - return TransientDirectoryProvider(config_volume, yaml_filename, py_filename) + return TransientDirectoryProvider(config_volume, yaml_filename, py_filename, kubernetes=kubernetes) diff --git a/config_app/config_util/k8sinterface.py b/config_app/config_util/k8sinterface.py new file mode 100644 index 000000000..b71b50c71 --- /dev/null +++ b/config_app/config_util/k8sinterface.py @@ -0,0 +1,182 @@ +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', 'quay-enterprise-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 CannotWriteConfigException(str(ioe)) + # + # self._update_secret_file(relative_file_path, buf.getvalue()) + # finally: + # buf.close() + + def get_num_qe_pods(self): + # TODO: change to get /deployments?labelSelector={labelName}%3D{labelValue} + # right now just get the hardcoded deployment name + deployment_url = 'namespaces/%s/deployments/%s' % (QE_NAMESPACE, QE_DEPLOYMENT_SELECTOR) + response = self._execute_k8s_api('GET', deployment_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 CannotWriteConfigException('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 CannotWriteConfigException(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) diff --git a/config_app/docs/kube_setup.md b/config_app/docs/kube_setup.md new file mode 100644 index 000000000..7b98dfb07 --- /dev/null +++ b/config_app/docs/kube_setup.md @@ -0,0 +1,26 @@ +# Configuring Quay on Kubernetes + + +... include old setup here, with extra steps: + +# Configuring RBAC for the configuration tool +```bash +kubectl apply -f config-tool-serviceaccount.yaml +``` +```bash +kubectl apply -f config-tool-servicetoken-role.yaml +``` +```bash +kubectl apply -f config-tool-servicetoken-role-binding.yaml +``` +```bash +kubectl apply -f qe-config-tool.yml +``` + +Make a nodeservice for it: +```bash +kubectl apply -f config-tool-service-nodeport.yml +``` + + + diff --git a/config_app/js/components/config-setup-app/config-setup-app.component.html b/config_app/js/components/config-setup-app/config-setup-app.component.html index 1cd0dd93c..7dc314a2b 100644 --- a/config_app/js/components/config-setup-app/config-setup-app.component.html +++ b/config_app/js/components/config-setup-app/config-setup-app.component.html @@ -1,4 +1,4 @@ -
+
+
+ +
- + + + diff --git a/config_app/js/components/config-setup-app/config-setup-app.component.ts b/config_app/js/components/config-setup-app/config-setup-app.component.ts index 550741d60..adc9e7b37 100644 --- a/config_app/js/components/config-setup-app/config-setup-app.component.ts +++ b/config_app/js/components/config-setup-app/config-setup-app.component.ts @@ -1,6 +1,8 @@ import { Component, Inject } from 'ng-metadata/core'; const templateUrl = require('./config-setup-app.component.html'); +declare var window: any; + /** * Initial Screen and Choice in the Config App */ @@ -13,12 +15,18 @@ export class ConfigSetupAppComponent { : 'choice' | 'setup' | 'load' - | 'download'; + | 'populate' + | 'download' + | 'deploy'; private loadedConfig = false; + private isKubernetes: boolean = false; constructor(@Inject('ApiService') private apiService) { this.state = 'choice'; + if (window.__is_kubernetes) { + this.isKubernetes = true; + } } private chooseSetup(): void { @@ -36,6 +44,10 @@ export class ConfigSetupAppComponent { this.loadedConfig = true; } + private choosePopulate(): void { + this.state = 'populate'; + } + private configLoaded(): void { this.state = 'setup'; } @@ -43,4 +55,8 @@ export class ConfigSetupAppComponent { private setupCompleted(): void { this.state = 'download'; } + + private chooseDeploy(): void { + this.state = 'deploy'; + } } diff --git a/config_app/js/components/download-tarball-modal/download-tarball-modal.component.html b/config_app/js/components/download-tarball-modal/download-tarball-modal.component.html index faa206531..9b69a3faf 100644 --- a/config_app/js/components/download-tarball-modal/download-tarball-modal.component.html +++ b/config_app/js/components/download-tarball-modal/download-tarball-modal.component.html @@ -41,13 +41,17 @@
- - + diff --git a/config_app/js/components/download-tarball-modal/download-tarball-modal.component.ts b/config_app/js/components/download-tarball-modal/download-tarball-modal.component.ts index 943c0c015..99fb585b8 100644 --- a/config_app/js/components/download-tarball-modal/download-tarball-modal.component.ts +++ b/config_app/js/components/download-tarball-modal/download-tarball-modal.component.ts @@ -1,4 +1,4 @@ -import { Input, Component, Inject } from 'ng-metadata/core'; +import {Input, Component, Inject, Output, EventEmitter} from 'ng-metadata/core'; const templateUrl = require('./download-tarball-modal.component.html'); const styleUrl = require('./download-tarball-modal.css'); @@ -14,12 +14,14 @@ declare const FileSaver: any; }) export class DownloadTarballModalComponent { @Input('<') public loadedConfig; + @Input('<') public isKubernetes; + @Output() public chooseDeploy = new EventEmitter(); constructor(@Inject('ApiService') private ApiService) { } - private downloadTarball() { + private downloadTarball(): void { const errorDisplay: Function = this.ApiService.errorDisplay( 'Could not save configuration. Please report this error.' ); @@ -31,4 +33,8 @@ export class DownloadTarballModalComponent { FileSaver.saveAs(resp, 'quay-config.tar.gz'); }, errorDisplay); } + + private goToDeploy() { + this.chooseDeploy.emit({}); + } } diff --git a/config_app/js/components/download-tarball-modal/download-tarball-modal.css b/config_app/js/components/download-tarball-modal/download-tarball-modal.css index 3a02be753..ed5c9b256 100644 --- a/config_app/js/components/download-tarball-modal/download-tarball-modal.css +++ b/config_app/js/components/download-tarball-modal/download-tarball-modal.css @@ -1,7 +1,13 @@ .co-dialog .modal-body.download-tarball-modal { padding: 15px; + display: flex; + flex-direction: column; } .modal__warning-box { margin-top: 15px; } + +.download-tarball-modal .download-button { + align-self: center; +} diff --git a/config_app/js/components/kube-deploy-modal/kube-deploy-modal.component.html b/config_app/js/components/kube-deploy-modal/kube-deploy-modal.component.html new file mode 100644 index 000000000..6ab8948b4 --- /dev/null +++ b/config_app/js/components/kube-deploy-modal/kube-deploy-modal.component.html @@ -0,0 +1,28 @@ +
+ +
\ No newline at end of file diff --git a/config_app/js/components/kube-deploy-modal/kube-deploy-modal.component.ts b/config_app/js/components/kube-deploy-modal/kube-deploy-modal.component.ts new file mode 100644 index 000000000..9f15712ba --- /dev/null +++ b/config_app/js/components/kube-deploy-modal/kube-deploy-modal.component.ts @@ -0,0 +1,20 @@ +import {Component, Inject} from 'ng-metadata/core'; +const templateUrl = require('./kube-deploy-modal.component.html'); + +@Component({ + selector: 'kube-deploy-modal', + templateUrl, +}) +export class KubeDeployModalComponent { + private loading: boolean = true; + private deployments: any; + + constructor(@Inject('ApiService') private ApiService) { + ApiService.scGetNumDeployments().then(resp => { + console.log(resp) + this.loading = false; + }).catch(err => { + this.loading = false; + }) + } +} \ No newline at end of file diff --git a/config_app/js/config-app.module.ts b/config_app/js/config-app.module.ts index f5aa0c532..5c8a2c0bb 100644 --- a/config_app/js/config-app.module.ts +++ b/config_app/js/config-app.module.ts @@ -4,6 +4,7 @@ import * as restangular from 'restangular'; import { ConfigSetupAppComponent } from './components/config-setup-app/config-setup-app.component'; import { DownloadTarballModalComponent } from './components/download-tarball-modal/download-tarball-modal.component'; import { LoadConfigComponent } from './components/load-config/load-config.component'; +import { KubeDeployModalComponent } from './components/kube-deploy-modal/kube-deploy-modal.component'; const quayDependencies: string[] = [ 'restangular', @@ -45,6 +46,7 @@ function provideConfig($provide: ng.auto.IProvideService, ConfigSetupAppComponent, DownloadTarballModalComponent, LoadConfigComponent, + KubeDeployModalComponent, ], providers: [] }) diff --git a/config_app/k8s_templates/config-tool-service-nodeport.yml b/config_app/k8s_templates/config-tool-service-nodeport.yml new file mode 100644 index 000000000..3e37e7b6b --- /dev/null +++ b/config_app/k8s_templates/config-tool-service-nodeport.yml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + namespace: quay-enterprise + name: quay-enterprise-config-tool +spec: + type: NodePort + ports: + - protocol: TCP + port: 443 + targetPort: 443 + nodePort: 30080 + selector: + quay-enterprise-component: app # TODO: change me to config tool selectgor diff --git a/config_app/k8s_templates/config-tool-serviceaccount.yaml b/config_app/k8s_templates/config-tool-serviceaccount.yaml new file mode 100644 index 000000000..2af878c5a --- /dev/null +++ b/config_app/k8s_templates/config-tool-serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: qe-config-tool-serviceaccount + namespace: quay-enterprise diff --git a/config_app/k8s_templates/config-tool-servicetoken-role-binding.yaml b/config_app/k8s_templates/config-tool-servicetoken-role-binding.yaml new file mode 100644 index 000000000..fa12b296d --- /dev/null +++ b/config_app/k8s_templates/config-tool-servicetoken-role-binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: quay-enterprise-config-tool-writer + namespace: quay-enterprise +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: quay-enterprise-config-tool-role +subjects: +- kind: ServiceAccount + name: qe-config-tool-serviceaccount diff --git a/config_app/k8s_templates/config-tool-servicetoken-role.yaml b/config_app/k8s_templates/config-tool-servicetoken-role.yaml new file mode 100644 index 000000000..777b89f04 --- /dev/null +++ b/config_app/k8s_templates/config-tool-servicetoken-role.yaml @@ -0,0 +1,31 @@ +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: Role +metadata: + name: quay-enterprise-config-tool-role + namespace: quay-enterprise +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - put + - patch + - update +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get +- apiGroups: + - "extensions" + - "apps" + resources: + - deployments + verbs: + - get + - put + - patch + - update diff --git a/config_app/k8s_templates/qe-config-tool.yml b/config_app/k8s_templates/qe-config-tool.yml new file mode 100644 index 000000000..225a5257c --- /dev/null +++ b/config_app/k8s_templates/qe-config-tool.yml @@ -0,0 +1,36 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + namespace: quay-enterprise + name: quay-enterprise-app + labels: + quay-enterprise-component: app # TODO: change to config tool selector +spec: + replicas: 1 + selector: + matchLabels: + quay-enterprise-component: app # TODO: change to config tool selector + template: + metadata: + namespace: quay-enterprise + labels: + quay-enterprise-component: app # TODO: change to config tool selector + spec: + serviceAccountName: qe-config-tool-serviceaccount + volumes: + - name: configvolume + secret: + secretName: quay-enterprise-config-secret + containers: + - name: quay-enterprise-app + 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 diff --git a/config_app/static/css/config-setup-app-component.css b/config_app/static/css/config-setup-app-component.css index 8a61dcec7..eb17cdfa1 100644 --- a/config_app/static/css/config-setup-app-component.css +++ b/config_app/static/css/config-setup-app-component.css @@ -18,6 +18,10 @@ padding-bottom: 10px; } +.config-setup_option div { + text-align: center; +} + .config-setup_option:hover { background-color: #dddddd; text-decoration: none; diff --git a/config_app/templates/index.html b/config_app/templates/index.html index a09623555..0c380a0bc 100644 --- a/config_app/templates/index.html +++ b/config_app/templates/index.html @@ -4,6 +4,7 @@