You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

338 lines
12 KiB

package api
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"strconv"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
"codefirst.iut.uca.fr/git/thomas.bellembois/codefirst-dockerrunner-common/v2/callbacks"
"codefirst.iut.uca.fr/git/thomas.bellembois/codefirst-dockerrunner-common/v2/errors"
"codefirst.iut.uca.fr/git/thomas.bellembois/codefirst-dockerrunner-common/v2/messages"
"codefirst.iut.uca.fr/git/thomas.bellembois/codefirst-dockerrunner-common/v2/models"
"codefirst.iut.uca.fr/git/thomas.bellembois/codefirst-dockerrunner/v2/globals"
)
func getContainers(filter map[string]string) (containers []types.Container, err error) {
dockerCtx := context.Background()
f := filters.NewArgs()
for k, v := range filter {
f.Add(k, v)
}
if containers, err = globals.DockerClient.ContainerList(dockerCtx, types.ContainerListOptions{All: true, Filters: f}); err != nil {
return
}
return
}
func GetContainerLog(wsCtx context.Context, wsConnection *websocket.Conn, codeFirstContainer models.CodeFirstContainer) {
dockerCtx := context.Background()
options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true}
var (
err error
out io.ReadCloser
)
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeGetContainerLogCallback})
if out, err = globals.DockerClient.ContainerLogs(dockerCtx, codeFirstContainer.ID, options); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.GetContainerLog, SourceErrorMessage: err.Error()})
return
}
wsmessageResult := messages.WSMessage{Action: callbacks.AfterGetContainerLogCallback, Container: codeFirstContainer}
var (
s string
b bytes.Buffer
)
if _, err = io.Copy(bufio.NewWriter(&b), out); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.GetContainerLog, SourceErrorMessage: err.Error()})
return
}
// FIXME: improve me
// 32768 is the default max size for wsjson.Read
// Remove some byte for the other messages.WSMessage fields.
resultSize := 32768 / 2
if len(s) > resultSize {
wsmessageResult.Message = b.String()[len(s)-(32768/2):]
} else {
wsmessageResult.Message = b.String()
}
wsjson.Write(wsCtx, wsConnection, wsmessageResult)
}
func GetContainers(wsCtx context.Context, wsConnection *websocket.Conn, codeFirstContainer models.CodeFirstContainer, user string) {
var (
err error
containers []types.Container
codefirstContainers []models.CodeFirstContainer
)
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeGetContainersCallback})
if containers, err = getContainers(map[string]string{"label": "codefirst-user=" + user}); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.GetContainers, SourceErrorMessage: err.Error()})
return
}
for i := range containers {
c := containers[i]
var private bool
if c.Labels["traefik.enable"] == "false" {
private = true
}
codefirstContainers = append(codefirstContainers, models.CodeFirstContainer{
ID: c.ID,
Name: c.Names[0],
ImageURL: c.Image,
EndpointURL: fmt.Sprintf("%s://%s/%s%s", globals.Scheme, globals.HostName, globals.DockerPathPrefix, c.Names[0]),
Private: private,
})
}
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterGetContainersCallback, Containers: codefirstContainers})
}
// TODO: check owner
func RemoveContainer(wsCtx context.Context, wsConnection *websocket.Conn, codeFirstContainer models.CodeFirstContainer, user string) {
dockerCtx := context.Background()
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeStopContainerCallback, Container: codeFirstContainer})
if err := globals.DockerClient.ContainerStop(dockerCtx, codeFirstContainer.ID, nil); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.StopContainer, SourceErrorMessage: err.Error()})
return
}
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterStopContainerCallback, Container: codeFirstContainer})
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeRemoveContainerCallback, Container: codeFirstContainer})
if err := globals.DockerClient.ContainerRemove(dockerCtx, codeFirstContainer.ID, types.ContainerRemoveOptions{
RemoveVolumes: true,
Force: true,
}); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.RemoveContainer, SourceErrorMessage: err.Error()})
return
}
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterRemoveContainerCallback, Container: codeFirstContainer})
}
// TODO: check owner
func ExecContainer(wsCtx context.Context, wsConnection *websocket.Conn, codeFirstContainer models.CodeFirstContainer, exec types.ExecConfig, user string) {
dockerCtx := context.Background()
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeExecContainerCallback, Container: codeFirstContainer})
var (
containers []types.Container
err error
)
if containers, err = getContainers(map[string]string{"label": "codefirst-name=" + codeFirstContainer.Name}); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.ExecContainer, SourceErrorMessage: err.Error()})
return
}
if len(containers) != 1 {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.NonExistingContainer})
return
}
codeFirstContainer.ID = containers[0].ID
exec.AttachStderr = true
exec.AttachStdout = true
var (
responseID types.IDResponse
response types.HijackedResponse
)
for i := range exec.Cmd {
fmt.Printf("cmd %d: %s\n", i, exec.Cmd[i])
}
if responseID, err = globals.DockerClient.ContainerExecCreate(dockerCtx, codeFirstContainer.ID, exec); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.ExecContainer, SourceErrorMessage: err.Error()})
return
}
if response, err = globals.DockerClient.ContainerExecAttach(context.Background(), responseID.ID, types.ExecStartCheck{}); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.ExecContainer, SourceErrorMessage: err.Error()})
return
}
defer response.Close()
var data []byte
if data, err = ioutil.ReadAll(response.Reader); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.ExecContainer, SourceErrorMessage: err.Error()})
return
}
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterExecContainerCallback, Container: codeFirstContainer, Message: string(data)})
}
func StartContainer(wsCtx context.Context, wsConnection *websocket.Conn, codeFirstContainer models.CodeFirstContainer, user string) {
dockerCtx := context.Background()
var (
err error
containers []types.Container
)
// Get number of containers for user.
if containers, err = getContainers(map[string]string{"label": "codefirst-user=" + user}); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.GetContainers, SourceErrorMessage: err.Error()})
return
}
if len(containers) >= globals.MaxAllowedContainers {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.MaxContainersReached})
return
}
// Validate container.
if codeFirstContainer.ImageURL == "" {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.ImageURLValidation})
return
}
if codeFirstContainer.Name == "" {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.ImageNameValidation})
return
}
// Rename container - prepend the logged user.
codeFirstContainer.Name = fmt.Sprintf("%s-%s", user, codeFirstContainer.Name)
// Check if container with same name already exist.
var (
conts []types.Container
)
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeGetContainerWithSameNameCallback, Container: codeFirstContainer})
if conts, err = getContainers(map[string]string{"label": "codefirst-name=" + codeFirstContainer.Name}); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.GetContainers, SourceErrorMessage: err.Error()})
return
}
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterGetContainerWithSameNameCallback, Container: codeFirstContainer})
// Removing it.
if len(conts) > 0 && conts[0].ID != "" {
if codeFirstContainer.Overwrite {
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.Log, Message: (fmt.Sprintf("Removing existing container: %s", conts[0].ID))})
RemoveContainer(wsCtx, wsConnection, models.CodeFirstContainer{ID: conts[0].ID}, user)
} else {
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterStartContainerCallback, Message: fmt.Sprintf("Can not overwrite existing container: %s", conts[0].ID)})
return
}
}
// Pull the Docker image.
var (
reader io.ReadCloser
)
imagePullOptions := types.ImagePullOptions{
RegistryAuth: codeFirstContainer.Base64Credentials,
}
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforePullImageCallback, Container: codeFirstContainer})
if reader, err = globals.DockerClient.ImagePull(dockerCtx, codeFirstContainer.ImageURL, imagePullOptions); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.DockerPull, SourceErrorMessage: err.Error()})
return
}
if _, err = io.Copy(os.Stdout, reader); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.Server, SourceErrorMessage: err.Error()})
return
}
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterPullImageCallback, Container: codeFirstContainer})
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeCreateContainerCallback, Container: codeFirstContainer})
var traefikEnable bool
if !codeFirstContainer.Private {
traefikEnable = true
}
// Configure the container.
containerLabels := map[string]string{
"codefirst-user": user,
"codefirst-name": codeFirstContainer.Name,
"codefirst-usercontainer": "true",
"traefik.enable": strconv.FormatBool(traefikEnable),
fmt.Sprintf("traefik.http.routers.%s.rule", codeFirstContainer.Name): fmt.Sprintf("Host(`%s`) && PathPrefix(`/%s/%s`)", globals.HostName, globals.DockerPathPrefix, codeFirstContainer.Name),
fmt.Sprintf("traefik.http.routers.%s.entrypoints", codeFirstContainer.Name): "websecure",
fmt.Sprintf("traefik.http.routers.%s.tls.certresolver", codeFirstContainer.Name): "letsEncrypt",
fmt.Sprintf("traefik.http.middlewares.strip-%s.stripprefix.prefixes", codeFirstContainer.Name): fmt.Sprintf("/%s/%s", globals.DockerPathPrefix, codeFirstContainer.Name),
fmt.Sprintf("traefik.http.routers.%s.middlewares", codeFirstContainer.Name): fmt.Sprintf("strip-%s@docker", codeFirstContainer.Name),
"traefik.docker.network": globals.DockerNetworkName,
}
containerConfig := &container.Config{
Image: codeFirstContainer.ImageURL,
Labels: containerLabels,
Env: codeFirstContainer.Env,
}
hostConfig := &container.HostConfig{
NetworkMode: container.NetworkMode(globals.DockerNetworkName),
RestartPolicy: container.RestartPolicy{Name: "unless-stopped"},
}
networkingConfig := &network.NetworkingConfig{}
// Create the container.
var containerCreateResponse container.ContainerCreateCreatedBody
if containerCreateResponse, err = globals.DockerClient.ContainerCreate(
dockerCtx,
containerConfig,
hostConfig,
networkingConfig,
nil,
codeFirstContainer.Name); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.CreateContainer, SourceErrorMessage: err.Error()})
return
}
codeFirstContainer.ID = containerCreateResponse.ID
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterCreateContainerCallback, Container: codeFirstContainer})
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeStartContainerCallback, Container: codeFirstContainer})
// Start the container.
if err := globals.DockerClient.ContainerStart(dockerCtx, containerCreateResponse.ID, types.ContainerStartOptions{}); err != nil {
sendError(wsCtx, wsConnection, errors.AppError{Type: errors.StartContainer, SourceErrorMessage: err.Error()})
return
}
wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterStartContainerCallback, Message: containerCreateResponse.ID})
}