Implement core functionality of AWS integration
This commit is contained in:
@ -2,34 +2,49 @@ package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/openHPI/poseidon/internal/config"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/pkg/execution"
|
||||
"io"
|
||||
)
|
||||
|
||||
var ErrWrongMessageType = errors.New("received message that is not a text messages")
|
||||
|
||||
type awsFunctionRequest struct {
|
||||
Action string `json:"action"`
|
||||
Cmd []string `json:"cmd"`
|
||||
Files map[dto.FilePath][]byte `json:"files"`
|
||||
}
|
||||
|
||||
// AWSFunctionWorkload is an abstraction to build a request to an AWS Lambda Function.
|
||||
type AWSFunctionWorkload struct {
|
||||
InactivityTimer
|
||||
id string
|
||||
fs map[dto.FilePath][]byte
|
||||
executions execution.Storer
|
||||
onDestroy destroyRunnerHandler
|
||||
id string
|
||||
fs map[dto.FilePath][]byte
|
||||
executions execution.Storer
|
||||
onDestroy destroyRunnerHandler
|
||||
environment ExecutionEnvironment
|
||||
}
|
||||
|
||||
// NewAWSFunctionWorkload creates a new AWSFunctionWorkload with the provided id.
|
||||
func NewAWSFunctionWorkload(onDestroy destroyRunnerHandler) (*AWSFunctionWorkload, error) {
|
||||
func NewAWSFunctionWorkload(
|
||||
environment ExecutionEnvironment, onDestroy destroyRunnerHandler) (*AWSFunctionWorkload, error) {
|
||||
newUUID, err := uuid.NewUUID()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed generating runner id: %w", err)
|
||||
}
|
||||
|
||||
workload := &AWSFunctionWorkload{
|
||||
id: newUUID.String(),
|
||||
executions: execution.NewLocalStorage(),
|
||||
onDestroy: onDestroy,
|
||||
fs: make(map[dto.FilePath][]byte),
|
||||
id: newUUID.String(),
|
||||
fs: make(map[dto.FilePath][]byte),
|
||||
executions: execution.NewLocalStorage(),
|
||||
onDestroy: onDestroy,
|
||||
environment: environment,
|
||||
}
|
||||
workload.InactivityTimer = NewInactivityTimer(workload, onDestroy)
|
||||
return workload, nil
|
||||
@ -40,26 +55,114 @@ func (w *AWSFunctionWorkload) ID() string {
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) MappedPorts() []*dto.MappedPort {
|
||||
panic("implement me")
|
||||
return []*dto.MappedPort{}
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) StoreExecution(_ string, _ *dto.ExecutionRequest) {
|
||||
panic("implement me")
|
||||
func (w *AWSFunctionWorkload) StoreExecution(id string, request *dto.ExecutionRequest) {
|
||||
w.executions.Add(execution.ID(id), request)
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) ExecutionExists(_ string) bool {
|
||||
panic("implement me")
|
||||
func (w *AWSFunctionWorkload) ExecutionExists(id string) bool {
|
||||
return w.executions.Exists(execution.ID(id))
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) ExecuteInteractively(_ string, _ io.ReadWriter, _, _ io.Writer) (
|
||||
exit <-chan ExitInfo, cancel context.CancelFunc, err error) {
|
||||
panic("implement me")
|
||||
func (w *AWSFunctionWorkload) ExecuteInteractively(id string, _ io.ReadWriter, stdout, stderr io.Writer) (
|
||||
<-chan ExitInfo, context.CancelFunc, error) {
|
||||
w.ResetTimeout()
|
||||
request, ok := w.executions.Pop(execution.ID(id))
|
||||
if !ok {
|
||||
return nil, nil, ErrorUnknownExecution
|
||||
}
|
||||
|
||||
command, ctx, cancel := prepareExecution(request)
|
||||
exit := make(chan ExitInfo, 1)
|
||||
go w.executeCommand(ctx, command, stdout, stderr, exit)
|
||||
return exit, cancel, nil
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) UpdateFileSystem(_ *dto.UpdateFileSystemRequest) error {
|
||||
panic("implement me")
|
||||
// UpdateFileSystem copies Files into the executor.
|
||||
// ToDo: Currently, file deletion is not supported (but it could be).
|
||||
func (w *AWSFunctionWorkload) UpdateFileSystem(request *dto.UpdateFileSystemRequest) error {
|
||||
for _, file := range request.Copy {
|
||||
w.fs[file.Path] = file.Content
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) Destroy() error {
|
||||
panic("implement me")
|
||||
if err := w.onDestroy(w); err != nil {
|
||||
return fmt.Errorf("error while destroying aws runner: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) executeCommand(ctx context.Context, command []string,
|
||||
stdout, stderr io.Writer, exit chan<- ExitInfo,
|
||||
) {
|
||||
data := &awsFunctionRequest{
|
||||
Action: w.environment.Image(),
|
||||
Cmd: command,
|
||||
Files: w.fs,
|
||||
}
|
||||
rawData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
exit <- ExitInfo{uint8(1), fmt.Errorf("cannot stingify aws function request: %w", err)}
|
||||
return
|
||||
}
|
||||
|
||||
wsConn, response, err := websocket.DefaultDialer.Dial(config.Config.AWS.Endpoint, nil)
|
||||
if err != nil {
|
||||
exit <- ExitInfo{uint8(1), fmt.Errorf("failed to establish aws connection: %w", err)}
|
||||
return
|
||||
}
|
||||
_ = response.Body.Close()
|
||||
defer wsConn.Close()
|
||||
err = wsConn.WriteMessage(websocket.TextMessage, rawData)
|
||||
if err != nil {
|
||||
exit <- ExitInfo{uint8(1), fmt.Errorf("cannot send aws request: %w", err)}
|
||||
return
|
||||
}
|
||||
|
||||
exitCode, err := w.receiveOutput(wsConn, stdout, stderr, ctx)
|
||||
if w.TimeoutPassed() {
|
||||
err = ErrorRunnerInactivityTimeout
|
||||
}
|
||||
exit <- ExitInfo{exitCode, err}
|
||||
close(exit)
|
||||
}
|
||||
|
||||
func (w *AWSFunctionWorkload) receiveOutput(
|
||||
conn *websocket.Conn, stdout, stderr io.Writer, ctx context.Context) (uint8, error) {
|
||||
for ctx.Err() == nil {
|
||||
messageType, reader, err := conn.NextReader()
|
||||
if err != nil {
|
||||
return 1, fmt.Errorf("cannot read from aws connection: %w", err)
|
||||
}
|
||||
if messageType != websocket.TextMessage {
|
||||
return 1, ErrWrongMessageType
|
||||
}
|
||||
var wsMessage dto.WebSocketMessage
|
||||
err = json.NewDecoder(reader).Decode(&wsMessage)
|
||||
if err != nil {
|
||||
return 1, fmt.Errorf("failed to decode message from aws: %w", err)
|
||||
}
|
||||
|
||||
log.WithField("msg", wsMessage).Info("New Message from AWS function")
|
||||
|
||||
switch wsMessage.Type {
|
||||
default:
|
||||
log.WithField("data", wsMessage).Warn("unexpected message from aws function")
|
||||
case dto.WebSocketExit:
|
||||
return wsMessage.ExitCode, nil
|
||||
case dto.WebSocketOutputStdout:
|
||||
// We do not check the written bytes as the rawToCodeOceanWriter receives everything or nothing.
|
||||
_, err = stdout.Write([]byte(wsMessage.Data))
|
||||
case dto.WebSocketOutputStderr:
|
||||
_, err = stderr.Write([]byte(wsMessage.Data))
|
||||
}
|
||||
if err != nil {
|
||||
return 1, fmt.Errorf("failed to forward message: %w", err)
|
||||
}
|
||||
}
|
||||
return 1, fmt.Errorf("receiveOutput stpped by context: %w", ctx.Err())
|
||||
}
|
||||
|
Reference in New Issue
Block a user