diff --git a/api/archive/tar.go b/api/archive/tar.go new file mode 100644 index 000000000..4040a9ec7 --- /dev/null +++ b/api/archive/tar.go @@ -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 +} diff --git a/api/http/proxy/build.go b/api/http/proxy/build.go new file mode 100644 index 000000000..0deab93b9 --- /dev/null +++ b/api/http/proxy/build.go @@ -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 +} diff --git a/api/http/proxy/transport.go b/api/http/proxy/transport.go index 83edcaf37..3b0b1fa3c 100644 --- a/api/http/proxy/transport.go +++ b/api/http/proxy/transport.go @@ -27,6 +27,7 @@ type ( labelBlackList []portainer.Pair } restrictedOperationRequest func(*http.Request, *http.Response, *operationExecutor) error + operationRequest func(*http.Request) 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) case strings.HasPrefix(path, "/tasks"): return p.proxyTaskRequest(request) + case strings.HasPrefix(path, "/build"): + return p.proxyBuildRequest(request) default: 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 // before executing the original request. 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) } +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) { response, err := p.executeDockerRequest(request) if err != nil { diff --git a/app/docker/__module.js b/app/docker/__module.js index 1e15df46e..e4777fcb9 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -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 = { name: 'docker.networks', url: '/networks', @@ -457,6 +468,7 @@ angular.module('portainer.docker', ['portainer.app']) $stateRegistryProvider.register(events); $stateRegistryProvider.register(images); $stateRegistryProvider.register(image); + $stateRegistryProvider.register(imageBuild); $stateRegistryProvider.register(networks); $stateRegistryProvider.register(network); $stateRegistryProvider.register(networkCreation); diff --git a/app/docker/components/datatables/images-datatable/imagesDatatable.html b/app/docker/components/datatables/images-datatable/imagesDatatable.html index a22ffc57c..89d916dab 100644 --- a/app/docker/components/datatables/images-datatable/imagesDatatable.html +++ b/app/docker/components/datatables/images-datatable/imagesDatatable.html @@ -25,6 +25,9 @@
  • Force Remove
  • +