Implement setup tool support for Clair

Fixes #1387
This commit is contained in:
Joseph Schorr 2016-05-02 15:29:31 -04:00
parent 53ce4de6aa
commit 2cbdecb043
23 changed files with 584 additions and 116 deletions

View file

@ -545,6 +545,18 @@ a:focus {
margin-left: 10px;
}
.config-service-key-field-element {
position: relative;
}
.config-service-key-field-element .co-modify-link {
margin-left: 10px;
}
.config-service-key-field-element .fa-check {
margin-right: 4px;
}
.co-checkbox {
position: relative;
}
@ -1457,4 +1469,11 @@ a:focus {
padding-bottom: 10px;
}
.co-option-table .help-text {
margin-top: 4px;
margin-bottom: 10px;
font-size: 14px;
color: #aaa;
}

View file

@ -0,0 +1,11 @@
.request-service-key-dialog-element .co-option-table {
margin-top: 20px;
}
.request-service-key-dialog-element .key-display {
margin-top: 10px;
font-size: 12px;
font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
background: white;
min-height: 500px;
}

View file

@ -0,0 +1,29 @@
<div class="config-service-key-field-element">
<!-- Loading -->
<div class="cor-loader" ng-if="loading"></div>
<!-- Loading error -->
<div class="co-alert co-alert-warning" ng-if="loadError">
Could not load service keys
</div>
<!-- Key config -->
<div ng-show="!loading && !loadError">
<div ng-show="hasValidKey">
<i class="fa fa-check"></i>
Valid key for service <code>{{ serviceName }}</code> exists
</div>
<div ng-show="!hasValidKey">
No valid key found for service <code>{{ serviceName }}</code>
<a class="co-modify-link" ng-click="showRequestServiceKey()">Create Key</a>
</div>
</div>
<!-- Note: This field is a hidden text field that binds to a model that is set to non-empty
when there is a valid key. This allows us to use the existing Angular form validation
code.
-->
<input type="text" ng-model="hasValidKeyStr" ng-required="true" style="position: absolute; top: 0px; left: 0px; visibility: hidden; width: 0px; height: 0px;">
<div class="request-service-key-dialog" request-key-info="requestKeyInfo" key-created="handleKeyCreated(key)"></div>
</div>

View file

@ -286,6 +286,53 @@
</div>
</div>
<!-- Security Scanner -->
<div class="co-panel">
<div class="co-panel-heading">
<i class="fa fa-bug"></i> Security Scanner
</div>
<div class="co-panel-body">
<div class="description">
<p>If enabled, all images pushed to Quay will be scanned via the external security scanning service, with vulnerability information available in the UI and API, as well
as async notification support.
</p>
</div>
<div class="co-checkbox">
<input id="ftsecurity" type="checkbox" ng-model="config.FEATURE_SECURITY_SCANNER">
<label for="ftsecurity">Enable Security Scanning</label>
</div>
<div class="co-alert co-alert-info" ng-if="config.FEATURE_SECURITY_SCANNER" style="margin-top: 20px;">
A scanner compliant with the Quay Security Scanning API must be running to use this feature. Documentation on running <a href="https://github.com/coreos/clair" ng-safenewtab>Clair</a> can be found at <a href="https://tectonic.com/quay-enterprise/docs/latest/clair.html" ng-safenewtab>Running Clair Security Scanner</a>.
</div>
<table class="config-table" ng-if="config.FEATURE_SECURITY_SCANNER">
<tr>
<td>Security Scanner Endpoint:</td>
<td>
<span class="config-string-field" binding="config.SECURITY_SCANNER_ENDPOINT"
placeholder="Security Scanner API endpoint (Example: http://myhost:6060)"
pattern="http(s)?://.+"></span>
<div class="help-text">
The HTTP URL at which the security scanner is running.
</div>
</td>
</tr>
<tr>
<td>Authentication Key:</td>
<td>
<span class="config-service-key-field" service-name="{{ config.SECURITY_SCANNER_ISSUER_NAME }}"></span>
<div class="help-text">
The security scanning service requires an authorized service key to speak to Quay. Once setup, the key
can be managed in the Service Keys panel under the Super User Admin Panel.
</div>
</td>
</tr>
</table>
</div>
</div>
<!-- ACI Conversion -->
<div class="co-panel">
<div class="co-panel-heading">

View file

@ -0,0 +1,134 @@
<div class="request-service-key-dialog-element">
<!-- Modal message dialog -->
<div class="co-dialog modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true" ng-show="!working">&times;</button>
<h4 class="modal-title">Create key for service {{ requestKeyInfo.service }}</h4>
</div>
<div class="modal-body" ng-show="working">
<div class="cor-loader"></div>
</div>
<div class="modal-body" ng-show="!working">
<!-- Step 0 -->
<div ng-show="step == 0">
<table class="co-option-table">
<tr>
<td><input type="radio" id="automaticKey" ng-model="requestKind" value="automatic"></td>
<td>
<label for="automaticKey">Have the service provide a key</label>
<div class="help-text">Recommended for <code>{{ requestKeyInfo.service }}</code> installations where the single instance is setup now.</div>
</td>
</tr>
<tr>
<td><input type="radio" id="presharedKey" ng-model="requestKind" value="preshared"></td>
<td>
<label for="presharedKey">Generate shared key</label>
<div class="help-text">Recommended for <code>{{ requestKeyInfo.service }}</code> installations where the instances are dynamically started.</div>
</td>
</tr>
</table>
</div>
<!-- Step 1 (automatic) -->
<div ng-show="step == 1 && requestKind == 'automatic'" style="text-align: center">
<div style="margin-top: 20px;">
Please start the <code>{{ requestKeyInfo.service }}</code> service now, configured for <a href="https://github.com/coreos/jwtproxy#autogenerated-private-key" ng-safenewtab>autogenerated private key</a>. The key approval process will continue automatically once the service connects to Quay.
</div>
<div style="margin-top: 20px;">
Waiting for service to connect
</div>
<div style="margin-top: 10px; margin-bottom: 20px;">
<div class="cor-loader-inline"></div>
</div>
</div>
<!-- Step 2 (automatic) -->
<div ng-show="step == 2 && requestKind == 'automatic'" style="text-align: center">
A key for service <code>{{ requestKeyInfo.service }}</code> has been automatically generated, approved and saved in the service's keystore.
</div>
<!-- Step 1 (generate) -->
<div ng-show="step == 1 && requestKind == 'preshared'">
<form name="createForm" ng-submit="createPresharedKey()">
<table class="co-form-table">
<tr>
<td><label for="create-key-name">Key Name:</label></td>
<td>
<input class="form-control" name="create-key-name" type="text" ng-model="preshared.name" placeholder="Friendly Key Name">
<span class="co-help-text">
A friendly name for the key for later reference.
</span>
</td>
</tr>
<tr>
<td><label for="create-key-expiration">Expiration date (optional):</label></td>
<td>
<span class="datetime-picker" datetime="preshared.expiration"></span>
<span class="co-help-text">
The date and time that the key expires. If left blank, the key will never expire.
</span>
</td>
</tr>
<tr>
<td><label for="create-key-notes">Approval Notes (optional):</label></td>
<td>
<div class="markdown-editor" content="preshared.notes"></div>
<span class="co-help-text">
Optional notes for additional human-readable information about why the key was created.
</span>
</td>
</tr>
</table>
</form>
</div>
<!-- Step 2 (generate) -->
<div ng-show="step == 2 && requestKind == 'preshared'">
<div class="co-alert co-alert-warning">
The following key has been generated for service <code>{{ requestKeyInfo.service }}</code>.
<br><br>
Please copy the key's ID and copy/download the key's private contents and place it in the directory with the service's configuration.
<br><br>
<strong>Once this dialog is closed this private key will not be accessible anywhere else!</strong>
</div>
<label>Key ID:</label>
<div class="copy-box" value="createdKey.kid"></div>
<label>Private Key (PEM):</label>
<textarea class="key-display form-control" onclick="this.focus();this.select()" readonly>{{ createdKey.private_key }}</textarea>
</div>
</div>
<div class="modal-footer" ng-show="!working">
<button type="button" class="btn btn-primary" ng-show="step == 1 && requestKind == 'preshared'"
ng-disabled="createForm.$invalid"
ng-click="createPresharedKey()">
Generate Key
</button>
<button type="button" class="btn btn-primary" ng-show="step == 0 && requestKind == 'preshared'"
ng-click="showGenerate()">
Continue
</button>
<button type="button" class="btn btn-primary" ng-show="step == 0 && requestKind == 'automatic'"
ng-click="startApproval()">
Start Approval
</button>
<button type="button" class="btn btn-primary" ng-click="downloadPrivateKey(createdKey)" ng-if="createdKey && isDownloadSupported()">
<i class="fa fa-download"></i> Download Private Key
</button>
<button type="button" class="btn btn-default" data-dismiss="modal" ng-show="step == 2">Close</button>
<button type="button" class="btn btn-default" data-dismiss="modal" ng-show="step != 2">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>
</div>

View file

