mirror of https://github.com/portainer/portainer
feat(templates): LinuxServer.io templates integration (#761)
parent
16166c3367
commit
b8803f380b
|
@ -42,7 +42,7 @@ func (server *Server) Start() error {
|
||||||
var settingsHandler = NewSettingsHandler(middleWareService)
|
var settingsHandler = NewSettingsHandler(middleWareService)
|
||||||
settingsHandler.settings = server.Settings
|
settingsHandler.settings = server.Settings
|
||||||
var templatesHandler = NewTemplatesHandler(middleWareService)
|
var templatesHandler = NewTemplatesHandler(middleWareService)
|
||||||
templatesHandler.templatesURL = server.TemplatesURL
|
templatesHandler.containerTemplatesURL = server.TemplatesURL
|
||||||
var dockerHandler = NewDockerHandler(middleWareService, server.ResourceControlService)
|
var dockerHandler = NewDockerHandler(middleWareService, server.ResourceControlService)
|
||||||
dockerHandler.EndpointService = server.EndpointService
|
dockerHandler.EndpointService = server.EndpointService
|
||||||
var websocketHandler = NewWebSocketHandler()
|
var websocketHandler = NewWebSocketHandler()
|
||||||
|
|
|
@ -12,10 +12,14 @@ import (
|
||||||
// TemplatesHandler represents an HTTP API handler for managing templates.
|
// TemplatesHandler represents an HTTP API handler for managing templates.
|
||||||
type TemplatesHandler struct {
|
type TemplatesHandler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
templatesURL string
|
containerTemplatesURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
containerTemplatesURLLinuxServerIo = "http://tools.linuxserver.io/portainer.json"
|
||||||
|
)
|
||||||
|
|
||||||
// NewTemplatesHandler returns a new instance of TemplatesHandler.
|
// NewTemplatesHandler returns a new instance of TemplatesHandler.
|
||||||
func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler {
|
func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler {
|
||||||
h := &TemplatesHandler{
|
h := &TemplatesHandler{
|
||||||
|
@ -27,14 +31,30 @@ func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetTemplates handles GET requests on /templates
|
// handleGetTemplates handles GET requests on /templates?key=<key>
|
||||||
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
handleNotAllowed(w, []string{http.MethodGet})
|
handleNotAllowed(w, []string{http.MethodGet})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.Get(handler.templatesURL)
|
key := r.FormValue("key")
|
||||||
|
if key == "" {
|
||||||
|
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var templatesURL string
|
||||||
|
if key == "containers" {
|
||||||
|
templatesURL = handler.containerTemplatesURL
|
||||||
|
} else if key == "linuxserver.io" {
|
||||||
|
templatesURL = containerTemplatesURLLinuxServerIo
|
||||||
|
} else {
|
||||||
|
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(templatesURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
|
|
21
app/app.js
21
app/app.js
|
@ -456,6 +456,27 @@ angular.module('portainer', [
|
||||||
})
|
})
|
||||||
.state('templates', {
|
.state('templates', {
|
||||||
url: '/templates/',
|
url: '/templates/',
|
||||||
|
params: {
|
||||||
|
key: 'containers',
|
||||||
|
hide_descriptions: false
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
"content@": {
|
||||||
|
templateUrl: 'app/components/templates/templates.html',
|
||||||
|
controller: 'TemplatesController'
|
||||||
|
},
|
||||||
|
"sidebar@": {
|
||||||
|
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||||
|
controller: 'SidebarController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.state('templates_linuxserver', {
|
||||||
|
url: '^/templates/linuxserver.io',
|
||||||
|
params: {
|
||||||
|
key: 'linuxserver.io',
|
||||||
|
hide_descriptions: true
|
||||||
|
},
|
||||||
views: {
|
views: {
|
||||||
"content@": {
|
"content@": {
|
||||||
templateUrl: 'app/components/templates/templates.html',
|
templateUrl: 'app/components/templates/templates.html',
|
||||||
|
|
|
@ -21,6 +21,9 @@
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list">
|
<li class="sidebar-list">
|
||||||
<a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket"></span></a>
|
<a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket"></span></a>
|
||||||
|
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'templates' || $state.current.name === 'templates_linuxserver')">
|
||||||
|
<a ui-sref="templates_linuxserver" ui-sref-active="active">LinuxServer.io</a>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||||
<a ui-sref="services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt"></span></a>
|
<a ui-sref="services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt"></span></a>
|
||||||
|
|
|
@ -14,17 +14,13 @@
|
||||||
</rd-widget-custom-header>
|
</rd-widget-custom-header>
|
||||||
<rd-widget-body classes="padding">
|
<rd-widget-body classes="padding">
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
<div class="form-group" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
<!-- description -->
|
||||||
|
<div class="form-group" ng-if="state.selectedTemplate.Description">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<span class="small text-muted">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 class="small" style="margin-left: 5px;">{{ state.selectedTemplate.Description }}</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
|
||||||
<span class="small text-muted">App templates cannot be used with swarm-mode at the moment. You can still use them to quickly deploy containers to the Docker host.</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- !description -->
|
||||||
<!-- name-and-network-inputs -->
|
<!-- name-and-network-inputs -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
|
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
|
||||||
|
@ -200,6 +196,13 @@
|
||||||
<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="createContainerSpinner" 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'">
|
||||||
|
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 ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'" 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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -228,7 +231,7 @@
|
||||||
<div dir-paginate="tpl in templates | itemsPerPage: state.pagination_count" class="container-template hvr-underline-from-center" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index)">
|
<div dir-paginate="tpl in templates | itemsPerPage: state.pagination_count" class="container-template hvr-underline-from-center" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index)">
|
||||||
<img class="logo" ng-src="{{ tpl.Logo }}" />
|
<img class="logo" ng-src="{{ tpl.Logo }}" />
|
||||||
<div class="title">{{ tpl.Title }}</div>
|
<div class="title">{{ tpl.Title }}</div>
|
||||||
<div class="description">{{ tpl.Description }}</div>
|
<div class="description" ng-if="tpl.Description && !state.hideDescriptions">{{ tpl.Description }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="!templates" class="text-center text-muted">
|
<div ng-if="!templates" class="text-center text-muted">
|
||||||
Loading...
|
Loading...
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
angular.module('templates', [])
|
angular.module('templates', [])
|
||||||
.controller('TemplatesController', ['$scope', '$q', '$state', '$anchorScroll', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Messages', 'Pagination', 'ResourceControlService', 'Authentication',
|
.controller('TemplatesController', ['$scope', '$q', '$state', '$stateParams', '$anchorScroll', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Messages', 'Pagination', 'ResourceControlService', 'Authentication',
|
||||||
function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Messages, Pagination, ResourceControlService, Authentication) {
|
function ($scope, $q, $state, $stateParams, $anchorScroll, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Messages, Pagination, ResourceControlService, Authentication) {
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
selectedTemplate: null,
|
selectedTemplate: null,
|
||||||
showAdvancedOptions: false,
|
showAdvancedOptions: false,
|
||||||
|
hideDescriptions: $stateParams.hide_descriptions,
|
||||||
pagination_count: Pagination.getPaginationCount('templates')
|
pagination_count: Pagination.getPaginationCount('templates')
|
||||||
};
|
};
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
|
@ -124,7 +125,7 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, Container
|
||||||
if (endpointProvider === 'DOCKER_SWARM' || endpointProvider === 'DOCKER_SWARM_MODE') {
|
if (endpointProvider === 'DOCKER_SWARM' || endpointProvider === 'DOCKER_SWARM_MODE') {
|
||||||
if (endpointProvider === 'DOCKER_SWARM') {
|
if (endpointProvider === 'DOCKER_SWARM') {
|
||||||
networks = NetworkService.filterGlobalNetworks(networks);
|
networks = NetworkService.filterGlobalNetworks(networks);
|
||||||
} else {
|
} else {
|
||||||
networks = NetworkService.filterSwarmModeAttachableNetworks(networks);
|
networks = NetworkService.filterSwarmModeAttachableNetworks(networks);
|
||||||
}
|
}
|
||||||
$scope.globalNetworkCount = networks.length;
|
$scope.globalNetworkCount = networks.length;
|
||||||
|
@ -134,15 +135,20 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, Container
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTemplates() {
|
function initTemplates() {
|
||||||
|
var templatesKey = $stateParams.key;
|
||||||
Config.$promise.then(function (c) {
|
Config.$promise.then(function (c) {
|
||||||
$q.all({
|
$q.all({
|
||||||
templates: TemplateService.getTemplates(),
|
templates: TemplateService.getTemplates(templatesKey),
|
||||||
containers: ContainerService.getContainers(0, c.hiddenLabels),
|
containers: ContainerService.getContainers(0, c.hiddenLabels),
|
||||||
networks: NetworkService.getNetworks(),
|
networks: NetworkService.getNetworks(),
|
||||||
volumes: VolumeService.getVolumes()
|
volumes: VolumeService.getVolumes()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.templates = data.templates;
|
var templates = data.templates;
|
||||||
|
if (templatesKey === 'linuxserver.io') {
|
||||||
|
templates = TemplateService.filterLinuxServerIOTemplates(templates);
|
||||||
|
}
|
||||||
|
$scope.templates = templates;
|
||||||
$scope.runningContainers = data.containers;
|
$scope.runningContainers = data.containers;
|
||||||
$scope.availableNetworks = filterNetworksBasedOnProvider(data.networks);
|
$scope.availableNetworks = filterNetworksBasedOnProvider(data.networks);
|
||||||
$scope.availableVolumes = data.volumes.Volumes;
|
$scope.availableVolumes = data.volumes.Volumes;
|
||||||
|
|
|
@ -93,5 +93,22 @@ angular.module('portainer.helpers')
|
||||||
return count;
|
return count;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
helper.filterLinuxServerIOTemplates = function(templates) {
|
||||||
|
return templates.filter(function f(template) {
|
||||||
|
var valid = false;
|
||||||
|
if (template.Category) {
|
||||||
|
angular.forEach(template.Category, function(category) {
|
||||||
|
if (_.startsWith(category, 'Network')) {
|
||||||
|
valid = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}).map(function(template, idx) {
|
||||||
|
template.index = idx;
|
||||||
|
return template;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return helper;
|
return helper;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
function TemplateViewModel(data) {
|
function TemplateViewModel(data) {
|
||||||
this.Title = data.title;
|
this.Title = data.title;
|
||||||
this.Description = data.description;
|
this.Description = data.description;
|
||||||
|
this.Category = data.category;
|
||||||
this.Logo = data.logo;
|
this.Logo = data.logo;
|
||||||
this.Image = data.image;
|
this.Image = data.image;
|
||||||
this.Registry = data.registry ? data.registry : '';
|
this.Registry = data.registry ? data.registry : '';
|
||||||
|
|
|
@ -3,9 +3,9 @@ angular.module('portainer.services')
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
service.getTemplates = function() {
|
service.getTemplates = function(key) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
Template.get().$promise
|
Template.get({key: key}).$promise
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var templates = data.map(function (tpl, idx) {
|
var templates = data.map(function (tpl, idx) {
|
||||||
var template = new TemplateViewModel(tpl);
|
var template = new TemplateViewModel(tpl);
|
||||||
|
@ -20,6 +20,10 @@ angular.module('portainer.services')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.filterLinuxServerIOTemplates = function(templates) {
|
||||||
|
return TemplateHelper.filterLinuxServerIOTemplates(templates);
|
||||||
|
};
|
||||||
|
|
||||||
service.createTemplateConfiguration = function(template, containerName, network, containerMapping) {
|
service.createTemplateConfiguration = function(template, containerName, network, containerMapping) {
|
||||||
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.Image, template.Registry);
|
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.Image, template.Registry);
|
||||||
var containerConfiguration = service.createContainerConfiguration(template, containerName, network, containerMapping);
|
var containerConfiguration = service.createContainerConfiguration(template, containerName, network, containerMapping);
|
||||||
|
|
|
@ -301,6 +301,10 @@ ul.sidebar {
|
||||||
bottom: 40px;
|
bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.sidebar .sidebar-title {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
ul.sidebar .sidebar-list a.active {
|
ul.sidebar .sidebar-list a.active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-indent: 22px;
|
text-indent: 22px;
|
||||||
|
@ -308,6 +312,19 @@ ul.sidebar .sidebar-list a.active {
|
||||||
background: #2d3e63;
|
background: #2d3e63;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.sidebar .sidebar-list .sidebar-sublist a {
|
||||||
|
text-indent: 35px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #b2bfdc;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
||||||
|
color: #fff;
|
||||||
|
border-left: 3px solid #fff;
|
||||||
|
background: #2d3e63;
|
||||||
|
}
|
||||||
|
|
||||||
@media(min-width: 768px) and (max-width: 992px) {
|
@media(min-width: 768px) and (max-width: 992px) {
|
||||||
.margin-sm-top {
|
.margin-sm-top {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
|
Loading…
Reference in New Issue