mirror of https://github.com/portainer/portainer
Merge branch 'develop' into feat2182-upload-files-host
commit
c4d647887f
|
@ -45,11 +45,7 @@ func (scheduler *JobScheduler) ScheduleEndpointSyncJob(endpointFilePath string,
|
||||||
// ScheduleSnapshotJob schedules a cron job to create endpoint snapshots
|
// ScheduleSnapshotJob schedules a cron job to create endpoint snapshots
|
||||||
func (scheduler *JobScheduler) ScheduleSnapshotJob(interval string) error {
|
func (scheduler *JobScheduler) ScheduleSnapshotJob(interval string) error {
|
||||||
job := newEndpointSnapshotJob(scheduler.endpointService, scheduler.snapshotter)
|
job := newEndpointSnapshotJob(scheduler.endpointService, scheduler.snapshotter)
|
||||||
|
go job.Snapshot()
|
||||||
err := job.Snapshot()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return scheduler.cron.AddJob("@every "+interval, job)
|
return scheduler.cron.AddJob("@every "+interval, job)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,6 @@ func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Add("X-Frame-Options", "DENY")
|
|
||||||
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
||||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||||
handler.Handler.ServeHTTP(w, r)
|
handler.Handler.ServeHTTP(w, r)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
type stackMigratePayload struct {
|
type stackMigratePayload struct {
|
||||||
EndpointID int
|
EndpointID int
|
||||||
SwarmID string
|
SwarmID string
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *stackMigratePayload) Validate(r *http.Request) error {
|
func (payload *stackMigratePayload) Validate(r *http.Request) error {
|
||||||
|
@ -89,11 +90,17 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||||
stack.SwarmID = payload.SwarmID
|
stack.SwarmID = payload.SwarmID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldName := stack.Name
|
||||||
|
if payload.Name != "" {
|
||||||
|
stack.Name = payload.Name
|
||||||
|
}
|
||||||
|
|
||||||
migrationError := handler.migrateStack(r, stack, targetEndpoint)
|
migrationError := handler.migrateStack(r, stack, targetEndpoint)
|
||||||
if migrationError != nil {
|
if migrationError != nil {
|
||||||
return migrationError
|
return migrationError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stack.Name = oldName
|
||||||
err = handler.deleteStack(stack, endpoint)
|
err = handler.deleteStack(stack, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||||
|
|
|
@ -114,7 +114,6 @@ func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portain
|
||||||
// mwSecureHeaders provides secure headers middleware for handlers.
|
// mwSecureHeaders provides secure headers middleware for handlers.
|
||||||
func mwSecureHeaders(next http.Handler) http.Handler {
|
func mwSecureHeaders(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Add("X-Frame-Options", "DENY")
|
|
||||||
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
||||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
|
|
|
@ -4160,6 +4160,10 @@ definitions:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "jpofkc0i9uo9wtx1zesuk649w"
|
example: "jpofkc0i9uo9wtx1zesuk649w"
|
||||||
description: "Swarm cluster identifier, must match the identifier of the cluster where the stack will be relocated"
|
description: "Swarm cluster identifier, must match the identifier of the cluster where the stack will be relocated"
|
||||||
|
Name:
|
||||||
|
type: "string"
|
||||||
|
example: "new-stack"
|
||||||
|
description: "If provided will rename the migrated stack"
|
||||||
StackCreateRequest:
|
StackCreateRequest:
|
||||||
type: "object"
|
type: "object"
|
||||||
required:
|
required:
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<div class="col-sm-5 col-lg-4">
|
<div class="col-sm-5 col-lg-4">
|
||||||
<select class="form-control" ng-model="$ctrl.selectedNetwork" id="container_network">
|
<select class="form-control" ng-model="$ctrl.selectedNetwork" id="container_network">
|
||||||
<option selected disabled hidden value="">Select a network</option>
|
<option selected disabled hidden value="">Select a network</option>
|
||||||
<option ng-repeat="net in $ctrl.availableNetworks" ng-value="net.Id">{{ net.Name }}</option>
|
<option ng-repeat="net in $ctrl.availableNetworks | orderBy: 'Name'" ng-value="net.Id">{{ net.Name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-1">
|
<div class="col-sm-1">
|
||||||
|
|
|
@ -68,8 +68,8 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<a ui-sref="docker.nodes.node({id: item.Id})" ng-if="$ctrl.accessToNodeDetails">{{ item.Hostname }}</a>
|
<a ui-sref="docker.nodes.node({id: item.Id})" ng-if="$ctrl.accessToNodeDetails">{{ item.Name || item.Hostname }}</a>
|
||||||
<span ng-if="!$ctrl.accessToNodeDetails">{{ item.Hostname }}</span>
|
<span ng-if="!$ctrl.accessToNodeDetails">{{ item.Name || item.Hostname }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.Role }}</td>
|
<td>{{ item.Role }}</td>
|
||||||
<td>{{ item.CPUs / 1000000000 }}</td>
|
<td>{{ item.CPUs / 1000000000 }}</td>
|
||||||
|
|
|
@ -309,7 +309,7 @@
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<select class="form-control" ng-model="config.HostConfig.NetworkMode" id="container_network" ng-change="resetNetworkConfig()">
|
<select class="form-control" ng-model="config.HostConfig.NetworkMode" id="container_network" ng-change="resetNetworkConfig()">
|
||||||
<option selected disabled hidden value="">Select a network</option>
|
<option selected disabled hidden value="">Select a network</option>
|
||||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
<option ng-repeat="net in availableNetworks | orderBy: 'Name'" ng-value="net.Name">{{ net.Name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -354,7 +354,7 @@
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<select class="form-control" ng-model="formValues.Network">
|
<select class="form-control" ng-model="formValues.Network">
|
||||||
<option selected disabled hidden value="">Select a network</option>
|
<option selected disabled hidden value="">Select a network</option>
|
||||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
<option ng-repeat="net in availableNetworks | orderBy: 'Name'" ng-value="net.Name">{{ net.Name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-2"></div>
|
<div class="col-sm-2"></div>
|
||||||
|
|
|
@ -84,7 +84,7 @@
|
||||||
<div class="node_info">
|
<div class="node_info">
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<b>{{ node.Hostname }}</b>
|
<b>{{ node.Name || node.Hostname }}</b>
|
||||||
<span class="node_platform">
|
<span class="node_platform">
|
||||||
<i class="fab fa-linux" aria-hidden="true" ng-if="node.PlatformOS === 'linux'"></i>
|
<i class="fab fa-linux" aria-hidden="true" ng-if="node.PlatformOS === 'linux'"></i>
|
||||||
<i class="fab fa-windows" aria-hidden="true" ng-if="node.PlatformOS === 'windows'"></i>
|
<i class="fab fa-windows" aria-hidden="true" ng-if="node.PlatformOS === 'windows'"></i>
|
||||||
|
@ -97,7 +97,7 @@
|
||||||
<div><span class="label label-{{ node.Status | nodestatusbadge }}">{{ node.Status }}</span></div>
|
<div><span class="label label-{{ node.Status | nodestatusbadge }}">{{ node.Status }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tasks">
|
<div class="tasks">
|
||||||
<div class="task task_{{ task.Status.State | visualizerTask }}" style="border: 2px solid {{ task.ServiceId | visualizerTaskBorderColor }}" ng-repeat="task in node.Tasks | filter: (state.DisplayOnlyRunningTasks || '') && { Status: { State: 'running' } }">
|
<div class="task task_{{ task.Status.State | visualizerTask }}" style="border: 2px solid {{ task.ServiceId | visualizerTaskBorderColor }}" ng-repeat="task in node.Tasks | orderBy: 'ServiceName' | filter: (state.DisplayOnlyRunningTasks || '') && { Status: { State: 'running' } }">
|
||||||
<div class="service_name">{{ task.ServiceName }}</div>
|
<div class="service_name">{{ task.ServiceName }}</div>
|
||||||
<div>Image: {{ task.Spec.ContainerSpec.Image | hideshasum }}</div>
|
<div>Image: {{ task.Spec.ContainerSpec.Image | hideshasum }}</div>
|
||||||
<div>Status: {{ task.Status.State }}</div>
|
<div>Status: {{ task.Status.State }}</div>
|
||||||
|
|
|
@ -287,8 +287,8 @@ angular.module('portainer.app', [])
|
||||||
};
|
};
|
||||||
|
|
||||||
var stackCreation = {
|
var stackCreation = {
|
||||||
name: 'portainer.stacks.new',
|
name: 'portainer.newstack',
|
||||||
url: '/new',
|
url: '/newstack',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
templateUrl: 'app/portainer/views/stacks/create/createstack.html',
|
templateUrl: 'app/portainer/views/stacks/create/createstack.html',
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.stacks.new">
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.newstack">
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack
|
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
angular.module('portainer.app').controller('EndpointListController', [
|
||||||
|
function EndpointListController() {
|
||||||
|
var ctrl = this;
|
||||||
|
ctrl.state = {
|
||||||
|
textFilter: '',
|
||||||
|
filteredEndpoints: []
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.$onChanges = $onChanges;
|
||||||
|
ctrl.onFilterChanged = onFilterChanged;
|
||||||
|
|
||||||
|
function $onChanges(changesObj) {
|
||||||
|
handleEndpointsChange(changesObj.endpoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEndpointsChange(endpoints) {
|
||||||
|
if (!endpoints) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!endpoints.currentValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFilterChanged() {
|
||||||
|
var filterValue = ctrl.state.textFilter;
|
||||||
|
ctrl.state.filteredEndpoints = filterEndpoints(
|
||||||
|
ctrl.endpoints,
|
||||||
|
filterValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterEndpoints(endpoints, filterValue) {
|
||||||
|
if (!endpoints || !endpoints.length || !filterValue) {
|
||||||
|
return endpoints;
|
||||||
|
}
|
||||||
|
var keywords = filterValue.split(' ');
|
||||||
|
return _.filter(endpoints, function(endpoint) {
|
||||||
|
var statusString = convertStatusToString(endpoint.Status);
|
||||||
|
return _.every(keywords, function(keyword) {
|
||||||
|
var lowerCaseKeyword = keyword.toLowerCase();
|
||||||
|
return (
|
||||||
|
_.includes(endpoint.Name.toLowerCase(), lowerCaseKeyword) ||
|
||||||
|
_.includes(endpoint.GroupName.toLowerCase(), lowerCaseKeyword) ||
|
||||||
|
_.some(endpoint.Tags, function(tag) {
|
||||||
|
return _.includes(tag.toLowerCase(), lowerCaseKeyword);
|
||||||
|
}) ||
|
||||||
|
_.includes(statusString, keyword)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertStatusToString(status) {
|
||||||
|
return status === 1 ? 'up' : 'down';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
|
@ -1,10 +1,6 @@
|
||||||
angular.module('portainer.app').component('endpointList', {
|
angular.module('portainer.app').component('endpointList', {
|
||||||
templateUrl: 'app/portainer/components/endpoint-list/endpointList.html',
|
templateUrl: 'app/portainer/components/endpoint-list/endpointList.html',
|
||||||
controller: function() {
|
controller: 'EndpointListController',
|
||||||
this.state = {
|
|
||||||
textFilter: ''
|
|
||||||
};
|
|
||||||
},
|
|
||||||
bindings: {
|
bindings: {
|
||||||
titleText: '@',
|
titleText: '@',
|
||||||
titleIcon: '@',
|
titleIcon: '@',
|
||||||
|
|
|
@ -16,12 +16,17 @@
|
||||||
|
|
||||||
<div class="searchBar">
|
<div class="searchBar">
|
||||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search by name, group, tag..." auto-focus>
|
<input
|
||||||
|
type="text"
|
||||||
|
class="searchInput"
|
||||||
|
ng-model="$ctrl.state.textFilter"
|
||||||
|
ng-change="$ctrl.onFilterChanged()"
|
||||||
|
placeholder="Search by name, group, tag, status..." auto-focus>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="blocklist">
|
<div class="blocklist">
|
||||||
<endpoint-item
|
<endpoint-item
|
||||||
ng-repeat="endpoint in $ctrl.endpoints | filter:$ctrl.state.textFilter"
|
ng-repeat="endpoint in $ctrl.state.filteredEndpoints"
|
||||||
model="endpoint"
|
model="endpoint"
|
||||||
on-select="$ctrl.dashboardAction"
|
on-select="$ctrl.dashboardAction"
|
||||||
on-edit="$ctrl.editAction"
|
on-edit="$ctrl.editAction"
|
||||||
|
@ -30,7 +35,7 @@
|
||||||
<div ng-if="!$ctrl.endpoints" class="text-center text-muted">
|
<div ng-if="!$ctrl.endpoints" class="text-center text-muted">
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="($ctrl.endpoints | filter:$ctrl.state.textFilter).length === 0" class="text-center text-muted">
|
<div ng-if="!$ctrl.state.filteredEndpoints.length" class="text-center text-muted">
|
||||||
No endpoint available.
|
No endpoint available.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
angular.module('portainer.app').controller('StackDuplicationFormController', [
|
||||||
|
'Notifications',
|
||||||
|
function StackDuplicationFormController(Notifications) {
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
ctrl.state = {
|
||||||
|
duplicationInProgress: false,
|
||||||
|
migrationInProgress: false
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.formValues = {
|
||||||
|
endpoint: null,
|
||||||
|
newName: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.isFormValidForDuplication = isFormValidForDuplication;
|
||||||
|
ctrl.isFormValidForMigration = isFormValidForMigration;
|
||||||
|
ctrl.duplicateStack = duplicateStack;
|
||||||
|
ctrl.migrateStack = migrateStack;
|
||||||
|
ctrl.isMigrationButtonDisabled = isMigrationButtonDisabled;
|
||||||
|
|
||||||
|
function isFormValidForMigration() {
|
||||||
|
return ctrl.formValues.endpoint && ctrl.formValues.endpoint.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFormValidForDuplication() {
|
||||||
|
return isFormValidForMigration() && ctrl.formValues.newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function duplicateStack() {
|
||||||
|
if (!ctrl.formValues.newName) {
|
||||||
|
Notifications.error(
|
||||||
|
'Failure',
|
||||||
|
null,
|
||||||
|
'Stack name is required for duplication'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctrl.state.duplicationInProgress = true;
|
||||||
|
ctrl.onDuplicate({
|
||||||
|
endpointId: ctrl.formValues.endpoint.Id,
|
||||||
|
name: ctrl.formValues.newName ? ctrl.formValues.newName : undefined
|
||||||
|
})
|
||||||
|
.finally(function() {
|
||||||
|
ctrl.state.duplicationInProgress = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateStack() {
|
||||||
|
ctrl.state.migrationInProgress = true;
|
||||||
|
ctrl.onMigrate({
|
||||||
|
endpointId: ctrl.formValues.endpoint.Id,
|
||||||
|
name: ctrl.formValues.newName ? ctrl.formValues.newName : undefined
|
||||||
|
})
|
||||||
|
.finally(function() {
|
||||||
|
ctrl.state.migrationInProgress = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMigrationButtonDisabled() {
|
||||||
|
return (
|
||||||
|
!ctrl.isFormValidForMigration() ||
|
||||||
|
ctrl.state.duplicationInProgress ||
|
||||||
|
ctrl.state.migrationInProgress ||
|
||||||
|
isTargetEndpointAndCurrentEquals()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTargetEndpointAndCurrentEquals() {
|
||||||
|
return (
|
||||||
|
ctrl.formValues.endpoint &&
|
||||||
|
ctrl.formValues.endpoint.Id === ctrl.currentEndpointId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
|
@ -0,0 +1,43 @@
|
||||||
|
<div>
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Stack duplication / migration
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="small" style="margin-top: 10px;">
|
||||||
|
<p class="text-muted">
|
||||||
|
This feature allows you to duplicate or migrate this stack.
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input class="form-control" placeholder="Stack name (optional for migration)"
|
||||||
|
aria-placeholder="Stack name"
|
||||||
|
ng-model="$ctrl.formValues.newName" />
|
||||||
|
</div>
|
||||||
|
<endpoint-selector ng-if="$ctrl.endpoints && $ctrl.groups" model="$ctrl.formValues.endpoint"
|
||||||
|
endpoints="$ctrl.endpoints" groups="$ctrl.groups"></endpoint-selector>
|
||||||
|
<button class="btn btn-sm btn-primary" ng-click="$ctrl.migrateStack()"
|
||||||
|
ng-disabled="$ctrl.isMigrationButtonDisabled()"
|
||||||
|
style="margin-top: 7px; margin-left: 0;"
|
||||||
|
button-spinner="$ctrl.state.migrationInProgress">
|
||||||
|
<span ng-hide="$ctrl.state.migrationInProgress">
|
||||||
|
<i class="fa fa-long-arrow-alt-right space-right"
|
||||||
|
aria-hidden="true"></i> Migrate
|
||||||
|
</span>
|
||||||
|
<span ng-show="$ctrl.state.migrationInProgress">Migration in progress...</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
ng-click="$ctrl.duplicateStack()"
|
||||||
|
ng-disabled="!$ctrl.isFormValidForDuplication() || $ctrl.state.duplicationInProgress || $ctrl.state.migrationInProgress"
|
||||||
|
style="margin-top: 7px; margin-left: 0;"
|
||||||
|
button-spinner="$ctrl.state.duplicationInProgress">
|
||||||
|
<span ng-hide="$ctrl.state.duplicationInProgress">
|
||||||
|
<i class="fa fa-clone space-right"
|
||||||
|
aria-hidden="true"></i> Duplicate
|
||||||
|
</span>
|
||||||
|
<span ng-show="$ctrl.state.duplicationInProgress">Duplication in progress...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,12 @@
|
||||||
|
angular.module('portainer.app').component('stackDuplicationForm', {
|
||||||
|
templateUrl:
|
||||||
|
'app/portainer/components/stack-duplication-form/stack-duplication-form.html',
|
||||||
|
controller: 'StackDuplicationFormController',
|
||||||
|
bindings: {
|
||||||
|
onDuplicate: '&',
|
||||||
|
onMigrate: '&',
|
||||||
|
endpoints: '<',
|
||||||
|
groups: '<',
|
||||||
|
currentEndpointId: '<'
|
||||||
|
}
|
||||||
|
});
|
|
@ -4,6 +4,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
|
|
||||||
service.stack = function(id) {
|
service.stack = function(id) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.migrateSwarmStack = function(stack, targetEndpointId) {
|
service.migrateSwarmStack = function(stack, targetEndpointId, newName) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
EndpointProvider.setEndpointID(targetEndpointId);
|
EndpointProvider.setEndpointID(targetEndpointId);
|
||||||
|
@ -45,8 +46,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||||
deferred.reject({ msg: 'Target endpoint is located in the same Swarm cluster as the current endpoint', err: null });
|
deferred.reject({ msg: 'Target endpoint is located in the same Swarm cluster as the current endpoint', err: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
return Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, SwarmID: swarm.Id, Name: newName }).$promise;
|
||||||
return Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, SwarmID: swarm.Id }).$promise;
|
|
||||||
})
|
})
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
|
@ -61,12 +61,12 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.migrateComposeStack = function(stack, targetEndpointId) {
|
service.migrateComposeStack = function(stack, targetEndpointId, newName) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
EndpointProvider.setEndpointID(targetEndpointId);
|
EndpointProvider.setEndpointID(targetEndpointId);
|
||||||
|
|
||||||
Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId }).$promise
|
Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, Name: newName }).$promise
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
})
|
})
|
||||||
|
@ -258,8 +258,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
SwarmService.swarm()
|
SwarmService.swarm()
|
||||||
.then(function success(data) {
|
.then(function success(swarm) {
|
||||||
var swarm = data;
|
|
||||||
var payload = {
|
var payload = {
|
||||||
Name: name,
|
Name: name,
|
||||||
SwarmID: swarm.Id,
|
SwarmID: swarm.Id,
|
||||||
|
@ -321,5 +320,10 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.duplicateStack = function duplicateStack(name, stackFileContent, env, endpointId, type) {
|
||||||
|
var action = type === 1 ? service.createSwarmStackFromFileContent : service.createComposeStackFromFileContent;
|
||||||
|
return action(name, stackFileContent, env, endpointId);
|
||||||
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -46,31 +46,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !stack-details -->
|
<!-- !stack-details -->
|
||||||
<!-- stack-migration -->
|
<stack-duplication-form
|
||||||
<div ng-if="!state.externalStack && endpoints.length > 0">
|
ng-if="!state.externalStack && endpoints.length > 0"
|
||||||
<div class="col-sm-12 form-section-title">
|
endpoints="endpoints"
|
||||||
Stack migration
|
groups="groups"
|
||||||
</div>
|
current-endpoint-id="currentEndpointId"
|
||||||
<div class="form-group">
|
on-duplicate="duplicateStack(name, endpointId)"
|
||||||
<span class="small" style="margin-top: 10px;">
|
on-migrate="migrateStack(name, endpointId)"
|
||||||
<p class="text-muted">
|
>
|
||||||
This feature allows you to migrate this stack to an alternate compatible endpoint.
|
</stack-duplication-form>
|
||||||
</p>
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<endpoint-selector ng-if="endpoints && groups"
|
|
||||||
model="formValues.Endpoint"
|
|
||||||
endpoints="endpoints"
|
|
||||||
groups="groups"
|
|
||||||
></endpoint-selector>
|
|
||||||
<button class="btn btn-sm btn-primary" ng-click="migrateStack()" ng-disabled="!formValues.Endpoint || state.migrationInProgress" style="margin-top: 7px; margin-left: 0;" button-spinner="state.migrationInProgress">
|
|
||||||
<span ng-hide="state.migrationInProgress"><i class="fa fa-long-arrow-alt-right space-right" aria-hidden="true"></i> Migrate</span>
|
|
||||||
<span ng-show="state.migrationInProgress">Migration in progress...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !stack-migration -->
|
|
||||||
</div>
|
</div>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
<!-- !tab-info -->
|
<!-- !tab-info -->
|
||||||
|
|
|
@ -14,24 +14,47 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
|
||||||
Endpoint: null
|
Endpoint: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.duplicateStack = function duplicateStack(name, endpointId) {
|
||||||
|
var stack = $scope.stack;
|
||||||
|
var env = FormHelper.removeInvalidEnvVars(stack.Env);
|
||||||
|
EndpointProvider.setEndpointID(endpointId);
|
||||||
|
|
||||||
|
return StackService.duplicateStack(name, $scope.stackFileContent, env, endpointId, stack.Type)
|
||||||
|
.then(onDuplicationSuccess)
|
||||||
|
.catch(notifyOnError);
|
||||||
|
|
||||||
|
function onDuplicationSuccess() {
|
||||||
|
Notifications.success('Stack successfully duplicated');
|
||||||
|
$state.go('portainer.stacks', {}, { reload: true });
|
||||||
|
EndpointProvider.setEndpointID(stack.EndpointId);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyOnError(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to duplicate stack');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
$scope.showEditor = function() {
|
$scope.showEditor = function() {
|
||||||
$scope.state.showEditorTab = true;
|
$scope.state.showEditorTab = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.migrateStack = function() {
|
$scope.migrateStack = function (name, endpointId) {
|
||||||
ModalService.confirm({
|
return $q(function (resolve) {
|
||||||
title: 'Are you sure?',
|
ModalService.confirm({
|
||||||
message: 'This action will deploy a new instance of this stack on the target endpoint, please note that this does NOT relocate the content of any persistent volumes that may be attached to this stack.',
|
title: 'Are you sure?',
|
||||||
buttons: {
|
message: 'This action will deploy a new instance of this stack on the target endpoint, please note that this does NOT relocate the content of any persistent volumes that may be attached to this stack.',
|
||||||
confirm: {
|
buttons: {
|
||||||
label: 'Migrate',
|
confirm: {
|
||||||
className: 'btn-danger'
|
label: 'Migrate',
|
||||||
|
className: 'btn-danger'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
callback: function onConfirm(confirmed) {
|
||||||
|
if (!confirmed) { return resolve(); }
|
||||||
|
return resolve(migrateStack(name, endpointId));
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
callback: function onConfirm(confirmed) {
|
|
||||||
if(!confirmed) { return; }
|
|
||||||
migrateStack();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,9 +68,9 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function migrateStack() {
|
function migrateStack(name, endpointId) {
|
||||||
var stack = $scope.stack;
|
var stack = $scope.stack;
|
||||||
var targetEndpointId = $scope.formValues.Endpoint.Id;
|
var targetEndpointId = endpointId;
|
||||||
|
|
||||||
var migrateRequest = StackService.migrateSwarmStack;
|
var migrateRequest = StackService.migrateSwarmStack;
|
||||||
if (stack.Type === 2) {
|
if (stack.Type === 2) {
|
||||||
|
@ -58,13 +81,13 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
|
||||||
// The EndpointID property is not available for these stacks, we can pass
|
// The EndpointID property is not available for these stacks, we can pass
|
||||||
// the current endpoint identifier as a part of the migrate request. It will be used if
|
// the current endpoint identifier as a part of the migrate request. It will be used if
|
||||||
// the EndpointID property is not defined on the stack.
|
// the EndpointID property is not defined on the stack.
|
||||||
var endpointId = EndpointProvider.endpointID();
|
var originalEndpointId = EndpointProvider.endpointID();
|
||||||
if (stack.EndpointId === 0) {
|
if (stack.EndpointId === 0) {
|
||||||
stack.EndpointId = endpointId;
|
stack.EndpointId = originalEndpointId;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.state.migrationInProgress = true;
|
$scope.state.migrationInProgress = true;
|
||||||
migrateRequest(stack, targetEndpointId)
|
return migrateRequest(stack, targetEndpointId, name)
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
Notifications.success('Stack successfully migrated', stack.Name);
|
Notifications.success('Stack successfully migrated', stack.Name);
|
||||||
$state.go('portainer.stacks', {}, {reload: true});
|
$state.go('portainer.stacks', {}, {reload: true});
|
||||||
|
@ -134,7 +157,6 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
|
||||||
|
|
||||||
function loadStack(id) {
|
function loadStack(id) {
|
||||||
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
|
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
|
||||||
var endpointId = EndpointProvider.endpointID();
|
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
stack: StackService.stack(id),
|
stack: StackService.stack(id),
|
||||||
|
@ -143,9 +165,7 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var stack = data.stack;
|
var stack = data.stack;
|
||||||
$scope.endpoints = data.endpoints.filter(function(endpoint) {
|
$scope.endpoints = data.endpoints;
|
||||||
return endpoint.Id !== endpointId;
|
|
||||||
});
|
|
||||||
$scope.groups = data.groups;
|
$scope.groups = data.groups;
|
||||||
$scope.stack = stack;
|
$scope.stack = stack;
|
||||||
|
|
||||||
|
@ -256,6 +276,7 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
|
||||||
var stackName = $transition$.params().name;
|
var stackName = $transition$.params().name;
|
||||||
$scope.stackName = stackName;
|
$scope.stackName = stackName;
|
||||||
var external = $transition$.params().external;
|
var external = $transition$.params().external;
|
||||||
|
$scope.currentEndpointId = EndpointProvider.endpointID();
|
||||||
|
|
||||||
if (external === 'true') {
|
if (external === 'true') {
|
||||||
$scope.state.externalStack = true;
|
$scope.state.externalStack = true;
|
||||||
|
|
|
@ -111,7 +111,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_network" class="col-sm-2 control-label text-left">Network</label>
|
<label for="container_network" class="col-sm-2 control-label text-left">Network</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<select class="form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
|
<select class="form-control" ng-options="net.Name for net in availableNetworks | orderBy: 'Name'" ng-model="formValues.network">
|
||||||
<option disabled hidden value="">Select a network</option>
|
<option disabled hidden value="">Select a network</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue