Files
poseidon/api/dto/dto.go
Konrad Hanff 3afcdeaba8 Execute commands in runner via WebSocket
This enables executing commands in runners and forwarding input and
output between the runner and the websocket to the client.

Co-authored-by: Maximilian Paß <maximilian.pass@student.hpi.uni-potsdam.de>
2021-05-31 12:32:51 +02:00

170 lines
5.3 KiB
Go

package dto
import (
"encoding/json"
"errors"
"fmt"
)
// RunnerRequest is the expected json structure of the request body for the ProvideRunner function.
type RunnerRequest struct {
ExecutionEnvironmentId int `json:"executionEnvironmentId"`
InactivityTimeout int `json:"inactivityTimeout"`
}
// ExecutionRequest is the expected json structure of the request body for the ExecuteCommand function.
type ExecutionRequest struct {
Command string
TimeLimit int
Environment map[string]string
}
func (er *ExecutionRequest) FullCommand() []string {
var command []string
command = append(command, "env", "-")
for variable, value := range er.Environment {
command = append(command, fmt.Sprintf("%s=%s", variable, value))
}
command = append(command, "sh", "-c", er.Command)
return command
}
// ExecutionEnvironmentRequest is the expected json structure of the request body for the create execution environment function.
// nolint:unused,structcheck
type ExecutionEnvironmentRequest struct {
prewarmingPoolSize uint
cpuLimit uint
memoryLimit uint
image string
networkAccess bool
exposedPorts []uint16
}
// RunnerResponse is the expected response when providing a runner.
type RunnerResponse struct {
Id string `json:"runnerId"`
}
// FileCreation is the expected json structure of the request body for the copy files route.
// TODO: specify content of the struct
type FileCreation struct{}
// ExecutionResponse is the expected response when creating an execution for a runner.
type ExecutionResponse struct {
WebSocketUrl string `json:"websocketUrl"`
}
// WebSocketMessageType is the type for the messages from Poseidon to the client.
type WebSocketMessageType string
const (
WebSocketOutputStdout WebSocketMessageType = "stdout"
WebSocketOutputStderr WebSocketMessageType = "stderr"
WebSocketOutputError WebSocketMessageType = "error"
WebSocketMetaStart WebSocketMessageType = "start"
WebSocketMetaTimeout WebSocketMessageType = "timeout"
WebSocketExit WebSocketMessageType = "exit"
)
// WebSocketMessage is the type for all messages send in the WebSocket to the client.
// Depending on the MessageType the Data or ExitCode might not be included in the marshaled json message.
type WebSocketMessage struct {
Type WebSocketMessageType
Data string
ExitCode uint8
}
// MarshalJSON implements the json.Marshaler interface.
// This converts the WebSocketMessage into the expected schema (see docs/websocket.schema.json).
func (m WebSocketMessage) MarshalJSON() ([]byte, error) {
switch m.Type {
case WebSocketOutputStdout, WebSocketOutputStderr, WebSocketOutputError:
return json.Marshal(struct {
MessageType WebSocketMessageType `json:"type"`
Data string `json:"data"`
}{m.Type, m.Data})
case WebSocketMetaStart, WebSocketMetaTimeout:
return json.Marshal(struct {
MessageType WebSocketMessageType `json:"type"`
}{m.Type})
case WebSocketExit:
return json.Marshal(struct {
MessageType WebSocketMessageType `json:"type"`
ExitCode uint8 `json:"data"`
}{m.Type, m.ExitCode})
}
return nil, errors.New("unhandled WebSocket message type")
}
// UnmarshalJSON implements the json.Unmarshaler interface.
// It is used by tests in order to ReceiveNextWebSocketMessage.
func (m *WebSocketMessage) UnmarshalJSON(rawMessage []byte) error {
messageMap := make(map[string]interface{})
err := json.Unmarshal(rawMessage, &messageMap)
if err != nil {
return err
}
messageType, ok := messageMap["type"]
if !ok {
return errors.New("missing key type")
}
messageTypeString, ok := messageType.(string)
if !ok {
return errors.New("value of key type must be a string")
}
switch messageType := WebSocketMessageType(messageTypeString); messageType {
case WebSocketExit:
data, ok := messageMap["data"]
if !ok {
return errors.New("missing key data")
}
// json.Unmarshal converts any number to a float64 in the massageMap, so we must first cast it to the float.
exit, ok := data.(float64)
if !ok {
return errors.New("value of key data must be a number")
}
if exit != float64(uint8(exit)) {
return errors.New("value of key data must be uint8")
}
m.Type = messageType
m.ExitCode = uint8(exit)
case WebSocketOutputStdout, WebSocketOutputStderr, WebSocketOutputError:
data, ok := messageMap["data"]
if !ok {
return errors.New("missing key data")
}
text, ok := data.(string)
if !ok {
return errors.New("value of key data must be a string")
}
m.Type = messageType
m.Data = text
case WebSocketMetaStart, WebSocketMetaTimeout:
m.Type = messageType
default:
return errors.New("unknown WebSocket message type")
}
return nil
}
// ClientError is the response interface if the request is not valid.
type ClientError struct {
Message string `json:"message"`
}
// InternalServerError is the response interface that is returned when an error occurs.
type InternalServerError struct {
Message string `json:"message"`
ErrorCode ErrorCode `json:"errorCode"`
}
// ErrorCode is the type for error codes expected by CodeOcean.
type ErrorCode string
const (
ErrorNomadUnreachable ErrorCode = "NOMAD_UNREACHABLE"
ErrorNomadOverload ErrorCode = "NOMAD_OVERLOAD"
ErrorNomadInternalServerError ErrorCode = "NOMAD_INTERNAL_SERVER_ERROR"
ErrorUnknown ErrorCode = "UNKNOWN"
)