Copy files with relative path to active workspace directory of container
This commit is contained in:
@ -66,18 +66,14 @@ type File struct {
|
|||||||
Content []byte `json:"content"`
|
Content []byte `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToAbsolute returns the absolute path of the FilePath with respect to the given basePath. If the FilePath already is absolute, basePath will be ignored.
|
// Cleaned returns the cleaned path of the FilePath.
|
||||||
func (f FilePath) ToAbsolute(basePath string) string {
|
func (f FilePath) Cleaned() string {
|
||||||
filePathString := string(f)
|
return path.Clean(string(f))
|
||||||
if path.IsAbs(filePathString) {
|
|
||||||
return path.Clean(filePathString)
|
|
||||||
}
|
|
||||||
return path.Clean(path.Join(basePath, filePathString))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AbsolutePath returns the absolute path of the file. See FilePath.ToAbsolute for details.
|
// CleanedPath returns the cleaned path of the file.
|
||||||
func (f File) AbsolutePath(basePath string) string {
|
func (f File) CleanedPath() string {
|
||||||
return f.Path.ToAbsolute(basePath)
|
return f.Path.Cleaned()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDirectory returns true iff the path of the File ends with a /.
|
// IsDirectory returns true iff the path of the File ends with a /.
|
||||||
|
@ -32,9 +32,6 @@ var (
|
|||||||
TLS: false,
|
TLS: false,
|
||||||
Namespace: "default",
|
Namespace: "default",
|
||||||
},
|
},
|
||||||
Runner: runner{
|
|
||||||
WorkspacePath: "/home/python",
|
|
||||||
},
|
|
||||||
Logger: logger{
|
Logger: logger{
|
||||||
Level: "INFO",
|
Level: "INFO",
|
||||||
},
|
},
|
||||||
@ -68,11 +65,6 @@ type nomad struct {
|
|||||||
Namespace string
|
Namespace string
|
||||||
}
|
}
|
||||||
|
|
||||||
// runner configures the runners on the executor
|
|
||||||
type runner struct {
|
|
||||||
WorkspacePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// logger configures the used logger.
|
// logger configures the used logger.
|
||||||
type logger struct {
|
type logger struct {
|
||||||
Level string
|
Level string
|
||||||
@ -82,7 +74,6 @@ type logger struct {
|
|||||||
type configuration struct {
|
type configuration struct {
|
||||||
Server server
|
Server server
|
||||||
Nomad nomad
|
Nomad nomad
|
||||||
Runner runner
|
|
||||||
Logger logger
|
Logger logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,11 +26,6 @@ nomad:
|
|||||||
# Nomad namespace to use. If unset, 'default' is used
|
# Nomad namespace to use. If unset, 'default' is used
|
||||||
namespace: poseidon
|
namespace: poseidon
|
||||||
|
|
||||||
# Configuration of the runners
|
|
||||||
runner:
|
|
||||||
# Directory where all files with relative paths will be copied into. Must be writable by the default user in the container.
|
|
||||||
workspacepath: /home/python
|
|
||||||
|
|
||||||
# Configuration of the logger
|
# Configuration of the logger
|
||||||
logger:
|
logger:
|
||||||
# Log level that is used after reading the config (INFO until then)
|
# Log level that is used after reading the config (INFO until then)
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/config"
|
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/nomad"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/nomad"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
@ -28,7 +27,6 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ErrorFileCopyFailed = errors.New("file copy failed")
|
ErrorFileCopyFailed = errors.New("file copy failed")
|
||||||
FileCopyBasePath = config.Config.Runner.WorkspacePath
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Runner interface {
|
type Runner interface {
|
||||||
@ -111,7 +109,7 @@ func (r *NomadAllocation) UpdateFileSystem(copyRequest *dto.UpdateFileSystemRequ
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileDeletionCommand := fileDeletionCommand(copyRequest.Delete)
|
fileDeletionCommand := fileDeletionCommand(copyRequest.Delete)
|
||||||
copyCommand := "tar --extract --absolute-names --verbose --directory=/ --file=/dev/stdin;"
|
copyCommand := "tar --extract --absolute-names --verbose --file=/dev/stdin;"
|
||||||
updateFileCommand := (&dto.ExecutionRequest{Command: fileDeletionCommand + copyCommand}).FullCommand()
|
updateFileCommand := (&dto.ExecutionRequest{Command: fileDeletionCommand + copyCommand}).FullCommand()
|
||||||
stdOut := bytes.Buffer{}
|
stdOut := bytes.Buffer{}
|
||||||
stdErr := bytes.Buffer{}
|
stdErr := bytes.Buffer{}
|
||||||
@ -155,15 +153,15 @@ func createTarArchiveForFiles(filesToCopy []dto.File, w io.Writer) error {
|
|||||||
return tarWriter.Close()
|
return tarWriter.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileDeletionCommand(filesToDelete []dto.FilePath) string {
|
func fileDeletionCommand(pathsToDelete []dto.FilePath) string {
|
||||||
if len(filesToDelete) == 0 {
|
if len(pathsToDelete) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
command := "rm --recursive --force "
|
command := "rm --recursive --force "
|
||||||
for _, filePath := range filesToDelete {
|
for _, filePath := range pathsToDelete {
|
||||||
// To avoid command injection, filenames need to be quoted.
|
// To avoid command injection, filenames need to be quoted.
|
||||||
// See https://unix.stackexchange.com/questions/347332/what-characters-need-to-be-escaped-in-files-without-quotes for details.
|
// See https://unix.stackexchange.com/questions/347332/what-characters-need-to-be-escaped-in-files-without-quotes for details.
|
||||||
singleQuoteEscapedFileName := strings.ReplaceAll(filePath.ToAbsolute(FileCopyBasePath), "'", "'\\''")
|
singleQuoteEscapedFileName := strings.ReplaceAll(filePath.Cleaned(), "'", "'\\''")
|
||||||
command += fmt.Sprintf("'%s' ", singleQuoteEscapedFileName)
|
command += fmt.Sprintf("'%s' ", singleQuoteEscapedFileName)
|
||||||
}
|
}
|
||||||
command += ";"
|
command += ";"
|
||||||
@ -174,13 +172,13 @@ func tarHeader(file dto.File) *tar.Header {
|
|||||||
if file.IsDirectory() {
|
if file.IsDirectory() {
|
||||||
return &tar.Header{
|
return &tar.Header{
|
||||||
Typeflag: tar.TypeDir,
|
Typeflag: tar.TypeDir,
|
||||||
Name: file.AbsolutePath(FileCopyBasePath),
|
Name: file.CleanedPath(),
|
||||||
Mode: 0755,
|
Mode: 0755,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return &tar.Header{
|
return &tar.Header{
|
||||||
Typeflag: tar.TypeReg,
|
Typeflag: tar.TypeReg,
|
||||||
Name: file.AbsolutePath(FileCopyBasePath),
|
Name: file.CleanedPath(),
|
||||||
Mode: 0744,
|
Mode: 0744,
|
||||||
Size: int64(len(file.Content)),
|
Size: int64(len(file.Content)),
|
||||||
}
|
}
|
||||||
|
@ -181,16 +181,15 @@ func (s *UpdateFileSystemTestSuite) TestFilesToCopyAreIncludedInTarArchive() {
|
|||||||
s.Equal(tests.DefaultFileContent, tarFile.Content)
|
s.Equal(tests.DefaultFileContent, tarFile.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UpdateFileSystemTestSuite) TestFilesWithRelativePathArePutInDefaultLocation() {
|
func (s *UpdateFileSystemTestSuite) TestTarFilesContainCorrectPathForRelativeFilePath() {
|
||||||
s.mockedExecuteCommandCall.Return(0, nil)
|
s.mockedExecuteCommandCall.Return(0, nil)
|
||||||
copyRequest := &dto.UpdateFileSystemRequest{Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}}}
|
copyRequest := &dto.UpdateFileSystemRequest{Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}}}
|
||||||
_ = s.runner.UpdateFileSystem(copyRequest)
|
_ = s.runner.UpdateFileSystem(copyRequest)
|
||||||
|
|
||||||
tarFiles := s.readFilesFromTarArchive(s.stdin)
|
tarFiles := s.readFilesFromTarArchive(s.stdin)
|
||||||
s.Len(tarFiles, 1)
|
s.Len(tarFiles, 1)
|
||||||
tarFile := tarFiles[0]
|
// tar is extracted in the active workdir of the container, file will be put relative to that
|
||||||
s.True(strings.HasSuffix(tarFile.Name, tests.DefaultFileName))
|
s.Equal(tests.DefaultFileName, tarFiles[0].Name)
|
||||||
s.True(strings.HasPrefix(tarFile.Name, FileCopyBasePath))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UpdateFileSystemTestSuite) TestFilesWithAbsolutePathArePutInAbsoluteLocation() {
|
func (s *UpdateFileSystemTestSuite) TestFilesWithAbsolutePathArePutInAbsoluteLocation() {
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/api"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/api"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/runner"
|
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/tests"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/tests"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/tests/helpers"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/tests/helpers"
|
||||||
"io"
|
"io"
|
||||||
@ -74,13 +73,13 @@ func (s *E2ETestSuite) TestDeleteRunnerRoute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *E2ETestSuite) TestCopyFilesRoute() {
|
func (s *E2ETestSuite) TestCopyFilesRoute() {
|
||||||
runnerId, err := ProvideRunner(&dto.RunnerRequest{})
|
runnerID, err := ProvideRunner(&dto.RunnerRequest{})
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
copyFilesRequestByteString, _ := json.Marshal(&dto.UpdateFileSystemRequest{
|
copyFilesRequestByteString, _ := json.Marshal(&dto.UpdateFileSystemRequest{
|
||||||
Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}},
|
Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}},
|
||||||
})
|
})
|
||||||
sendCopyRequest := func(reader io.Reader) (*http.Response, error) {
|
sendCopyRequest := func(reader io.Reader) (*http.Response, error) {
|
||||||
return helpers.HttpPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerId, api.UpdateFileSystemPath), "application/json", reader)
|
return helpers.HttpPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), "application/json", reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Run("File copy with valid payload succeeds", func() {
|
s.Run("File copy with valid payload succeeds", func() {
|
||||||
@ -89,7 +88,33 @@ func (s *E2ETestSuite) TestCopyFilesRoute() {
|
|||||||
s.Equal(http.StatusNoContent, resp.StatusCode)
|
s.Equal(http.StatusNoContent, resp.StatusCode)
|
||||||
|
|
||||||
s.Run("File content can be printed on runner", func() {
|
s.Run("File content can be printed on runner", func() {
|
||||||
s.Equal(tests.DefaultFileContent, s.ContentOfFileOnRunner(runnerId, tests.DefaultFileName))
|
s.Equal(tests.DefaultFileContent, s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("Files are put in correct location", func() {
|
||||||
|
relativeFilePath := "relative/file/path.txt"
|
||||||
|
relativeFileContent := "Relative file content"
|
||||||
|
absoluteFilePath := "/tmp/absolute/file/path.txt"
|
||||||
|
absoluteFileContent := "Absolute file content"
|
||||||
|
testFilePathsCopyRequestString, _ := json.Marshal(&dto.UpdateFileSystemRequest{
|
||||||
|
Copy: []dto.File{
|
||||||
|
{Path: dto.FilePath(relativeFilePath), Content: []byte(relativeFileContent)},
|
||||||
|
{Path: dto.FilePath(absoluteFilePath), Content: []byte(absoluteFileContent)},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
resp, err := sendCopyRequest(bytes.NewReader(testFilePathsCopyRequestString))
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal(http.StatusNoContent, resp.StatusCode)
|
||||||
|
|
||||||
|
s.Run("File content of file with relative path can be printed on runner", func() {
|
||||||
|
// the print command is executed in the context of the default working directory of the container
|
||||||
|
s.Equal(relativeFileContent, s.PrintContentOfFileOnRunner(runnerID, relativeFilePath))
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("File content of file with absolute path can be printed on runner", func() {
|
||||||
|
s.Equal(absoluteFileContent, s.PrintContentOfFileOnRunner(runnerID, absoluteFilePath))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -103,7 +128,7 @@ func (s *E2ETestSuite) TestCopyFilesRoute() {
|
|||||||
s.Equal(http.StatusNoContent, resp.StatusCode)
|
s.Equal(http.StatusNoContent, resp.StatusCode)
|
||||||
|
|
||||||
s.Run("File content can no longer be printed", func() {
|
s.Run("File content can no longer be printed", func() {
|
||||||
s.Contains(s.ContentOfFileOnRunner(runnerId, tests.DefaultFileName), "No such file or directory")
|
s.Contains(s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName), "No such file or directory")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -116,9 +141,10 @@ func (s *E2ETestSuite) TestCopyFilesRoute() {
|
|||||||
resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString))
|
resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString))
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.Equal(http.StatusNoContent, resp.StatusCode)
|
s.Equal(http.StatusNoContent, resp.StatusCode)
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
s.Run("File content can be printed on runner", func() {
|
s.Run("File content can be printed on runner", func() {
|
||||||
s.Equal(tests.DefaultFileContent, s.ContentOfFileOnRunner(runnerId, tests.DefaultFileName))
|
s.Equal(tests.DefaultFileContent, s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -138,14 +164,15 @@ func (s *E2ETestSuite) TestCopyFilesRoute() {
|
|||||||
err = json.NewDecoder(resp.Body).Decode(internalServerError)
|
err = json.NewDecoder(resp.Body).Decode(internalServerError)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.Contains(internalServerError.Message, "Cannot open: Permission denied")
|
s.Contains(internalServerError.Message, "Cannot open: Permission denied")
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
s.Run("File content can be printed on runner", func() {
|
s.Run("File content can be printed on runner", func() {
|
||||||
s.Equal(string(newFileContent), s.ContentOfFileOnRunner(runnerId, tests.DefaultFileName))
|
s.Equal(string(newFileContent), s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
s.Run("File copy with invalid payload returns bad request", func() {
|
s.Run("File copy with invalid payload returns bad request", func() {
|
||||||
resp, err := helpers.HttpPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerId, api.UpdateFileSystemPath), "text/html", strings.NewReader(""))
|
resp, err := helpers.HttpPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), "text/html", strings.NewReader(""))
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.Equal(http.StatusBadRequest, resp.StatusCode)
|
s.Equal(http.StatusBadRequest, resp.StatusCode)
|
||||||
})
|
})
|
||||||
@ -157,8 +184,8 @@ func (s *E2ETestSuite) TestCopyFilesRoute() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *E2ETestSuite) ContentOfFileOnRunner(runnerId string, filename string) string {
|
func (s *E2ETestSuite) PrintContentOfFileOnRunner(runnerId string, filename string) string {
|
||||||
webSocketURL, _ := ProvideWebSocketURL(&s.Suite, runnerId, &dto.ExecutionRequest{Command: fmt.Sprintf("cat %s/%s", runner.FileCopyBasePath, filename)})
|
webSocketURL, _ := ProvideWebSocketURL(&s.Suite, runnerId, &dto.ExecutionRequest{Command: fmt.Sprintf("cat %s", filename)})
|
||||||
connection, _ := ConnectToWebSocket(webSocketURL)
|
connection, _ := ConnectToWebSocket(webSocketURL)
|
||||||
|
|
||||||
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
||||||
|
Reference in New Issue
Block a user