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
|
||||
QUOTA_CREATION_ATTRIBUTES = quotas.CREATION_ATTRIBUTES
|
||||
CLUSTER_UPDATE_ALLOWED_PROPERTIES = set(['/node_count'])
|
||||
DEFAULT_API_VERSION = '1.10'
|
||||
DEFAULT_API_VERSION = '1.12'
|
||||
|
||||
|
||||
def _cleanup_params(attrs, create, **params):
|
||||
@@ -264,6 +264,10 @@ def certificate_rotate(request, id):
|
||||
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):
|
||||
return magnumclient(request).stats.list(project_id=project_id)
|
||||
|
||||
|
@@ -375,6 +375,17 @@ class Certificates(generic.View):
|
||||
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
|
||||
class Stats(generic.View):
|
||||
"""API for Magnum Stats"""
|
||||
|
@@ -39,6 +39,7 @@
|
||||
'horizon.dashboard.container-infra.clusters.show-certificate.service',
|
||||
'horizon.dashboard.container-infra.clusters.sign-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.resourceType'
|
||||
];
|
||||
@@ -53,6 +54,7 @@
|
||||
showCertificateService,
|
||||
signCertificateService,
|
||||
rotateCertificateService,
|
||||
rotateCredentialService,
|
||||
getClusterConfigService,
|
||||
resourceType) {
|
||||
|
||||
@@ -120,6 +122,14 @@
|
||||
text: gettext('Rolling Cluster Upgrade')
|
||||
}
|
||||
})
|
||||
.append({
|
||||
id: 'rotateCredentialAction',
|
||||
service: rotateCredentialService,
|
||||
template: {
|
||||
type: 'danger',
|
||||
text: gettext('Rotate Cluster Credentials')
|
||||
}
|
||||
})
|
||||
.append({
|
||||
id: 'deleteClusterAction',
|
||||
service: deleteClusterService,
|
||||
|
@@ -50,6 +50,11 @@
|
||||
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() {
|
||||
var actions = registry.getResourceType('OS::Magnum::Cluster').itemActions;
|
||||
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,
|
||||
signCertificate: signCertificate,
|
||||
rotateCertificate: rotateCertificate,
|
||||
rotateCredential: rotateCredential,
|
||||
getStats: getStats,
|
||||
getIngressControllers: getIngressControllers,
|
||||
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 //
|
||||
///////////
|
||||
|
@@ -219,6 +219,13 @@
|
||||
"error": "Unable to rotate the certificate.",
|
||||
"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",
|
||||
"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
|
||||
# you find any incorrect lower bounds, let us know or propose a fix.
|
||||
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
|
||||
|
||||
horizon>=17.1.0 # Apache-2.0
|
||||
|
Reference in New Issue
Block a user