Add UI to the setup tool for enabling ACI conversion

Fixes #1211
This commit is contained in:
Joseph Schorr 2016-02-16 15:31:23 -05:00
parent ded0a27901
commit 1940fd9939
11 changed files with 106 additions and 18 deletions

View file

@ -199,6 +199,9 @@ class DefaultConfig(object):
# Feature Flag: Whether or not to rotate old action logs to storage.
FEATURE_ACTION_LOG_ROTATION = False
# Feature Flag: Whether to enable conversion to ACIs.
FEATURE_ACI_CONVERSION = False
# Feature Flag: Whether to allow for "namespace-less" repositories when pulling and pushing from
# Docker.
FEATURE_LIBRARY_SUPPORT = True

View file

@ -207,6 +207,7 @@ def render_page_template(name, route_data=None, **kwargs):
sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''),
is_debug=str(app.config.get('DEBUGGING', False)).lower(),
show_chat=features.OLARK_CHAT,
aci_conversion=features.ACI_CONVERSION,
has_billing=features.BILLING,
contact_href=contact_href,
hostname=app.config['SERVER_HOSTNAME'],

View file

@ -351,6 +351,7 @@ def os_arch_checker(os, arch):
return checker
@route_show_if(features.ACI_CONVERSION)
@anon_protect
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/sig/<os>/<arch>/', methods=['GET'])
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci.asc/<os>/<arch>/', methods=['GET'])
@ -360,6 +361,7 @@ def get_aci_signature(server, namespace, repository, tag, os, arch):
os=os, arch=arch)
@route_show_if(features.ACI_CONVERSION)
@anon_protect
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci/<os>/<arch>/', methods=['GET', 'HEAD'])
@process_auth

View file

@ -99,6 +99,7 @@ def snapshot(path = ''):
abort(404)
@route_show_if(features.ACI_CONVERSION)
@web.route('/aci-signing-key')
@no_cache
@anon_protect

View file

@ -286,6 +286,52 @@
</div>
</div>
<!-- ACI Conversion -->
<div class="co-panel">
<div class="co-panel-heading">
<i class="fa rocket-icon" style="width: 20px; height: 20px; background-size: cover; vertical-align: middle;"></i> <a href="http://github.com/coreos/rkt" target="_blank">rkt</a> Conversion
</div>
<div class="co-panel-body">
<div class="description">
<p>If enabled, all images in the registry can be fetched via <code>rkt fetch</code> or any other <a href="https://github.com/appc/spec/blob/master/spec/discovery.md" target="_blank">AppC discovery</a>-compliant implementation.</p>
</div>
<div class="co-checkbox">
<input id="ftmail" type="checkbox" ng-model="config.FEATURE_ACI_CONVERSION">
<label for="ftmail">Enable ACI Conversion</label>
</div>
<div class="co-alert co-alert-info" ng-if="config.FEATURE_ACI_CONVERSION" style="margin-top: 20px;">
Documentation on generating these keys can be found at <a href="https://tectonic.com/quay-enterprise/docs/latest/aci-signing-keys.html" target="_blank">Generating ACI Signing Keys</a>.
</div>
<table class="config-table" ng-if="config.FEATURE_ACI_CONVERSION">
<tr>
<td class="non-input">GPG2 Public Key File:</td>
<td>
<span class="config-file-field" filename="signing-public.gpg" has-file="hasfile.gpgSigningPublic"></span>
<div class="help-text">
The certificate must be in PEM format.
</div
</td>
</tr>
<tr>
<td class="non-input">GPG2 Private Key File:</td>
<td>
<span class="config-file-field" filename="signing-private.gpg" has-file="hasfile.gpgSigningPrivate"></span>
</td>
</tr>
<tr>
<td class="non-input">GPG2 Private Key Name:</td>
<td>
<span class="config-string-field" binding="config.GPG2_PRIVATE_KEY_NAME"
placeholder="Name of the private key in the private key file (Example: EAB32227)"></span>
</td>
</tr>
</table>
</div>
</div>
<!-- E-mail -->
<div class="co-panel">
<div class="co-panel-heading">

