mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 18:40:01 -03:30
add api for testing credential plugins
This commit is contained in:
parent
7a43f00a5d
commit
0ee223f799
@ -13,6 +13,7 @@ from awx.api.views import (
|
||||
CredentialOwnerTeamsList,
|
||||
CredentialCopy,
|
||||
CredentialInputSourceSubList,
|
||||
CredentialExternalTest,
|
||||
)
|
||||
|
||||
|
||||
@ -26,6 +27,7 @@ urls = [
|
||||
url(r'^(?P<pk>[0-9]+)/owner_teams/$', CredentialOwnerTeamsList.as_view(), name='credential_owner_teams_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/copy/$', CredentialCopy.as_view(), name='credential_copy'),
|
||||
url(r'^(?P<pk>[0-9]+)/input_sources/$', CredentialInputSourceSubList.as_view(), name='credential_input_source_sublist'),
|
||||
url(r'^(?P<pk>[0-9]+)/test/$', CredentialExternalTest.as_view(), name='credential_external_test'),
|
||||
]
|
||||
|
||||
__all__ = ['urls']
|
||||
|
||||
@ -8,6 +8,7 @@ from awx.api.views import (
|
||||
CredentialTypeDetail,
|
||||
CredentialTypeCredentialList,
|
||||
CredentialTypeActivityStreamList,
|
||||
CredentialTypeExternalTest,
|
||||
)
|
||||
|
||||
|
||||
@ -16,6 +17,7 @@ urls = [
|
||||
url(r'^(?P<pk>[0-9]+)/$', CredentialTypeDetail.as_view(), name='credential_type_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/credentials/$', CredentialTypeCredentialList.as_view(), name='credential_type_credential_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/activity_stream/$', CredentialTypeActivityStreamList.as_view(), name='credential_type_activity_stream_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/test/$', CredentialTypeExternalTest.as_view(), name='credential_type_external_test'),
|
||||
]
|
||||
|
||||
__all__ = ['urls']
|
||||
|
||||
@ -1419,6 +1419,32 @@ class CredentialCopy(CopyAPIView):
|
||||
copy_return_serializer_class = serializers.CredentialSerializer
|
||||
|
||||
|
||||
class CredentialExternalTest(SubDetailAPIView):
|
||||
"""
|
||||
Test updates to the input values of an external credential before
|
||||
saving them.
|
||||
"""
|
||||
|
||||
view_name = _('External Credential Test')
|
||||
|
||||
model = models.Credential
|
||||
serializer_class = serializers.EmptySerializer
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
test_inputs = {}
|
||||
for field_name, value in request.data.get('inputs', {}).items():
|
||||
if value == '$encrypted$':
|
||||
test_inputs[field_name] = obj.get_input(field_name)
|
||||
else:
|
||||
test_inputs[field_name] = value
|
||||
try:
|
||||
obj.credential_type.plugin.backend(None, **test_inputs)
|
||||
return Response({}, status=status.HTTP_202_ACCEPTED)
|
||||
except Exception as exc:
|
||||
return Response({'inputs': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView):
|
||||
|
||||
view_name = _("Credential Input Source Detail")
|
||||
@ -1446,6 +1472,27 @@ class CredentialInputSourceSubList(SubListCreateAttachDetachAPIView):
|
||||
parent_key = 'target_credential'
|
||||
|
||||
|
||||
class CredentialTypeExternalTest(SubDetailAPIView):
|
||||
"""
|
||||
Test a complete set of input values for an external credential before
|
||||
saving it.
|
||||
"""
|
||||
|
||||
view_name = _('External Credential Type Test')
|
||||
|
||||
model = models.CredentialType
|
||||
serializer_class = serializers.EmptySerializer
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
test_inputs = request.data.get('inputs', {})
|
||||
try:
|
||||
obj.plugin.backend(None, **test_inputs)
|
||||
return Response({}, status=status.HTTP_202_ACCEPTED)
|
||||
except Exception as exc:
|
||||
return Response({'inputs': str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class HostRelatedSearchMixin(object):
|
||||
|
||||
@property
|
||||
|
||||
@ -577,6 +577,16 @@ class CredentialType(CommonModelNameNotUnique):
|
||||
if field.get('ask_at_runtime', False) is True
|
||||
]
|
||||
|
||||
@property
|
||||
def plugin(self):
|
||||
if self.kind != 'external':
|
||||
raise AttributeError('plugin')
|
||||
[plugin] = [
|
||||
plugin for ns, plugin in credential_plugins.items()
|
||||
if ns == self.namespace
|
||||
]
|
||||
return plugin
|
||||
|
||||
def default_for_field(self, field_id):
|
||||
for field in self.inputs.get('fields', []):
|
||||
if field['id'] == field_id:
|
||||
@ -1336,11 +1346,7 @@ class CredentialInputSource(PrimordialModel):
|
||||
super(CredentialInputSource, self).save(*args, **kwargs)
|
||||
|
||||
def get_input_value(self):
|
||||
[backend] = [
|
||||
plugin.backend for ns, plugin in credential_plugins.items()
|
||||
if ns == self.source_credential.credential_type.namespace
|
||||
]
|
||||
|
||||
backend = self.source_credential.credential_type.plugin.backend
|
||||
backend_kwargs = {}
|
||||
for field_name, value in self.source_credential.inputs.items():
|
||||
if field_name in self.source_credential.credential_type.secret_fields:
|
||||
|
||||
@ -4,7 +4,9 @@ function AddCredentialsController (
|
||||
$scope,
|
||||
strings,
|
||||
componentsStrings,
|
||||
ConfigService
|
||||
ConfigService,
|
||||
ngToast,
|
||||
$filter
|
||||
) {
|
||||
const vm = this || {};
|
||||
|
||||
@ -36,6 +38,7 @@ function AddCredentialsController (
|
||||
vm.form.credential_type._route = 'credentials.add.credentialType';
|
||||
vm.form.credential_type._model = credentialType;
|
||||
vm.form.credential_type._placeholder = strings.get('inputs.CREDENTIAL_TYPE_PLACEHOLDER');
|
||||
vm.isTestable = credentialType.get('kind') === 'external';
|
||||
|
||||
const gceFileInputSchema = {
|
||||
id: 'gce_service_account_key',
|
||||
@ -62,6 +65,8 @@ function AddCredentialsController (
|
||||
become._choices = Array.from(apiConfig.become_methods, method => method[0]);
|
||||
}
|
||||
|
||||
vm.isTestable = credentialType.get('kind') === 'external';
|
||||
|
||||
return fields;
|
||||
},
|
||||
_source: vm.form.credential_type,
|
||||
@ -69,6 +74,45 @@ function AddCredentialsController (
|
||||
_key: 'inputs'
|
||||
};
|
||||
|
||||
vm.form.secondary = ({ inputs }) => {
|
||||
const name = $filter('sanitize')(credentialType.get('name'));
|
||||
const endpoint = `${credentialType.get('id')}/test/`;
|
||||
|
||||
return credentialType.http.post({ url: endpoint, data: { inputs }, replace: false })
|
||||
.then(() => {
|
||||
ngToast.success({
|
||||
content: `
|
||||
<div class="Toast-wrapper">
|
||||
<div class="Toast-icon">
|
||||
<i class="fa fa-check-circle Toast-successIcon"></i>
|
||||
</div>
|
||||
<div>
|
||||
<b>${name}:</b> ${strings.get('edit.TEST_PASSED')}
|
||||
</div>
|
||||
</div>`,
|
||||
dismissButton: false,
|
||||
dismissOnTimeout: true
|
||||
});
|
||||
})
|
||||
.catch(({ data }) => {
|
||||
const msg = data.inputs ? `${$filter('sanitize')(data.inputs)}` : strings.get('edit.TEST_FAILED');
|
||||
|
||||
ngToast.danger({
|
||||
content: `
|
||||
<div class="Toast-wrapper">
|
||||
<div class="Toast-icon">
|
||||
<i class="fa fa-exclamation-triangle Toast-successIcon"></i>
|
||||
</div>
|
||||
<div>
|
||||
<b>${name}:</b> ${msg}
|
||||
</div>
|
||||
</div>`,
|
||||
dismissButton: false,
|
||||
dismissOnTimeout: true
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
vm.form.save = data => {
|
||||
data.user = me.get('id');
|
||||
|
||||
@ -149,7 +193,9 @@ AddCredentialsController.$inject = [
|
||||
'$scope',
|
||||
'CredentialsStrings',
|
||||
'ComponentsStrings',
|
||||
'ConfigService'
|
||||
'ConfigService',
|
||||
'ngToast',
|
||||
'$filter'
|
||||
];
|
||||
|
||||
export default AddCredentialsController;
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
</at-input-group>
|
||||
|
||||
<at-action-group col="12" pos="right">
|
||||
<at-form-action type="secondary" ng-if="vm.isTestable"></at-form-action>
|
||||
<at-form-action type="cancel" to="credentials"></at-form-action>
|
||||
<at-form-action type="save"></at-form-action>
|
||||
</at-action-group>
|
||||
|
||||
@ -26,6 +26,11 @@ function CredentialsStrings (BaseString) {
|
||||
PANEL_TITLE: t.s('NEW CREDENTIAL')
|
||||
};
|
||||
|
||||
ns.edit = {
|
||||
TEST_PASSED: t.s('Test passed.'),
|
||||
TEST_FAILED: t.s('Test failed.')
|
||||
};
|
||||
|
||||
ns.permissions = {
|
||||
TITLE: t.s('CREDENTIALS PERMISSIONS')
|
||||
};
|
||||
|
||||
@ -4,7 +4,10 @@ function EditCredentialsController (
|
||||
$scope,
|
||||
strings,
|
||||
componentsStrings,
|
||||
ConfigService
|
||||
ConfigService,
|
||||
ngToast,
|
||||
Wait,
|
||||
$filter,
|
||||
) {
|
||||
const vm = this || {};
|
||||
|
||||
@ -83,6 +86,7 @@ function EditCredentialsController (
|
||||
vm.form.credential_type._value = credentialType.get('id');
|
||||
vm.form.credential_type._displayValue = credentialType.get('name');
|
||||
vm.form.credential_type._placeholder = strings.get('inputs.CREDENTIAL_TYPE_PLACEHOLDER');
|
||||
vm.isTestable = (isEditable && credentialType.get('kind') === 'external');
|
||||
|
||||
const gceFileInputSchema = {
|
||||
id: 'gce_service_account_key',
|
||||
@ -124,6 +128,7 @@ function EditCredentialsController (
|
||||
}
|
||||
}
|
||||
}
|
||||
vm.isTestable = (isEditable && credentialType.get('kind') === 'external');
|
||||
|
||||
return fields;
|
||||
},
|
||||
@ -132,6 +137,45 @@ function EditCredentialsController (
|
||||
_key: 'inputs'
|
||||
};
|
||||
|
||||
vm.form.secondary = ({ inputs }) => {
|
||||
const name = $filter('sanitize')(credentialType.get('name'));
|
||||
const endpoint = `${credential.get('id')}/test/`;
|
||||
|
||||
return credential.http.post({ url: endpoint, data: { inputs }, replace: false })
|
||||
.then(() => {
|
||||
ngToast.success({
|
||||
content: `
|
||||
<div class="Toast-wrapper">
|
||||
<div class="Toast-icon">
|
||||
<i class="fa fa-check-circle Toast-successIcon"></i>
|
||||
</div>
|
||||
<div>
|
||||
<b>${name}:</b> ${strings.get('edit.TEST_PASSED')}
|
||||
</div>
|
||||
</div>`,
|
||||
dismissButton: false,
|
||||
dismissOnTimeout: true
|
||||
});
|
||||
})
|
||||
.catch(({ data }) => {
|
||||
const msg = data.inputs ? `${$filter('sanitize')(data.inputs)}` : strings.get('edit.TEST_FAILED');
|
||||
|
||||
ngToast.danger({
|
||||
content: `
|
||||
<div class="Toast-wrapper">
|
||||
<div class="Toast-icon">
|
||||
<i class="fa fa-exclamation-triangle Toast-successIcon"></i>
|
||||
</div>
|
||||
<div>
|
||||
<b>${name}:</b> ${msg}
|
||||
</div>
|
||||
</div>`,
|
||||
dismissButton: false,
|
||||
dismissOnTimeout: true
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* If a credential's `credential_type` is changed while editing, the inputs associated with
|
||||
* the old type need to be cleared before saving the inputs associated with the new type.
|
||||
@ -210,7 +254,10 @@ EditCredentialsController.$inject = [
|
||||
'$scope',
|
||||
'CredentialsStrings',
|
||||
'ComponentsStrings',
|
||||
'ConfigService'
|
||||
'ConfigService',
|
||||
'ngToast',
|
||||
'Wait',
|
||||
'$filter',
|
||||
];
|
||||
|
||||
export default EditCredentialsController;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
.at-ActionGroup {
|
||||
margin-top: @at-margin-panel;
|
||||
|
||||
button:last-child {
|
||||
margin-left: @at-margin-panel-inset;
|
||||
button {
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,9 @@ function atFormActionController ($state, strings) {
|
||||
case 'save':
|
||||
vm.setSaveDefaults();
|
||||
break;
|
||||
case 'secondary':
|
||||
vm.setSecondaryDefaults();
|
||||
break;
|
||||
default:
|
||||
vm.setCustomDefaults();
|
||||
}
|
||||
@ -43,6 +46,13 @@ function atFormActionController ($state, strings) {
|
||||
scope.color = 'success';
|
||||
scope.action = () => { form.submit(); };
|
||||
};
|
||||
|
||||
vm.setSecondaryDefaults = () => {
|
||||
scope.text = strings.get('TEST');
|
||||
scope.fill = '';
|
||||
scope.color = 'info';
|
||||
scope.action = () => { form.submitSecondary(); };
|
||||
};
|
||||
}
|
||||
|
||||
atFormActionController.$inject = ['$state', 'ComponentsStrings'];
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<button class="btn at-Button{{ fill }}--{{ color }}"
|
||||
ng-disabled="type !== 'cancel' && (form.disabled || (type === 'save' && !form.isValid))"
|
||||
ng-disabled="type !== 'cancel' && (form.disabled
|
||||
|| (type === 'save' && !form.isValid))
|
||||
|| (type === 'secondary' && !form.isValid)"
|
||||
ng-click="action()">
|
||||
{{::text}}
|
||||
</button>
|
||||
|
||||
@ -62,6 +62,40 @@ function AtFormController (eventService, strings) {
|
||||
scope.$apply(vm.submit);
|
||||
};
|
||||
|
||||
vm.getSubmitData = () => vm.components
|
||||
.filter(component => component.category === 'input')
|
||||
.reduce((values, component) => {
|
||||
if (component.state._value === undefined) {
|
||||
return values;
|
||||
}
|
||||
|
||||
if (component.state._format === 'selectFromOptions') {
|
||||
values[component.state.id] = component.state._value[0];
|
||||
} else if (component.state._key && typeof component.state._value === 'object') {
|
||||
values[component.state.id] = component.state._value[component.state._key];
|
||||
} else if (component.state._group) {
|
||||
values[component.state._key] = values[component.state._key] || {};
|
||||
values[component.state._key][component.state.id] = component.state._value;
|
||||
} else {
|
||||
values[component.state.id] = component.state._value;
|
||||
}
|
||||
|
||||
return values;
|
||||
}, {});
|
||||
|
||||
vm.submitSecondary = () => {
|
||||
if (!vm.state.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
vm.state.disabled = true;
|
||||
|
||||
const data = vm.getSubmitData();
|
||||
|
||||
scope.state.secondary(data)
|
||||
.finally(() => { vm.state.disabled = false; });
|
||||
};
|
||||
|
||||
vm.submit = () => {
|
||||
if (!vm.state.isValid) {
|
||||
return;
|
||||
@ -69,26 +103,7 @@ function AtFormController (eventService, strings) {
|
||||
|
||||
vm.state.disabled = true;
|
||||
|
||||
const data = vm.components
|
||||
.filter(component => component.category === 'input')
|
||||
.reduce((values, component) => {
|
||||
if (component.state._value === undefined) {
|
||||
return values;
|
||||
}
|
||||
|
||||
if (component.state._format === 'selectFromOptions') {
|
||||
values[component.state.id] = component.state._value[0];
|
||||
} else if (component.state._key && typeof component.state._value === 'object') {
|
||||
values[component.state.id] = component.state._value[component.state._key];
|
||||
} else if (component.state._group) {
|
||||
values[component.state._key] = values[component.state._key] || {};
|
||||
values[component.state._key][component.state.id] = component.state._value;
|
||||
} else {
|
||||
values[component.state.id] = component.state._value;
|
||||
}
|
||||
|
||||
return values;
|
||||
}, {});
|
||||
const data = vm.getSubmitData();
|
||||
|
||||
scope.state.save(data)
|
||||
.then(scope.state.onSaveSuccess)
|
||||
|
||||
@ -153,17 +153,22 @@ function httpPost (config = {}) {
|
||||
const req = {
|
||||
method: 'POST',
|
||||
url: this.path,
|
||||
data: config.data
|
||||
data: config.data,
|
||||
};
|
||||
|
||||
if (config.url) {
|
||||
req.url = `${this.path}${config.url}`;
|
||||
}
|
||||
|
||||
if (!('replace' in config)) {
|
||||
config.replace = true;
|
||||
}
|
||||
|
||||
return $http(req)
|
||||
.then(res => {
|
||||
this.model.GET = res.data;
|
||||
|
||||
if (config.replace) {
|
||||
this.model.GET = res.data;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
@ -73,6 +73,7 @@ function BaseStringService (namespace) {
|
||||
this.COPY = t.s('COPY');
|
||||
this.YES = t.s('YES');
|
||||
this.CLOSE = t.s('CLOSE');
|
||||
this.TEST = t.s('TEST');
|
||||
this.SUCCESSFUL_CREATION = resource => t.s('{{ resource }} successfully created', { resource: $filter('sanitize')(resource) });
|
||||
|
||||
this.deleteResource = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user