mirror of
https://github.com/portainer/portainer.git
synced 2025-08-07 23:05:26 +02:00
feat(jobs): add the ability to run a job on a target endpoint #2374
* feat(jobs): adding the ability to run scripts on endpoints fix(job): click on containerId in JobsDatatable redirects to container's logs refactor(job): remove the jobs datatable settings + texts changes on JobCreation view fix(jobs): jobs payloads are now following API rules and case feat(jobs): adding the capability to run scripts on hosts * feat(jobs): adding the ability to purge jobs containers * refactor(job): apply review changes * feat(job-creation): store image name in local storage * feat(host): disable job exec link in non-agent Swarm setup * feat(host): only display execute job in agent setups or standalone * feat(job): job execution overhaul * docs(swagger): update EndpointJob documentation
This commit is contained in:
parent
6ab510e5cb
commit
354fda31f1
37 changed files with 739 additions and 100 deletions
|
@ -0,0 +1,69 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('JobFormController', ['$state', 'LocalStorage', 'EndpointService', 'EndpointProvider', 'Notifications',
|
||||
function ($state, LocalStorage, EndpointService, EndpointProvider, Notifications) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.$onInit = onInit;
|
||||
ctrl.editorUpdate = editorUpdate;
|
||||
ctrl.executeJob = executeJob;
|
||||
|
||||
ctrl.state = {
|
||||
Method: 'editor',
|
||||
formValidationError: '',
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
ctrl.formValues = {
|
||||
Image: 'ubuntu:latest',
|
||||
JobFileContent: '',
|
||||
JobFile: null
|
||||
};
|
||||
|
||||
function onInit() {
|
||||
var storedImage = LocalStorage.getJobImage();
|
||||
if (storedImage) {
|
||||
ctrl.formValues.Image = storedImage;
|
||||
}
|
||||
}
|
||||
|
||||
function editorUpdate(cm) {
|
||||
ctrl.formValues.JobFileContent = cm.getValue();
|
||||
}
|
||||
|
||||
function createJob(image, method) {
|
||||
var endpointId = EndpointProvider.endpointID();
|
||||
var nodeName = ctrl.nodeName;
|
||||
|
||||
if (method === 'editor') {
|
||||
var jobFileContent = ctrl.formValues.JobFileContent;
|
||||
return EndpointService.executeJobFromFileContent(image, jobFileContent, endpointId, nodeName);
|
||||
}
|
||||
|
||||
var jobFile = ctrl.formValues.JobFile;
|
||||
return EndpointService.executeJobFromFileUpload(image, jobFile, endpointId, nodeName);
|
||||
}
|
||||
|
||||
function executeJob() {
|
||||
var method = ctrl.state.Method;
|
||||
if (method === 'editor' && ctrl.formValues.JobFileContent === '') {
|
||||
ctrl.state.formValidationError = 'Script file content must not be empty';
|
||||
return;
|
||||
}
|
||||
|
||||
var image = ctrl.formValues.Image;
|
||||
LocalStorage.storeJobImage(image);
|
||||
|
||||
ctrl.state.actionInProgress = true;
|
||||
createJob(image, method)
|
||||
.then(function success() {
|
||||
Notifications.success('Job successfully created');
|
||||
$state.go('^');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Job execution failure', err);
|
||||
})
|
||||
.finally(function final() {
|
||||
ctrl.state.actionInProgress = false;
|
||||
});
|
||||
}
|
||||
}]);
|
|
@ -0,0 +1,110 @@
|
|||
<form class="form-horizontal" name="executeJobForm">
|
||||
<!-- image-input -->
|
||||
<div class="form-group">
|
||||
<label for="job_image" class="col-sm-1 control-label text-left">Image</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.formValues.Image" id="job_image" name="job_image" placeholder="e.g. ubuntu:latest" required auto-focus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="executeJobForm.job_image.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="executeJobForm.job_image.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !image-input -->
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
This job will run inside a privileged container on the host. You can access the host filesystem under the
|
||||
<code>/host</code> folder.
|
||||
</span>
|
||||
</div>
|
||||
<!-- execution-method -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Job creation
|
||||
</div>
|
||||
<div class="form-group"></div>
|
||||
<div class="form-group" style="margin-bottom: 0">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="method_editor" ng-model="$ctrl.state.Method" value="editor">
|
||||
<label for="method_editor">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Web editor
|
||||
</div>
|
||||
<p>Use our Web editor</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="method_upload" ng-model="$ctrl.state.Method" value="upload">
|
||||
<label for="method_upload">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-upload" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Upload
|
||||
</div>
|
||||
<p>Upload from your computer</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !execution-method -->
|
||||
<!-- web-editor -->
|
||||
<div ng-show="$ctrl.state.Method === 'editor'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Web editor
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<code-editor
|
||||
identifier="execute-job-editor"
|
||||
placeholder="# Define or paste the content of your script file here"
|
||||
on-change="$ctrl.editorUpdate">
|
||||
</code-editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !web-editor -->
|
||||
<!-- upload -->
|
||||
<div ng-show="$ctrl.state.Method === 'upload'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Upload
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
You can upload a script file from your computer.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formValues.JobFile">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ $ctrl.formValues.JobFile.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!$ctrl.formValues.JobFile" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !upload -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
ng-disabled="$ctrl.state.actionInProgress || !executeJobForm.$valid
|
||||
|| ($ctrl.state.Method === 'upload' && !$ctrl.formValues.JobFile)"
|
||||
ng-click="$ctrl.executeJob()"
|
||||
button-spinner="$ctrl.state.actionInProgress">
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Execute</span>
|
||||
<span ng-show="$ctrl.state.actionInProgress">Starting job...</span>
|
||||
</button>
|
||||
<span class="text-danger" ng-if="$ctrl.state.formValidationError" style="margin-left: 5px;">
|
||||
{{ $ctrl.state.formValidationError }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
|
@ -0,0 +1,7 @@
|
|||
angular.module('portainer.app').component('executeJobForm', {
|
||||
templateUrl: 'app/portainer/components/forms/execute-job-form/execute-job-form.html',
|
||||
controller: 'JobFormController',
|
||||
bindings: {
|
||||
nodeName: '<'
|
||||
}
|
||||
});
|
|
@ -7,6 +7,7 @@ angular.module('portainer.app')
|
|||
update: { method: 'PUT', params: { id: '@id' } },
|
||||
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
|
||||
remove: { method: 'DELETE', params: { id: '@id'} },
|
||||
snapshot: { method: 'POST', params: { id: 'snapshot' }}
|
||||
snapshot: { method: 'POST', params: { id: 'snapshot' }},
|
||||
executeJob: { method: 'POST', ignoreLoadingBar: true, params: { id: '@id', action: 'job' } }
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -100,5 +100,18 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.executeJobFromFileUpload = function (image, jobFile, endpointId, nodeName) {
|
||||
return FileUploadService.executeEndpointJob(image, jobFile, endpointId, nodeName);
|
||||
};
|
||||
|
||||
service.executeJobFromFileContent = function (image, jobFileContent, endpointId, nodeName) {
|
||||
var payload = {
|
||||
Image: image,
|
||||
FileContent: jobFileContent
|
||||
};
|
||||
|
||||
return Endpoints.executeJob({ id: endpointId, method: 'string', nodeName: nodeName }, payload).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
|
|
|
@ -64,6 +64,17 @@ angular.module('portainer.app')
|
|||
});
|
||||
};
|
||||
|
||||
service.executeEndpointJob = function (imageName, file, endpointId, nodeName) {
|
||||
return Upload.upload({
|
||||
url: 'api/endpoints/' + endpointId + '/job?method=file&nodeName=' + nodeName,
|
||||
data: {
|
||||
File: file,
|
||||
Image: imageName
|
||||
},
|
||||
ignoreLoadingBar: true
|
||||
});
|
||||
};
|
||||
|
||||
service.createEndpoint = function(name, type, URL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||
return Upload.upload({
|
||||
url: 'api/endpoints',
|
||||
|
|
|
@ -89,6 +89,12 @@ angular.module('portainer.app')
|
|||
getColumnVisibilitySettings: function(key) {
|
||||
return localStorageService.get('col_visibility_' + key);
|
||||
},
|
||||
storeJobImage: function(data) {
|
||||
localStorageService.set('job_image', data);
|
||||
},
|
||||
getJobImage: function() {
|
||||
return localStorageService.get('job_image');
|
||||
},
|
||||
clean: function() {
|
||||
localStorageService.clearAll();
|
||||
}
|
||||
|
|
|
@ -13,7 +13,9 @@ angular.module('portainer.app')
|
|||
|
||||
service.error = function(title, e, fallbackText) {
|
||||
var msg = fallbackText;
|
||||
if (e.data && e.data.message) {
|
||||
if (e.data && e.data.details) {
|
||||
msg = e.data.details;
|
||||
} else if (e.data && e.data.message) {
|
||||
msg = e.data.message;
|
||||
} else if (e.message) {
|
||||
msg = e.message;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue