mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
feat(global): add templates support ('apps') (#154)
This commit is contained in:
parent
faccf2a651
commit
1c8aa35479
15 changed files with 366 additions and 13 deletions
76
app/components/templates/templates.html
Normal file
76
app/components/templates/templates.html
Normal file
|
@ -0,0 +1,76 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Apps list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Apps</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-rocket" title="Available apps">
|
||||
<div class="pull-right">
|
||||
<i id="loadTemplatesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<div class="template-list">
|
||||
<div ng-repeat="tpl in templates" class="container-template hvr-grow" id="template_{{ $index }}" ng-click="selectTemplate($index)">
|
||||
<img class="logo" ng-src="{{ tpl.logo }}" />
|
||||
<div class="title">{{ tpl.title }}</div>
|
||||
<div class="comment">{{ tpl.comment }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="selectedTemplate">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-cogs" title="Configuration"></rd-widget-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<!-- name-and-network-inputs -->
|
||||
<div class="form-group">
|
||||
<label for="image_registry" class="col-sm-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)">
|
||||
</div>
|
||||
<label for="container_network" class="col-sm-2 control-label text-right">Network</label>
|
||||
<div class="col-sm-4">
|
||||
<select class="selectpicker form-control" ng-model="formValues.network">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-and-network-inputs -->
|
||||
<div ng-repeat="var in selectedTemplate.env" class="form-group">
|
||||
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="selectedTemplate">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
|
||||
<div>
|
||||
<i id="createContainerSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default btn-lg" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
|
||||
</div>
|
||||
</div>
|
170
app/components/templates/templatesController.js
Normal file
170
app/components/templates/templatesController.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
angular.module('templates', [])
|
||||
.controller('TemplatesController', ['$scope', '$q', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Templates', 'Messages', 'errorMsgFilter',
|
||||
function ($scope, $q, $state, Config, Container, Image, Volume, Network, Templates, Messages, errorMsgFilter) {
|
||||
$scope.templates = [];
|
||||
$scope.selectedTemplate = null;
|
||||
$scope.formValues = {
|
||||
network: "",
|
||||
name: ""
|
||||
};
|
||||
|
||||
var selectedItem = -1;
|
||||
|
||||
// TODO: centralize, already present in createContainerController
|
||||
function createContainer(config) {
|
||||
Container.create(config, function (d) {
|
||||
if (d.Id) {
|
||||
var reqBody = config.HostConfig || {};
|
||||
reqBody.id = d.Id;
|
||||
Container.start(reqBody, function (cd) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.send('Container Started', d.Id);
|
||||
$state.go('containers', {}, {reload: true});
|
||||
}, function (e) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', errorMsgFilter(e));
|
||||
});
|
||||
} else {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', errorMsgFilter(d));
|
||||
}
|
||||
}, function (e) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', errorMsgFilter(e));
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: centralize, already present in createContainerController
|
||||
function pullImageAndCreateContainer(imageConfig, containerConfig) {
|
||||
Image.create(imageConfig, function (data) {
|
||||
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
||||
if (err) {
|
||||
var detail = data[data.length - 1];
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', detail.error);
|
||||
} else {
|
||||
createContainer(containerConfig);
|
||||
}
|
||||
}, function (e) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', 'Unable to pull image ' + image);
|
||||
});
|
||||
}
|
||||
|
||||
function createConfigFromTemplate(template) {
|
||||
var containerConfig = {
|
||||
Env: [],
|
||||
OpenStdin: false,
|
||||
Tty: false,
|
||||
ExposedPorts: {},
|
||||
HostConfig: {
|
||||
RestartPolicy: {
|
||||
Name: 'no'
|
||||
},
|
||||
PortBindings: {},
|
||||
Binds: [],
|
||||
NetworkMode: $scope.formValues.network,
|
||||
Privileged: false
|
||||
},
|
||||
Image: template.image,
|
||||
Volumes: {},
|
||||
name: $scope.formValues.name
|
||||
};
|
||||
if (template.env) {
|
||||
template.env.forEach(function (v) {
|
||||
if (v.value) {
|
||||
containerConfig.Env.push(v.name + "=" + v.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (template.ports) {
|
||||
template.ports.forEach(function (p) {
|
||||
containerConfig.ExposedPorts[p] = {};
|
||||
containerConfig.HostConfig.PortBindings[p] = [{ HostPort: ""}];
|
||||
});
|
||||
}
|
||||
return containerConfig;
|
||||
}
|
||||
|
||||
function prepareVolumeQueries(template, containerConfig) {
|
||||
var volumeQueries = [];
|
||||
if (template.volumes) {
|
||||
template.volumes.forEach(function (vol) {
|
||||
volumeQueries.push(
|
||||
Volume.create({}, function (d) {
|
||||
if (d.Name) {
|
||||
Messages.send("Volume created", d.Name);
|
||||
containerConfig.Volumes[vol] = {};
|
||||
containerConfig.HostConfig.Binds.push(d.Name + ':' + vol);
|
||||
} else {
|
||||
Messages.error('Unable to create volume', errorMsgFilter(d));
|
||||
}
|
||||
}, function (e) {
|
||||
Messages.error('Unable to create volume', e.data);
|
||||
}).$promise
|
||||
);
|
||||
});
|
||||
}
|
||||
return volumeQueries;
|
||||
}
|
||||
|
||||
$scope.createTemplate = function() {
|
||||
$('#createContainerSpinner').show();
|
||||
var template = $scope.selectedTemplate;
|
||||
var containerConfig = createConfigFromTemplate(template);
|
||||
var imageConfig = {
|
||||
fromImage: template.image.split(':')[0],
|
||||
tag: template.image.split(':')[1] ? template.image.split(':')[1] : 'latest'
|
||||
};
|
||||
var createVolumeQueries = prepareVolumeQueries(template, containerConfig);
|
||||
$q.all(createVolumeQueries).then(function (d) {
|
||||
pullImageAndCreateContainer(imageConfig, containerConfig);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.selectTemplate = function(id) {
|
||||
$('#template_' + id).toggleClass("container-template--selected");
|
||||
if (selectedItem === id) {
|
||||
selectedItem = -1;
|
||||
$scope.selectedTemplate = null;
|
||||
} else {
|
||||
$('#template_' + selectedItem).toggleClass("container-template--selected");
|
||||
selectedItem = id;
|
||||
$scope.selectedTemplate = $scope.templates[id];
|
||||
}
|
||||
};
|
||||
|
||||
function initTemplates() {
|
||||
Templates.get(function (data) {
|
||||
$scope.templates = data;
|
||||
$('#loadTemplatesSpinner').hide();
|
||||
}, function (e) {
|
||||
$('#loadTemplatesSpinner').hide();
|
||||
Messages.error("Unable to retrieve apps list", e.data);
|
||||
});
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
var swarm = c.swarm;
|
||||
Network.query({}, function (d) {
|
||||
var networks = d;
|
||||
if (swarm) {
|
||||
networks = d.filter(function (network) {
|
||||
if (network.Scope === 'global') {
|
||||
return network;
|
||||
}
|
||||
});
|
||||
$scope.globalNetworkCount = networks.length;
|
||||
networks.push({Name: "bridge"});
|
||||
networks.push({Name: "host"});
|
||||
networks.push({Name: "none"});
|
||||
} else {
|
||||
$scope.formValues.network = "bridge";
|
||||
}
|
||||
$scope.availableNetworks = networks;
|
||||
}, function (e) {
|
||||
Messages.error("Unable to retrieve available networks", e.data);
|
||||
});
|
||||
initTemplates();
|
||||
});
|
||||
}]);
|
Loading…
Add table
Add a link
Reference in a new issue