mirror of https://github.com/portainer/portainer
				
				
				
			feat(stacks): add the ability to stop a stack (#4042)
* feat(stacks): add stack status * feat(stacks): add empty start/stop handlers * feat(stacks): show start/stop button * feat(stacks): implement stack stop * feat(stacks): implement start stack * feat(stacks): filter by active/inactive stacks * fix(stacks): update authorizations for stack start/stop * feat(stacks): assign default status on create * fix(bolt): fix import * fix(stacks): show external stacks * fix(stacks): reload on stop/start * feat(stacks): confirm before stoppull/4147/head
							parent
							
								
									da143a7a22
								
							
						
					
					
						commit
						4d5836138b
					
				| 
						 | 
				
			
			@ -1,5 +1,7 @@
 | 
			
		|||
package migrator
 | 
			
		||||
 | 
			
		||||
import portainer "github.com/portainer/portainer/api"
 | 
			
		||||
 | 
			
		||||
func (m *Migrator) updateSettingsToDB24() error {
 | 
			
		||||
	legacySettings, err := m.settingsService.Settings()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -12,3 +14,21 @@ func (m *Migrator) updateSettingsToDB24() error {
 | 
			
		|||
 | 
			
		||||
	return m.settingsService.UpdateSettings(legacySettings)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Migrator) updateStacksToDB24() error {
 | 
			
		||||
	stacks, err := m.stackService.Stacks()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for idx := range stacks {
 | 
			
		||||
		stack := &stacks[idx]
 | 
			
		||||
		stack.Status = portainer.StackStatusActive
 | 
			
		||||
		err := m.stackService.UpdateStack(stack.ID, stack)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -335,6 +335,11 @@ func (m *Migrator) Migrate() error {
 | 
			
		|||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = m.updateStacksToDB24()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return m.versionService.StoreDBVersion(portainer.DBVersion)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,6 +66,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
 | 
			
		|||
		EndpointID: endpoint.ID,
 | 
			
		||||
		EntryPoint: filesystem.ComposeFileDefaultName,
 | 
			
		||||
		Env:        payload.Env,
 | 
			
		||||
		Status:     portainer.StackStatusActive,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stackFolder := strconv.Itoa(int(stack.ID))
 | 
			
		||||
| 
						 | 
				
			
			@ -151,6 +152,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
 | 
			
		|||
		EndpointID: endpoint.ID,
 | 
			
		||||
		EntryPoint: payload.ComposeFilePathInRepository,
 | 
			
		||||
		Env:        payload.Env,
 | 
			
		||||
		Status:     portainer.StackStatusActive,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
 | 
			
		||||
| 
						 | 
				
			
			@ -246,6 +248,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
 | 
			
		|||
		EndpointID: endpoint.ID,
 | 
			
		||||
		EntryPoint: filesystem.ComposeFileDefaultName,
 | 
			
		||||
		Env:        payload.Env,
 | 
			
		||||
		Status:     portainer.StackStatusActive,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stackFolder := strconv.Itoa(int(stack.ID))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,6 +62,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
 | 
			
		|||
		EndpointID: endpoint.ID,
 | 
			
		||||
		EntryPoint: filesystem.ComposeFileDefaultName,
 | 
			
		||||
		Env:        payload.Env,
 | 
			
		||||
		Status:     portainer.StackStatusActive,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stackFolder := strconv.Itoa(int(stack.ID))
 | 
			
		||||
| 
						 | 
				
			
			@ -151,6 +152,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
 | 
			
		|||
		EndpointID: endpoint.ID,
 | 
			
		||||
		EntryPoint: payload.ComposeFilePathInRepository,
 | 
			
		||||
		Env:        payload.Env,
 | 
			
		||||
		Status:     portainer.StackStatusActive,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
 | 
			
		||||
| 
						 | 
				
			
			@ -254,6 +256,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
 | 
			
		|||
		EndpointID: endpoint.ID,
 | 
			
		||||
		EntryPoint: filesystem.ComposeFileDefaultName,
 | 
			
		||||
		Env:        payload.Env,
 | 
			
		||||
		Status:     portainer.StackStatusActive,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stackFolder := strconv.Itoa(int(stack.ID))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,6 +54,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
 | 
			
		|||
		bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackFile))).Methods(http.MethodGet)
 | 
			
		||||
	h.Handle("/stacks/{id}/migrate",
 | 
			
		||||
		bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackMigrate))).Methods(http.MethodPost)
 | 
			
		||||
	h.Handle("/stacks/{id}/start",
 | 
			
		||||
		bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackStart))).Methods(http.MethodPost)
 | 
			
		||||
	h.Handle("/stacks/{id}/stop",
 | 
			
		||||
		bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackStop))).Methods(http.MethodPost)
 | 
			
		||||
	return h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