View file

@ -35,6 +35,10 @@ angular.module("core-config-setup", ['angularFileUpload'])
return config.AUTHENTICATION_TYPE == 'Keystone';
}, 'password': true},
{'id': 'signer', 'title': 'ACI Signing', 'condition': function(config) {
return config.FEATURE_ACI_CONVERSION;
}},
{'id': 'mail', 'title': 'E-mail Support', 'condition': function(config) {
return config.FEATURE_MAILING;
}},

View file

@ -12,7 +12,7 @@ angular.module('quay').directive('fetchTagDialog', function () {
'repository': '=repository',
'actionHandler': '=actionHandler'
},
controller: function($scope, $element, $timeout, ApiService, UserService, Config) {
controller: function($scope, $element, $timeout, ApiService, UserService, Config, Features) {
$scope.clearCounter = 0;
$scope.currentFormat = null;
$scope.currentEntity = null;
@ -35,11 +35,13 @@ angular.module('quay').directive('fetchTagDialog', function () {
});
}
$scope.formats.push({
'title': 'Rocket Fetch',
'icon': 'rocket-icon',
'command': 'rkt fetch {hostname}/{namespace}/{name}:{tag}'
});
if (Features.ACI_CONVERSION) {
$scope.formats.push({
'title': 'Rocket Fetch',
'icon': 'rocket-icon',
'command': 'rkt fetch {hostname}/{namespace}/{name}:{tag}'
});
}
$scope.formats.push({
'title': 'Basic Docker Pull',

View file

@ -10,9 +10,11 @@
<meta id="descriptionTag" name="description" content="Quay is the best place to build, store, and distribute your containers. Public repositories are always free."></meta>
<meta name="google-site-verification" content="GalDznToijTsHYmLjJvE4QaB9uk_IP16aaGDz5D75T4" />
<meta name="fragment" content="!" />
{% if aci_conversion %}
<meta name="ac-discovery" content="{{ hostname }} {{ preferred_scheme }}://{{ hostname }}/c1/aci/{name}/{version}/{ext}/{os}/{arch}/">
<meta name="ac-discovery-pubkeys" content="{{ hostname }} {{ preferred_scheme }}://{{ hostname }}/aci-signing-key">
{% endif %}
{% endblock %}

View file

@ -21,6 +21,14 @@ def add_enterprise_config_defaults(config_obj, current_secret_key, hostname):
# Default features that are off.
config_obj['FEATURE_MAILING'] = config_obj.get('FEATURE_MAILING', False)
config_obj['FEATURE_BUILD_SUPPORT'] = config_obj.get('FEATURE_BUILD_SUPPORT', False)
config_obj['FEATURE_ACI_CONVERSION'] = config_obj.get('FEATURE_ACI_CONVERSION', True)
# Default the signer config.
config_obj['GPG2_PRIVATE_KEY_FILENAME'] = config_obj.get('GPG2_PRIVATE_KEY_FILENAME',
'signing-private.gpg')
config_obj['GPG2_PUBLIC_KEY_FILENAME'] = config_obj.get('GPG2_PUBLIC_KEY_FILENAME',
'signing-public.gpg')
config_obj['SIGNING_ENGINE'] = config_obj.get('SIGNING_ENGINE', 'gpg2')
# Default auth type.
if not 'AUTHENTICATION_TYPE' in config_obj:

View file

@ -6,6 +6,7 @@ import peewee
import OpenSSL
import logging
from StringIO import StringIO
from fnmatch import fnmatch
from data.users.keystone import KeystoneUsers
from data.users.externaljwt import ExternalJWTAuthN
@ -18,6 +19,7 @@ from storage import get_storage_driver
from auth.auth_context import get_authenticated_user
from util.config.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig
from bitbucket import BitBucket
from util.security.signing import SIGNING_ENGINES
from app import app, config_provider, get_app_url, OVERRIDE_CONFIG_DIRECTORY
@ -27,8 +29,9 @@ logger = logging.getLogger(__name__)
SSL_FILENAMES = ['ssl.cert', 'ssl.key']
DB_SSL_FILENAMES = ['database.pem']
JWT_FILENAMES = ['jwt-authn.cert']
ACI_CERT_FILENAMES = ['signing-public.gpg', 'signing-private.gpg']
CONFIG_FILENAMES = SSL_FILENAMES + DB_SSL_FILENAMES + JWT_FILENAMES
CONFIG_FILENAMES = SSL_FILENAMES + DB_SSL_FILENAMES + JWT_FILENAMES + ACI_CERT_FILENAMES
def get_storage_providers(config):
storage_config = config.get('DISTRIBUTED_STORAGE_CONFIG', {})
@ -409,6 +412,18 @@ def _validate_keystone(config, password):
'OR Keystone auth is misconfigured.') % (username, err_msg))
def _validate_signer(config, _):
""" Validates the GPG public+private key pair used for signing converted ACIs. """
if config.get('SIGNING_ENGINE') is None:
return
if config['SIGNING_ENGINE'] not in SIGNING_ENGINES:
raise Exception('Unknown signing engine: %s' % config['SIGNING_ENGINE'])
engine = SIGNING_ENGINES[config['SIGNING_ENGINE']](config, OVERRIDE_CONFIG_DIRECTORY)
engine.detached_sign(StringIO('test string'))
_VALIDATORS = {
'database': _validate_database,
'redis': _validate_redis,
@ -423,4 +438,5 @@ _VALIDATORS = {
'ldap': _validate_ldap,
'jwt': _validate_jwt,
'keystone': _validate_keystone,
'signer': _validate_signer,
}

View file

@ -4,22 +4,22 @@ from StringIO import StringIO
class GPG2Signer(object):
""" Helper class for signing data using GPG2. """
def __init__(self, app, key_directory):
if not app.config.get('GPG2_PRIVATE_KEY_NAME'):
def __init__(self, config, key_directory):
if not config.get('GPG2_PRIVATE_KEY_NAME'):
raise Exception('Missing configuration key GPG2_PRIVATE_KEY_NAME')
if not app.config.get('GPG2_PRIVATE_KEY_FILENAME'):
if not config.get('GPG2_PRIVATE_KEY_FILENAME'):
raise Exception('Missing configuration key GPG2_PRIVATE_KEY_FILENAME')
if not app.config.get('GPG2_PUBLIC_KEY_FILENAME'):
if not config.get('GPG2_PUBLIC_KEY_FILENAME'):
raise Exception('Missing configuration key GPG2_PUBLIC_KEY_FILENAME')
self._ctx = gpgme.Context()
self._ctx.armor = True
self._private_key_name = app.config['GPG2_PRIVATE_KEY_NAME']
self._public_key_path = os.path.join(key_directory, app.config['GPG2_PUBLIC_KEY_FILENAME'])
self._private_key_name = config['GPG2_PRIVATE_KEY_NAME']
self._public_key_path = os.path.join(key_directory, config['GPG2_PUBLIC_KEY_FILENAME'])
key_file = os.path.join(key_directory, app.config['GPG2_PRIVATE_KEY_FILENAME'])
key_file = os.path.join(key_directory, config['GPG2_PRIVATE_KEY_FILENAME'])
if not os.path.exists(key_file):
raise Exception('Missing key file %s' % key_file)
@ -37,10 +37,13 @@ class GPG2Signer(object):
def detached_sign(self, stream):
""" Signs the given stream, returning the signature. """
ctx = self._ctx
ctx.signers = [ctx.get_key(self._private_key_name)]
try:
ctx.signers = [ctx.get_key(self._private_key_name)]
except:
raise Exception('Invalid private key name')
signature = StringIO()
new_sigs = ctx.sign(stream, signature, gpgme.SIG_MODE_DETACH)
signature.seek(0)
return signature.getvalue()
@ -58,7 +61,7 @@ class Signer(object):
if preference is None:
return None
return SIGNING_ENGINES[preference](app, key_directory)
return SIGNING_ENGINES[preference](app.config, key_directory)
def __getattr__(self, name):
return getattr(self.state, name, None)