Merge pull request #1244 from coreos-inc/enableaci

Add UI to the setup tool for enabling ACI conversion
This commit is contained in:
josephschorr 2016-02-17 12:29:48 -05:00
commit 11af123ba5
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 Flag: Whether or not to rotate old action logs to storage.
FEATURE_ACTION_LOG_ROTATION = False 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 # Feature Flag: Whether to allow for "namespace-less" repositories when pulling and pushing from
# Docker. # Docker.
FEATURE_LIBRARY_SUPPORT = True 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', ''), sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''),
is_debug=str(app.config.get('DEBUGGING', False)).lower(), is_debug=str(app.config.get('DEBUGGING', False)).lower(),
show_chat=features.OLARK_CHAT, show_chat=features.OLARK_CHAT,
aci_conversion=features.ACI_CONVERSION,
has_billing=features.BILLING, has_billing=features.BILLING,
contact_href=contact_href, contact_href=contact_href,
hostname=app.config['SERVER_HOSTNAME'], hostname=app.config['SERVER_HOSTNAME'],

View file

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

View file

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

View file

@ -286,6 +286,52 @@
</div> </div>
</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 --> <!-- E-mail -->
<div class="co-panel"> <div class="co-panel">
<div class="co-panel-heading"> <div class="co-panel-heading">

View file

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

View file

@ -12,7 +12,7 @@ angular.module('quay').directive('fetchTagDialog', function () {
'repository': '=repository', 'repository': '=repository',
'actionHandler': '=actionHandler' 'actionHandler': '=actionHandler'
}, },
controller: function($scope, $element, $timeout, ApiService, UserService, Config) { controller: function($scope, $element, $timeout, ApiService, UserService, Config, Features) {
$scope.clearCounter = 0; $scope.clearCounter = 0;
$scope.currentFormat = null; $scope.currentFormat = null;
$scope.currentEntity = null; $scope.currentEntity = null;
@ -35,11 +35,13 @@ angular.module('quay').directive('fetchTagDialog', function () {
}); });
} }
if (Features.ACI_CONVERSION) {
$scope.formats.push({ $scope.formats.push({
'title': 'Rocket Fetch', 'title': 'Rocket Fetch',
'icon': 'rocket-icon', 'icon': 'rocket-icon',
'command': 'rkt fetch {hostname}/{namespace}/{name}:{tag}' 'command': 'rkt fetch {hostname}/{namespace}/{name}:{tag}'
}); });
}
$scope.formats.push({ $scope.formats.push({
'title': 'Basic Docker Pull', '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 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="google-site-verification" content="GalDznToijTsHYmLjJvE4QaB9uk_IP16aaGDz5D75T4" />
<meta name="fragment" content="!" /> <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" 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"> <meta name="ac-discovery-pubkeys" content="{{ hostname }} {{ preferred_scheme }}://{{ hostname }}/aci-signing-key">
{% endif %}
{% endblock %} {% endblock %}

View file

@ -21,6 +21,14 @@ def add_enterprise_config_defaults(config_obj, current_secret_key, hostname):
# Default features that are off. # Default features that are off.
config_obj['FEATURE_MAILING'] = config_obj.get('FEATURE_MAILING', False) 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_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. # Default auth type.
if not 'AUTHENTICATION_TYPE' in config_obj: if not 'AUTHENTICATION_TYPE' in config_obj:

View file

@ -6,6 +6,7 @@ import peewee
import OpenSSL import OpenSSL
import logging import logging
from StringIO import StringIO
from fnmatch import fnmatch from fnmatch import fnmatch
from data.users.keystone import KeystoneUsers from data.users.keystone import KeystoneUsers
from data.users.externaljwt import ExternalJWTAuthN 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 auth.auth_context import get_authenticated_user
from util.config.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig from util.config.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig
from bitbucket import BitBucket from bitbucket import BitBucket
from util.security.signing import SIGNING_ENGINES
from app import app, config_provider, get_app_url, OVERRIDE_CONFIG_DIRECTORY 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'] SSL_FILENAMES = ['ssl.cert', 'ssl.key']
DB_SSL_FILENAMES = ['database.pem'] DB_SSL_FILENAMES = ['database.pem']
JWT_FILENAMES = ['jwt-authn.cert'] 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): def get_storage_providers(config):
storage_config = config.get('DISTRIBUTED_STORAGE_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)) '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 = { _VALIDATORS = {
'database': _validate_database, 'database': _validate_database,
'redis': _validate_redis, 'redis': _validate_redis,
@ -423,4 +438,5 @@ _VALIDATORS = {
'ldap': _validate_ldap, 'ldap': _validate_ldap,
'jwt': _validate_jwt, 'jwt': _validate_jwt,
'keystone': _validate_keystone, 'keystone': _validate_keystone,
'signer': _validate_signer,
} }

View file

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