<commit_before>package service

import (
	"context"
	"fmt"
	"log"
	"net"
	"net/url"
	"time"

	"github.com/aerokube/selenoid/config"
	"github.com/aerokube/selenoid/session"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/network"
	"github.com/docker/docker/api/types/strslice"
	"github.com/docker/docker/client"
	"github.com/docker/go-connections/nat"
	"strings"
)

const (
	comma    = ","
	sysAdmin = "SYS_ADMIN"
)

// Docker - docker container manager
type Docker struct {
	ServiceBase
	Environment
	session.Caps
	LogConfig *container.LogConfig
	Client    *client.Client
}

type portConfig struct {
	SeleniumPort nat.Port
	VNCPort      nat.Port
	PortBindings nat.PortMap
	ExposedPorts map[nat.Port]struct{}
}

// StartWithCancel - Starter interface implementation
func (d *Docker) StartWithCancel() (*StartedService, error) {
	portConfig, err := getPortConfig(d.Service, d.Caps, d.Environment)
	if err != nil {
		return nil, fmt.Errorf("configuring ports: %v", err)
	}
	selenium := portConfig.SeleniumPort
	vnc := portConfig.VNCPort
	ctx := context.Background()
	log.Printf("[%d] [CREATING_CONTAINER] [%s]\n", d.RequestId, d.Service.Image)
	hostConfig := container.HostConfig{
		Binds:        d.Service.Volumes,
		AutoRemove:   true,
		PortBindings: portConfig.PortBindings,
		LogConfig:    getLogConfig(*d.LogConfig, d.Caps),
		NetworkMode:  container.NetworkMode(d.Network),
		Tmpfs:        d.Service.Tmpfs,
		ShmSize:      getShmSize(d.Service),
		Privileged:   d.Privileged,
		Resources: container.Resources{
			Memory:   d.Memory,
			NanoCPUs: d.CPU,
		},
		ExtraHosts: getExtraHosts(d.Service, d.Caps),
	}
	if !d.Privileged {
		hostConfig.CapAdd = strslice.StrSlice{sysAdmin}
	}
	if d.ApplicationContainers != "" {
		links := strings.Split(d.ApplicationContainers, comma)
		hostConfig.Links = links
	}
	container, err := d.Client.ContainerCreate(ctx,
		&container.Config{
			Hostname:     getContainerHostname(d.Caps),
			Image:        d.Service.Image.(string),
			Env:          getEnv(d.ServiceBase, d.Caps),
			ExposedPorts: portConfig.ExposedPorts,
		},
		&hostConfig,
		&network.NetworkingConfig{}, "")
	if err != nil {
		return nil, fmt.Errorf("create container: %v", err)
	}
	containerStartTime := time.Now()
	log.Printf("[%d] [STARTING_CONTAINER] [%s] [%s]\n", d.RequestId, d.Service.Image, container.ID)
	err = d.Client.ContainerStart(ctx, container.ID, types.ContainerStartOptions{})
	if err != nil {
		d.removeContainer(ctx, d.Client, container.ID)
		return nil, fmt.Errorf("start container: %v", err)
	}
	log.Printf("[%d] [CONTAINER_STARTED] [%s] [%s] [%v]\n", d.RequestId, d.Service.Image, container.ID, time.Since(containerStartTime))
	stat, err := d.Client.ContainerInspect(ctx, container.ID)
	if err != nil {
		d.removeContainer(ctx, d.Client, container.ID)
		return nil, fmt.Errorf("inspect container %s: %s", container.ID, err)
	}
	_, ok := stat.NetworkSettings.Ports[selenium]
	if !ok {
		d.removeContainer(ctx, d.Client, container.ID)
		return nil, fmt.Errorf("no bindings available for %v", selenium)
	}
	seleniumHostPort, vncHostPort := getHostPort(d.Environment, d.Service, d.Caps, stat, selenium, vnc)
	u := &url.URL{Scheme: "http", Host: seleniumHostPort, Path: d.Service.Path}
	serviceStartTime := time.Now()
	err = wait(u.String(), d.StartupTimeout)
	if err != nil {
		d.removeContainer(ctx, d.Client, container.ID)
		return nil, fmt.Errorf("wait: %v", err)
	}
	log.Printf("[%d] [SERVICE_STARTED] [%s] [%s] [%v]\n", d.RequestId, d.Service.Image, container.ID, time.Since(serviceStartTime))
	log.Printf("[%d] [PROXY_TO] [%s] [%s] [%s]\n", d.RequestId, d.Service.Image, container.ID, u.String())
	s := StartedService{
		Url:         u,
		ID:          container.ID,
		VNCHostPort: vncHostPort,
		Cancel:      func() { d.removeContainer(ctx, d.Client, container.ID) },
	}
	return &s, nil
}

func getPortConfig(service *config.Browser, caps session.Caps, env Environment) (*portConfig, error) {
	selenium, err := nat.NewPort("tcp", service.Port)
	if err != nil {
		return nil, fmt.Errorf("new selenium port: %v", err)
	}
	exposedPorts := map[nat.Port]struct{}{selenium: {}}
	var vnc nat.Port
	if caps.VNC {
		vnc, err = nat.NewPort("tcp", "5900")
		if err != nil {
			return nil, fmt.Errorf("new vnc port: %v", err)
		}
		exposedPorts[vnc] = struct{}{}
	}
	portBindings := nat.PortMap{}
	if env.IP != "" || !env.InDocker {
		portBindings[selenium] = []nat.PortBinding{{HostIP: "0.0.0.0"}}
		if caps.VNC {
			portBindings[vnc] = []nat.PortBinding{{HostIP: "0.0.0.0"}}
		}
	}
	return &portConfig{
		SeleniumPort: selenium,
		VNCPort:      vnc,
		PortBindings: portBindings,
		ExposedPorts: exposedPorts}, nil
}

func getLogConfig(logConfig container.LogConfig, caps session.Caps) container.LogConfig {
	if logConfig.Config != nil {
		const tag = "tag"
		_, ok := logConfig.Config[tag]
		if caps.Name != "" && !ok {
			logConfig.Config[tag] = caps.Name
		}
	}
	return logConfig
}

func getTimeZone(service ServiceBase, caps session.Caps) *time.Location {
	timeZone := time.Local
	if caps.TimeZone != "" {
		tz, err := time.LoadLocation(caps.TimeZone)
		if err != nil {
			log.Printf("[%d] [BAD_TIMEZONE] [%s]\n", service.RequestId, caps.TimeZone)
		} else {
			timeZone = tz
		}
	}
	return timeZone
}