@ -61,6 +61,10 @@ angular.module("core-config-setup", ['angularFileUpload'])
{'id': 'gitlab-trigger', 'title': 'GitLab Build Triggers', 'condition': function(config) {
return config.FEATURE_GITLAB_BUILD;
}},
{'id': 'security-scanner', 'title': 'Quay Security Scanner', 'condition': function(config) {
return config.FEATURE_SECURITY_SCANNER;
}}
];
@ -1029,6 +1033,87 @@ angular.module("core-config-setup", ['angularFileUpload'])
return directiveDefinitionObject;
})
.directive('configServiceKeyField', function (ApiService) {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/config/config-service-key-field.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'serviceName': '@serviceName',
},
controller: function($scope, $element) {
$scope.foundKeys = [];
$scope.loading = false;
$scope.loadError = false;
$scope.hasValidKey = false;
$scope.hasValidKeyStr = null;
$scope.updateKeys = function() {
$scope.foundKeys = [];
$scope.loading = true;
ApiService.listServiceKeys().then(function(resp) {
$scope.loading = false;
$scope.loadError = false;
resp['keys'].forEach(function(key) {
if (key['service'] == $scope.serviceName) {
$scope.foundKeys.push(key);
}
});
$scope.hasValidKey = checkValidKey($scope.foundKeys);
$scope.hasValidKeyStr = $scope.hasValidKey ? 'true' : '';
}, function() {
$scope.loading = false;
$scope.loadError = true;
});
};
// Perform initial loading of the keys.
$scope.updateKeys();
$scope.isKeyExpired = function(key) {
if (key.expiration_date != null) {
var expiration_date = moment(key.expiration_date);
return moment().isAfter(expiration_date);
}
return false;
};
$scope.showRequestServiceKey = function() {
$scope.requestKeyInfo = {
'service': $scope.serviceName
};
};
$scope.handleKeyCreated = function() {
$scope.updateKeys();
};
var checkValidKey = function(keys) {
for (var i = 0; i < keys.length; ++i) {
var key = keys[i];
if (!key.approval) {
continue;
}
if ($scope.isKeyExpired(key)) {
continue;
}
return true;
}
return false;
};
}
};
return directiveDefinitionObject;
})
.directive('configStringField', function () {
var directiveDefinitionObject = {
priority: 0,

View file

@ -0,0 +1,120 @@
/**
* An element which displays a dialog for requesting or creating a service key.
*/
angular.module('quay').directive('requestServiceKeyDialog', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/request-service-key-dialog.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'requestKeyInfo': '=requestKeyInfo',
'keyCreated': '&keyCreated'
},
controller: function($scope, $element, $timeout, ApiService) {
var handleNewKey = function(key) {
var data = {
'notes': 'Approved during setup of service ' + key.service
};
var params = {
'kid': key.kid
};
ApiService.approveServiceKey(data, params).then(function(resp) {
$scope.keyCreated({'key': key});
$scope.step = 2;
}, ApiService.errorDisplay('Could not approve service key'));
};
var checkKeys = function() {
var isShown = ($element.find('.modal').data('bs.modal') || {}).isShown;
if (!isShown) {
return;
}
// TODO: filter by service.
ApiService.listServiceKeys().then(function(resp) {
var keys = resp['keys'];
for (var i = 0; i < keys.length; ++i) {
var key = keys[i];
if (key.service == $scope.requestKeyInfo.service && !key.approval && key.rotation_duration) {
handleNewKey(key);
return;
}
}
$timeout(checkKeys, 1000);
}, ApiService.errorDisplay('Could not list service keys'));
};
$scope.show = function() {
$scope.working = false;
$scope.step = 0;
$scope.requestKind = null;
$scope.preshared = {
'name': $scope.requestKeyInfo.service + ' Service Key',
'notes': 'Created during setup for service `' + $scope.requestKeyInfo.service + '`'
};
$element.find('.modal').modal({});
};
$scope.hide = function() {
$scope.loading = false;
$element.find('.modal').modal('hide');
};
$scope.showGenerate = function() {
$scope.step = 1;
};
$scope.startApproval = function() {
$scope.step = 1;
checkKeys();
};
$scope.isDownloadSupported = function() {
var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent);
if (isSafari) {
// Doesn't work properly in Safari, sadly.
return false;
}
try { return !!new Blob(); } catch(e) {}
return false;
};
$scope.downloadPrivateKey = function(key) {
var blob = new Blob([key.private_key]);
saveAs(blob, key.service + '.pem');
};
$scope.createPresharedKey = function() {
$scope.working = true;
var data = {
'name': $scope.preshared.name,
'service': $scope.requestKeyInfo.service,
'expiration': $scope.preshared.expiration || null,
'notes': $scope.preshared.notes
};
ApiService.createServiceKey(data).then(function(resp) {
$scope.working = false;
$scope.step = 2;
$scope.createdKey = resp;
$scope.keyCreated({'key': resp});
}, ApiService.errorDisplay('Could not create service key'));
};
$scope.$watch('requestKeyInfo', function(info) {
if (info && info.service) {
$scope.show();
}
});
}
};
return directiveDefinitionObject;
});

View file

@ -1,13 +1,12 @@
(function() {
/**
* The Setup page provides a nice GUI walkthrough experience for setting up the Enterprise
* Registry.
* The Setup page provides a nice GUI walkthrough experience for setting up Quay Enterprise.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('setup', 'setup.html', SetupCtrl,
{
'newLayout': true,
'title': 'Enterprise Registry Setup'
'title': 'Quay Enterprise Setup'
})
}]);

View file

@ -1,6 +1,6 @@
(function() {
/**
* The superuser admin page provides a new management UI for the Enterprise Registry.
* The superuser admin page provides a new management UI for Quay Enterprise.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('superuser', 'super-user.html', SuperuserCtrl,

View file

@ -3,7 +3,7 @@
<div class="page-content" quay-show="Features.SUPER_USERS && currentStep == States.CONFIG">
<div class="cor-title">
<span class="cor-title-link"></span>
<span class="cor-title-content">Enterprise Registry Setup</span>
<span class="cor-title-content">Quay Enterprise Setup</span>
</div>
<div class="cor-tab-panel" style="padding: 20px;">