diff --git a/app/app.js b/app/app.js index 531cf4515..7596fece0 100644 --- a/app/app.js +++ b/app/app.js @@ -12,7 +12,7 @@ angular.module('dockerui', ['dockerui.templates', 'ngRoute', 'dockerui.services' }]) // This is your docker url that the api will use to make requests // You need to set this to the api endpoint without the port i.e. http://192.168.1.9 - .constant('DOCKER_ENDPOINT', '/dockerapi') + .constant('DOCKER_ENDPOINT', 'dockerapi') .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243 .constant('UI_VERSION', 'v0.6.0') .constant('DOCKER_API_VERSION', 'v1.16'); diff --git a/app/components/startContainer/startContainerController.js b/app/components/startContainer/startContainerController.js index fc99597af..f913d55a6 100644 --- a/app/components/startContainer/startContainerController.js +++ b/app/components/startContainer/startContainerController.js @@ -1,45 +1,126 @@ -angular.module('startContainer', []) -.controller('StartContainerController', ['$scope', '$routeParams', '$location', 'Container', 'Messages', -function($scope, $routeParams, $location, Container, Messages) { +angular.module('startContainer', ['ui.bootstrap']) +.controller('StartContainerController', ['$scope', '$routeParams', '$location', 'Container', 'Messages', 'containernameFilter', 'errorMsgFilter', +function($scope, $routeParams, $location, Container, Messages, containernameFilter, errorMsgFilter) { $scope.template = 'app/components/startContainer/startcontainer.html'; + + Container.query({all: 1}, function(d) { + $scope.containerNames = d.map(function(container){ + return containernameFilter(container); + }); + }); + $scope.config = { - name: '', - memory: 0, - memorySwap: 0, - cpuShares: 1024, - env: '', - commands: '', - volumesFrom: '' + Env: [], + Volumes: [], + SecurityOpts: [], + HostConfig: { + PortBindings: [], + Binds: [], + Links: [], + Dns: [], + DnsSearch: [], + VolumesFrom: [], + CapAdd: [], + CapDrop: [], + Devices: [], + LxcConf: [], + ExtraHosts: [] + } + }; + + $scope.menuStatus = { + containerOpen: true, + hostConfigOpen: false }; - $scope.commandPlaceholder = '["/bin/echo", "Hello world"]'; function failedRequestHandler(e, Messages) { - Messages.send({class: 'text-error', data: e.data}); + Messages.error('Error', errorMsgFilter(e)); + } + + function rmEmptyKeys(col) { + for (var key in col) { + if (col[key] === null || col[key] === undefined || col[key] === '' || $.isEmptyObject(col[key]) || col[key].length === 0) { + delete col[key]; + } + } + } + + function getNames(arr) { + return arr.map(function(item) {return item.name;}); } $scope.create = function() { - var cmds = null; - if ($scope.config.commands !== '') { - cmds = angular.fromJson($scope.config.commands); + // Copy the config before transforming fields to the remote API format + var config = angular.copy($scope.config); + + config.Image = $routeParams.id; + + if (config.Cmd && config.Cmd[0] === "[") { + config.Cmd = angular.fromJson(config.Cmd); + } else if (config.Cmd) { + config.Cmd = config.Cmd.split(' '); } - var id = $routeParams.id; + + config.Env = config.Env.map(function(envar) {return envar.name + '=' + envar.value;}); + + config.Volumes = getNames(config.Volumes); + config.SecurityOpts = getNames(config.SecurityOpts); + + config.HostConfig.VolumesFrom = getNames(config.HostConfig.VolumesFrom); + config.HostConfig.Binds = getNames(config.HostConfig.Binds); + config.HostConfig.Links = getNames(config.HostConfig.Links); + config.HostConfig.Dns = getNames(config.HostConfig.Dns); + config.HostConfig.DnsSearch = getNames(config.HostConfig.DnsSearch); + config.HostConfig.CapAdd = getNames(config.HostConfig.CapAdd); + config.HostConfig.CapDrop = getNames(config.HostConfig.CapDrop); + config.HostConfig.LxcConf = config.HostConfig.LxcConf.reduce(function(prev, cur, idx){ + prev[cur.name] = cur.value; + return prev; + }, {}); + config.HostConfig.ExtraHosts = config.HostConfig.ExtraHosts.map(function(entry) {return entry.host + ':' + entry.ip;}); + + var ExposedPorts = {}; + var PortBindings = {}; + config.HostConfig.PortBindings.forEach(function(portBinding) { + var intPort = portBinding.intPort + "/tcp"; + var binding = { + HostIp: portBinding.ip, + HostPort: portBinding.extPort + }; + if (portBinding.intPort) { + ExposedPorts[intPort] = {}; + if (intPort in PortBindings) { + PortBindings[intPort].push(binding); + } else { + PortBindings[intPort] = [binding]; + } + } else { + Messages.send('Warning', 'Internal port must be specified for PortBindings'); + } + }); + config.ExposedPorts = ExposedPorts; + config.HostConfig.PortBindings = PortBindings; + + // Remove empty fields from the request to avoid overriding defaults + rmEmptyKeys(config.HostConfig); + rmEmptyKeys(config); + var ctor = Container; var loc = $location; var s = $scope; - - Container.create({ - Image: id, - name: $scope.config.name, - Memory: $scope.config.memory, - MemorySwap: $scope.config.memorySwap, - CpuShares: $scope.config.cpuShares, - Cmd: cmds, - VolumesFrom: $scope.config.volumesFrom - }, function(d) { + Container.create(config, function(d) { if (d.Id) { ctor.start({id: d.Id}, function(cd) { - $('#create-modal').modal('hide'); - loc.path('/containers/' + d.Id + '/'); + if (cd.id) { + Messages.send('Container Started', d.Id); + $('#create-modal').modal('hide'); + loc.path('/containers/' + d.Id + '/'); + } else { + failedRequestHandler(cd, Messages); + ctor.remove({id: d.Id}, function() { + Messages.send('Container Removed', d.Id); + }); + } }, function(e) { failedRequestHandler(e, Messages); }); @@ -50,4 +131,12 @@ function($scope, $routeParams, $location, Container, Messages) { failedRequestHandler(e, Messages); }); }; + + $scope.addEntry = function(array, entry) { + array.push(entry); + }; + $scope.rmEntry = function(array, entry) { + var idx = array.indexOf(entry); + array.splice(idx, 1); + }; }]); diff --git a/app/components/startContainer/startContainerController.spec.js b/app/components/startContainer/startContainerController.spec.js new file mode 100644 index 000000000..6b359306e --- /dev/null +++ b/app/components/startContainer/startContainerController.spec.js @@ -0,0 +1,201 @@ +describe('startContainerController', function() { + var scope, $location, createController, mockContainer, $httpBackend; + + beforeEach(angular.mock.module('dockerui')); + + beforeEach(inject(function($rootScope, $controller, _$location_) { + $location = _$location_; + scope = $rootScope.$new(); + + createController = function() { + return $controller('StartContainerController', { + '$scope': scope + }); + }; + + angular.mock.inject(function(_Container_, _$httpBackend_) { + mockContainer = _Container_; + $httpBackend = _$httpBackend_; + }); + })); + function expectGetContainers() { + $httpBackend.expectGET('dockerapi/containers/json?all=1').respond([{ + 'Command': './dockerui -e /docker.sock', + 'Created': 1421817232, + 'Id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f', + 'Image': 'dockerui:latest', + 'Names': ['/dockerui'], + 'Ports': [{ + 'IP': '0.0.0.0', + 'PrivatePort': 9000, + 'PublicPort': 9000, + 'Type': 'tcp' + }], + 'Status': 'Up 2 minutes' + }]); + } + describe('Create and start a container with port bindings', function() { + it('should issue a correct create request to the Docker remote API', function() { + var controller = createController(); + var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c'; + var expectedBody = { + 'name': 'container-name', + 'ExposedPorts': { + '9000/tcp': {}, + }, + 'HostConfig': { + 'PortBindings': { + '9000/tcp': [{ + 'HostPort': '9999', + 'HostIp': '10.20.10.15', + }] + }, + } + }; + + expectGetContainers(); + + $httpBackend.expectPOST('dockerapi/containers/create?name=container-name', expectedBody).respond({ + 'Id': id, + 'Warnings': null + }); + $httpBackend.expectPOST('dockerapi/containers/' + id + '/start?').respond({ + 'id': id, + 'Warnings': null + }); + + scope.config.name = 'container-name'; + scope.config.HostConfig.PortBindings = [{ + ip: '10.20.10.15', + extPort: '9999', + intPort: '9000' + }] + + scope.create(); + $httpBackend.flush(); + }); + }); + + describe('Create and start a container with environment variables', function() { + it('should issue a correct create request to the Docker remote API', function() { + var controller = createController(); + var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c'; + var expectedBody = { + 'name': 'container-name', + 'Env': ['SHELL=/bin/bash', 'TERM=xterm-256color'] + }; + + expectGetContainers(); + + $httpBackend.expectPOST('dockerapi/containers/create?name=container-name', expectedBody).respond({ + 'Id': id, + 'Warnings': null + }); + $httpBackend.expectPOST('dockerapi/containers/' + id + '/start?').respond({ + 'id': id, + 'Warnings': null + }); + + scope.config.name = 'container-name'; + scope.config.Env = [{ + name: 'SHELL', + value: '/bin/bash' + }, { + name: 'TERM', + value: 'xterm-256color' + }]; + + scope.create(); + $httpBackend.flush(); + }); + }); + + describe('Create and start a container with volumesFrom', function() { + it('should issue a correct create request to the Docker remote API', function() { + var controller = createController(); + var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c'; + var expectedBody = { + HostConfig: { + 'VolumesFrom': ['parent', 'other:ro'] + }, + 'name': 'container-name' + }; + + expectGetContainers(); + + $httpBackend.expectPOST('dockerapi/containers/create?name=container-name', expectedBody).respond({ + 'Id': id, + 'Warnings': null + }); + $httpBackend.expectPOST('dockerapi/containers/' + id + '/start?').respond({ + 'id': id, + 'Warnings': null + }); + + scope.config.name = 'container-name'; + scope.config.HostConfig.VolumesFrom = [{name: 'parent'}, {name:'other:ro'}]; + + scope.create(); + $httpBackend.flush(); + }); + }); + + describe('Create and start a container with multiple options', function() { + it('should issue a correct create request to the Docker remote API', function() { + var controller = createController(); + var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c'; + var expectedBody = { + Volumes: ['/var/www'], + SecurityOpts: ['label:type:svirt_apache'], + HostConfig: { + Binds: ['/app:/app'], + Links: ['web:db'], + Dns: ['8.8.8.8'], + DnsSearch: ['example.com'], + CapAdd: ['cap_sys_admin'], + CapDrop: ['cap_foo_bar'], + Devices: [{ 'PathOnHost': '/dev/deviceName', 'PathInContainer': '/dev/deviceName', 'CgroupPermissions': 'mrw'}], + LxcConf: {'lxc.utsname':'docker'}, + ExtraHosts: ['hostname:127.0.0.1'], + RestartPolicy: {name: 'always', MaximumRetryCount: 5} + }, + name: 'container-name' + }; + + expectGetContainers(); + + $httpBackend.expectPOST('dockerapi/containers/create?name=container-name', expectedBody).respond({ + 'Id': id, + 'Warnings': null + }); + $httpBackend.expectPOST('dockerapi/containers/' + id + '/start?').respond({ + 'id': id, + 'Warnings': null + }); + + scope.config.name = 'container-name'; + scope.config.Volumes = [{name: '/var/www'}]; + scope.config.SecurityOpts = [{name: 'label:type:svirt_apache'}]; + scope.config.NetworkDisabled = true; + scope.config.Tty = true; + scope.config.OpenStdin = true; + scope.config.StdinOnce = true; + + scope.config.HostConfig.Binds = [{name: '/app:/app'}]; + scope.config.HostConfig.Links = [{name: 'web:db'}]; + scope.config.HostConfig.Dns = [{name: '8.8.8.8'}]; + scope.config.HostConfig.DnsSearch = [{name: 'example.com'}]; + scope.config.HostConfig.CapAdd = [{name: 'cap_sys_admin'}]; + scope.config.HostConfig.CapDrop = [{name: 'cap_foo_bar'}]; + scope.config.HostConfig.PublishAllPorts = true; + scope.config.HostConfig.Privileged = true; + scope.config.HostConfig.RestartPolicy = {name: 'always', MaximumRetryCount: 5}; + scope.config.HostConfig.Devices = [{ 'PathOnHost': '/dev/deviceName', 'PathInContainer': '/dev/deviceName', 'CgroupPermissions': 'mrw'}]; + scope.config.HostConfig.LxcConf = [{name: 'lxc.utsname', value: 'docker'}]; + scope.config.HostConfig.ExtraHosts = [{host: 'hostname', ip: '127.0.0.1'}]; + + scope.create(); + $httpBackend.flush(); + }); + }); +}); \ No newline at end of file diff --git a/app/components/startContainer/startcontainer.html b/app/components/startContainer/startcontainer.html index 42f5f5a7c..39285dcff 100644 --- a/app/components/startContainer/startcontainer.html +++ b/app/components/startContainer/startcontainer.html @@ -6,38 +6,301 @@