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