Merge pull request #1244 from coreos-inc/enableaci
Add UI to the setup tool for enabling ACI conversion
This commit is contained in:
commit
11af123ba5
11 changed files with 106 additions and 18 deletions
|
@ -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
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -99,6 +99,7 @@ def snapshot(path = ''):
|
|||
abort(404)
|
||||
|
||||
|
||||
@route_show_if(features.ACI_CONVERSION)
|
||||
@web.route('/aci-signing-key')
|
||||
@no_cache
|
||||
@anon_protect
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}},
|
||||
|
|
|
@ -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 () {
|
|||
});
|
||||
}
|
||||
|
||||
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',
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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)
|
||||
|
|
Reference in a new issue