package stacks
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	httperror "github.com/portainer/libhttp/error"
 | 
			
		||||
	"github.com/portainer/libhttp/request"
 | 
			
		||||
	"github.com/portainer/libhttp/response"
 | 
			
		||||
	"github.com/portainer/portainer/api"
 | 
			
		||||
	bolterrors "github.com/portainer/portainer/api/bolt/errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// POST request on /api/stacks/:id/start
 | 
			
		||||
func (handler *Handler) stackStart(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}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if stack.Status == portainer.StackStatusActive {
 | 
			
		||||
		return &httperror.HandlerError{http.StatusBadRequest, "Stack is already active", errors.New("Stack is already active")}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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.startStack(stack, endpoint)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to stop stack", err}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stack.Status = portainer.StackStatusActive
 | 
			
		||||
	err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update stack status", err}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return response.JSON(w, stack)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (handler *Handler) startStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
 | 
			
		||||
	switch stack.Type {
 | 
			
		||||
	case portainer.DockerComposeStack:
 | 
			
		||||
		return handler.ComposeStackManager.Up(stack, endpoint)
 | 
			
		||||
	case portainer.DockerSwarmStack:
 | 
			
		||||
		return handler.SwarmStackManager.Deploy(stack, true, endpoint)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
package stacks
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	httperror "github.com/portainer/libhttp/error"
 | 
			
		||||
	"github.com/portainer/libhttp/request"
 | 
			
		||||
	"github.com/portainer/libhttp/response"
 | 
			
		||||
	"github.com/portainer/portainer/api"
 | 
			
		||||
	bolterrors "github.com/portainer/portainer/api/bolt/errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// POST request on /api/stacks/:id/stop
 | 
			
		||||
func (handler *Handler) stackStop(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}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if stack.Status == portainer.StackStatusInactive {
 | 
			
		||||
		return &httperror.HandlerError{http.StatusBadRequest, "Stack is already inactive", errors.New("Stack is already inactive")}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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.stopStack(stack, endpoint)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to stop stack", err}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stack.Status = portainer.StackStatusInactive
 | 
			
		||||
	err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update stack status", err}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return response.JSON(w, stack)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (handler *Handler) stopStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
 | 
			
		||||
	switch stack.Type {
 | 
			
		||||
	case portainer.DockerComposeStack:
 | 
			
		||||
		return handler.ComposeStackManager.Down(stack, endpoint)
 | 
			
		||||
	case portainer.DockerSwarmStack:
 | 
			
		||||
		return handler.SwarmStackManager.Remove(stack, endpoint)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -547,12 +547,16 @@ type (
 | 
			
		|||
		EntryPoint      string           `json:"EntryPoint"`
 | 
			
		||||
		Env             []Pair           `json:"Env"`
 | 
			
		||||
		ResourceControl *ResourceControl `json:"ResourceControl"`
 | 
			
		||||
		Status          StackStatus      `json:"Status"`
 | 
			
		||||
		ProjectPath     string
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier)
 | 
			
		||||
	StackID int
 | 
			
		||||
 | 
			
		||||
	// StackStatus represent a status for a stack
 | 
			
		||||
	StackStatus int
 | 
			
		||||
 | 
			
		||||
	// StackType represents the type of the stack (compose v2, stack deploy v3)
 | 
			
		||||
	StackType int
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1302,6 +1306,13 @@ const (
 | 
			
		|||
	KubernetesStack
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// StackStatus represents a status for a stack
 | 
			
		||||
const (
 | 
			
		||||
	_ StackStatus = iota
 | 
			
		||||
	StackStatusActive
 | 
			
		||||
	StackStatusInactive
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	_ TemplateType = iota
 | 
			
		||||
	// ContainerTemplate represents a container template
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -72,7 +72,7 @@
 | 
			
		|||
        <table class="table table-hover nowrap-cells">
 | 
			
		||||
          <thead>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th>
 | 
			
		||||
              <th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.state.open">
 | 
			
		||||
                <span class="md-checkbox" ng-if="!$ctrl.offlineMode" authorization="PortainerStackCreate, PortainerStackDelete">
 | 
			
		||||
                  <input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
 | 
			
		||||
                  <label for="select_all"></label>
 | 
			
		||||
| 
						 | 
				
			
			@ -82,6 +82,32 @@
 | 
			
		|||
                  <i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
 | 
			
		||||
                  <i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
 | 
			
		||||
                </a>
 | 
			
		||||
                <div>
 | 
			
		||||
                  <span uib-dropdown-toggle ng-class="['table-filter', { 'filter-active': $ctrl.filters.state.enabled }]">
 | 
			
		||||
                    Filter
 | 
			
		||||
                    <i ng-class="['fa', { 'fa-filter': !$ctrl.filters.state.enabled, 'fa-check': $ctrl.filters.state.enabled }]" aria-hidden="true"></i>
 | 
			
		||||
                  </span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="dropdown-menu" uib-dropdown-menu>
 | 
			
		||||
                  <div class="tableMenu">
 | 
			
		||||
                    <div class="menuHeader">
 | 
			
		||||
                      Filter by activity
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="menuContent">
 | 
			
		||||
                      <div class="md-checkbox">
 | 
			
		||||
                        <input id="filter_usage_activeStacks" type="checkbox" ng-model="$ctrl.filters.state.showActiveStacks" ng-change="$ctrl.onFilterChange()" />
 | 
			
		||||
                        <label for="filter_usage_activeStacks">Active stacks</label>
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <div class="md-checkbox">
 | 
			
		||||
                        <input id="filter_usage_unactiveStacks" type="checkbox" ng-model="$ctrl.filters.state.showUnactiveStacks" ng-change="$ctrl.onFilterChange()" />
 | 
			
		||||
                        <label for="filter_usage_unactiveStacks">Inactive stacks</label>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div>
 | 
			
		||||
                      <a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.state.open = false;">Close</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </th>
 | 
			
		||||
              <th>
 | 
			
		||||
                <a ng-click="$ctrl.changeOrderBy('Type')">
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +128,7 @@
 | 
			
		|||
          </thead>
 | 
			
		||||
          <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))"
 | 
			
		||||
              dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
 | 
			
		||||
              ng-class="{ active: item.Checked }"
 | 
			
		||||
            >
 | 
			
		||||
              <td>
 | 
			
		||||
| 
						 | 
				
			
			@ -112,6 +138,7 @@
 | 
			
		|||
                </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>
 | 
			
		||||
                <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>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td>{{ item.Type === 1 ? 'Swarm' : 'Compose' }}</td>
 | 
			
		||||
              <td>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,15 @@ angular.module('portainer.app').controller('StacksDatatableController', [
 | 
			
		|||
  function ($scope, $controller, DatatableService, Authentication) {
 | 
			
		||||
    angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
 | 
			
		||||
 | 
			
		||||
    this.filters = {
 | 
			
		||||
      state: {
 | 
			
		||||
        open: false,
 | 
			
		||||
        enabled: false,
 | 
			
		||||
        showActiveStacks: true,
 | 
			
		||||
        showUnactiveStacks: true,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Do not allow external items
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +26,19 @@ angular.module('portainer.app').controller('StacksDatatableController', [
 | 
			
		|||
      return !(item.External && !this.isAdmin && !this.isEndpointAdmin);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.onFilterChange = onFilterChange.bind(this);
 | 
			
		||||
    function onFilterChange() {
 | 
			
		||||
      const { showActiveStacks, showUnactiveStacks } = this.filters.state;
 | 
			
		||||
      this.filters.state.enabled = !showActiveStacks || !showUnactiveStacks;
 | 
			
		||||
      DatatableService.setDataTableFilters(this.tableKey, this.filters);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.$onInit = function () {
 | 
			
		||||
      this.isAdmin = Authentication.isAdmin();
 | 
			
		||||
      this.isEndpointAdmin = Authentication.hasAuthorizations(['EndpointResourcesAccess']);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ export function StackViewModel(data) {
 | 
			
		|||
    this.ResourceControl = new ResourceControlViewModel(data.ResourceControl);
 | 
			
		||||
  }
 | 
			
		||||
  this.External = false;
 | 
			
		||||
  this.Status = data.Status;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ExternalStackViewModel(name, type) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,8 @@ angular.module('portainer.app').factory('Stack', [
 | 
			
		|||
        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 },
 | 
			
		||||
        start: { method: 'POST', params: { id: '@id', action: 'start' } },
 | 
			
		||||
        stop: { method: 'POST', params: { id: '@id', action: 'stop' } },
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -349,6 +349,16 @@ angular.module('portainer.app').factory('StackService', [
 | 
			
		|||
      return $async(kubernetesDeployAsync, endpointId, namespace, content, compose);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    service.start = start;
 | 
			
		||||
    function start(id) {
 | 
			
		||||
      return Stack.start({ id }).$promise;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    service.stop = stop;
 | 
			
		||||
    function stop(id) {
 | 
			
		||||
      return Stack.stop({ id }).$promise;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return service;
 | 
			
		||||
  },
 | 
			
		||||
]);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,6 +37,13 @@ angular.module('portainer.app').factory('ModalService', [
 | 
			
		|||
      });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    service.confirmAsync = confirmAsync;
 | 
			
		||||
    function confirmAsync(options) {
 | 
			
		||||
      return new Promise((resolve) => {
 | 
			
		||||
        service.confirm({ ...options, callback: (confirmed) => resolve(confirmed) });
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    service.confirm = function (options) {
 | 
			
		||||
      var box = bootbox.confirm({
 | 
			
		||||
        title: options.title,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,6 +47,28 @@
 | 
			
		|||
                    Create template from stack
 | 
			
		||||
                  </a>
 | 
			
		||||
 | 
			
		||||
                  <button
 | 
			
		||||
                    authorization="PortainerStackUpdate"
 | 
			
		||||
                    ng-if="!state.externalStack && stack.Status === 2"
 | 
			
		||||
                    ng-disabled="state.actionInProgress"
 | 
			
		||||
                    class="btn btn-xs btn-success"
 | 
			
		||||
                    ng-click="startStack()"
 | 
			
		||||
                  >
 | 
			
		||||
                    <i class="fa fa-play space-right" aria-hidden="true"></i>
 | 
			
		||||
                    Start this stack
 | 
			
		||||
                  </button>
 | 
			
		||||
 | 
			
		||||
                  <button
 | 
			
		||||
                    ng-if="!state.externalStack && stack.Status === 1"
 | 
			
		||||
                    authorization="PortainerStackUpdate"
 | 
			
		||||
                    ng-disabled="state.actionInProgress"
 | 
			
		||||
                    class="btn btn-xs btn-danger"
 | 
			
		||||
                    ng-click="stopStack()"
 | 
			
		||||
                  >
 | 
			
		||||
                    <i class="fa fa-stop space-right" aria-hidden="true"></i>
 | 
			
		||||
                    Stop this stack
 | 
			
		||||
                  </button>
 | 
			
		||||
 | 
			
		||||
                  <button authorization="PortainerStackDelete" class="btn btn-xs btn-danger" ng-click="removeStack()" ng-if="!state.externalStack || stack.Type === 1">
 | 
			
		||||
                    <i class="fa fa-trash-alt space-right" aria-hidden="true"></i>
 | 
			
		||||
                    Delete this stack
 | 
			
		||||
| 
						 | 
				
			
			@ -140,7 +162,13 @@
 | 
			
		|||
                </div>
 | 
			
		||||
                <div class="form-group">
 | 
			
		||||
                  <div class="col-sm-12">
 | 
			
		||||
                    <button type="button" class="btn btn-sm btn-primary" ng-disabled="state.actionInProgress" ng-click="deployStack()" button-spinner="state.actionInProgress">
 | 
			
		||||
                    <button
 | 
			
		||||
                      type="button"
 | 
			
		||||
                      class="btn btn-sm btn-primary"
 | 
			
		||||
                      ng-disabled="state.actionInProgress || stack.Status === 2"
 | 
			
		||||
                      ng-click="deployStack()"
 | 
			
		||||
                      button-spinner="state.actionInProgress"
 | 
			
		||||
                    >
 | 
			
		||||
                      <span ng-hide="state.actionInProgress">Update the stack</span>
 | 
			
		||||
                      <span ng-show="state.actionInProgress">Deployment in progress...</span>
 | 
			
		||||
                    </button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
angular.module('portainer.app').controller('StackController', [
 | 
			
		||||
  '$async',
 | 
			
		||||
  '$q',
 | 
			
		||||
  '$scope',
 | 
			
		||||
  '$state',
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +18,7 @@ angular.module('portainer.app').controller('StackController', [
 | 
			
		|||
  'GroupService',
 | 
			
		||||
  'ModalService',
 | 
			
		||||
  function (
 | 
			
		||||
    $async,
 | 
			
		||||
    $q,
 | 
			
		||||
    $scope,
 | 
			
		||||
    $state,
 | 
			
		||||
| 
						 | 
				
			
			@ -187,6 +189,46 @@ angular.module('portainer.app').controller('StackController', [
 | 
			
		|||
      $scope.stackFileContent = cm.getValue();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    $scope.stopStack = stopStack;
 | 
			
		||||
    function stopStack() {
 | 
			
		||||
      return $async(stopStackAsync);
 | 
			
		||||
    }
 | 
			
		||||
    async function stopStackAsync() {
 | 
			
		||||
      const confirmed = await ModalService.confirmAsync({
 | 
			
		||||
        title: 'Are you sure?',
 | 
			
		||||
        message: 'Are you sure you want to stop this stack?',
 | 
			
		||||
        buttons: { confirm: { label: 'Stop', className: 'btn-danger' } },
 | 
			
		||||
      });
 | 
			
		||||
      if (!confirmed) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      $scope.state.actionInProgress = true;
 | 
			
		||||
      try {
 | 
			
		||||
        await StackService.stop($scope.stack.Id);
 | 
			
		||||
        $state.reload();
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        Notifications.error('Failure', err, 'Unable to stop stack');
 | 
			
		||||
      }
 | 
			
		||||
      $scope.state.actionInProgress = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $scope.startStack = startStack;
 | 
			
		||||
    function startStack() {
 | 
			
		||||
      return $async(startStackAsync);
 | 
			
		||||
    }
 | 
			
		||||
    async function startStackAsync() {
 | 
			
		||||
      $scope.state.actionInProgress = true;
 | 
			
		||||
      const id = $scope.stack.Id;
 | 
			
		||||
      try {
 | 
			
		||||
        await StackService.start(id);
 | 
			
		||||
        $state.reload();
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        Notifications.error('Failure', err, 'Unable to start stack');
 | 
			
		||||
      }
 | 
			
		||||
      $scope.state.actionInProgress = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function loadStack(id) {
 | 
			
		||||
      var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -207,17 +249,24 @@ angular.module('portainer.app').controller('StackController', [
 | 
			
		|||
          $scope.groups = data.groups;
 | 
			
		||||
          $scope.stack = stack;
 | 
			
		||||
 | 
			
		||||
          let resourcesPromise = Promise.resolve({});
 | 
			
		||||
          if (stack.Status === 1) {
 | 
			
		||||
            resourcesPromise = stack.Type === 1 ? retrieveSwarmStackResources(stack.Name, agentProxy) : retrieveComposeStackResources(stack.Name);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return $q.all({
 | 
			
		||||
            stackFile: StackService.getStackFile(id),
 | 
			
		||||
            resources: stack.Type === 1 ? retrieveSwarmStackResources(stack.Name, agentProxy) : retrieveComposeStackResources(stack.Name),
 | 
			
		||||
            resources: resourcesPromise,
 | 
			
		||||
          });
 | 
			
		||||
        })
 | 
			
		||||
        .then(function success(data) {
 | 
			
		||||
          $scope.stackFileContent = data.stackFile;
 | 
			
		||||
          if ($scope.stack.Type === 1) {
 | 
			
		||||
            assignSwarmStackResources(data.resources, agentProxy);
 | 
			
		||||
          } else {
 | 
			
		||||
            assignComposeStackResources(data.resources);
 | 
			
		||||
          if ($scope.stack.Status === 1) {
 | 
			
		||||
            if ($scope.stack.Type === 1) {
 | 
			
		||||
              assignSwarmStackResources(data.resources, agentProxy);
 | 
			
		||||
            } else {
 | 
			
		||||
              assignComposeStackResources(data.resources);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .catch(function error(err) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue