mirror of https://github.com/portainer/portainer
				
				
				
			feat(templates): add support for stack templates (#1346)
							parent
							
								
									1b6b4733bd
								
							
						
					
					
						commit
						9ceb3a8051
					
				| 
						 | 
					@ -127,7 +127,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
 | 
				
			||||||
	if err == portainer.ErrSettingsNotFound {
 | 
						if err == portainer.ErrSettingsNotFound {
 | 
				
			||||||
		settings := &portainer.Settings{
 | 
							settings := &portainer.Settings{
 | 
				
			||||||
			LogoURL:                     *flags.Logo,
 | 
								LogoURL:                     *flags.Logo,
 | 
				
			||||||
			DisplayExternalContributors: true,
 | 
								DisplayExternalContributors: false,
 | 
				
			||||||
			AuthenticationMethod:        portainer.AuthenticationInternal,
 | 
								AuthenticationMethod:        portainer.AuthenticationInternal,
 | 
				
			||||||
			LDAPSettings: portainer.LDAPSettings{
 | 
								LDAPSettings: portainer.LDAPSettings{
 | 
				
			||||||
				TLSConfig: portainer.TLSConfiguration{},
 | 
									TLSConfig: portainer.TLSConfiguration{},
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,16 +43,17 @@ func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *ht
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var templatesURL string
 | 
						var templatesURL string
 | 
				
			||||||
	if key == "containers" {
 | 
						switch key {
 | 
				
			||||||
 | 
						case "containers":
 | 
				
			||||||
		settings, err := handler.SettingsService.Settings()
 | 
							settings, err := handler.SettingsService.Settings()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
 | 
								httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		templatesURL = settings.TemplatesURL
 | 
							templatesURL = settings.TemplatesURL
 | 
				
			||||||
	} else if key == "linuxserver.io" {
 | 
						case "linuxserver.io":
 | 
				
			||||||
		templatesURL = containerTemplatesURLLinuxServerIo
 | 
							templatesURL = containerTemplatesURLLinuxServerIo
 | 
				
			||||||
	} else {
 | 
						default:
 | 
				
			||||||
		httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
 | 
							httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,6 +40,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
 | 
				
			||||||
    if (!$scope.formValues.customTemplates) {
 | 
					    if (!$scope.formValues.customTemplates) {
 | 
				
			||||||
      settings.TemplatesURL = DEFAULT_TEMPLATES_URL;
 | 
					      settings.TemplatesURL = DEFAULT_TEMPLATES_URL;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    settings.DisplayExternalContributors = !$scope.formValues.externalContributions;
 | 
					    settings.DisplayExternalContributors = !$scope.formValues.externalContributions;
 | 
				
			||||||
    settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts;
 | 
					    settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts;
 | 
				
			||||||
    settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode;
 | 
					    settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,14 +3,79 @@
 | 
				
			||||||
    <a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
 | 
					    <a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
 | 
				
			||||||
      <i class="fa fa-refresh" aria-hidden="true"></i>
 | 
					      <i class="fa fa-refresh" aria-hidden="true"></i>
 | 
				
			||||||
    </a>
 | 
					    </a>
 | 
				
			||||||
    <i id="loadTemplatesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
 | 
					    <i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
 | 
				
			||||||
  </rd-header-title>
 | 
					  </rd-header-title>
 | 
				
			||||||
  <rd-header-content>Templates</rd-header-content>
 | 
					  <rd-header-content>Templates</rd-header-content>
 | 
				
			||||||
</rd-header>
 | 
					</rd-header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="row" style="height: 90%">
 | 
					<div class="row">
 | 
				
			||||||
 | 
					  <!-- stack-form -->
 | 
				
			||||||
 | 
					  <div class="col-sm-12" ng-if="state.selectedTemplate && state.filters.Type === 'stack'">
 | 
				
			||||||
 | 
					    <rd-widget>
 | 
				
			||||||
 | 
					      <rd-widget-custom-header icon="state.selectedTemplate.Logo" title="state.selectedTemplate.Title">
 | 
				
			||||||
 | 
					        <div class="pull-right">
 | 
				
			||||||
 | 
					          <button type="button" class="btn btn-sm btn-primary" ng-click="unselectTemplate()">Hide</button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </rd-widget-custom-header>
 | 
				
			||||||
 | 
					      <rd-widget-body classes="padding">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="col-sm-12" ng-if="state.selectedTemplate">
 | 
					        <form class="form-horizontal">
 | 
				
			||||||
 | 
					          <!-- description -->
 | 
				
			||||||
 | 
					          <div ng-if="state.selectedTemplate.Note">
 | 
				
			||||||
 | 
					            <div class="col-sm-12 form-section-title">
 | 
				
			||||||
 | 
					              Information
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="form-group">
 | 
				
			||||||
 | 
					              <div class="col-sm-12">
 | 
				
			||||||
 | 
					                <div class="template-note" ng-if="state.selectedTemplate.Note" ng-bind-html="state.selectedTemplate.Note"></div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <!-- !description -->
 | 
				
			||||||
 | 
					          <div class="col-sm-12 form-section-title">
 | 
				
			||||||
 | 
					            Configuration
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <!-- name-input -->
 | 
				
			||||||
 | 
					          <div class="form-group">
 | 
				
			||||||
 | 
					            <label for="container_name" class="col-sm-2 control-label text-left">Name</label>
 | 
				
			||||||
 | 
					            <div class="col-sm-10">
 | 
				
			||||||
 | 
					              <input type="text" name="container_name" class="form-control" ng-model="formValues.name" placeholder="e.g. myStack" required>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <!-- !name-input -->
 | 
				
			||||||
 | 
					          <!-- env -->
 | 
				
			||||||
 | 
					          <div ng-repeat="var in state.selectedTemplate.Env" ng-if="var.label && !var.set" class="form-group">
 | 
				
			||||||
 | 
					            <label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}<portainer-tooltip ng-if="var.description" position="bottom" message="{{ var.description }}"></portainer-tooltip></label>
 | 
				
			||||||
 | 
					            <div class="col-sm-10">
 | 
				
			||||||
 | 
					              <!-- <input ng-if="!var.values && (!var.type || !var.type === 'container')" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}"> -->
 | 
				
			||||||
 | 
					              <input type="text" class="form-control" ng-if="!var.select" ng-model="var.value" id="field_{{ $index }}">
 | 
				
			||||||
 | 
					              <select class="form-control" ng-if="var.select" ng-model="var.value" id="field_{{ $index }}">
 | 
				
			||||||
 | 
					                <option selected disabled hidden value="">Select value</option>
 | 
				
			||||||
 | 
					                <option ng-repeat="choice in var.select" value="{{ choice.value }}">{{ choice.text }}</option>
 | 
				
			||||||
 | 
					              </select>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <!-- !env -->
 | 
				
			||||||
 | 
					          <!-- access-control -->
 | 
				
			||||||
 | 
					          <por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
 | 
				
			||||||
 | 
					          <!-- !access-control -->
 | 
				
			||||||
 | 
					          <!-- actions -->
 | 
				
			||||||
 | 
					          <div class="form-group">
 | 
				
			||||||
 | 
					            <div class="col-sm-12">
 | 
				
			||||||
 | 
					              <button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.name" ng-click="createTemplate()">Create</button>
 | 
				
			||||||
 | 
					              <i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
 | 
				
			||||||
 | 
					              <span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <!-- !actions -->
 | 
				
			||||||
 | 
					        </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      </rd-widget-body>
 | 
				
			||||||
 | 
					    </rd-widget>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <!-- !stack-form -->
 | 
				
			||||||
 | 
					  <!-- container-form -->
 | 
				
			||||||
 | 
					  <div class="col-sm-12" ng-if="state.selectedTemplate && state.filters.Type === 'container'">
 | 
				
			||||||
    <rd-widget>
 | 
					    <rd-widget>
 | 
				
			||||||
      <rd-widget-custom-header icon="state.selectedTemplate.Logo" title="state.selectedTemplate.Image">
 | 
					      <rd-widget-custom-header icon="state.selectedTemplate.Logo" title="state.selectedTemplate.Image">
 | 
				
			||||||
        <div class="pull-right">
 | 
					        <div class="pull-right">
 | 
				
			||||||
| 
						 | 
					@ -225,14 +290,10 @@
 | 
				
			||||||
          <div class="form-group">
 | 
					          <div class="form-group">
 | 
				
			||||||
            <div class="col-sm-12">
 | 
					            <div class="col-sm-12">
 | 
				
			||||||
              <button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
 | 
					              <button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
 | 
				
			||||||
              <i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
 | 
					              <i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
 | 
				
			||||||
              <span class="small text-muted" style="margin-left: 10px" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && !state.formValidationError">
 | 
					              <span class="small text-muted" style="margin-left: 10px" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && !state.formValidationError">
 | 
				
			||||||
                When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.
 | 
					                When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.
 | 
				
			||||||
              </span>
 | 
					              </span>
 | 
				
			||||||
              <span ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && !state.formValidationError" style="margin-left: 10px">
 | 
					 | 
				
			||||||
                <i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
 | 
					 | 
				
			||||||
                <span class="small text-muted" style="margin-left: 5px;">App templates cannot be deployed as Swarm Mode services for the moment. You can still use them to quickly deploy containers on the Docker host.</span>
 | 
					 | 
				
			||||||
              </span>
 | 
					 | 
				
			||||||
              <span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
 | 
					              <span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
| 
						 | 
					@ -242,7 +303,11 @@
 | 
				
			||||||
      </rd-widget-body>
 | 
					      </rd-widget-body>
 | 
				
			||||||
    </rd-widget>
 | 
					    </rd-widget>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					  <!-- container-form -->
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="row">
 | 
				
			||||||
  <div class="col-sm-12" style="height: 100%">
 | 
					  <div class="col-sm-12" style="height: 100%">
 | 
				
			||||||
    <rd-template-widget>
 | 
					    <rd-template-widget>
 | 
				
			||||||
      <rd-widget-header icon="fa-rocket" title="Templates">
 | 
					      <rd-widget-header icon="fa-rocket" title="Templates">
 | 
				
			||||||
| 
						 | 
					@ -273,53 +338,92 @@
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </rd-widget-taskbar>
 | 
					      </rd-widget-taskbar>
 | 
				
			||||||
      <rd-widget-body classes="padding template-widget-body">
 | 
					      <rd-widget-body classes="padding template-widget-body">
 | 
				
			||||||
        <div class="template-list">
 | 
					        <form class="form-horizontal">
 | 
				
			||||||
          <!-- template -->
 | 
					          <div ng-if="templatesKey !== 'linuxserver.io' && state.showDeploymentSelector">
 | 
				
			||||||
          <div ng-repeat="tpl in templates | filter:state.filters:true" class="template-container" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index, $index)">
 | 
					            <div class="col-sm-12 form-section-title">
 | 
				
			||||||
            <div class="template-main">
 | 
					              Deployment method
 | 
				
			||||||
              <!-- template-image -->
 | 
					            </div>
 | 
				
			||||||
              <span class="">
 | 
					            <div class="form-group"></div>
 | 
				
			||||||
                <img class="template-logo" ng-src="{{ tpl.Logo }}" />
 | 
					            <div class="form-group" style="margin-bottom: 0">
 | 
				
			||||||
              </span>
 | 
					              <div class="boxselector_wrapper">
 | 
				
			||||||
              <!-- !template-image -->
 | 
					                <div ng-click="updateCategories(templates, state.filters.Type)">
 | 
				
			||||||
              <!-- template-details -->
 | 
					                  <input type="radio" id="registry_quay" ng-model="state.filters.Type" value="stack">
 | 
				
			||||||
              <span class="col-sm-12">
 | 
					                  <label for="registry_quay">
 | 
				
			||||||
                <!-- template-line1 -->
 | 
					                    <div class="boxselector_header">
 | 
				
			||||||
                <div class="template-line">
 | 
					                      <i class="fa fa-th-list" aria-hidden="true" style="margin-right: 2px;"></i>
 | 
				
			||||||
                  <span class="template-title">
 | 
					                      Stack
 | 
				
			||||||
                    {{ tpl.Title }}
 | 
					                    </div>
 | 
				
			||||||
                  </span>
 | 
					                    <p>Multi-containers deployment</p>
 | 
				
			||||||
                  <span>
 | 
					                  </label>
 | 
				
			||||||
                    <i class="fa fa-windows" aria-hidden="true" ng-if="tpl.Platform === 'windows'"></i>
 | 
					                </div>
 | 
				
			||||||
                    <i class="fa fa-linux" aria-hidden="true" ng-if="tpl.Platform === 'linux'"></i>
 | 
					                <div ng-click="updateCategories(templates, state.filters.Type)">
 | 
				
			||||||
                    <!-- Arch / Platform -->
 | 
					                  <input type="radio" id="registry_custom" ng-model="state.filters.Type" value="container">
 | 
				
			||||||
                  </span>
 | 
					                  <label for="registry_custom">
 | 
				
			||||||
                </div>
 | 
					                    <div class="boxselector_header">
 | 
				
			||||||
                <!-- !template-line1 -->
 | 
					                      <i class="fa fa-server" aria-hidden="true" style="margin-right: 2px;"></i>
 | 
				
			||||||
                <!-- template-line2 -->
 | 
					                      Container
 | 
				
			||||||
                <div class="template-line">
 | 
					                    </div>
 | 
				
			||||||
                  <span class="template-description">
 | 
					                    <p>Single container deployment</p>
 | 
				
			||||||
                    {{ tpl.Description }}
 | 
					                  </label>
 | 
				
			||||||
                  </span>
 | 
					                </div>
 | 
				
			||||||
                  <span class="small text-muted" ng-if="tpl.Categories.length > 0">
 | 
					              </div>
 | 
				
			||||||
                    {{ tpl.Categories.join(', ') }}
 | 
					 | 
				
			||||||
                  </span>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
                <!-- !template-line2 -->
 | 
					 | 
				
			||||||
              </span>
 | 
					 | 
				
			||||||
              <!-- !template-details -->
 | 
					 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <!-- !template -->
 | 
					 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div ng-if="!templates" class="text-center text-muted">
 | 
					
 | 
				
			||||||
            Loading...
 | 
					          <div ng-if="templatesKey !== 'linuxserver.io' && state.showDeploymentSelector">
 | 
				
			||||||
 | 
					            <div class="col-sm-12 form-section-title">
 | 
				
			||||||
 | 
					              Templates
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="form-group"></div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div ng-if="(templates | filter:state.filters:true).length == 0" class="text-center text-muted">
 | 
					
 | 
				
			||||||
            No templates available.
 | 
					          <div class="template-list">
 | 
				
			||||||
 | 
					            <!-- template -->
 | 
				
			||||||
 | 
					            <div ng-repeat="tpl in templates | filter:state.filters:true" class="template-container" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index, $index)">
 | 
				
			||||||
 | 
					              <div class="template-main">
 | 
				
			||||||
 | 
					                <!-- template-image -->
 | 
				
			||||||
 | 
					                <span class="">
 | 
				
			||||||
 | 
					                  <img class="template-logo" ng-src="{{ tpl.Logo }}" />
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					                <!-- !template-image -->
 | 
				
			||||||
 | 
					                <!-- template-details -->
 | 
				
			||||||
 | 
					                <span class="col-sm-12">
 | 
				
			||||||
 | 
					                  <!-- template-line1 -->
 | 
				
			||||||
 | 
					                  <div class="template-line">
 | 
				
			||||||
 | 
					                    <span class="template-title">
 | 
				
			||||||
 | 
					                      {{ tpl.Title }}
 | 
				
			||||||
 | 
					                    </span>
 | 
				
			||||||
 | 
					                    <span>
 | 
				
			||||||
 | 
					                      <i class="fa fa-windows" aria-hidden="true" ng-if="tpl.Platform === 'windows'"></i>
 | 
				
			||||||
 | 
					                      <i class="fa fa-linux" aria-hidden="true" ng-if="tpl.Platform === 'linux'"></i>
 | 
				
			||||||
 | 
					                      <!-- Arch / Platform -->
 | 
				
			||||||
 | 
					                    </span>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <!-- !template-line1 -->
 | 
				
			||||||
 | 
					                  <!-- template-line2 -->
 | 
				
			||||||
 | 
					                  <div class="template-line">
 | 
				
			||||||
 | 
					                    <span class="template-description">
 | 
				
			||||||
 | 
					                      {{ tpl.Description }}
 | 
				
			||||||
 | 
					                    </span>
 | 
				
			||||||
 | 
					                    <span class="small text-muted" ng-if="tpl.Categories.length > 0">
 | 
				
			||||||
 | 
					                      {{ tpl.Categories.join(', ') }}
 | 
				
			||||||
 | 
					                    </span>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  <!-- !template-line2 -->
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					                <!-- !template-details -->
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <!-- !template -->
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div ng-if="!templates" class="text-center text-muted">
 | 
				
			||||||
 | 
					              Loading...
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div ng-if="(templates | filter:state.filters:true).length == 0" class="text-center text-muted">
 | 
				
			||||||
 | 
					              No templates available.
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </form>
 | 
				
			||||||
      </rd-widget-body>
 | 
					      </rd-widget-body>
 | 
				
			||||||
    </rd-template-widget>
 | 
					    </rd-template-widget>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,16 @@
 | 
				
			||||||
angular.module('templates', [])
 | 
					angular.module('templates', [])
 | 
				
			||||||
.controller('TemplatesController', ['$scope', '$q', '$state', '$transition$', '$anchorScroll', '$filter', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', 'FormValidator', 'SettingsService',
 | 
					.controller('TemplatesController', ['$scope', '$q', '$state', '$transition$', '$anchorScroll', '$filter', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', 'FormValidator', 'SettingsService', 'StackService',
 | 
				
			||||||
function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication, FormValidator, SettingsService) {
 | 
					function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication, FormValidator, SettingsService, StackService) {
 | 
				
			||||||
  $scope.state = {
 | 
					  $scope.state = {
 | 
				
			||||||
    selectedTemplate: null,
 | 
					    selectedTemplate: null,
 | 
				
			||||||
    showAdvancedOptions: false,
 | 
					    showAdvancedOptions: false,
 | 
				
			||||||
    hideDescriptions: $transition$.params().hide_descriptions,
 | 
					    hideDescriptions: $transition$.params().hide_descriptions,
 | 
				
			||||||
    formValidationError: '',
 | 
					    formValidationError: '',
 | 
				
			||||||
 | 
					    showDeploymentSelector: false,
 | 
				
			||||||
    filters: {
 | 
					    filters: {
 | 
				
			||||||
      Categories: '!',
 | 
					      Categories: '!',
 | 
				
			||||||
      Platform: '!'
 | 
					      Platform: '!',
 | 
				
			||||||
 | 
					      Type: 'container'
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -54,19 +56,7 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $scope.createTemplate = function() {
 | 
					  function createContainerFromTemplate(template, userId, accessControlData) {
 | 
				
			||||||
    $('#createContainerSpinner').show();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    var userDetails = Authentication.getUserDetails();
 | 
					 | 
				
			||||||
    var accessControlData = $scope.formValues.AccessControlData;
 | 
					 | 
				
			||||||
    var isAdmin = userDetails.role === 1 ? true : false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!validateForm(accessControlData, isAdmin)) {
 | 
					 | 
				
			||||||
      $('#createContainerSpinner').hide();
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    var template = $scope.state.selectedTemplate;
 | 
					 | 
				
			||||||
    var templateConfiguration = createTemplateConfiguration(template);
 | 
					    var templateConfiguration = createTemplateConfiguration(template);
 | 
				
			||||||
    var generatedVolumeCount = TemplateHelper.determineRequiredGeneratedVolumeCount(template.Volumes);
 | 
					    var generatedVolumeCount = TemplateHelper.determineRequiredGeneratedVolumeCount(template.Volumes);
 | 
				
			||||||
    var generatedVolumeIds = [];
 | 
					    var generatedVolumeIds = [];
 | 
				
			||||||
| 
						 | 
					@ -85,7 +75,6 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .then(function success(data) {
 | 
					    .then(function success(data) {
 | 
				
			||||||
      var containerIdentifier = data.Id;
 | 
					      var containerIdentifier = data.Id;
 | 
				
			||||||
      var userId = userDetails.ID;
 | 
					 | 
				
			||||||
      return ResourceControlService.applyResourceControl('container', containerIdentifier, userId, accessControlData, generatedVolumeIds);
 | 
					      return ResourceControlService.applyResourceControl('container', containerIdentifier, userId, accessControlData, generatedVolumeIds);
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .then(function success() {
 | 
					    .then(function success() {
 | 
				
			||||||
| 
						 | 
					@ -96,8 +85,59 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
 | 
				
			||||||
      Notifications.error('Failure', err, err.msg);
 | 
					      Notifications.error('Failure', err, err.msg);
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .finally(function final() {
 | 
					    .finally(function final() {
 | 
				
			||||||
      $('#createContainerSpinner').hide();
 | 
					      $('#createResourceSpinner').hide();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function createStackFromTemplate(template, userId, accessControlData) {
 | 
				
			||||||
 | 
					    var stackName = $scope.formValues.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (var i = 0; i < template.Env.length; i++) {
 | 
				
			||||||
 | 
					      var envvar = template.Env[i];
 | 
				
			||||||
 | 
					      if (envvar.set) {
 | 
				
			||||||
 | 
					        envvar.value = envvar.set;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    StackService.createStackFromGitRepository(stackName, template.Repository.url, template.Repository.stackfile, template.Env)
 | 
				
			||||||
 | 
					    .then(function success() {
 | 
				
			||||||
 | 
					      Notifications.success('Stack successfully created');
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .catch(function error(err) {
 | 
				
			||||||
 | 
					      Notifications.warning('Deployment error', err.err.data.err);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .then(function success(data) {
 | 
				
			||||||
 | 
					      return ResourceControlService.applyResourceControl('stack', stackName, userId, accessControlData, []);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .then(function success() {
 | 
				
			||||||
 | 
					      $state.go('stacks', {}, {reload: true});
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .finally(function final() {
 | 
				
			||||||
 | 
					      $('#createResourceSpinner').hide();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $scope.createTemplate = function() {
 | 
				
			||||||
 | 
					    $('#createResourceSpinner').show();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var userDetails = Authentication.getUserDetails();
 | 
				
			||||||
 | 
					    var userId = userDetails.ID;
 | 
				
			||||||
 | 
					    var accessControlData = $scope.formValues.AccessControlData;
 | 
				
			||||||
 | 
					    var isAdmin = userDetails.role === 1 ? true : false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!validateForm(accessControlData, isAdmin)) {
 | 
				
			||||||
 | 
					      $('#createResourceSpinner').hide();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var template = $scope.state.selectedTemplate;
 | 
				
			||||||
 | 
					    var templatesKey = $scope.templatesKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (template.Type === 'stack') {
 | 
				
			||||||
 | 
					      createStackFromTemplate(template, userId, accessControlData);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      createContainerFromTemplate(template, userId, accessControlData);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $scope.unselectTemplate = function() {
 | 
					  $scope.unselectTemplate = function() {
 | 
				
			||||||
| 
						 | 
					@ -152,11 +192,22 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
 | 
				
			||||||
    return containerMapping;
 | 
					    return containerMapping;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function initTemplates() {
 | 
					  $scope.updateCategories = function(templates, type) {
 | 
				
			||||||
    var templatesKey = $transition$.params().key;
 | 
					    $scope.state.filters.Categories = '!';
 | 
				
			||||||
    var provider = $scope.applicationState.endpoint.mode.provider;
 | 
					    updateCategories(templates, type);
 | 
				
			||||||
    var apiVersion = $scope.applicationState.endpoint.apiVersion;
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function updateCategories(templates, type) {
 | 
				
			||||||
 | 
					    var availableCategories = [];
 | 
				
			||||||
 | 
					    angular.forEach(templates, function(template) {
 | 
				
			||||||
 | 
					      if (template.Type === type) {
 | 
				
			||||||
 | 
					        availableCategories = availableCategories.concat(template.Categories);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    $scope.availableCategories = _.sortBy(_.uniq(availableCategories));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function initTemplates(templatesKey, type, provider, apiVersion) {
 | 
				
			||||||
    $q.all({
 | 
					    $q.all({
 | 
				
			||||||
      templates: TemplateService.getTemplates(templatesKey),
 | 
					      templates: TemplateService.getTemplates(templatesKey),
 | 
				
			||||||
      containers: ContainerService.containers(0),
 | 
					      containers: ContainerService.containers(0),
 | 
				
			||||||
| 
						 | 
					@ -169,12 +220,9 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
 | 
				
			||||||
      settings: SettingsService.publicSettings()
 | 
					      settings: SettingsService.publicSettings()
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .then(function success(data) {
 | 
					    .then(function success(data) {
 | 
				
			||||||
      $scope.templates = data.templates;
 | 
					      var templates =  data.templates;
 | 
				
			||||||
      var availableCategories = [];
 | 
					      updateCategories(templates, type);
 | 
				
			||||||
      angular.forEach($scope.templates, function(template) {
 | 
					      $scope.templates = templates;
 | 
				
			||||||
        availableCategories = availableCategories.concat(template.Categories);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      $scope.availableCategories = _.sortBy(_.uniq(availableCategories));
 | 
					 | 
				
			||||||
      $scope.runningContainers = data.containers;
 | 
					      $scope.runningContainers = data.containers;
 | 
				
			||||||
      $scope.availableVolumes = data.volumes.Volumes;
 | 
					      $scope.availableVolumes = data.volumes.Volumes;
 | 
				
			||||||
      var networks = data.networks;
 | 
					      var networks = data.networks;
 | 
				
			||||||
| 
						 | 
					@ -182,17 +230,33 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
 | 
				
			||||||
      $scope.globalNetworkCount = networks.length;
 | 
					      $scope.globalNetworkCount = networks.length;
 | 
				
			||||||
      var settings = data.settings;
 | 
					      var settings = data.settings;
 | 
				
			||||||
      $scope.allowBindMounts = settings.AllowBindMountsForRegularUsers;
 | 
					      $scope.allowBindMounts = settings.AllowBindMountsForRegularUsers;
 | 
				
			||||||
      var userDetails = Authentication.getUserDetails();
 | 
					 | 
				
			||||||
      $scope.isAdmin = userDetails.role === 1 ? true : false;
 | 
					 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .catch(function error(err) {
 | 
					    .catch(function error(err) {
 | 
				
			||||||
      $scope.templates = [];
 | 
					      $scope.templates = [];
 | 
				
			||||||
      Notifications.error('Failure', err, 'An error occured during apps initialization.');
 | 
					      Notifications.error('Failure', err, 'An error occured during apps initialization.');
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .finally(function final(){
 | 
					    .finally(function final(){
 | 
				
			||||||
      $('#loadTemplatesSpinner').hide();
 | 
					      $('#loadingViewSpinner').hide();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  initTemplates();
 | 
					  function initView() {
 | 
				
			||||||
 | 
					    var templatesKey = $transition$.params().key;
 | 
				
			||||||
 | 
					    $scope.templatesKey = templatesKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var userDetails = Authentication.getUserDetails();
 | 
				
			||||||
 | 
					    $scope.isAdmin = userDetails.role === 1 ? true : false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var endpointMode = $scope.applicationState.endpoint.mode;
 | 
				
			||||||
 | 
					    var apiVersion = $scope.applicationState.endpoint.apiVersion;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER' && apiVersion >= 1.25) {
 | 
				
			||||||
 | 
					      $scope.state.filters.Type = 'stack';
 | 
				
			||||||
 | 
					      $scope.state.showDeploymentSelector = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    initTemplates(templatesKey, $scope.state.filters.Type, endpointMode.provider, apiVersion);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  initView();
 | 
				
			||||||
}]);
 | 
					}]);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					function StackTemplateViewModel(data) {
 | 
				
			||||||
 | 
					  this.Type = data.type;
 | 
				
			||||||
 | 
					  this.Title = data.title;
 | 
				
			||||||
 | 
					  this.Description = data.description;
 | 
				
			||||||
 | 
					  this.Note = data.note;
 | 
				
			||||||
 | 
					  this.Categories = data.categories ? data.categories : [];
 | 
				
			||||||
 | 
					  this.Platform = data.platform ? data.platform : 'undefined';
 | 
				
			||||||
 | 
					  this.Logo = data.logo;
 | 
				
			||||||
 | 
					  this.Repository = data.repository;
 | 
				
			||||||
 | 
					  this.Env = data.env ? data.env : [];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
function TemplateViewModel(data) {
 | 
					function TemplateViewModel(data) {
 | 
				
			||||||
 | 
					  this.Type = data.type;
 | 
				
			||||||
  this.Title = data.title;
 | 
					  this.Title = data.title;
 | 
				
			||||||
  this.Description = data.description;
 | 
					  this.Description = data.description;
 | 
				
			||||||
  this.Note = data.note;
 | 
					  this.Note = data.note;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,9 @@ angular.module('portainer.services')
 | 
				
			||||||
    .then(function success(data) {
 | 
					    .then(function success(data) {
 | 
				
			||||||
      var templates = data.map(function (tpl, idx) {
 | 
					      var templates = data.map(function (tpl, idx) {
 | 
				
			||||||
        var template;
 | 
					        var template;
 | 
				
			||||||
        if (key === 'linuxserver.io') {
 | 
					        if (tpl.type === 'stack') {
 | 
				
			||||||
 | 
					          template = new StackTemplateViewModel(tpl);
 | 
				
			||||||
 | 
					        } else if (tpl.type === 'container' && key === 'linuxserver.io') {
 | 
				
			||||||
          template = new TemplateLSIOViewModel(tpl);
 | 
					          template = new TemplateLSIOViewModel(tpl);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          template = new TemplateViewModel(tpl);
 | 
					          template = new TemplateViewModel(tpl);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -287,6 +287,10 @@ ul.sidebar .sidebar-title {
 | 
				
			||||||
  height: auto;
 | 
					  height: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ul.sidebar .sidebar-list a {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ul.sidebar .sidebar-list a.active {
 | 
					ul.sidebar .sidebar-list a.active {
 | 
				
			||||||
  color: #fff;
 | 
					  color: #fff;
 | 
				
			||||||
  text-indent: 22px;
 | 
					  text-indent: 22px;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue