Support credential API
Adds a 'Rotate Credential' cluster action as a drop down menu item for
clusters. This invokes PATCH /v1/credential/{cluster-uuid} against the
credential API in Magnum.
Change-Id: I44c667dbb34df1bd5835720a1c39337d6b11a34e
Signed-off-by: Matthew Northcott <matthewnorthcott@catalystcloud.nz>
This commit is contained in:
@@ -35,7 +35,7 @@ CLUSTER_CREATE_ATTRS = clusters.CREATION_ATTRIBUTES
|
|||||||
CERTIFICATE_CREATE_ATTRS = certificates.CREATION_ATTRIBUTES
|
CERTIFICATE_CREATE_ATTRS = certificates.CREATION_ATTRIBUTES
|
||||||
QUOTA_CREATION_ATTRIBUTES = quotas.CREATION_ATTRIBUTES
|
QUOTA_CREATION_ATTRIBUTES = quotas.CREATION_ATTRIBUTES
|
||||||
CLUSTER_UPDATE_ALLOWED_PROPERTIES = set(['/node_count'])
|
CLUSTER_UPDATE_ALLOWED_PROPERTIES = set(['/node_count'])
|
||||||
DEFAULT_API_VERSION = '1.10'
|
DEFAULT_API_VERSION = '1.12'
|
||||||
|
|
||||||
|
|
||||||
def _cleanup_params(attrs, create, **params):
|
def _cleanup_params(attrs, create, **params):
|
||||||
@@ -264,6 +264,10 @@ def certificate_rotate(request, id):
|
|||||||
return magnumclient(request).certificates.rotate_ca(**args)
|
return magnumclient(request).certificates.rotate_ca(**args)
|
||||||
|
|
||||||
|
|
||||||
|
def credential_rotate(request, id):
|
||||||
|
return magnumclient(request).credentials.update(id)
|
||||||
|
|
||||||
|
|
||||||
def stats_list(request, project_id=None):
|
def stats_list(request, project_id=None):
|
||||||
return magnumclient(request).stats.list(project_id=project_id)
|
return magnumclient(request).stats.list(project_id=project_id)
|
||||||
|
|
||||||
|
|||||||
@@ -375,6 +375,17 @@ class Certificates(generic.View):
|
|||||||
new_cert.to_dict())
|
new_cert.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@urls.register
|
||||||
|
class RotateCredential(generic.View):
|
||||||
|
"""API for rotating a cluster credential"""
|
||||||
|
url_regex = r'container_infra/credentials/(?P<cluster_id>[^/]+)$'
|
||||||
|
|
||||||
|
@rest_utils.ajax()
|
||||||
|
def patch(self, request, cluster_id):
|
||||||
|
"""Rotate the existing credential"""
|
||||||
|
return magnum.credential_rotate(request, cluster_id).to_dict()
|
||||||
|
|
||||||
|
|
||||||
@urls.register
|
@urls.register
|
||||||
class Stats(generic.View):
|
class Stats(generic.View):
|
||||||
"""API for Magnum Stats"""
|
"""API for Magnum Stats"""
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
'horizon.dashboard.container-infra.clusters.show-certificate.service',
|
'horizon.dashboard.container-infra.clusters.show-certificate.service',
|
||||||
'horizon.dashboard.container-infra.clusters.sign-certificate.service',
|
'horizon.dashboard.container-infra.clusters.sign-certificate.service',
|
||||||
'horizon.dashboard.container-infra.clusters.rotate-certificate.service',
|
'horizon.dashboard.container-infra.clusters.rotate-certificate.service',
|
||||||
|
'horizon.dashboard.container-infra.clusters.rotate-credential.service',
|
||||||
'horizon.dashboard.container-infra.clusters.config.service',
|
'horizon.dashboard.container-infra.clusters.config.service',
|
||||||
'horizon.dashboard.container-infra.clusters.resourceType'
|
'horizon.dashboard.container-infra.clusters.resourceType'
|
||||||
];
|
];
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
showCertificateService,
|
showCertificateService,
|
||||||
signCertificateService,
|
signCertificateService,
|
||||||
rotateCertificateService,
|
rotateCertificateService,
|
||||||
|
rotateCredentialService,
|
||||||
getClusterConfigService,
|
getClusterConfigService,
|
||||||
resourceType) {
|
resourceType) {
|
||||||
|
|
||||||
@@ -120,6 +122,14 @@
|
|||||||
text: gettext('Rolling Cluster Upgrade')
|
text: gettext('Rolling Cluster Upgrade')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.append({
|
||||||
|
id: 'rotateCredentialAction',
|
||||||
|
service: rotateCredentialService,
|
||||||
|
template: {
|
||||||
|
type: 'danger',
|
||||||
|
text: gettext('Rotate Cluster Credentials')
|
||||||
|
}
|
||||||
|
})
|
||||||
.append({
|
.append({
|
||||||
id: 'deleteClusterAction',
|
id: 'deleteClusterAction',
|
||||||
service: deleteClusterService,
|
service: deleteClusterService,
|
||||||
|
|||||||
@@ -50,6 +50,11 @@
|
|||||||
expect(actionHasId(actions, 'rotateCertificateAction')).toBe(true);
|
expect(actionHasId(actions, 'rotateCertificateAction')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('registers Rotate Credential as an item action', function() {
|
||||||
|
var actions = registry.getResourceType('OS::Magnum::Cluster').itemActions;
|
||||||
|
expect(actionHasId(actions, 'rotateCredentialAction')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('registers Resize Cluster as an item action', function() {
|
it('registers Resize Cluster as an item action', function() {
|
||||||
var actions = registry.getResourceType('OS::Magnum::Cluster').itemActions;
|
var actions = registry.getResourceType('OS::Magnum::Cluster').itemActions;
|
||||||
expect(actionHasId(actions, 'resizeClusterAction')).toBe(true);
|
expect(actionHasId(actions, 'resizeClusterAction')).toBe(true);
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
* not use this file except in compliance with the License. You may obtain
|
||||||
|
* a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc overview
|
||||||
|
* @name horizon.dashboard.container-infra.clusters.rotate-credential.service
|
||||||
|
* @description Service for the container-infra cluster rotate certificate
|
||||||
|
*/
|
||||||
|
angular
|
||||||
|
.module('horizon.dashboard.container-infra.clusters')
|
||||||
|
.factory(
|
||||||
|
'horizon.dashboard.container-infra.clusters.rotate-credential.service',
|
||||||
|
rotateCredentialService);
|
||||||
|
|
||||||
|
rotateCredentialService.$inject = [
|
||||||
|
'horizon.app.core.openstack-service-api.magnum',
|
||||||
|
'horizon.dashboard.container-infra.clusters.resourceType',
|
||||||
|
'horizon.framework.util.actions.action-result.service',
|
||||||
|
'horizon.framework.util.i18n.gettext',
|
||||||
|
'horizon.framework.util.q.extensions',
|
||||||
|
'horizon.framework.widgets.modal.simple-modal.service',
|
||||||
|
'horizon.framework.widgets.modal-wait-spinner.service',
|
||||||
|
'horizon.framework.widgets.toast.service'
|
||||||
|
];
|
||||||
|
|
||||||
|
function rotateCredentialService(
|
||||||
|
magnum, resourceType, actionResult, gettext, $qExtensions, modal, spinnerModal, toast
|
||||||
|
) {
|
||||||
|
var cluster;
|
||||||
|
var labels = {
|
||||||
|
title: gettext('Confirm Credential Rotation'),
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
message: gettext('You have chosen to rotate the credentials for cluster "%(name)s" (%(id)s). If you are not already the owner of this cluster, the cluster ownership will transfer to you.'),
|
||||||
|
submit: gettext('Rotate Cluster Credentials'),
|
||||||
|
};
|
||||||
|
var message = {
|
||||||
|
success: gettext('Credentials successfully rotated for cluster "%(name)s" (%(id)s).'),
|
||||||
|
error: gettext('Unable to rotate credentials for cluster "%(name)s" (%(id)s).'),
|
||||||
|
errorDetail: gettext('Unable to rotate credentials for cluster "%(name)s" (%(id)s): %(reason)s.')
|
||||||
|
};
|
||||||
|
var service = {
|
||||||
|
initAction: initAction,
|
||||||
|
perform: perform,
|
||||||
|
allowed: allowed
|
||||||
|
};
|
||||||
|
|
||||||
|
return service;
|
||||||
|
|
||||||
|
//////////////
|
||||||
|
|
||||||
|
function initAction() {
|
||||||
|
}
|
||||||
|
|
||||||
|
function perform(selected) {
|
||||||
|
cluster = { id: selected.id, name: selected.name };
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
title: labels.title,
|
||||||
|
body: getMessage(labels.message),
|
||||||
|
submit: labels.submit
|
||||||
|
};
|
||||||
|
|
||||||
|
modal.modal(options).result.then(onSubmit);
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowed() {
|
||||||
|
// NOTE(northcottmt): Consider hiding this if the user is unable to rotate credentials.
|
||||||
|
return $qExtensions.booleanAsPromise(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
spinnerModal.showModalSpinner(gettext('Loading'));
|
||||||
|
return magnum.rotateCredential(cluster.id)
|
||||||
|
.then(handleResponse)
|
||||||
|
.catch(onError)
|
||||||
|
.finally(spinnerModal.hideModalSpinner);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResponse(response) {
|
||||||
|
var result = { created: [], deleted: [], failed: [], updated: [] };
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.add('success', getMessage(message.success));
|
||||||
|
result = actionResult.getActionResult().updated(resourceType, cluster.id).result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(response) {
|
||||||
|
var msg;
|
||||||
|
if (response && response.data) {
|
||||||
|
msg = getMessage(message.errorDetail, { reason: response.data });
|
||||||
|
} else {
|
||||||
|
msg = getMessage(message.error);
|
||||||
|
}
|
||||||
|
toast.add('error', msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessage(message, params) {
|
||||||
|
return interpolate(message, Object.assign(cluster, params), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
* not use this file except in compliance with the License. You may obtain
|
||||||
|
* a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('horizon.dashboard.container-infra.clusters.rotate-credential.service', function() {
|
||||||
|
|
||||||
|
var $q, service, magnum, deferred, spinnerModal, modal, modalConfig, toast, toastState;
|
||||||
|
var selected = { id: '1', name: 'mycluster' };
|
||||||
|
|
||||||
|
///////////////////
|
||||||
|
|
||||||
|
beforeEach(module('horizon.app.core'));
|
||||||
|
beforeEach(module('horizon.framework'));
|
||||||
|
beforeEach(module('horizon.dashboard.container-infra.clusters'));
|
||||||
|
|
||||||
|
beforeEach(inject(function($injector, _$q_) {
|
||||||
|
$q = _$q_;
|
||||||
|
service = $injector.get(
|
||||||
|
'horizon.dashboard.container-infra.clusters.rotate-credential.service');
|
||||||
|
magnum = $injector.get('horizon.app.core.openstack-service-api.magnum');
|
||||||
|
spinnerModal = $injector.get('horizon.framework.widgets.modal-wait-spinner.service');
|
||||||
|
modal = $injector.get('horizon.framework.widgets.modal.simple-modal.service');
|
||||||
|
toast = $injector.get('horizon.framework.widgets.toast.service');
|
||||||
|
|
||||||
|
spyOn(spinnerModal, 'showModalSpinner').and.callFake(function() {});
|
||||||
|
spyOn(spinnerModal, 'hideModalSpinner').and.callFake(function() {});
|
||||||
|
spyOn(toast, 'add').and.callFake(function(state) {
|
||||||
|
toastState = state;
|
||||||
|
});
|
||||||
|
spyOn(modal, 'modal').and.callFake(function(config) {
|
||||||
|
deferred = $q.defer();
|
||||||
|
deferred.resolve(config);
|
||||||
|
modalConfig = config;
|
||||||
|
return { result: deferred.promise };
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should check the policy', function() {
|
||||||
|
var allowed = service.allowed();
|
||||||
|
expect(allowed).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open the modal, hide the loading spinner, and rotate credentials',
|
||||||
|
inject(function($timeout) {
|
||||||
|
deferred = $q.defer();
|
||||||
|
deferred.resolve({});
|
||||||
|
spyOn(magnum, 'rotateCredential').and.returnValue(deferred.promise);
|
||||||
|
|
||||||
|
service.perform(selected);
|
||||||
|
|
||||||
|
$timeout(function() {
|
||||||
|
expect(toast.add).toHaveBeenCalledTimes(1);
|
||||||
|
expect(modal.modal).toHaveBeenCalled();
|
||||||
|
expect(spinnerModal.showModalSpinner).toHaveBeenCalled();
|
||||||
|
expect(spinnerModal.hideModalSpinner).toHaveBeenCalled();
|
||||||
|
expect(magnum.rotateCredential).toHaveBeenCalled();
|
||||||
|
expect(modalConfig.title).toBeDefined();
|
||||||
|
expect(modalConfig.body).toBeDefined();
|
||||||
|
expect(modalConfig.submit).toBeDefined();
|
||||||
|
expect(toastState).toEqual('success');
|
||||||
|
}, 0);
|
||||||
|
$timeout.flush();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should open the modal, hide the loading spinner, and handle errors on failed rotation',
|
||||||
|
inject(function($timeout) {
|
||||||
|
deferred = $q.defer();
|
||||||
|
deferred.reject();
|
||||||
|
spyOn(magnum, 'rotateCredential').and.returnValue(deferred.promise);
|
||||||
|
|
||||||
|
service.perform(selected);
|
||||||
|
|
||||||
|
$timeout(function () {
|
||||||
|
expect(toast.add).toHaveBeenCalledTimes(1);
|
||||||
|
expect(modal.modal).toHaveBeenCalled();
|
||||||
|
expect(spinnerModal.showModalSpinner).toHaveBeenCalled();
|
||||||
|
expect(spinnerModal.hideModalSpinner).toHaveBeenCalled();
|
||||||
|
expect(magnum.rotateCredential).toHaveBeenCalled();
|
||||||
|
expect(modalConfig.title).toBeDefined();
|
||||||
|
expect(modalConfig.body).toBeDefined();
|
||||||
|
expect(modalConfig.submit).toBeDefined();
|
||||||
|
expect(toastState).toEqual('error');
|
||||||
|
}, 0);
|
||||||
|
$timeout.flush();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
showCertificate: showCertificate,
|
showCertificate: showCertificate,
|
||||||
signCertificate: signCertificate,
|
signCertificate: signCertificate,
|
||||||
rotateCertificate: rotateCertificate,
|
rotateCertificate: rotateCertificate,
|
||||||
|
rotateCredential: rotateCredential,
|
||||||
getStats: getStats,
|
getStats: getStats,
|
||||||
getIngressControllers: getIngressControllers,
|
getIngressControllers: getIngressControllers,
|
||||||
getAddons: getAddons,
|
getAddons: getAddons,
|
||||||
@@ -212,6 +213,25 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/////////////////
|
||||||
|
// Credentials //
|
||||||
|
/////////////////
|
||||||
|
|
||||||
|
function rotateCredential(id) {
|
||||||
|
return apiService.patch('/api/container_infra/credentials/' + id)
|
||||||
|
.catch(function onError(response) {
|
||||||
|
var msg, params;
|
||||||
|
if (response && response.data) {
|
||||||
|
msg = gettext('Unable to rotate credentials for cluster %(id)s: %(reason)s.');
|
||||||
|
params = { id: id, reason: response.data };
|
||||||
|
} else {
|
||||||
|
msg = gettext('Unable to rotate credentials for cluster %(id)s.');
|
||||||
|
params = { id: id };
|
||||||
|
}
|
||||||
|
toastService.add('error', interpolate(msg, params, true));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
///////////
|
///////////
|
||||||
// Stats //
|
// Stats //
|
||||||
///////////
|
///////////
|
||||||
|
|||||||
@@ -219,6 +219,13 @@
|
|||||||
"error": "Unable to rotate the certificate.",
|
"error": "Unable to rotate the certificate.",
|
||||||
"testInput": [123, [123]]
|
"testInput": [123, [123]]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"func": "rotateCredential",
|
||||||
|
"method": "patch",
|
||||||
|
"path": "/api/container_infra/credentials/123",
|
||||||
|
"error": "Unable to rotate credentials for cluster 123.",
|
||||||
|
"testInput": [123]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"func": "getQuotas",
|
"func": "getQuotas",
|
||||||
"method": "get",
|
"method": "get",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Support credential rotation. This adds
|
||||||
|
"Rotate Credential" action for cluster as item action.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
# date but we do not test them so no guarantee of having them all correct. If
|
# date but we do not test them so no guarantee of having them all correct. If
|
||||||
# you find any incorrect lower bounds, let us know or propose a fix.
|
# you find any incorrect lower bounds, let us know or propose a fix.
|
||||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||||
python-magnumclient>=4.2.0 # Apache-2.0
|
python-magnumclient>=4.9.0 # Apache-2.0
|
||||||
python-heatclient>=1.18.0
|
python-heatclient>=1.18.0
|
||||||
|
|
||||||
horizon>=17.1.0 # Apache-2.0
|
horizon>=17.1.0 # Apache-2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user