mirror of https://github.com/portainer/portainer
feat(image): upload local files for building image EE-3021 (#7507)
* support to make multiple files in archive buffer * upload files by multipartpull/7661/head
parent
a7d458f0bd
commit
d570aee554
|
@ -34,3 +34,45 @@ func TarFileInBuffer(fileContent []byte, fileName string, mode int64) ([]byte, e
|
||||||
|
|
||||||
return buffer.Bytes(), nil
|
return buffer.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tarFileInBuffer represents a tar archive buffer.
|
||||||
|
type tarFileInBuffer struct {
|
||||||
|
b *bytes.Buffer
|
||||||
|
w *tar.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTarFileInBuffer() *tarFileInBuffer {
|
||||||
|
var b bytes.Buffer
|
||||||
|
return &tarFileInBuffer{
|
||||||
|
b: &b,
|
||||||
|
w: tar.NewWriter(&b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put puts a single file to tar archive buffer.
|
||||||
|
func (t *tarFileInBuffer) Put(fileContent []byte, fileName string, mode int64) error {
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: fileName,
|
||||||
|
Mode: mode,
|
||||||
|
Size: int64(len(fileContent)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.w.WriteHeader(hdr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := t.w.Write(fileContent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the archive as a byte array.
|
||||||
|
func (t *tarFileInBuffer) Bytes() []byte {
|
||||||
|
return t.b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tarFileInBuffer) Close() error {
|
||||||
|
return t.w.Close()
|
||||||
|
}
|
||||||
|
|
|
@ -3,13 +3,17 @@ package docker
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/archive"
|
"github.com/portainer/portainer/api/archive"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const OneMegabyte = 1024768
|
||||||
|
|
||||||
type postDockerfileRequest struct {
|
type postDockerfileRequest struct {
|
||||||
Content string
|
Content string
|
||||||
}
|
}
|
||||||
|
@ -23,29 +27,81 @@ type postDockerfileRequest struct {
|
||||||
// In any other case, it will leave the request unaltered.
|
// In any other case, it will leave the request unaltered.
|
||||||
func buildOperation(request *http.Request) error {
|
func buildOperation(request *http.Request) error {
|
||||||
contentTypeHeader := request.Header.Get("Content-Type")
|
contentTypeHeader := request.Header.Get("Content-Type")
|
||||||
if contentTypeHeader != "" && !strings.Contains(contentTypeHeader, "application/json") {
|
|
||||||
return nil
|
mediaType, _, err := mime.ParseMediaType(contentTypeHeader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var dockerfileContent []byte
|
var buffer []byte
|
||||||
|
switch mediaType {
|
||||||
if contentTypeHeader == "" {
|
case "":
|
||||||
body, err := ioutil.ReadAll(request.Body)
|
body, err := ioutil.ReadAll(request.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dockerfileContent = body
|
|
||||||
} else {
|
buffer, err = archive.TarFileInBuffer(body, "Dockerfile", 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case "application/json":
|
||||||
var req postDockerfileRequest
|
var req postDockerfileRequest
|
||||||
if err := json.NewDecoder(request.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(request.Body).Decode(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dockerfileContent = []byte(req.Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer, err := archive.TarFileInBuffer(dockerfileContent, "Dockerfile", 0600)
|
buffer, err = archive.TarFileInBuffer([]byte(req.Content), "Dockerfile", 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case "multipart/form-data":
|
||||||
|
err := request.ParseMultipartForm(32 * OneMegabyte) // limit parser memory to 32MB
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.MultipartForm == nil || request.MultipartForm.File == nil {
|
||||||
|
return errors.New("uploaded files not found to build image")
|
||||||
|
}
|
||||||
|
|
||||||
|
tfb := archive.NewTarFileInBuffer()
|
||||||
|
defer tfb.Close()
|
||||||
|
|
||||||
|
for k := range request.MultipartForm.File {
|
||||||
|
f, hdr, err := request.FormFile(k)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
log.Printf("[INFO] [http,proxy,docker] [message: upload the file to build image] [filename: %s] [size: %d]", hdr.Filename, hdr.Size)
|
||||||
|
|
||||||
|
content, err := ioutil.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := hdr.Filename
|
||||||
|
if hdr.Filename == "blob" {
|
||||||
|
filename = "Dockerfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tfb.Put(content, filename, 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer = tfb.Bytes()
|
||||||
|
request.Form = nil
|
||||||
|
request.PostForm = nil
|
||||||
|
request.MultipartForm = nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
request.Body = ioutil.NopCloser(bytes.NewReader(buffer))
|
request.Body = ioutil.NopCloser(bytes.NewReader(buffer))
|
||||||
|
|
|
@ -66,6 +66,24 @@ angular.module('portainer.docker').factory('BuildService', [
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.buildImageFromDockerfileContentAndFiles = function (names, content, files) {
|
||||||
|
var dockerfile = new Blob([content], { type: 'text/plain' });
|
||||||
|
var uploadFiles = [dockerfile].concat(files);
|
||||||
|
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
FileUploadService.buildImageFromFiles(names, uploadFiles)
|
||||||
|
.then(function success(response) {
|
||||||
|
var model = new ImageBuildModel(response.data);
|
||||||
|
deferred.resolve(model);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -13,6 +13,7 @@ function BuildImageController($scope, $async, $window, ModalService, BuildServic
|
||||||
ImageNames: [{ Name: '', Valid: false, Unique: true }],
|
ImageNames: [{ Name: '', Valid: false, Unique: true }],
|
||||||
UploadFile: null,
|
UploadFile: null,
|
||||||
DockerFileContent: '',
|
DockerFileContent: '',
|
||||||
|
AdditionalFiles: [],
|
||||||
URL: '',
|
URL: '',
|
||||||
Path: 'Dockerfile',
|
Path: 'Dockerfile',
|
||||||
NodeName: null,
|
NodeName: null,
|
||||||
|
@ -74,7 +75,12 @@ function BuildImageController($scope, $async, $window, ModalService, BuildServic
|
||||||
return BuildService.buildImageFromURL(names, URL, dockerfilePath);
|
return BuildService.buildImageFromURL(names, URL, dockerfilePath);
|
||||||
} else {
|
} else {
|
||||||
var dockerfileContent = $scope.formValues.DockerFileContent;
|
var dockerfileContent = $scope.formValues.DockerFileContent;
|
||||||
return BuildService.buildImageFromDockerfileContent(names, dockerfileContent);
|
if ($scope.formValues.AdditionalFiles.length === 0) {
|
||||||
|
return BuildService.buildImageFromDockerfileContent(names, dockerfileContent);
|
||||||
|
} else {
|
||||||
|
var additionalFiles = $scope.formValues.AdditionalFiles;
|
||||||
|
return BuildService.buildImageFromDockerfileContentAndFiles(names, dockerfileContent, additionalFiles);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,4 +146,8 @@ function BuildImageController($scope, $async, $window, ModalService, BuildServic
|
||||||
return ModalService.confirmWebEditorDiscard();
|
return ModalService.confirmWebEditorDiscard();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.selectAdditionalFiles = function (files) {
|
||||||
|
$scope.formValues.AdditionalFiles = files;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,6 +132,20 @@
|
||||||
></code-editor>
|
></code-editor>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-sm-12 form-section-title"> Upload </div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
You can upload files from your local computer for referencing in your Dockerfile (using ADD filename) so they are included in your built image.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-primary" ngf-select="selectAdditionalFiles($files)" ngf-multiple="true">Select files</button>
|
||||||
|
<span ng-repeat="item in formValues.AdditionalFiles track by $index" class="mx-2">
|
||||||
|
{{ item.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !web-editor -->
|
<!-- !web-editor -->
|
||||||
<!-- upload -->
|
<!-- upload -->
|
||||||
|
|
|
@ -33,6 +33,23 @@ angular.module('portainer.app').factory('FileUploadService', [
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.buildImageFromFiles = function (names, files) {
|
||||||
|
var endpointID = EndpointProvider.endpointID();
|
||||||
|
return Upload.upload({
|
||||||
|
url: 'api/endpoints/' + endpointID + '/docker/build',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
data: { file: files },
|
||||||
|
params: {
|
||||||
|
t: names,
|
||||||
|
},
|
||||||
|
transformResponse: function (data) {
|
||||||
|
return jsonObjectsToArrayHandler(data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
service.loadImages = function (file) {
|
service.loadImages = function (file) {
|
||||||
var endpointID = EndpointProvider.endpointID();
|
var endpointID = EndpointProvider.endpointID();
|
||||||
return Upload.http({
|
return Upload.http({
|
||||||
|
|
Loading…
Reference in New Issue