func getEnv(service ServiceBase, caps session.Caps) []string {<commit_msg>FIX: log tag value: Name(capability browserName) -> TestName(capability name)<commit_after>package service

import (
	"context"
	"fmt"
	"log"
	"net"
	"net/url"
	"time"

	"github.com/aerokube/selenoid/config"
	"github.com/aerokube/selenoid/session"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/network"
	"github.com/docker/docker/api/types/strslice"
	"github.com/docker/docker/client"
	"github.com/docker/go-connections/nat"
	"strings"
)

const (
	comma    = ","
	sysAdmin = "SYS_ADMIN"
)

// Docker - docker container manager
type Docker struct {
	ServiceBase
	Environment
	session.Caps
	LogConfig *container.LogConfig
	Client    *client.Client
}

type portConfig struct {
	SeleniumPort nat.Port
	VNCPort      nat.Port
	PortBindings nat.PortMap
	ExposedPorts map[nat.Port]struct{}
}

// StartWithCancel - Starter interface implementation
func (d *Docker) StartWithCancel() (*StartedService, error) {
	portConfig, err := getPortConfig(d.Service, d.Caps, d.Environment)
	if err != nil {
		return nil, fmt.Errorf("configuring ports: %v", err)
	}
	selenium := portConfig.SeleniumPort
	vnc := portConfig.VNCPort
	ctx := context.Background()
	log.Printf("[%d] [CREATING_CONTAINER] [%s]\n", d.RequestId, d.Service.Image)
	hostConfig := container.HostConfig{
		Binds:        d.Service.Volumes,
		AutoRemove:   true,
		PortBindings: portConfig.PortBindings,
		LogConfig:    getLogConfig(*d.LogConfig, d.Caps),
		NetworkMode:  container.NetworkMode(d.Network),
		Tmpfs:        d.Service.Tmpfs,
		ShmSize:      getShmSize(d.Service),
		Privileged:   d.Privileged,
		Resources: container.Resources{
			Memory:   d.Memory,
			NanoCPUs: d.CPU,
		},
		ExtraHosts: getExtraHosts(d.Service, d.Caps),
	}
	if !d.Privileged {
		hostConfig.CapAdd = strslice.StrSlice{sysAdmin}
	}
	if d.ApplicationContainers != "" {
		links := strings.Split(d.ApplicationContainers, comma)
		hostConfig.Links = links
	}
	container, err := d.Client.ContainerCreate(ctx,
		&container.Config{
			Hostname:     getContainerHostname(d.Caps),
			Image:        d.Service.Image.(string),
			Env:          getEnv(d.ServiceBase, d.Caps),
			ExposedPorts: portConfig.ExposedPorts,
		},
		&hostConfig,
		&network.NetworkingConfig{}, "")
	if err != nil {
		return nil, fmt.Errorf("create container: %v", err)
	}
	containerStartTime := time.Now()
	log.Printf("[%d] [STARTING_CONTAINER] [%s] [%s]\n", d.RequestId, d.Service.Image, container.ID)
	err = d.Client.ContainerStart(ctx, container.ID, types.ContainerStartOptions{})
	if err != nil {
		d.removeContainer(ctx, d.Client, container.ID)
		return nil, fmt.Errorf("start container: %v", err)
	}
	log.Printf("[%d] [CONTAINER_STARTED] [%s] [%s] [%v]\n", d.RequestId, d.Service.Image, container.ID, time.Since(containerStartTime))
	stat, err := d.Client.ContainerInspect(ctx, container.ID)
	if err != nil {
		d.removeContainer(ctx, d.Client, container.ID)
		return nil, fmt.Errorf("inspect container %s: %s", container.ID, err)
	}
	_, ok := stat.NetworkSettings.Ports[selenium]
	if !ok {
		d.removeContainer(ctx, d.Client, container.ID)
		return nil, fmt.Errorf("no bindings available for %v", selenium)
	}
	seleniumHostPort, vncHostPort := getHostPort(d.Environment, d.Service, d.Caps, stat, selenium, vnc)
	u := &url.URL{Scheme: "http", Host: seleniumHostPort, Path: d.Service.Path}
	serviceStartTime := time.Now()
	err = wait(u.String(), d.StartupTimeout)
	if err != nil {
		d.removeContainer(ctx, d.Client, container.ID)
		return nil, fmt.Errorf("wait: %v", err)
	}
	log.Printf("[%d] [SERVICE_STARTED] [%s] [%s] [%v]\n", d.RequestId, d.Service.Image, container.ID, time.Since(serviceStartTime))
	log.Printf("[%d] [PROXY_TO] [%s] [%s] [%s]\n", d.RequestId, d.Service.Image, container.ID, u.String())
	s := StartedService{
		Url:         u,
		ID:          container.ID,
		VNCHostPort: vncHostPort,
		Cancel:      func() { d.removeContainer(ctx, d.Client, container.ID) },
	}
	return &s, nil
}

func getPortConfig(service *config.Browser, caps session.Caps, env Environment) (*portConfig, error) {
	selenium, err := nat.NewPort("tcp", service.Port)
	if err != nil {
		return nil, fmt.Errorf("new selenium port: %v", err)
	}
	exposedPorts := map[nat.Port]struct{}{selenium: {}}
	var vnc nat.Port
	if caps.VNC {
		vnc, err = nat.NewPort("tcp", "5900")
		if err != nil {
			return nil, fmt.Errorf("new vnc port: %v", err)
		}
		exposedPorts[vnc] = struct{}{}
	}
	portBindings := nat.PortMap{}
	if env.IP != "" || !env.InDocker {
		portBindings[selenium] = []nat.PortBinding{{HostIP: "0.0.0.0"}}
		if caps.VNC {
			portBindings[vnc] = []nat.PortBinding{{HostIP: "0.0.0.0"}}
		}
	}
	return &portConfig{
		SeleniumPort: selenium,
		VNCPort:      vnc,
		PortBindings: portBindings,
		ExposedPorts: exposedPorts}, nil
}

func getLogConfig(logConfig container.LogConfig, caps session.Caps) container.LogConfig {
	if logConfig.Config != nil {
		const tag = "tag"
		_, ok := logConfig.Config[tag]
		if caps.TestName != "" && !ok {
			logConfig.Config[tag] = caps.TestName
		}
	}
	return logConfig
}

func getTimeZone(service ServiceBase, caps session.Caps) *time.Location {
	timeZone := time.Local
	if caps.TimeZone != "" {
		tz, err := time.LoadLocation(caps.TimeZone)
		if err != nil {
			log.Printf("[%d] [BAD_TIMEZONE] [%s]\n", service.RequestId, caps.TimeZone)
		} else {
			timeZone = tz
		}
	}
	return timeZone
}

func getEnv(service ServiceBase, caps session.Caps) []string {
{'fraction_non_alphanumeric': 0.15724316334072433, 'fraction_numerical': 0.0022172949002217295, 'mean_word_length': 4.149857278782112, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 4, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 4, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6297979', 'n_tokens_mistral': 4241, 'n_tokens_neox': 4029, 'n_words': 807}