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
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})
|
|
}
|