mirror of https://github.com/portainer/portainer
* feat(stack): add the ability for an administrator user to manage orphaned stacks (#4397) * feat(stack): apply small font size to the information text of associate (#4397) Co-authored-by: Simon Meng <simon.meng@portainer.io>pull/4444/merge
parent
eae2f5c9fc
commit
26ead28d7b
|
@ -52,6 +52,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/stacks/{id}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/stacks/{id}/associate",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.stackAssociate))).Methods(http.MethodPut)
|
||||
h.Handle("/stacks/{id}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/stacks/{id}/file",
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PUT request on /api/stacks/:id/associate?endpointId=<endpointId>&swarmId=<swarmId>&orphanedRunning=<orphanedRunning>
|
||||
func (handler *Handler) stackAssociate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
||||
}
|
||||
|
||||
swarmId, err := request.RetrieveQueryParameter(r, "swarmId", true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: swarmId", err}
|
||||
}
|
||||
|
||||
orphanedRunning, err := request.RetrieveBooleanQueryParameter(r, "orphanedRunning", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: orphanedRunning", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
|
||||
}
|
||||
|
||||
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
resourceControl.ResourceID = fmt.Sprintf("%d_%s", endpointID, stack.Name)
|
||||
|
||||
err = handler.DataStore.ResourceControl().UpdateResourceControl(resourceControl.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control changes inside the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
stack.EndpointID = portainer.EndpointID(endpointID)
|
||||
stack.SwarmID = swarmId
|
||||
|
||||
if orphanedRunning {
|
||||
stack.Status = portainer.StackStatusActive
|
||||
} else {
|
||||
stack.Status = portainer.StackStatusInactive
|
||||
}
|
||||
|
||||
stack.CreationDate = time.Now().Unix()
|
||||
stack.CreatedBy = user.Username
|
||||
stack.UpdateDate = 0
|
||||
stack.UpdatedBy = ""
|
||||
|
||||
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
|
||||
}
|
||||
|
||||
stack.ResourceControl = resourceControl
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
|
@ -58,42 +58,42 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
||||
// The EndpointID property is not available for these stacks, this API endpoint
|
||||
// can use the optional EndpointID query parameter to set a valid endpoint identifier to be
|
||||
// used in the context of this request.
|
||||
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
||||
}
|
||||
endpointIdentifier := stack.EndpointID
|
||||
if endpointID != 0 {
|
||||
endpointIdentifier = portainer.EndpointID(endpointID)
|
||||
|
||||
isOrphaned := portainer.EndpointID(endpointID) != stack.EndpointID
|
||||
|
||||
if isOrphaned && !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to remove orphaned stack", errors.New("Permission denied to remove orphaned stack")}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointIdentifier)
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
||||
if !isOrphaned {
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.deleteStack(stack, endpoint)
|
||||
|
|
|
@ -46,34 +46,38 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
|
||||
|
||||
if endpoint != nil {
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
|
||||
}
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
@ -8,7 +9,6 @@ import (
|
|||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
@ -40,38 +40,42 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
if endpoint != nil {
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
|
||||
}
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
stack.ResourceControl = resourceControl
|
||||
if resourceControl != nil {
|
||||
stack.ResourceControl = resourceControl
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package stacks
|
||||
|
||||
import (
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
@ -12,8 +13,9 @@ import (
|
|||
)
|
||||
|
||||
type stackListOperationFilters struct {
|
||||
SwarmID string `json:"SwarmID"`
|
||||
EndpointID int `json:"EndpointID"`
|
||||
SwarmID string `json:"SwarmID"`
|
||||
EndpointID int `json:"EndpointID"`
|
||||
IncludeOrphanedStacks bool `json:"IncludeOrphanedStacks"`
|
||||
}
|
||||
|
||||
// @id StackList
|
||||
|
@ -37,11 +39,16 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: filters", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
|
||||
}
|
||||
|
||||
stacks, err := handler.DataStore.Stack().Stacks()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err}
|
||||
}
|
||||
stacks = filterStacks(stacks, &filters)
|
||||
stacks = filterStacks(stacks, &filters, endpoints)
|
||||
|
||||
resourceControls, err := handler.DataStore.ResourceControl().ResourceControls()
|
||||
if err != nil {
|
||||
|
@ -56,6 +63,10 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
|
|||
stacks = authorization.DecorateStacks(stacks, resourceControls)
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
if filters.IncludeOrphanedStacks {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access orphaned stacks", httperrors.ErrUnauthorized}
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user information from the database", err}
|
||||
|
@ -72,13 +83,20 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
|
|||
return response.JSON(w, stacks)
|
||||
}
|
||||
|
||||
func filterStacks(stacks []portainer.Stack, filters *stackListOperationFilters) []portainer.Stack {
|
||||
func filterStacks(stacks []portainer.Stack, filters *stackListOperationFilters, endpoints []portainer.Endpoint) []portainer.Stack {
|
||||
if filters.EndpointID == 0 && filters.SwarmID == "" {
|
||||
return stacks
|
||||
}
|
||||
|
||||
filteredStacks := make([]portainer.Stack, 0, len(stacks))
|
||||
for _, stack := range stacks {
|
||||
if filters.IncludeOrphanedStacks && isOrphanedStack(stack, endpoints) {
|
||||
if (stack.Type == portainer.DockerComposeStack && filters.SwarmID == "") || (stack.Type == portainer.DockerSwarmStack && filters.SwarmID != "") {
|
||||
filteredStacks = append(filteredStacks, stack)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if stack.Type == portainer.DockerComposeStack && stack.EndpointID == portainer.EndpointID(filters.EndpointID) {
|
||||
filteredStacks = append(filteredStacks, stack)
|
||||
}
|
||||
|
@ -89,3 +107,13 @@ func filterStacks(stacks []portainer.Stack, filters *stackListOperationFilters)
|
|||
|
||||
return filteredStacks
|
||||
}
|
||||
|
||||
func isOrphanedStack(stack portainer.Stack, endpoints []portainer.Endpoint) bool {
|
||||
for _, endpoint := range endpoints {
|
||||
if stack.EndpointID == endpoint.ID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -456,7 +456,7 @@ angular.module('portainer.docker', ['portainer.app']).config([
|
|||
|
||||
var stack = {
|
||||
name: 'docker.stacks.stack',
|
||||
url: '/:name?id&type&external',
|
||||
url: '/:name?id&type®ular&external&orphaned&orphanedRunning',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: '~Portainer/views/stacks/edit/stack.html',
|
||||
|
|
|
@ -8,5 +8,6 @@ angular.module('portainer.app').component('porAccessControlForm', {
|
|||
// Optional. An existing resource control object that will be used to set
|
||||
// the default values of the component.
|
||||
resourceControl: '<',
|
||||
hideTitle: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
<div ng-if="!$ctrl.hideTitle" class="col-sm-12 form-section-title">
|
||||
Access control
|
||||
</div>
|
||||
<!-- access-control-switch -->
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
</div>
|
||||
<div class="menuContent">
|
||||
<div>
|
||||
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
|
||||
<input id="setting_all_orphaned_stacks" type="checkbox" ng-model="$ctrl.settings.allOrphanedStacks" ng-change="$ctrl.onSettingsAllOrphanedStacksChange()" />
|
||||
<label for="setting_all_orphaned_stacks">Show all orphaned stacks</label>
|
||||
</div>
|
||||
<div class="md-checkbox">
|
||||
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
|
||||
<label for="setting_auto_refresh">Auto refresh</label>
|
||||
|
@ -151,12 +155,26 @@
|
|||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="!$ctrl.allowSelection(item)" />
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.stacks.stack({ name: item.Name, id: item.Id, type: item.Type, external: item.External })">{{ item.Name }}</a>
|
||||
<a
|
||||
ng-if="!$ctrl.offlineMode"
|
||||
ui-sref="docker.stacks.stack({ name: item.Name, id: item.Id, type: item.Type, regular: item.Regular, external: item.External, orphaned: item.Orphaned, orphanedRunning: item.OrphanedRunning })"
|
||||
>{{ item.Name }}</a
|
||||
>
|
||||
<span ng-if="$ctrl.offlineMode">{{ item.Name }}</span>
|
||||
<span ng-if="item.Status == 2" style="margin-left: 10px;" class="label label-warning image-tag space-left">Inactive</span>
|
||||
<span ng-if="item.Regular && item.Status == 2" style="margin-left: 10px;" class="label label-warning image-tag space-left">Inactive</span>
|
||||
</td>
|
||||
<td>{{ item.Type === 1 ? 'Swarm' : 'Compose' }}</td>
|
||||
<td>
|
||||
<span
|
||||
ng-if="item.Orphaned"
|
||||
class="interactive"
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="bottom"
|
||||
tooltip-class="portainer-tooltip"
|
||||
uib-tooltip="This stack was created inside an endpoint that is no longer registered inside Portainer."
|
||||
>
|
||||
Orphaned <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-left: 2px;"></i>
|
||||
</span>
|
||||
<span
|
||||
ng-if="item.External"
|
||||
class="interactive"
|
||||
|
@ -167,7 +185,7 @@
|
|||
>
|
||||
Limited <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-left: 2px;"></i>
|
||||
</span>
|
||||
<span ng-if="!item.External">Total</span>
|
||||
<span ng-if="item.Regular">Total</span>
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="item.CreationDate">{{ item.CreationDate | getisodatefromtimestamp }} {{ item.CreatedBy ? 'by ' + item.CreatedBy : '' }}</span>
|
||||
|
|
|
@ -47,7 +47,11 @@ angular.module('portainer.app').controller('StacksDatatableController', [
|
|||
this.applyFilters = applyFilters.bind(this);
|
||||
function applyFilters(stack) {
|
||||
const { showActiveStacks, showUnactiveStacks } = this.filters.state;
|
||||
return (stack.Status === 1 && showActiveStacks) || (stack.Status === 2 && showUnactiveStacks) || stack.External;
|
||||
if (stack.Orphaned) {
|
||||
return stack.OrphanedRunning || this.settings.allOrphanedStacks;
|
||||
} else {
|
||||
return (stack.Status === 1 && showActiveStacks) || (stack.Status === 2 && showUnactiveStacks) || stack.External;
|
||||
}
|
||||
}
|
||||
|
||||
this.onFilterChange = onFilterChange.bind(this);
|
||||
|
@ -57,6 +61,10 @@ angular.module('portainer.app').controller('StacksDatatableController', [
|
|||
DatatableService.setDataTableFilters(this.tableKey, this.filters);
|
||||
}
|
||||
|
||||
this.onSettingsAllOrphanedStacksChange = function () {
|
||||
DatatableService.setDataTableSettings(this.tableKey, this.settings);
|
||||
};
|
||||
|
||||
this.$onInit = function () {
|
||||
this.isAdmin = Authentication.isAdmin();
|
||||
this.setDefaults();
|
||||
|
@ -87,6 +95,7 @@ angular.module('portainer.app').controller('StacksDatatableController', [
|
|||
if (storedSettings !== null) {
|
||||
this.settings = storedSettings;
|
||||
this.settings.open = false;
|
||||
this.settings.allOrphanedStacks = this.settings.allOrphanedStacks && this.isAdmin;
|
||||
}
|
||||
this.onSettingsRepeaterChange();
|
||||
|
||||
|
|
|
@ -4,25 +4,54 @@ export function StackViewModel(data) {
|
|||
this.Id = data.Id;
|
||||
this.Type = data.Type;
|
||||
this.Name = data.Name;
|
||||
this.Checked = false;
|
||||
this.EndpointId = data.EndpointId;
|
||||
this.SwarmId = data.SwarmId;
|
||||
this.Env = data.Env ? data.Env : [];
|
||||
if (data.ResourceControl && data.ResourceControl.Id !== 0) {
|
||||
this.ResourceControl = new ResourceControlViewModel(data.ResourceControl);
|
||||
}
|
||||
this.External = false;
|
||||
this.Status = data.Status;
|
||||
this.CreationDate = data.CreationDate;
|
||||
this.CreatedBy = data.CreatedBy;
|
||||
this.UpdateDate = data.UpdateDate;
|
||||
this.UpdatedBy = data.UpdatedBy;
|
||||
|
||||
this.Regular = true;
|
||||
this.External = false;
|
||||
this.Orphaned = false;
|
||||
this.Checked = false;
|
||||
}
|
||||
|
||||
export function ExternalStackViewModel(name, type, creationDate) {
|
||||
this.Name = name;
|
||||
this.Type = type;
|
||||
this.External = true;
|
||||
this.Checked = false;
|
||||
this.CreationDate = creationDate;
|
||||
|
||||
this.Regular = false;
|
||||
this.External = true;
|
||||
this.Orphaned = false;
|
||||
this.Checked = false;
|
||||
}
|
||||
|
||||
export function OrphanedStackViewModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.Type = data.Type;
|
||||
this.Name = data.Name;
|
||||
this.EndpointId = data.EndpointId;
|
||||
this.SwarmId = data.SwarmId;
|
||||
this.Env = data.Env ? data.Env : [];
|
||||
if (data.ResourceControl && data.ResourceControl.Id !== 0) {
|
||||
this.ResourceControl = new ResourceControlViewModel(data.ResourceControl);
|
||||
}
|
||||
this.Status = data.Status;
|
||||
this.CreationDate = data.CreationDate;
|
||||
this.CreatedBy = data.CreatedBy;
|
||||
this.UpdateDate = data.UpdateDate;
|
||||
this.UpdatedBy = data.UpdatedBy;
|
||||
|
||||
this.Regular = false;
|
||||
this.External = false;
|
||||
this.Orphaned = true;
|
||||
this.OrphanedRunning = false;
|
||||
this.Checked = false;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ angular.module('portainer.app').factory('Stack', [
|
|||
query: { method: 'GET', isArray: true },
|
||||
create: { method: 'POST', ignoreLoadingBar: true },
|
||||
update: { method: 'PUT', params: { id: '@id' }, ignoreLoadingBar: true },
|
||||
associate: { method: 'PUT', params: { id: '@id', swarmId: '@swarmId', endpointId: '@endpointId', orphanedRunning: '@orphanedRunning', action: 'associate' } },
|
||||
remove: { method: 'DELETE', params: { id: '@id', external: '@external', endpointId: '@endpointId' } },
|
||||
getStackFile: { method: 'GET', params: { id: '@id', action: 'file' } },
|
||||
migrate: { method: 'POST', params: { id: '@id', action: 'migrate', endpointId: '@endpointId' }, ignoreLoadingBar: true },
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import _ from 'lodash-es';
|
||||
import { StackViewModel } from '../../models/stack';
|
||||
import { StackViewModel, OrphanedStackViewModel } from '../../models/stack';
|
||||
|
||||
angular.module('portainer.app').factory('StackService', [
|
||||
'$q',
|
||||
|
@ -88,15 +88,15 @@ angular.module('portainer.app').factory('StackService', [
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.stacks = function (compose, swarm, endpointId) {
|
||||
service.stacks = function (compose, swarm, endpointId, includeOrphanedStacks = false) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
var queries = [];
|
||||
if (compose) {
|
||||
queries.push(service.composeStacks(true, { EndpointID: endpointId }));
|
||||
queries.push(service.composeStacks(endpointId, true, { EndpointID: endpointId, IncludeOrphanedStacks: includeOrphanedStacks }));
|
||||
}
|
||||
if (swarm) {
|
||||
queries.push(service.swarmStacks(true));
|
||||
queries.push(service.swarmStacks(endpointId, true, { IncludeOrphanedStacks: includeOrphanedStacks }));
|
||||
}
|
||||
|
||||
$q.all(queries)
|
||||
|
@ -145,7 +145,22 @@ angular.module('portainer.app').factory('StackService', [
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.composeStacks = function (includeExternalStacks, filters) {
|
||||
service.unionStacks = function (stacks, externalStacks) {
|
||||
stacks.forEach((stack) => {
|
||||
externalStacks.forEach((externalStack) => {
|
||||
if (stack.Orphaned && stack.Name == externalStack.Name) {
|
||||
stack.OrphanedRunning = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
const result = _.unionWith(stacks, externalStacks, function (a, b) {
|
||||
return a.Name === b.Name;
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
service.composeStacks = function (endpointId, includeExternalStacks, filters) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
$q.all({
|
||||
|
@ -154,14 +169,15 @@ angular.module('portainer.app').factory('StackService', [
|
|||
})
|
||||
.then(function success(data) {
|
||||
var stacks = data.stacks.map(function (item) {
|
||||
item.External = false;
|
||||
return new StackViewModel(item);
|
||||
if (item.EndpointId == endpointId) {
|
||||
return new StackViewModel(item);
|
||||
} else {
|
||||
return new OrphanedStackViewModel(item);
|
||||
}
|
||||
});
|
||||
var externalStacks = data.externalStacks;
|
||||
|
||||
var result = _.unionWith(stacks, externalStacks, function (a, b) {
|
||||
return a.Name === b.Name;
|
||||
});
|
||||
var externalStacks = data.externalStacks;
|
||||
const result = service.unionStacks(stacks, externalStacks);
|
||||
deferred.resolve(result);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
@ -171,13 +187,13 @@ angular.module('portainer.app').factory('StackService', [
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.swarmStacks = function (includeExternalStacks) {
|
||||
service.swarmStacks = function (endpointId, includeExternalStacks, filters = {}) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
SwarmService.swarm()
|
||||
.then(function success(data) {
|
||||
var swarm = data;
|
||||
var filters = { SwarmID: swarm.Id };
|
||||
filters = { SwarmID: swarm.Id, ...filters };
|
||||
|
||||
return $q.all({
|
||||
stacks: Stack.query({ filters: filters }).$promise,
|
||||
|
@ -186,14 +202,15 @@ angular.module('portainer.app').factory('StackService', [
|
|||
})
|
||||
.then(function success(data) {
|
||||
var stacks = data.stacks.map(function (item) {
|
||||
item.External = false;
|
||||
return new StackViewModel(item);
|
||||
if (item.EndpointId == endpointId) {
|
||||
return new StackViewModel(item);
|
||||
} else {
|
||||
return new OrphanedStackViewModel(item);
|
||||
}
|
||||
});
|
||||
var externalStacks = data.externalStacks;
|
||||
|
||||
var result = _.unionWith(stacks, externalStacks, function (a, b) {
|
||||
return a.Name === b.Name;
|
||||
});
|
||||
var externalStacks = data.externalStacks;
|
||||
const result = service.unionStacks(stacks, externalStacks);
|
||||
deferred.resolve(result);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
@ -217,6 +234,34 @@ angular.module('portainer.app').factory('StackService', [
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.associate = function (stack, endpointId, orphanedRunning) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
if (stack.Type == 1) {
|
||||
SwarmService.swarm()
|
||||
.then(function success(data) {
|
||||
const swarm = data;
|
||||
return Stack.associate({ id: stack.Id, endpointId: endpointId, swarmId: swarm.Id, orphanedRunning }).$promise;
|
||||
})
|
||||
.then(function success(data) {
|
||||
deferred.resolve(data);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to associate the stack', err: err });
|
||||
});
|
||||
} else {
|
||||
Stack.associate({ id: stack.Id, endpointId: endpointId, orphanedRunning })
|
||||
.$promise.then(function success(data) {
|
||||
deferred.resolve(data);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to associate the stack', err: err });
|
||||
});
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.updateStack = function (stack, stackFile, env, prune) {
|
||||
return Stack.update({ endpointId: stack.EndpointId }, { id: stack.Id, StackFileContent: stackFile, Env: env, Prune: prune }).$promise;
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<uib-tab-heading> <i class="fa fa-th-list" aria-hidden="true"></i> Stack </uib-tab-heading>
|
||||
<div style="margin-top: 10px;">
|
||||
<!-- stack-information -->
|
||||
<div ng-if="state.externalStack">
|
||||
<div ng-if="external || orphaned">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Information
|
||||
</div>
|
||||
|
@ -25,7 +25,8 @@
|
|||
<span class="small">
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This stack was created outside of Portainer. Control over this stack is limited.
|
||||
<span ng-if="external">This stack was created outside of Portainer. Control over this stack is limited.</span>
|
||||
<span ng-if="orphaned">This stack is orphaned. You can reassociate it with the current environment using the "Associate to this endpoint" feature.</span>
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -41,7 +42,7 @@
|
|||
|
||||
<button
|
||||
authorization="PortainerStackUpdate"
|
||||
ng-if="!state.externalStack && stack.Status === 2"
|
||||
ng-if="regular && stack.Status === 2"
|
||||
ng-disabled="state.actionInProgress"
|
||||
class="btn btn-xs btn-success"
|
||||
ng-click="startStack()"
|
||||
|
@ -51,7 +52,7 @@
|
|||
</button>
|
||||
|
||||
<button
|
||||
ng-if="!state.externalStack && stack.Status === 1"
|
||||
ng-if="regular && stack.Status === 1"
|
||||
authorization="PortainerStackUpdate"
|
||||
ng-disabled="state.actionInProgress"
|
||||
class="btn btn-xs btn-danger"
|
||||
|
@ -61,13 +62,13 @@
|
|||
Stop this stack
|
||||
</button>
|
||||
|
||||
<button authorization="PortainerStackDelete" class="btn btn-xs btn-danger" ng-click="removeStack()" ng-if="!state.externalStack || stack.Type === 1">
|
||||
<button authorization="PortainerStackDelete" class="btn btn-xs btn-danger" ng-click="removeStack()" ng-if="!external || stack.Type == 1">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>
|
||||
Delete this stack
|
||||
</button>
|
||||
|
||||
<button
|
||||
ng-if="!state.externalStack && stackFileContent"
|
||||
ng-if="regular && stackFileContent"
|
||||
class="btn btn-primary btn-xs"
|
||||
ui-sref="docker.templates.custom.new({fileContent: stackFileContent, type: stack.Type})"
|
||||
>
|
||||
|
@ -77,8 +78,40 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !stack-details -->
|
||||
|
||||
<!-- associate -->
|
||||
<div ng-if="orphaned">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Associate to this endpoint
|
||||
</div>
|
||||
<p class="small text-muted">
|
||||
This feature allows you to reassociate this stack to the current endpoint.
|
||||
</p>
|
||||
<form class="form-horizontal">
|
||||
<por-access-control-form form-data="formValues.AccessControlData" hide-title="true"></por-access-control-form>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress"
|
||||
ng-click="associateStack()"
|
||||
button-spinner="state.actionInProgress"
|
||||
style="margin-left: -5px;"
|
||||
>
|
||||
<i class="fa fa-sync" aria-hidden="true" style="margin-right: 3px;"></i>
|
||||
<span ng-hide="state.actionInProgress">Associate</span>
|
||||
<span ng-show="state.actionInProgress">Association in progress...</span>
|
||||
</button>
|
||||
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- !associate -->
|
||||
|
||||
<stack-duplication-form
|
||||
ng-if="!state.externalStack && endpoints.length > 0"
|
||||
ng-if="regular && endpoints.length > 0"
|
||||
endpoints="endpoints"
|
||||
groups="groups"
|
||||
current-endpoint-id="currentEndpointId"
|
||||
|
@ -91,7 +124,7 @@
|
|||
</uib-tab>
|
||||
<!-- !tab-info -->
|
||||
<!-- tab-file -->
|
||||
<uib-tab index="1" select="showEditor()" ng-if="!state.externalStack">
|
||||
<uib-tab index="1" select="showEditor()" ng-if="!external">
|
||||
<uib-tab-heading> <i class="fa fa-pencil-alt space-right" aria-hidden="true"></i> Editor </uib-tab-heading>
|
||||
<form class="form-horizontal" ng-if="state.showEditorTab" style="margin-top: 10px;">
|
||||
<div class="form-group">
|
||||
|
@ -108,6 +141,7 @@
|
|||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<code-editor
|
||||
read-only="orphaned"
|
||||
identifier="stack-editor"
|
||||
placeholder="# Define or paste the content of your docker-compose file here"
|
||||
yml="true"
|
||||
|
@ -173,7 +207,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
ng-disabled="state.actionInProgress || stack.Status === 2 || !stackFileContent"
|
||||
ng-disabled="state.actionInProgress || stack.Status === 2 || !stackFileContent || orphaned"
|
||||
ng-click="deployStack()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
|
@ -192,7 +226,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="containers">
|
||||
<div class="row" ng-if="containers && (!orphaned || orphanedRunning)">
|
||||
<div class="col-sm-12">
|
||||
<containers-datatable
|
||||
title-text="Containers"
|
||||
|
@ -206,7 +240,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="services">
|
||||
<div class="row" ng-if="services && (!orphaned || orphanedRunning)">
|
||||
<div class="col-sm-12">
|
||||
<services-datatable
|
||||
title-text="Services"
|
||||
|
@ -226,6 +260,6 @@
|
|||
</div>
|
||||
|
||||
<!-- access-control-panel -->
|
||||
<por-access-control-panel ng-if="stack" resource-id="stack.EndpointId + '_' + stack.Name" resource-control="stack.ResourceControl" resource-type="'stack'">
|
||||
<por-access-control-panel ng-if="stack && !orphaned" resource-id="stack.EndpointId + '_' + stack.Name" resource-control="stack.ResourceControl" resource-type="'stack'">
|
||||
</por-access-control-panel>
|
||||
<!-- !access-control-panel -->
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
|
||||
angular.module('portainer.app').controller('StackController', [
|
||||
'$async',
|
||||
'$q',
|
||||
|
@ -19,6 +21,8 @@ angular.module('portainer.app').controller('StackController', [
|
|||
'GroupService',
|
||||
'ModalService',
|
||||
'StackHelper',
|
||||
'ResourceControlService',
|
||||
'Authentication',
|
||||
'ContainerHelper',
|
||||
function (
|
||||
$async,
|
||||
|
@ -41,12 +45,13 @@ angular.module('portainer.app').controller('StackController', [
|
|||
GroupService,
|
||||
ModalService,
|
||||
StackHelper,
|
||||
ResourceControlService,
|
||||
Authentication,
|
||||
ContainerHelper
|
||||
) {
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
migrationInProgress: false,
|
||||
externalStack: false,
|
||||
showEditorTab: false,
|
||||
yamlError: false,
|
||||
isEditorDirty: false,
|
||||
|
@ -55,6 +60,7 @@ angular.module('portainer.app').controller('StackController', [
|
|||
$scope.formValues = {
|
||||
Prune: false,
|
||||
Endpoint: null,
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
};
|
||||
|
||||
$window.onbeforeunload = () => {
|
||||
|
@ -162,6 +168,31 @@ angular.module('portainer.app').controller('StackController', [
|
|||
});
|
||||
}
|
||||
|
||||
$scope.associateStack = function () {
|
||||
var endpointId = +$state.params.endpointId;
|
||||
var stack = $scope.stack;
|
||||
var accessControlData = $scope.formValues.AccessControlData;
|
||||
$scope.state.actionInProgress = true;
|
||||
|
||||
StackService.associate(stack, endpointId, $scope.orphanedRunning)
|
||||
.then(function success(data) {
|
||||
const resourceControl = data.ResourceControl;
|
||||
const userDetails = Authentication.getUserDetails();
|
||||
const userId = userDetails.ID;
|
||||
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Stack successfully associated', stack.Name);
|
||||
$state.go('docker.stacks');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to associate stack ' + stack.Name);
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deployStack = function () {
|
||||
var stackFile = $scope.stackFileContent;
|
||||
var env = FormHelper.removeInvalidEnvVars($scope.stack.Env);
|
||||
|
@ -391,14 +422,27 @@ angular.module('portainer.app').controller('StackController', [
|
|||
async function initView() {
|
||||
var stackName = $transition$.params().name;
|
||||
$scope.stackName = stackName;
|
||||
var external = $transition$.params().external;
|
||||
|
||||
$scope.currentEndpointId = EndpointProvider.endpointID();
|
||||
|
||||
if (external === 'true') {
|
||||
$scope.state.externalStack = true;
|
||||
const regular = $transition$.params().regular == 'true';
|
||||
$scope.regular = regular;
|
||||
|
||||
var external = $transition$.params().external == 'true';
|
||||
$scope.external = external;
|
||||
|
||||
const orphaned = $transition$.params().orphaned == 'true';
|
||||
$scope.orphaned = orphaned;
|
||||
|
||||
const orphanedRunning = $transition$.params().orphanedRunning == 'true';
|
||||
$scope.orphanedRunning = orphanedRunning;
|
||||
|
||||
if (external || (orphaned && orphanedRunning)) {
|
||||
loadExternalStack(stackName);
|
||||
} else {
|
||||
var stackId = $transition$.params().id;
|
||||
}
|
||||
|
||||
if (regular || orphaned) {
|
||||
const stackId = $transition$.params().id;
|
||||
loadStack(stackId);
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,8 @@ function StacksController($scope, $state, Notifications, StackService, ModalServ
|
|||
var endpointMode = $scope.applicationState.endpoint.mode;
|
||||
var endpointId = EndpointProvider.endpointID();
|
||||
|
||||
StackService.stacks(true, endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER', endpointId)
|
||||
const includeOrphanedStacks = Authentication.isAdmin();
|
||||
StackService.stacks(true, endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER', endpointId, includeOrphanedStacks)
|
||||
.then(function success(data) {
|
||||
var stacks = data;
|
||||
$scope.stacks = stacks;
|
||||
|
|
Loading…
Reference in New Issue