1
0
Fork 0
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:
baron_l 2018-10-28 07:06:50 +01:00 committed by Anthony Lapenna
parent 6ab510e5cb
commit 354fda31f1
37 changed files with 739 additions and 100 deletions

View file

@ -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;
});
}
}]);

View file

@ -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>

View file

@ -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: '<'
}
});

View file

@ -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' } }
});
}]);

View file

@ -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;
}]);

View file

@ -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',

View file

@ -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();
}

View file

@ -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;