mirror of
https://github.com/azukaar/Cosmos-Server.git
synced 2026-01-05 20:05:02 -06:00
1027 lines
34 KiB
Go
1027 lines
34 KiB
Go
package docker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
"bufio"
|
|
"strconv"
|
|
"os"
|
|
"io/ioutil"
|
|
"os/user"
|
|
"errors"
|
|
"github.com/docker/go-connections/nat"
|
|
"github.com/docker/docker/api/types/mount"
|
|
"github.com/docker/docker/api/types/network"
|
|
conttype "github.com/docker/docker/api/types/container"
|
|
doctype "github.com/docker/docker/api/types"
|
|
strslice "github.com/docker/docker/api/types/strslice"
|
|
volumetype "github.com/docker/docker/api/types/volume"
|
|
|
|
"github.com/azukaar/cosmos-server/src/utils"
|
|
)
|
|
|
|
type ContainerCreateRequestServiceNetwork struct {
|
|
Aliases []string `json:"aliases,omitempty"`
|
|
IPV4Address string `json:"ipv4_address,omitempty"`
|
|
IPV6Address string `json:"ipv6_address,omitempty"`
|
|
}
|
|
|
|
type ContainerCreateRequestContainerHealthcheck struct {
|
|
Test []string `json:"test"`
|
|
Interval int `json:"interval"`
|
|
Timeout int `json:"timeout"`
|
|
Retries int `json:"retries"`
|
|
StartPeriod int `json:"start_period"`
|
|
}
|
|
|
|
|
|
type ContainerCreateRequestContainer struct {
|
|
Name string `json:"container_name"`
|
|
Image string `json:"image"`
|
|
Environment []string `json:"environment"`
|
|
Labels map[string]string `json:"labels"`
|
|
Ports []string `json:"ports"`
|
|
Volumes []mount.Mount `json:"volumes"`
|
|
Networks map[string]ContainerCreateRequestServiceNetwork `json:"networks"`
|
|
Routes []utils.ProxyRouteConfig `json:"routes"`
|
|
Links []string `json:"links,omitempty"`
|
|
|
|
RestartPolicy string `json:"restart,omitempty"`
|
|
Devices []string `json:"devices"`
|
|
Expose []string `json:"expose"`
|
|
DependsOn []string `json:"depends_on"`
|
|
Tty bool `json:"tty,omitempty"`
|
|
StdinOpen bool `json:"stdin_open,omitempty"`
|
|
|
|
Command string `json:"command,omitempty"`
|
|
Entrypoint string `json:"entrypoint,omitempty"`
|
|
WorkingDir string `json:"working_dir,omitempty"`
|
|
User string `json:"user,omitempty"`
|
|
UID int `json:"uid,omitempty"`
|
|
GID int `json:"gid,omitempty"`
|
|
Hostname string `json:"hostname,omitempty"`
|
|
Domainname string `json:"domainname,omitempty"`
|
|
MacAddress string `json:"mac_address,omitempty"`
|
|
Privileged bool `json:"privileged,omitempty"`
|
|
NetworkMode string `json:"network_mode,omitempty"`
|
|
StopSignal string `json:"stop_signal,omitempty"`
|
|
StopGracePeriod int `json:"stop_grace_period,omitempty"`
|
|
HealthCheck ContainerCreateRequestContainerHealthcheck `json:"healthcheck,omitempty"`
|
|
DNS []string `json:"dns,omitempty"`
|
|
DNSSearch []string `json:"dns_search,omitempty"`
|
|
ExtraHosts []string `json:"extra_hosts,omitempty"`
|
|
SecurityOpt []string `json:"security_opt,omitempty"`
|
|
StorageOpt map[string]string `json:"storage_opt,omitempty"`
|
|
Sysctls map[string]string `json:"sysctls,omitempty"`
|
|
Isolation string `json:"isolation,omitempty"`
|
|
|
|
CapAdd []string `json:"cap_add,omitempty"`
|
|
CapDrop []string `json:"cap_drop,omitempty"`
|
|
SysctlsMap map[string]string `json:"sysctls,omitempty"`
|
|
|
|
PostInstall []string `json:"post_install,omitempty"`
|
|
}
|
|
|
|
type ContainerCreateRequestVolume struct {
|
|
// name must be unique
|
|
Name string `json:"name"`
|
|
Driver string `json:"driver"`
|
|
Source string `json:"source"`
|
|
Target string `json:"target"`
|
|
}
|
|
|
|
type ContainerCreateRequestNetworkIPAMConfig struct {
|
|
Subnet string `json:"subnet"`
|
|
Gateway string `json:"gateway"`
|
|
}
|
|
|
|
type ContainerCreateRequestNetwork struct {
|
|
// name must be unique
|
|
Name string `json:"name"`
|
|
Driver string `json:"driver"`
|
|
Attachable bool `json:"attachable"`
|
|
Internal bool `json:"internal"`
|
|
EnableIPv6 bool `json:"enable_ipv6"`
|
|
Labels map[string]string `json:"labels"`
|
|
IPAM struct {
|
|
Driver string `json:"driver"`
|
|
Config []ContainerCreateRequestNetworkIPAMConfig `json:"config"`
|
|
} `json:"ipam"`
|
|
}
|
|
|
|
type DockerServiceCreateRequest struct {
|
|
Services map[string]ContainerCreateRequestContainer `json:"services"`
|
|
Volumes map[string]ContainerCreateRequestVolume `json:"volumes"`
|
|
Networks map[string]ContainerCreateRequestNetwork `json:"networks"`
|
|
}
|
|
|
|
type DockerServiceCreateRollback struct {
|
|
// action: disconnect, remove, etc...
|
|
Action string `json:"action"`
|
|
// type: container, volume, network
|
|
Type string `json:"type"`
|
|
// name: container name, volume name, network name
|
|
Name string `json:"name"`
|
|
// was: container old settings
|
|
Was doctype.ContainerJSON `json:"was"`
|
|
}
|
|
|
|
func Rollback(actions []DockerServiceCreateRollback , OnLog func(string)) {
|
|
for i := len(actions) - 1; i >= 0; i-- {
|
|
action := actions[i]
|
|
switch action.Type {
|
|
case "container":
|
|
if action.Action == "remove" {
|
|
utils.Log(fmt.Sprintf("Removing container %s...", action.Name))
|
|
|
|
DockerClient.ContainerKill(DockerContext, action.Name, "SIGKILL")
|
|
err := DockerClient.ContainerRemove(DockerContext, action.Name, doctype.ContainerRemoveOptions{})
|
|
|
|
if err != nil {
|
|
utils.Error("Rollback: Container", err)
|
|
OnLog(utils.DoErr("Rollback: Container %s", err))
|
|
} else {
|
|
utils.Log(fmt.Sprintf("Rolled back container %s", action.Name))
|
|
OnLog(fmt.Sprintf("Rolled back container %s\n", action.Name))
|
|
}
|
|
} else if action.Action == "revert" {
|
|
utils.Log(fmt.Sprintf("Reverting container %s...", action.Name))
|
|
|
|
// Edit Container
|
|
_, err := EditContainer(action.Name, action.Was, false)
|
|
|
|
if err != nil {
|
|
utils.Error("Rollback: Container", err)
|
|
OnLog(utils.DoErr("Rollback: Container %s", err))
|
|
} else {
|
|
utils.Log(fmt.Sprintf("Rolled back container %s", action.Name))
|
|
OnLog(fmt.Sprintf("Rolled back container %s\n", action.Name))
|
|
}
|
|
}
|
|
case "volume":
|
|
utils.Log(fmt.Sprintf("Removing volume %s...", action.Name))
|
|
|
|
err := DockerClient.VolumeRemove(DockerContext, action.Name, true)
|
|
if err != nil {
|
|
utils.Error("Rollback: Volume", err)
|
|
OnLog(utils.DoErr("Rollback: Volume %s", err))
|
|
} else {
|
|
utils.Log(fmt.Sprintf("Rolled back volume %s", action.Name))
|
|
OnLog(fmt.Sprintf("Rolled back volume %s\n", action.Name))
|
|
}
|
|
case "network":
|
|
utils.Log(fmt.Sprintf("Removing network %s...", action.Name))
|
|
|
|
if os.Getenv("HOSTNAME") != "" {
|
|
DockerClient.NetworkDisconnect(DockerContext, action.Name, os.Getenv("HOSTNAME"), true)
|
|
}
|
|
err := DockerClient.NetworkRemove(DockerContext, action.Name)
|
|
if err != nil {
|
|
utils.Error("Rollback: Network", err)
|
|
OnLog(utils.DoErr("Rollback: Network %s", err))
|
|
} else {
|
|
utils.Log(fmt.Sprintf("Rolled back network %s", action.Name))
|
|
OnLog(fmt.Sprintf("Rolled back network %s\n", action.Name))
|
|
}
|
|
}
|
|
}
|
|
|
|
// After all operations
|
|
utils.Error("CreateService", fmt.Errorf("Operation failed. Changes have been rolled back."))
|
|
OnLog("[OPERATION FAILED]. CHANGES HAVE BEEN ROLLEDBACK.\n")
|
|
}
|
|
|
|
func CreateServiceRoute(w http.ResponseWriter, req *http.Request) {
|
|
if utils.AdminOnly(w, req) != nil {
|
|
return
|
|
}
|
|
|
|
errD := Connect()
|
|
if errD != nil {
|
|
utils.Error("CreateService - connect - ", errD)
|
|
utils.HTTPError(w, "Internal server error: " + errD.Error(), http.StatusInternalServerError, "DS002")
|
|
return
|
|
}
|
|
|
|
if req.Method == "POST" {
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.Header().Set("Transfer-Encoding", "chunked")
|
|
|
|
flusher, ok := w.(http.Flusher)
|
|
if !ok {
|
|
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
decoder := json.NewDecoder(req.Body)
|
|
var serviceRequest DockerServiceCreateRequest
|
|
err := decoder.Decode(&serviceRequest)
|
|
if err != nil {
|
|
utils.Error("CreateService - decode - ", err)
|
|
fmt.Fprintf(w, "[OPERATION FAILED] Bad request: "+err.Error(), http.StatusBadRequest, "DS003")
|
|
flusher.Flush()
|
|
utils.HTTPError(w, "Bad request: " + err.Error(), http.StatusBadRequest, "DS003")
|
|
return
|
|
}
|
|
|
|
CreateService(serviceRequest,
|
|
func (msg string) {
|
|
fmt.Fprintf(w, msg)
|
|
flusher.Flush()
|
|
},
|
|
)
|
|
} else {
|
|
utils.Error("CreateService: Method not allowed" + req.Method, nil)
|
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
|
return
|
|
}
|
|
}
|
|
|
|
// generatePorts is a helper function to generate a slice of ports from a string range.
|
|
func generatePorts(portRangeStr string) []string {
|
|
portsStr := strings.Split(portRangeStr, "-")
|
|
if len(portsStr) != 2 {
|
|
return []string{
|
|
portsStr[0],
|
|
}
|
|
}
|
|
start, _ := strconv.Atoi(portsStr[0])
|
|
end, _ := strconv.Atoi(portsStr[1])
|
|
|
|
ports := make([]string, end-start+1)
|
|
for i := range ports {
|
|
ports[i] = strconv.Itoa(start + i)
|
|
}
|
|
|
|
return ports
|
|
}
|
|
|
|
func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)) error {
|
|
utils.ConfigLock.Lock()
|
|
defer utils.ConfigLock.Unlock()
|
|
|
|
utils.Log("Starting creation of new service...")
|
|
OnLog("Starting creation of new service...\n")
|
|
|
|
needsHTTPRestart := false
|
|
config := utils.ReadConfigFromFile()
|
|
configRoutes := config.HTTPConfig.ProxyConfig.Routes
|
|
|
|
var rollbackActions []DockerServiceCreateRollback
|
|
var err error
|
|
|
|
// Create networks
|
|
for networkToCreateName, networkToCreate := range serviceRequest.Networks {
|
|
utils.Log(fmt.Sprintf("Creating network %s...", networkToCreateName))
|
|
OnLog(fmt.Sprintf("Creating network %s...\n", networkToCreateName))
|
|
|
|
// check if network already exists
|
|
exNetworkDef, err := DockerClient.NetworkInspect(DockerContext, networkToCreateName, doctype.NetworkInspectOptions{})
|
|
|
|
if err == nil {
|
|
if networkToCreate.Driver == "" {
|
|
networkToCreate.Driver = "bridge"
|
|
}
|
|
|
|
if (exNetworkDef.Driver != networkToCreate.Driver) {
|
|
utils.Error("CreateService: Network", err)
|
|
OnLog(utils.DoErr("Network %s already exists with incompatible settings, cannot merge new network into it.\n", networkToCreateName))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
} else {
|
|
utils.Warn(fmt.Sprintf("Network %s already exists, skipping creation", networkToCreateName))
|
|
OnLog(utils.DoWarn("Network %s already exists, skipping creation\n", networkToCreateName))
|
|
continue
|
|
}
|
|
}
|
|
|
|
ipamConfig := make([]network.IPAMConfig, len(networkToCreate.IPAM.Config))
|
|
if networkToCreate.IPAM.Config != nil {
|
|
for i, config := range networkToCreate.IPAM.Config {
|
|
ipamConfig[i] = network.IPAMConfig{
|
|
Subnet: config.Subnet,
|
|
}
|
|
}
|
|
}
|
|
|
|
_, err = DockerClient.NetworkCreate(DockerContext, networkToCreateName, doctype.NetworkCreate{
|
|
Driver: networkToCreate.Driver,
|
|
Attachable: networkToCreate.Attachable,
|
|
Internal: networkToCreate.Internal,
|
|
EnableIPv6: networkToCreate.EnableIPv6,
|
|
IPAM: &network.IPAM{
|
|
Driver: networkToCreate.IPAM.Driver,
|
|
Config: ipamConfig,
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
utils.Error("CreateService: Rolling back changes because of -- Network", err)
|
|
OnLog(utils.DoErr("Rolling back changes because of -- Network creation error: %s\n", err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
|
|
rollbackActions = append(rollbackActions, DockerServiceCreateRollback{
|
|
Action: "remove",
|
|
Type: "network",
|
|
Name: networkToCreateName,
|
|
})
|
|
|
|
// Write a response to the client
|
|
utils.Log(fmt.Sprintf("Network %s created", networkToCreateName))
|
|
OnLog(fmt.Sprintf("Network %s created\n", networkToCreateName))
|
|
}
|
|
|
|
// Create volumes
|
|
for volumeName, volume := range serviceRequest.Volumes {
|
|
if volume.Name == "" {
|
|
volume.Name = volumeName
|
|
}
|
|
|
|
// check if volume already exists
|
|
_, err := DockerClient.VolumeInspect(DockerContext, volume.Name)
|
|
if err == nil {
|
|
utils.Warn(fmt.Sprintf("Volume %s already exists, skipping creation", volume.Name))
|
|
OnLog(utils.DoWarn("Volume %s already exists, skipping creation\n", volume.Name))
|
|
continue
|
|
}
|
|
|
|
utils.Log(fmt.Sprintf("Creating volume %s...", volume.Name))
|
|
OnLog(fmt.Sprintf("Creating volume %s...\n", volume.Name))
|
|
|
|
_, err = DockerClient.VolumeCreate(DockerContext, volumetype.CreateOptions{
|
|
Driver: volume.Driver,
|
|
Name: volume.Name,
|
|
})
|
|
|
|
if err != nil {
|
|
utils.Error("CreateService: Rolling back changes because of -- Volume", err)
|
|
OnLog(utils.DoErr("Rolling back changes because of -- Volume creation error: %s\n", err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
|
|
rollbackActions = append(rollbackActions, DockerServiceCreateRollback{
|
|
Action: "remove",
|
|
Type: "volume",
|
|
Name: volume.Name,
|
|
})
|
|
|
|
// Write a response to the client
|
|
utils.Log(fmt.Sprintf("Volume %s created", volume.Name))
|
|
OnLog(fmt.Sprintf("Volume %s created\n", volume.Name))
|
|
}
|
|
|
|
// pull images
|
|
for _, container := range serviceRequest.Services {
|
|
// Write a response to the client
|
|
utils.Log(fmt.Sprintf("Pulling image %s", container.Image))
|
|
OnLog(fmt.Sprintf("Pulling image %s\n", container.Image))
|
|
|
|
out, err := DockerPullImage(container.Image)
|
|
if err != nil {
|
|
utils.Error("CreateService: Rolling back changes because of -- Image pull", err)
|
|
OnLog(utils.DoErr("Rolling back changes because of -- Image pull error: %s\n", err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
// wait for image pull to finish
|
|
scanner := bufio.NewScanner(out)
|
|
for scanner.Scan() {
|
|
OnLog(fmt.Sprintf("%s\n", scanner.Text()))
|
|
}
|
|
|
|
// Write a response to the client
|
|
utils.Log(fmt.Sprintf("Image %s pulled", container.Image))
|
|
OnLog(fmt.Sprintf("Image %s pulled\n", container.Image))
|
|
}
|
|
|
|
|
|
// Create containers
|
|
for serviceName, container := range serviceRequest.Services {
|
|
utils.Log(fmt.Sprintf("Checking service %s...", serviceName))
|
|
OnLog(fmt.Sprintf("Checking service %s...\n", serviceName))
|
|
|
|
// If container request a Cosmos network, create and attach it
|
|
if (container.Labels["cosmos-force-network-secured"] == "true" || strings.ToLower(container.Labels["cosmos-network-name"]) == "auto") &&
|
|
container.Labels["cosmos-network-name"] == "" {
|
|
utils.Log(fmt.Sprintf("Forcing secure %s...", serviceName))
|
|
OnLog(fmt.Sprintf("Forcing secure %s...\n", serviceName))
|
|
|
|
newNetwork, errNC := CreateCosmosNetwork()
|
|
if errNC != nil {
|
|
utils.Error("CreateService: Network", err)
|
|
OnLog(utils.DoErr("Network %s cant be created\n", newNetwork))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
|
|
container.Labels["cosmos-network-name"] = newNetwork
|
|
|
|
AttachNetworkToCosmos(newNetwork)
|
|
|
|
if container.Networks == nil {
|
|
container.Networks = make(map[string]ContainerCreateRequestServiceNetwork)
|
|
}
|
|
|
|
container.Networks[newNetwork] = ContainerCreateRequestServiceNetwork{}
|
|
|
|
rollbackActions = append(rollbackActions, DockerServiceCreateRollback{
|
|
Action: "remove",
|
|
Type: "network",
|
|
Name: newNetwork,
|
|
})
|
|
|
|
utils.Log(fmt.Sprintf("Created secure network %s", newNetwork))
|
|
OnLog(fmt.Sprintf("Created secure network %s\n", newNetwork))
|
|
} else if container.Labels["cosmos-network-name"] != "" {
|
|
// Container has a declared a Cosmos network, check if it exists and connect to it
|
|
utils.Log(fmt.Sprintf("Checking declared network %s...", container.Labels["cosmos-network-name"]))
|
|
OnLog(fmt.Sprintf("Checking declared network %s...\n", container.Labels["cosmos-network-name"]))
|
|
|
|
_, err := DockerClient.NetworkInspect(DockerContext, container.Labels["cosmos-network-name"], doctype.NetworkInspectOptions{})
|
|
if err == nil {
|
|
utils.Log(fmt.Sprintf("Connecting to declared network %s...", container.Labels["cosmos-network-name"]))
|
|
OnLog(fmt.Sprintf("Connecting to declared network %s...\n", container.Labels["cosmos-network-name"]))
|
|
|
|
AttachNetworkToCosmos(container.Labels["cosmos-network-name"])
|
|
}
|
|
}
|
|
|
|
utils.Log(fmt.Sprintf("Creating container %s...", container.Name))
|
|
OnLog(fmt.Sprintf("Creating container %s...\n", container.Name))
|
|
|
|
containerConfig := &conttype.Config{
|
|
Image: container.Image,
|
|
Env: container.Environment,
|
|
Labels: container.Labels,
|
|
ExposedPorts: nat.PortSet{},
|
|
WorkingDir: container.WorkingDir,
|
|
User: container.User,
|
|
Hostname: container.Hostname,
|
|
Domainname: container.Domainname,
|
|
MacAddress: container.MacAddress,
|
|
StopSignal: container.StopSignal,
|
|
StopTimeout: &container.StopGracePeriod,
|
|
Tty: container.Tty,
|
|
OpenStdin: container.StdinOpen,
|
|
}
|
|
|
|
// check if there's an empty TZ env, if so, replace it with the host's TZ
|
|
if containerConfig.Env != nil {
|
|
for i, env := range containerConfig.Env {
|
|
if strings.HasPrefix(env, "TZ=") {
|
|
if strings.TrimPrefix(env, "TZ=") == "auto" {
|
|
if os.Getenv("TZ") != "" {
|
|
containerConfig.Env[i] = "TZ=" + os.Getenv("TZ")
|
|
} else {
|
|
containerConfig.Env = append(containerConfig.Env[:i], containerConfig.Env[i+1:]...)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if container.Command != "" {
|
|
containerConfig.Cmd = strings.Fields(container.Command)
|
|
}
|
|
|
|
if container.Entrypoint != "" {
|
|
containerConfig.Entrypoint = strslice.StrSlice(strings.Fields(container.Entrypoint))
|
|
}
|
|
|
|
// For Expose / Ports
|
|
|
|
for _, expose := range container.Expose {
|
|
exposePort := nat.Port(expose)
|
|
containerConfig.ExposedPorts[exposePort] = struct{}{}
|
|
}
|
|
|
|
PortBindings := nat.PortMap{}
|
|
finalPorts := []string{}
|
|
|
|
for _, portRaw := range container.Ports {
|
|
portStuff := strings.Split(portRaw, "/")
|
|
|
|
if len(portStuff) == 1 {
|
|
portStuff = append(portStuff, "tcp")
|
|
}
|
|
|
|
port, protocol := portStuff[0], portStuff[1]
|
|
|
|
hostPorts := []string{}
|
|
containerPorts := []string{}
|
|
|
|
ports := strings.Split(port, ":")
|
|
|
|
hostPorts = generatePorts(ports[len(ports)-2])
|
|
containerPorts = generatePorts(ports[len(ports)-1])
|
|
|
|
ipExposed := ""
|
|
if len(portStuff) > 2 {
|
|
ipExposed = strings.Join(portStuff[0:len(portStuff)-2], ":")
|
|
}
|
|
|
|
for i := 0; i < utils.Max(len(hostPorts), len(containerPorts)); i++ {
|
|
hostPort := hostPorts[i%len(hostPorts)]
|
|
containerPort := containerPorts[i%len(containerPorts)]
|
|
|
|
finalPorts = append(finalPorts, fmt.Sprintf("%s@%s:%s/%s", ipExposed, hostPort, containerPort, protocol))
|
|
}
|
|
}
|
|
|
|
utils.Debug(fmt.Sprintf("Final ports: %s", finalPorts))
|
|
|
|
hostPortsBound := make(map[string]bool)
|
|
|
|
for _, portRaw := range finalPorts {
|
|
portStuff := strings.Split(portRaw, "/")
|
|
ipport := strings.Split(portStuff[0], "@")
|
|
|
|
ipdest := ""
|
|
portdef := ipport[0]
|
|
if len(ipport) > 1 {
|
|
portdef = ipport[1]
|
|
ipdest = ipport[0]
|
|
}
|
|
|
|
port, protocol := portdef, portStuff[1]
|
|
|
|
if protocol == "" {
|
|
protocol = "tcp"
|
|
}
|
|
|
|
nextPort := strings.Split(port, ":")
|
|
hostPort, contPort := nextPort[0], nextPort[1]
|
|
|
|
contPort = contPort + "/" + protocol
|
|
|
|
if hostPortsBound[hostPort + "/" + protocol] {
|
|
utils.Warn("Port " + hostPort + " already bound, skipping")
|
|
continue
|
|
}
|
|
|
|
// Get the existing bindings for this container port, if any
|
|
bindings := PortBindings[nat.Port(contPort)]
|
|
|
|
// Append a new PortBinding to the slice of bindings
|
|
pb := nat.PortBinding {
|
|
HostPort: hostPort,
|
|
}
|
|
|
|
if ipdest != "" {
|
|
pb.HostIP = ipdest
|
|
}
|
|
|
|
bindings = append(bindings, pb)
|
|
|
|
// Update the port bindings for this container port
|
|
PortBindings[nat.Port(contPort)] = bindings
|
|
|
|
// Mark the container port as exposed
|
|
containerConfig.ExposedPorts[nat.Port(contPort)] = struct{}{}
|
|
|
|
hostPortsBound[hostPort + "/" + protocol] = true
|
|
}
|
|
|
|
// Create missing folders for bind mounts
|
|
for _, newmount := range container.Volumes {
|
|
if newmount.Type == mount.TypeBind {
|
|
newSource := newmount.Source
|
|
|
|
if os.Getenv("HOSTNAME") != "" {
|
|
if _, err := os.Stat("/mnt/host"); os.IsNotExist(err) {
|
|
utils.Error("CreateService: Unable to create directory for bind mount in the host directory. Please mount the host / in Cosmos with -v /:/mnt/host to enable folder creations, or create the bind folder yourself", err)
|
|
OnLog(utils.DoErr("Unable to create directory for bind mount in the host directory. Please mount the host / in Cosmos with -v /:/mnt/host to enable folder creations, or create the bind folder yourself: %s\n", err.Error()))
|
|
|
|
continue
|
|
} else {
|
|
newSource = "/mnt/host" + newSource
|
|
}
|
|
}
|
|
|
|
utils.Log(fmt.Sprintf("Checking directory %s for bind mount", newSource))
|
|
OnLog(fmt.Sprintf("Checking directory %s for bind mount\n", newSource))
|
|
|
|
if _, err := os.Stat(newSource); os.IsNotExist(err) {
|
|
utils.Log(fmt.Sprintf("Not found. Creating directory %s for bind mount", newSource))
|
|
OnLog(fmt.Sprintf("Not found. Creating directory %s for bind mount\n", newSource))
|
|
|
|
err := os.MkdirAll(newSource, 0750)
|
|
|
|
if err != nil {
|
|
utils.Error("CreateService: Unable to create directory for bind mount. Make sure parent directories exist, and that Cosmos has permissions to create directories in the host directory", err)
|
|
OnLog(utils.DoErr("Unable to create directory for bind mount. Make sure parent directories exist, and that Cosmos has permissions to create directories in the host directory: %s\n", err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
|
|
if container.UID != 0 {
|
|
// Change the ownership of the directory to the container.UID
|
|
err = os.Chown(newSource, container.UID, container.GID)
|
|
if err != nil {
|
|
utils.Error("CreateService: Unable to change ownership of directory", err)
|
|
OnLog(utils.DoErr("Unable to change ownership of directory: " + err.Error()))
|
|
}
|
|
} else if container.User != "" && strings.Contains(container.User, ":") {
|
|
uidgid := strings.Split(container.User, ":")
|
|
uid, _ := strconv.Atoi(uidgid[0])
|
|
gid, _ := strconv.Atoi(uidgid[1])
|
|
err = os.Chown(newSource, uid, gid)
|
|
if err != nil {
|
|
utils.Error("CreateService: Unable to change ownership of directory", err)
|
|
OnLog(utils.DoErr("Unable to change ownership of directory: " + err.Error()))
|
|
}
|
|
} else if container.User != "" {
|
|
// Change the ownership of the directory to the container.User
|
|
userInfo, err := user.Lookup(container.User)
|
|
if err != nil {
|
|
utils.Error("CreateService: Unable to lookup user", err)
|
|
OnLog(utils.DoErr("Unable to lookup user " + container.User + ". " +err.Error()))
|
|
} else {
|
|
uid, _ := strconv.Atoi(userInfo.Uid)
|
|
gid, _ := strconv.Atoi(userInfo.Gid)
|
|
err = os.Chown(newSource, uid, gid)
|
|
if err != nil {
|
|
utils.Error("CreateService: Unable to change ownership of directory", err)
|
|
OnLog(utils.DoErr("Unable to change ownership of directory: " + err.Error()))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
hostConfig := &conttype.HostConfig{
|
|
PortBindings: PortBindings,
|
|
Mounts: container.Volumes,
|
|
RestartPolicy: conttype.RestartPolicy{
|
|
Name: container.RestartPolicy,
|
|
},
|
|
Privileged: container.Privileged,
|
|
NetworkMode: conttype.NetworkMode(container.NetworkMode),
|
|
DNS: container.DNS,
|
|
DNSSearch: container.DNSSearch,
|
|
ExtraHosts: container.ExtraHosts,
|
|
SecurityOpt: container.SecurityOpt,
|
|
StorageOpt: container.StorageOpt,
|
|
Sysctls: container.Sysctls,
|
|
Isolation: conttype.Isolation(container.Isolation),
|
|
CapAdd: container.CapAdd,
|
|
CapDrop: container.CapDrop,
|
|
}
|
|
|
|
// For Healthcheck
|
|
if len(container.HealthCheck.Test) > 0 {
|
|
containerConfig.Healthcheck = &conttype.HealthConfig{
|
|
Test: container.HealthCheck.Test,
|
|
Interval: time.Duration(container.HealthCheck.Interval) * time.Second,
|
|
Timeout: time.Duration(container.HealthCheck.Timeout) * time.Second,
|
|
StartPeriod: time.Duration(container.HealthCheck.StartPeriod) * time.Second,
|
|
Retries: container.HealthCheck.Retries,
|
|
}
|
|
}
|
|
|
|
// For Devices
|
|
devices := []conttype.DeviceMapping{}
|
|
|
|
for _, device := range container.Devices {
|
|
deviceSplit := strings.Split(device, ":")
|
|
devices = append(devices, conttype.DeviceMapping{
|
|
PathOnHost: deviceSplit[0],
|
|
PathInContainer: deviceSplit[1],
|
|
CgroupPermissions: "rwm", // This can be "r", "w", "m", or any combination
|
|
})
|
|
}
|
|
|
|
hostConfig.Devices = devices
|
|
|
|
networkingConfig := &network.NetworkingConfig{
|
|
EndpointsConfig: make(map[string]*network.EndpointSettings),
|
|
}
|
|
|
|
// check if container exist
|
|
existingContainer, err := DockerClient.ContainerInspect(DockerContext, container.Name)
|
|
if err == nil {
|
|
utils.Warn("CreateService: Container " + container.Name + " already exist, overwriting.")
|
|
OnLog(utils.DoWarn("Container " + container.Name + " already exist, overwriting.\n"))
|
|
|
|
// Edit Container
|
|
newConfig := doctype.ContainerJSON{}
|
|
newConfig.ContainerJSONBase = new(doctype.ContainerJSONBase)
|
|
newConfig.Config = containerConfig
|
|
newConfig.HostConfig = hostConfig
|
|
|
|
// check if there are persistent env var
|
|
if containerConfig.Labels["cosmos-persistent-env"] != "" {
|
|
// split env vars
|
|
envVars := strings.Split(containerConfig.Labels["cosmos-persistent-env"], ",")
|
|
// get existing env vars
|
|
existingEnvVars := existingContainer.Config.Env
|
|
// loop through env vars
|
|
for _, envVar := range envVars {
|
|
envVar = strings.TrimSpace(envVar)
|
|
|
|
// check if env var already exist
|
|
exists := false
|
|
existingEnvVarValue := ""
|
|
for _, existingEnvVar := range existingEnvVars {
|
|
if strings.HasPrefix(existingEnvVar, envVar + "=") {
|
|
exists = true
|
|
existingEnvVarValue = existingEnvVar
|
|
break
|
|
}
|
|
}
|
|
// if it exist, copy value to new container
|
|
if exists {
|
|
wasReplace := false
|
|
for i, newEnvVar := range newConfig.Config.Env {
|
|
if strings.HasPrefix(newEnvVar, envVar + "=") {
|
|
newConfig.Config.Env[i] = envVar + "=" + strings.TrimPrefix(existingEnvVarValue, envVar + "=")
|
|
wasReplace = true
|
|
break
|
|
}
|
|
}
|
|
if !wasReplace {
|
|
newConfig.Config.Env = append(newConfig.Config.Env, envVar + "=" + strings.TrimPrefix(existingEnvVarValue, envVar + "="))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_, errEdit := EditContainer(container.Name, newConfig, false)
|
|
|
|
if errEdit != nil {
|
|
utils.Error("CreateService: Rolling back changes because of -- Container", err)
|
|
OnLog(utils.DoErr("Rolling back changes because of -- Container creation error: "+err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
|
|
// rollback action
|
|
|
|
rollbackActions = append(rollbackActions, DockerServiceCreateRollback{
|
|
Action: "revert",
|
|
Type: "container",
|
|
Name: container.Name,
|
|
Was: existingContainer,
|
|
})
|
|
} else {
|
|
_, err = DockerClient.ContainerCreate(DockerContext, containerConfig, hostConfig, networkingConfig, nil, container.Name)
|
|
|
|
if err != nil {
|
|
utils.Error("CreateService: Rolling back changes because of -- Container", err)
|
|
OnLog(utils.DoErr("Rolling back changes because of -- Container creation error: "+err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
|
|
rollbackActions = append(rollbackActions, DockerServiceCreateRollback{
|
|
Action: "remove",
|
|
Type: "container",
|
|
Name: container.Name,
|
|
})
|
|
}
|
|
|
|
// connect to networks
|
|
for netName, netConfig := range container.Networks {
|
|
utils.Log("CreateService: Connecting to network: " + netName)
|
|
err = DockerClient.NetworkConnect(DockerContext, netName, container.Name, &network.EndpointSettings{
|
|
Aliases: netConfig.Aliases,
|
|
IPAddress: netConfig.IPV4Address,
|
|
GlobalIPv6Address: netConfig.IPV6Address,
|
|
})
|
|
if err != nil && !strings.Contains(err.Error(), "already exists in network") {
|
|
utils.Error("CreateService: Rolling back changes because of -- Network Connection -- ", err)
|
|
OnLog(utils.DoErr("Rolling back changes because of -- Network connection error: "+err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
} else if err != nil && strings.Contains(err.Error(), "already exists in network") {
|
|
utils.Warn("CreateService: Container " + container.Name + " already connected to network " + netName + ", skipping.")
|
|
OnLog(utils.DoWarn("Container %s already connected to network %s, skipping.", container.Name, netName))
|
|
}
|
|
}
|
|
|
|
// add routes
|
|
for _, route := range container.Routes {
|
|
// check if route already exists
|
|
exists := false
|
|
existsAt := 0
|
|
for destRouteIndex, configRoute := range configRoutes {
|
|
if configRoute.Name == route.Name {
|
|
exists = true
|
|
existsAt = destRouteIndex
|
|
break
|
|
}
|
|
}
|
|
|
|
if !exists {
|
|
needsHTTPRestart = true
|
|
configRoutes = append([]utils.ProxyRouteConfig{(utils.ProxyRouteConfig)(route)}, configRoutes...)
|
|
} else {
|
|
// utils.Error("CreateService: Rolling back changes because of -- Route already exist", nil)
|
|
// OnLog(utils.DoErr("Rolling back changes because of -- Route already exist"))
|
|
// Rollback(rollbackActions, OnLog)
|
|
// return errors.New("Route already exist")
|
|
|
|
//overwrite route
|
|
configRoutes[existsAt] = (utils.ProxyRouteConfig)(route)
|
|
utils.Warn("CreateService: Route " + route.Name + " already exist, overwriting.")
|
|
OnLog(utils.DoWarn("Route " + route.Name + " already exist, overwriting.\n"))
|
|
}
|
|
}
|
|
|
|
|
|
// Create the networks for links
|
|
for _, targetContainer := range container.Links {
|
|
if targetContainer == "" {
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(targetContainer, ":") {
|
|
err = errors.New("Link network cannot contain ':' please use container name only")
|
|
utils.Error("CreateService: Rolling back changes because of -- Link network", err)
|
|
OnLog(utils.DoErr("Rolling back changes because of -- Link network creation error: "+err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
|
|
err = CreateLinkNetwork(container.Name, targetContainer)
|
|
if err != nil {
|
|
utils.Error("CreateService: Rolling back changes because of -- Link network", err)
|
|
OnLog(utils.DoErr("Rolling back changes because of -- Link network creation error: "+err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Write a response to the client
|
|
utils.Log(fmt.Sprintf("Container %s created", container.Name))
|
|
OnLog(fmt.Sprintf("Container %s created", container.Name))
|
|
}
|
|
|
|
// re-order containers dpeneding on depends_on
|
|
startOrder, err := ReOrderServices(serviceRequest.Services)
|
|
if err != nil {
|
|
utils.Error("CreateService: Rolling back changes because of -- Container", err)
|
|
OnLog(utils.DoErr("Rolling back changes because of -- Container creation error: "+err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
|
|
// Start all the newly created containers
|
|
for _, container := range startOrder {
|
|
err = DockerClient.ContainerStart(DockerContext, container.Name, doctype.ContainerStartOptions{})
|
|
if err != nil {
|
|
utils.Error("CreateService: Start Container", err)
|
|
OnLog(utils.DoErr("Rolling back changes because of -- Container start error: "+err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
|
|
// Write a response to the client
|
|
utils.Log(fmt.Sprintf("Container %s initiated", container.Name))
|
|
OnLog(fmt.Sprintf("Container %s initiated", container.Name))
|
|
|
|
// if post install
|
|
if len(container.PostInstall) > 0 {
|
|
utils.Log(fmt.Sprintf("Waiting for container %s to start", container.Name))
|
|
OnLog(fmt.Sprintf("Waiting for container %s to start", container.Name))
|
|
|
|
// wait for container to start
|
|
for {
|
|
time.Sleep(1 * time.Second)
|
|
inspect, _ := DockerClient.ContainerInspect(DockerContext, container.Name)
|
|
if inspect.State.Running {
|
|
break
|
|
}
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// run post install commands
|
|
for _, cmd := range container.PostInstall {
|
|
utils.Log(fmt.Sprintf("Running post install command: %s", cmd))
|
|
OnLog(fmt.Sprintf("Running post install command: %s", cmd))
|
|
|
|
// setup the execution of command
|
|
execResponse, err := DockerClient.ContainerExecCreate(DockerContext, container.Name, doctype.ExecConfig{
|
|
Cmd: []string{"/bin/sh", "-c", cmd},
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
})
|
|
|
|
if err != nil {
|
|
utils.Error("CreateService: Post Install", err)
|
|
OnLog(utils.DoErr("Rolling back changes because of -- Post install error: "+err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
|
|
// attach to the exec instance
|
|
response, err := DockerClient.ContainerExecAttach(DockerContext, execResponse.ID, doctype.ExecStartCheck{})
|
|
if err != nil {
|
|
utils.Error("CreateService: Post Install", err)
|
|
OnLog(utils.DoErr("Rolling back changes because of -- Post install error: "+err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
defer response.Close()
|
|
|
|
// run the command
|
|
err = DockerClient.ContainerExecStart(DockerContext, execResponse.ID, doctype.ExecStartCheck{})
|
|
if err != nil {
|
|
utils.Error("CreateService: Post Install", err)
|
|
OnLog(utils.DoErr("Rolling back changes because of -- Post install error: "+err.Error()))
|
|
Rollback(rollbackActions, OnLog)
|
|
return err
|
|
}
|
|
|
|
// read the output
|
|
out, _ := ioutil.ReadAll(response.Reader)
|
|
OnLog(fmt.Sprintf("----> %s", out))
|
|
}
|
|
|
|
// restart container
|
|
DockerClient.ContainerRestart(DockerContext, container.Name, conttype.StopOptions{})
|
|
}
|
|
|
|
}
|
|
|
|
// Save the route configs
|
|
config.HTTPConfig.ProxyConfig.Routes = configRoutes
|
|
utils.SaveConfigTofile(config)
|
|
|
|
if needsHTTPRestart {
|
|
utils.RestartHTTPServer()
|
|
}
|
|
|
|
// After all operations
|
|
utils.Log("CreateService: Operation succeeded. SERVICE STARTED")
|
|
OnLog("\n")
|
|
OnLog(utils.DoSuccess("[OPERATION SUCCEEDED]. SERVICE STARTED\n"))
|
|
|
|
return nil
|
|
}
|
|
|
|
func ReOrderServices(serviceMap map[string]ContainerCreateRequestContainer) ([]ContainerCreateRequestContainer, error) {
|
|
startOrder := []ContainerCreateRequestContainer{}
|
|
|
|
for len(serviceMap) > 0 {
|
|
// Keep track of whether we've added any services in this iteration
|
|
changed := false
|
|
|
|
for name, service := range serviceMap {
|
|
// Check if all dependencies are already in startOrder
|
|
allDependenciesStarted := true
|
|
for _, dependency := range service.DependsOn {
|
|
dependencyStarted := false
|
|
for _, startedService := range startOrder {
|
|
if startedService.Name == dependency {
|
|
dependencyStarted = true
|
|
break
|
|
}
|
|
}
|
|
if !dependencyStarted {
|
|
allDependenciesStarted = false
|
|
break
|
|
}
|
|
}
|
|
|
|
// If all dependencies are started, we can add this service to startOrder
|
|
if allDependenciesStarted {
|
|
startOrder = append(startOrder, service)
|
|
delete(serviceMap, name)
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// If we haven't added any services in this iteration, then there must be a circular dependency
|
|
if !changed {
|
|
break
|
|
}
|
|
}
|
|
|
|
// If there are any services left in serviceMap, they couldn't be started due to unsatisfied dependencies or circular dependencies
|
|
if len(serviceMap) > 0 {
|
|
errorMessage := "Could not start all services due to unsatisfied dependencies or circular dependencies:\n"
|
|
for name, _ := range serviceMap {
|
|
errorMessage += "Could not start service: " + name + "\n"
|
|
errorMessage += "Unsatisfied dependencies:\n"
|
|
for _, dependency := range serviceMap[name].DependsOn {
|
|
_, ok := serviceMap[dependency]
|
|
if ok {
|
|
errorMessage += dependency + "\n"
|
|
}
|
|
}
|
|
}
|
|
return nil, errors.New(errorMessage)
|
|
}
|
|
|
|
return startOrder, nil
|
|
} |