mirror of https://github.com/portainer/portainer
feat(image-build): add the ability to build images (#1672)
parent
e065bd4a47
commit
81de2a5afb
|
@ -0,0 +1,36 @@
|
||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TarFileInBuffer will create a tar archive containing a single file named via fileName and using the content
|
||||||
|
// specified in fileContent. Returns the archive as a byte array.
|
||||||
|
func TarFileInBuffer(fileContent []byte, fileName string) ([]byte, error) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
tarWriter := tar.NewWriter(&buffer)
|
||||||
|
|
||||||
|
header := &tar.Header{
|
||||||
|
Name: fileName,
|
||||||
|
Mode: 0600,
|
||||||
|
Size: int64(len(fileContent)),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tarWriter.WriteHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tarWriter.Write(fileContent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tarWriter.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/archive"
|
||||||
|
)
|
||||||
|
|
||||||
|
type postDockerfileRequest struct {
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildOperation inspects the "Content-Type" header to determine if it needs to alter the request.
|
||||||
|
// If the value of the header is empty, it means that a Dockerfile is posted via upload, the function
|
||||||
|
// will extract the file content from the request body, tar it, and rewrite the body.
|
||||||
|
// If the value of the header contains "application/json", it means that the content of a Dockerfile is posted
|
||||||
|
// in the request payload as JSON, the function will create a new file called Dockerfile inside a tar archive and
|
||||||
|
// rewrite the body of the request.
|
||||||
|
// In any other case, it will leave the request unaltered.
|
||||||
|
func buildOperation(request *http.Request) error {
|
||||||
|
contentTypeHeader := request.Header.Get("Content-Type")
|
||||||
|
if contentTypeHeader != "" && !strings.Contains(contentTypeHeader, "application/json") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var dockerfileContent []byte
|
||||||
|
|
||||||
|
if contentTypeHeader == "" {
|
||||||
|
body, err := ioutil.ReadAll(request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dockerfileContent = body
|
||||||
|
} else {
|
||||||
|
var req postDockerfileRequest
|
||||||
|
if err := json.NewDecoder(request.Body).Decode(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dockerfileContent = []byte(req.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer, err := archive.TarFileInBuffer(dockerfileContent, "Dockerfile")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Body = ioutil.NopCloser(bytes.NewReader(buffer))
|
||||||
|
request.ContentLength = int64(len(buffer))
|
||||||
|
request.Header.Set("Content-Type", "application/x-tar")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ type (
|
||||||
labelBlackList []portainer.Pair
|
labelBlackList []portainer.Pair
|
||||||
}
|
}
|
||||||
restrictedOperationRequest func(*http.Request, *http.Response, *operationExecutor) error
|
restrictedOperationRequest func(*http.Request, *http.Response, *operationExecutor) error
|
||||||
|
operationRequest func(*http.Request) error
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
@ -59,6 +60,8 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
|
||||||
return p.proxyNodeRequest(request)
|
return p.proxyNodeRequest(request)
|
||||||
case strings.HasPrefix(path, "/tasks"):
|
case strings.HasPrefix(path, "/tasks"):
|
||||||
return p.proxyTaskRequest(request)
|
return p.proxyTaskRequest(request)
|
||||||
|
case strings.HasPrefix(path, "/build"):
|
||||||
|
return p.proxyBuildRequest(request)
|
||||||
default:
|
default:
|
||||||
return p.executeDockerRequest(request)
|
return p.executeDockerRequest(request)
|
||||||
}
|
}
|
||||||
|
@ -228,6 +231,10 @@ func (p *proxyTransport) proxyTaskRequest(request *http.Request) (*http.Response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *proxyTransport) proxyBuildRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
return p.interceptAndRewriteRequest(request, buildOperation)
|
||||||
|
}
|
||||||
|
|
||||||
// restrictedOperation ensures that the current user has the required authorizations
|
// restrictedOperation ensures that the current user has the required authorizations
|
||||||
// before executing the original request.
|
// before executing the original request.
|
||||||
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
|
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
|
||||||
|
@ -300,6 +307,15 @@ func (p *proxyTransport) rewriteOperation(request *http.Request, operation restr
|
||||||
return p.executeRequestAndRewriteResponse(request, operation, executor)
|
return p.executeRequestAndRewriteResponse(request, operation, executor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *proxyTransport) interceptAndRewriteRequest(request *http.Request, operation operationRequest) (*http.Response, error) {
|
||||||
|
err := operation(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
|
func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
|
||||||
response, err := p.executeDockerRequest(request)
|
response, err := p.executeDockerRequest(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -179,6 +179,17 @@ angular.module('portainer.docker', ['portainer.app'])
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var imageBuild = {
|
||||||
|
name: 'docker.images.build',
|
||||||
|
url: '/build',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/docker/views/images/build/buildimage.html',
|
||||||
|
controller: 'BuildImageController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var networks = {
|
var networks = {
|
||||||
name: 'docker.networks',
|
name: 'docker.networks',
|
||||||
url: '/networks',
|
url: '/networks',
|
||||||
|
@ -457,6 +468,7 @@ angular.module('portainer.docker', ['portainer.app'])
|
||||||
$stateRegistryProvider.register(events);
|
$stateRegistryProvider.register(events);
|
||||||
$stateRegistryProvider.register(images);
|
$stateRegistryProvider.register(images);
|
||||||
$stateRegistryProvider.register(image);
|
$stateRegistryProvider.register(image);
|
||||||
|
$stateRegistryProvider.register(imageBuild);
|
||||||
$stateRegistryProvider.register(networks);
|
$stateRegistryProvider.register(networks);
|
||||||
$stateRegistryProvider.register(network);
|
$stateRegistryProvider.register(network);
|
||||||
$stateRegistryProvider.register(networkCreation);
|
$stateRegistryProvider.register(networkCreation);
|
||||||
|
|
|
@ -25,6 +25,9 @@
|
||||||
<li><a ng-click="$ctrl.forceRemoveAction($ctrl.state.selectedItems, true)" ng-disabled="$ctrl.state.selectedItemCount === 0">Force Remove</a></li>
|
<li><a ng-click="$ctrl.forceRemoveAction($ctrl.state.selectedItems, true)" ng-disabled="$ctrl.state.selectedItemCount === 0">Force Remove</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.images.build">
|
||||||
|
<i class="fa fa-plus space-right" aria-hidden="true"></i>Build a new image
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
|
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
|
||||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
|
|
|
@ -8,3 +8,24 @@ function ImageViewModel(data) {
|
||||||
this.VirtualSize = data.VirtualSize;
|
this.VirtualSize = data.VirtualSize;
|
||||||
this.ContainerCount = data.ContainerCount;
|
this.ContainerCount = data.ContainerCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ImageBuildModel(data) {
|
||||||
|
this.hasError = false;
|
||||||
|
var buildLogs = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
var line = data[i];
|
||||||
|
|
||||||
|
if (line.stream) {
|
||||||
|
line = line.stream.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||||||
|
buildLogs.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.errorDetail) {
|
||||||
|
buildLogs.push(line.errorDetail.message);
|
||||||
|
this.hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buildLogs = buildLogs;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.factory('Build', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function BuildFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||||
|
'use strict';
|
||||||
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/build', {
|
||||||
|
endpointId: EndpointProvider.endpointID
|
||||||
|
},
|
||||||
|
{
|
||||||
|
buildImage: {
|
||||||
|
method: 'POST', ignoreLoadingBar: true,
|
||||||
|
transformResponse: jsonObjectsToArrayHandler, isArray: true,
|
||||||
|
headers: { 'Content-Type': 'application/x-tar' }
|
||||||
|
},
|
||||||
|
buildImageOverride: {
|
||||||
|
method: 'POST', ignoreLoadingBar: true,
|
||||||
|
transformResponse: jsonObjectsToArrayHandler, isArray: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -0,0 +1,10 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.factory('Commit', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function CommitFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||||
|
'use strict';
|
||||||
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/commit', {
|
||||||
|
endpointId: EndpointProvider.endpointID
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commitContainer: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}, ignoreLoadingBar: true}
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -1,10 +0,0 @@
|
||||||
angular.module('portainer.docker')
|
|
||||||
.factory('ContainerCommit', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ContainerCommitFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
|
||||||
'use strict';
|
|
||||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/commit', {
|
|
||||||
endpointId: EndpointProvider.endpointID
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commit: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}, ignoreLoadingBar: true}
|
|
||||||
});
|
|
||||||
}]);
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.factory('BuildService', ['$q', 'Build', 'FileUploadService', function BuildServiceFactory($q, Build, FileUploadService) {
|
||||||
|
'use strict';
|
||||||
|
var service = {};
|
||||||
|
|
||||||
|
service.buildImageFromUpload = function(names, file, path) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
FileUploadService.buildImage(names, file, path)
|
||||||
|
.then(function success(response) {
|
||||||
|
var model = new ImageBuildModel(response.data);
|
||||||
|
deferred.resolve(model);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.buildImageFromURL = function(names, url, path) {
|
||||||
|
var params = {
|
||||||
|
t: names,
|
||||||
|
remote: url,
|
||||||
|
dockerfile: path
|
||||||
|
};
|
||||||
|
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Build.buildImage(params, {}).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var model = new ImageBuildModel(data);
|
||||||
|
deferred.resolve(model);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.buildImageFromDockerfileContent = function(names, content) {
|
||||||
|
var params = {
|
||||||
|
t: names
|
||||||
|
};
|
||||||
|
var payload = {
|
||||||
|
content: content
|
||||||
|
};
|
||||||
|
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Build.buildImageOverride(params, payload).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var model = new ImageBuildModel(data);
|
||||||
|
deferred.resolve(model);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
return service;
|
||||||
|
}]);
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', 'Container', 'ContainerCommit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService',
|
.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', 'Container', 'Commit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService',
|
||||||
function ($q, $scope, $state, $transition$, $filter, Container, ContainerCommit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService) {
|
function ($q, $scope, $state, $transition$, $filter, Container, Commit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService) {
|
||||||
$scope.activityTime = 0;
|
$scope.activityTime = 0;
|
||||||
$scope.portBindings = [];
|
$scope.portBindings = [];
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ function ($q, $scope, $state, $transition$, $filter, Container, ContainerCommit,
|
||||||
var image = $scope.config.Image;
|
var image = $scope.config.Image;
|
||||||
var registry = $scope.config.Registry;
|
var registry = $scope.config.Registry;
|
||||||
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL);
|
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL);
|
||||||
ContainerCommit.commit({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
|
Commit.commitContainer({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
|
||||||
update();
|
update();
|
||||||
Notifications.success('Container commited', $transition$.params().id);
|
Notifications.success('Container commited', $transition$.params().id);
|
||||||
}, function (e) {
|
}, function (e) {
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.controller('BuildImageController', ['$scope', '$state', 'BuildService', 'Notifications',
|
||||||
|
function ($scope, $state, BuildService, Notifications) {
|
||||||
|
|
||||||
|
$scope.state = {
|
||||||
|
BuildType: 'editor',
|
||||||
|
actionInProgress: false,
|
||||||
|
activeTab: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.formValues = {
|
||||||
|
ImageNames: [{ Name: '' }],
|
||||||
|
UploadFile: null,
|
||||||
|
DockerFileContent: '',
|
||||||
|
URL: '',
|
||||||
|
Path: 'Dockerfile'
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.addImageName = function() {
|
||||||
|
$scope.formValues.ImageNames.push({ Name: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.removeImageName = function(index) {
|
||||||
|
$scope.formValues.ImageNames.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildImageBasedOnBuildType(method, names) {
|
||||||
|
var buildType = $scope.state.BuildType;
|
||||||
|
var dockerfilePath = $scope.formValues.Path;
|
||||||
|
|
||||||
|
if (buildType === 'upload') {
|
||||||
|
var file = $scope.formValues.UploadFile;
|
||||||
|
return BuildService.buildImageFromUpload(names, file, dockerfilePath);
|
||||||
|
} else if (buildType === 'url') {
|
||||||
|
var URL = $scope.formValues.URL;
|
||||||
|
return BuildService.buildImageFromURL(names, URL, dockerfilePath);
|
||||||
|
} else {
|
||||||
|
var dockerfileContent = $scope.formValues.DockerFileContent;
|
||||||
|
return BuildService.buildImageFromDockerfileContent(names, dockerfileContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.buildImage = function() {
|
||||||
|
var buildType = $scope.state.BuildType;
|
||||||
|
|
||||||
|
if (buildType === 'editor' && $scope.formValues.DockerFileContent === '') {
|
||||||
|
$scope.state.formValidationError = 'Dockerfile content must not be empty';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.state.actionInProgress = true;
|
||||||
|
|
||||||
|
var imageNames = $scope.formValues.ImageNames.filter(function filterNull(x) {
|
||||||
|
return x.Name;
|
||||||
|
}).map(function getNames(x) {
|
||||||
|
return x.Name;
|
||||||
|
});
|
||||||
|
|
||||||
|
buildImageBasedOnBuildType(buildType, imageNames)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.buildLogs = data.buildLogs;
|
||||||
|
$scope.state.activeTab = 1;
|
||||||
|
if (data.hasError) {
|
||||||
|
Notifications.error('An error occured during build', { msg: 'Please check build logs output' });
|
||||||
|
} else {
|
||||||
|
Notifications.success('Image successfully built');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to build image');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$scope.state.actionInProgress = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.validImageNames = function() {
|
||||||
|
for (var i = 0; i < $scope.formValues.ImageNames.length; i++) {
|
||||||
|
var item = $scope.formValues.ImageNames[i];
|
||||||
|
if (item.Name !== '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.editorUpdate = function(cm) {
|
||||||
|
$scope.formValues.DockerFileContent = cm.getValue();
|
||||||
|
};
|
||||||
|
}]);
|
|
@ -0,0 +1,231 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Build image"></rd-header-title>
|
||||||
|
<rd-header-content>
|
||||||
|
<a ui-sref="docker.images">Images</a> > Build image
|
||||||
|
</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-body>
|
||||||
|
<uib-tabset active="state.activeTab">
|
||||||
|
<uib-tab index="0">
|
||||||
|
<uib-tab-heading>
|
||||||
|
<i class="fa fa-wrench space-right" aria-hidden="true"></i> Builder
|
||||||
|
</uib-tab-heading>
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Naming
|
||||||
|
</div>
|
||||||
|
<!-- names -->
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
You can specify multiple names to your image.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label class="control-label text-left">Names</label>
|
||||||
|
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addImageName()">
|
||||||
|
<i class="fa fa-plus-circle" aria-hidden="true"></i> add additional name
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !names -->
|
||||||
|
<div class="form-group" ng-if="formValues.ImageNames.length === 0">
|
||||||
|
<span class="col-sm-12 text-danger small">
|
||||||
|
<i class="fa fa-exclamation-triangle space-right" aria-hidden="true"></i>You must specify at least one name for the image.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- name-input-list -->
|
||||||
|
<div ng-if="formValues.ImageNames.length > 0">
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
A name must be specified in one of the following formats: <code>name:tag</code>, <code>repository/name:tag</code> or <code>registryfqdn:port/repository/name:tag</code> format. If you omit the tag the default <b>latest</b> value is assumed.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" >
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||||
|
<div ng-repeat="item in formValues.ImageNames track by $index" style="margin-top: 2px;">
|
||||||
|
<!-- name-input -->
|
||||||
|
<div class="input-group col-sm-5 input-group-sm">
|
||||||
|
<span class="input-group-addon">name</span>
|
||||||
|
<input type="text" class="form-control" ng-model="item.Name" placeholder="e.g. myImage:myTag" auto-focus>
|
||||||
|
<span class="input-group-addon"><i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[item.Name !== '']" aria-hidden="true"></i></span>
|
||||||
|
</div>
|
||||||
|
<!-- !name-input -->
|
||||||
|
<!-- actions -->
|
||||||
|
<div class="input-group col-sm-2 input-group-sm">
|
||||||
|
<button class="btn btn-sm btn-danger" type="button" ng-click="removeImageName($index)">
|
||||||
|
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- !actions -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !name-input-list -->
|
||||||
|
<!-- build-method -->
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Build method
|
||||||
|
</div>
|
||||||
|
<div class="form-group"></div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0">
|
||||||
|
<div class="boxselector_wrapper">
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="method_editor" ng-model="state.BuildType" value="editor" ng-click="toggleEditor()">
|
||||||
|
<label for="method_editor">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Web editor
|
||||||
|
</div>
|
||||||
|
<p>Use our Web editor</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="method_upload" ng-model="state.BuildType" value="upload" ng-click="saveEditorContent()">
|
||||||
|
<label for="method_upload">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-upload" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Upload
|
||||||
|
</div>
|
||||||
|
<p>Upload a tarball or a Dockerfile from your computer</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="method_url" ng-model="state.BuildType" value="url" ng-click="saveEditorContent()">
|
||||||
|
<label for="method_url">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-globe" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
URL
|
||||||
|
</div>
|
||||||
|
<p>Specify an URL to a file</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !build-method -->
|
||||||
|
<!-- web-editor -->
|
||||||
|
<div ng-show="state.BuildType === 'editor'">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Web editor
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
You can get more information about Dockerfile format in the <a href="https://docs.docker.com/engine/reference/builder/" target="_blank">official documentation</a>.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<code-editor
|
||||||
|
identifier="image-build-editor"
|
||||||
|
placeholder="Define or paste the content of your Dockerfile here"
|
||||||
|
yml="false"
|
||||||
|
on-change="editorUpdate"
|
||||||
|
></code-editor>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !web-editor -->
|
||||||
|
<!-- upload -->
|
||||||
|
<div ng-show="state.BuildType === 'upload'">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Upload
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
You can upload a Dockerfile or a tar archive containing a Dockerfile from your computer. When using an tarball, the root folder will be used as the build context.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.UploadFile">Select file</button>
|
||||||
|
<span style="margin-left: 5px;">
|
||||||
|
{{ formValues.UploadFile.name }}
|
||||||
|
<i class="fa fa-times red-icon" ng-if="!formValues.UploadFile" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ng-if="formValues.UploadFile.type === 'application/gzip' || formValues.UploadFile.type === 'application/x-tar'">
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
Indicate the path to the Dockerfile within the tarball.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="image_path" class="col-sm-2 control-label text-left">Dockerfile path</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" ng-model="formValues.Path" id="image_path" placeholder="Dockerfile">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !upload -->
|
||||||
|
<!-- url -->
|
||||||
|
<div ng-show="state.BuildType === 'url'">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
URL
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
Specify the URL to a Dockerfile, a tarball or a public Git repository (suffixed by <b>.git</b>). When using a tarball or a Git repository URL, the root folder will be used as the build context.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="image_url" class="col-sm-2 control-label text-left">URL</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" ng-model="formValues.URL" id="image_url" placeholder="https://myhost.mydomain/myimage.tar.gz">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
Indicate the path to the Dockerfile within the tarball/repository (ignored when using a Dockerfile).
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="image_path" class="col-sm-2 control-label text-left">Dockerfile path</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" ng-model="formValues.Path" id="image_path" placeholder="Dockerfile">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !url -->
|
||||||
|
<!-- actions -->
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Actions
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm"
|
||||||
|
ng-disabled="state.actionInProgress
|
||||||
|
|| (state.BuildType === 'upload' && (!formValues.UploadFile || !formValues.Path))
|
||||||
|
|| (state.BuildType === 'url' && (!formValues.URL || !formValues.Path))
|
||||||
|
|| (formValues.ImageNames.length === 0 || !validImageNames())"
|
||||||
|
ng-click="buildImage()" button-spinner="state.actionInProgress">
|
||||||
|
<span ng-hide="state.actionInProgress">Build the image</span>
|
||||||
|
<span ng-show="state.actionInProgress">Image building in progress...</span>
|
||||||
|
</button>
|
||||||
|
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !actions -->
|
||||||
|
</form>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab index="1" disable="!buildLogs">
|
||||||
|
<uib-tab-heading>
|
||||||
|
<i class="fa fa-file-text space-right" aria-hidden="true"></i> Output
|
||||||
|
</uib-tab-heading>
|
||||||
|
<pre class="log_viewer">
|
||||||
|
<div ng-repeat="line in buildLogs track by $index" class="line"><p class="inner_line" ng-click="active=!active" ng-class="{'line_selected': active}">{{ line }}</p></div>
|
||||||
|
<div ng-if="!buildLogs.length" class="line"><p class="inner_line">No build output available.</p></div>
|
||||||
|
</pre>
|
||||||
|
</uib-tab>
|
||||||
|
</uib-tabset>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -2,6 +2,8 @@ angular.module('portainer.app')
|
||||||
.factory('CodeMirrorService', function CodeMirrorService() {
|
.factory('CodeMirrorService', function CodeMirrorService() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var service = {};
|
||||||
|
|
||||||
var codeMirrorGenericOptions = {
|
var codeMirrorGenericOptions = {
|
||||||
lineNumbers: true
|
lineNumbers: true
|
||||||
};
|
};
|
||||||
|
@ -12,8 +14,6 @@ angular.module('portainer.app')
|
||||||
lint: true
|
lint: true
|
||||||
};
|
};
|
||||||
|
|
||||||
var service = {};
|
|
||||||
|
|
||||||
service.applyCodeMirrorOnElement = function(element, yamlLint, readOnly) {
|
service.applyCodeMirrorOnElement = function(element, yamlLint, readOnly) {
|
||||||
var options = angular.copy(codeMirrorGenericOptions);
|
var options = angular.copy(codeMirrorGenericOptions);
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,26 @@ angular.module('portainer.app')
|
||||||
return Upload.upload({ url: url, data: { file: file }});
|
return Upload.upload({ url: url, data: { file: file }});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
service.buildImage = function(names, file, path) {
|
||||||
|
var endpointID = EndpointProvider.endpointID();
|
||||||
|
Upload.setDefaults({ ngfMinSize: 10 });
|
||||||
|
return Upload.http({
|
||||||
|
url: 'api/endpoints/' + endpointID + '/docker/build',
|
||||||
|
headers : {
|
||||||
|
'Content-Type': file.type
|
||||||
|
},
|
||||||
|
data: file,
|
||||||
|
params: {
|
||||||
|
t: names,
|
||||||
|
dockerfile: path
|
||||||
|
},
|
||||||
|
ignoreLoadingBar: true,
|
||||||
|
transformResponse: function(data, headers) {
|
||||||
|
return jsonObjectsToArrayHandler(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
service.createStack = function(stackName, swarmId, file, env) {
|
service.createStack = function(stackName, swarmId, file, env) {
|
||||||
var endpointID = EndpointProvider.endpointID();
|
var endpointID = EndpointProvider.endpointID();
|
||||||
return Upload.upload({
|
return Upload.upload({
|
||||||
|
|
|
@ -693,6 +693,30 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log_viewer {
|
||||||
|
height:100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: black;
|
||||||
|
font-size: .85em;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_viewer .inner_line {
|
||||||
|
padding: 0 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_viewer .inner_line:empty::after {
|
||||||
|
content: '.';
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log_viewer .line_selected {
|
||||||
|
background-color: #C5CAE9;
|
||||||
|
}
|
||||||
|
|
||||||
/*bootbox override*/
|
/*bootbox override*/
|
||||||
.modal-open {
|
.modal-open {
|
||||||
padding-right: 0 !important;
|
padding-right: 0 !important;
|
||||||
|
|
Loading…
Reference in New Issue