mirror of
https://gitee.com/rainbond/Rainbond.git
synced 2024-12-10 19:59:01 +08:00
331 lines
12 KiB
Go
331 lines
12 KiB
Go
// Copyright (C) 2014-2018 Goodrain Co., Ltd.
|
|
// RAINBOND, Application Management Platform
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version. For any non-GPL usage of Rainbond,
|
|
// one or multiple Commercial Licenses authorized by Goodrain Co., Ltd.
|
|
// must be obtained first.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package compose
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"strconv"
|
|
"strings"
|
|
|
|
libcomposeyaml "github.com/docker/libcompose/yaml"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/twinj/uuid"
|
|
|
|
"github.com/docker/cli/cli/compose/loader"
|
|
"github.com/docker/cli/cli/compose/types"
|
|
|
|
"os"
|
|
|
|
"github.com/goodrain/rainbond/util"
|
|
)
|
|
|
|
// converts os.Environ() ([]string) to map[string]string
|
|
// based on https://github.com/docker/cli/blob/5dd30732a23bbf14db1c64d084ae4a375f592cfa/cli/command/stack/deploy_composefile.go#L143
|
|
func buildEnvironment() (map[string]string, error) {
|
|
env := os.Environ()
|
|
result := make(map[string]string, len(env))
|
|
for _, s := range env {
|
|
// if value is empty, s is like "K=", not "K".
|
|
if !strings.Contains(s, "=") {
|
|
return result, fmt.Errorf("unexpected environment %q", s)
|
|
}
|
|
kv := strings.SplitN(s, "=", 2)
|
|
result[kv[0]] = kv[1]
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// The purpose of this is not to deploy, but to be able to parse
|
|
// v3 of Docker Compose into a suitable format. In this case, whatever is returned
|
|
// by docker/cli's ServiceConfig
|
|
func parseV3(bodys [][]byte) (ComposeObject, error) {
|
|
|
|
// In order to get V3 parsing to work, we have to go through some preliminary steps
|
|
// for us to hack up github.com/docker/cli in order to correctly convert to a ComposeObject
|
|
cacheName := uuid.NewV4().String()
|
|
if err := util.CheckAndCreateDir("/cache/docker-compose/" + cacheName); err != nil {
|
|
return ComposeObject{}, fmt.Errorf("create cache docker compose file dir error:%s", err.Error())
|
|
}
|
|
var files []string
|
|
for i, body := range bodys {
|
|
filename := fmt.Sprintf("/cache/docker-compose/%s/%d-docker-compose.yml", cacheName, i)
|
|
if err := ioutil.WriteFile(filename, body, 0755); err != nil {
|
|
return ComposeObject{}, fmt.Errorf("write cache docker compose file error:%s", err.Error())
|
|
}
|
|
files = append(files, filename)
|
|
}
|
|
// Gather the working directory
|
|
workingDir := "/cache/docker-compose/" + cacheName
|
|
|
|
// Parse the Compose File
|
|
parsedComposeFile, err := loader.ParseYAML(bodys[0])
|
|
if err != nil {
|
|
return ComposeObject{}, err
|
|
}
|
|
|
|
// Config file
|
|
configFile := types.ConfigFile{
|
|
Filename: files[0],
|
|
Config: parsedComposeFile,
|
|
}
|
|
|
|
// get environment variables
|
|
env, err := buildEnvironment()
|
|
if err != nil {
|
|
return ComposeObject{}, fmt.Errorf("cannot build environment variables")
|
|
}
|
|
|
|
// Config details
|
|
configDetails := types.ConfigDetails{
|
|
WorkingDir: workingDir,
|
|
ConfigFiles: []types.ConfigFile{configFile},
|
|
Environment: env,
|
|
}
|
|
|
|
// Actual config
|
|
// We load it in order to retrieve the parsed output configuration!
|
|
// This will output a github.com/docker/cli ServiceConfig
|
|
// Which is similar to our version of ServiceConfig
|
|
config, err := loader.Load(configDetails)
|
|
if err != nil {
|
|
return ComposeObject{}, err
|
|
}
|
|
|
|
// TODO: Check all "unsupported" keys and output details
|
|
// Specifically, keys such as "volumes_from" are not supported in V3.
|
|
|
|
// Finally, we convert the object from docker/cli's ServiceConfig to our appropriate one
|
|
komposeObject, err := dockerComposeToKomposeMapping(config)
|
|
if err != nil {
|
|
return ComposeObject{}, err
|
|
}
|
|
|
|
return komposeObject, nil
|
|
}
|
|
|
|
// Convert the Docker Compose v3 volumes to []string (the old way)
|
|
// TODO: Check to see if it's a "bind" or "volume". Ignore for now.
|
|
// TODO: Refactor it similar to loadV3Ports
|
|
// See: https://docs.docker.com/compose/compose-file/#long-syntax-2
|
|
func loadV3Volumes(volumes []types.ServiceVolumeConfig) []string {
|
|
var volArray []string
|
|
for _, vol := range volumes {
|
|
|
|
// There will *always* be Source when parsing
|
|
v := normalizeServiceNames(vol.Source)
|
|
|
|
if vol.Target != "" {
|
|
v = v + ":" + vol.Target
|
|
}
|
|
|
|
if vol.ReadOnly {
|
|
v = v + ":ro"
|
|
}
|
|
|
|
volArray = append(volArray, v)
|
|
}
|
|
return volArray
|
|
}
|
|
|
|
// Convert Docker Compose v3 ports to Ports
|
|
func loadV3Ports(ports []types.ServicePortConfig) []Ports {
|
|
komposePorts := []Ports{}
|
|
|
|
for _, port := range ports {
|
|
|
|
// Convert to a kobject struct with ports
|
|
// NOTE: V3 doesn't use IP (they utilize Swarm instead for host-networking).
|
|
// Thus, IP is blank.
|
|
komposePorts = append(komposePorts, Ports{
|
|
HostPort: int32(port.Published),
|
|
ContainerPort: int32(port.Target),
|
|
HostIP: "",
|
|
Protocol: strings.ToUpper(string(port.Protocol)),
|
|
})
|
|
|
|
}
|
|
|
|
return komposePorts
|
|
}
|
|
|
|
func dockerComposeToKomposeMapping(composeObject *types.Config) (ComposeObject, error) {
|
|
|
|
// Step 1. Initialize what's going to be returned
|
|
co := ComposeObject{
|
|
ServiceConfigs: make(map[string]ServiceConfig),
|
|
}
|
|
|
|
// Step 2. Parse through the object and convert it to ComposeObject!
|
|
// Here we "clean up" the service configuration so we return something that includes
|
|
// all relevant information as well as avoid the unsupported keys as well.
|
|
for _, composeServiceConfig := range composeObject.Services {
|
|
|
|
// Standard import
|
|
// No need to modify before importation
|
|
name := composeServiceConfig.Name
|
|
logrus.Debugf("compose 3 service conf name is %s", name)
|
|
serviceConfig := ServiceConfig{}
|
|
serviceConfig.ContainerName = composeServiceConfig.ContainerName
|
|
if serviceConfig.ContainerName == "" {
|
|
serviceConfig.ContainerName = name
|
|
}
|
|
serviceConfig.Image = composeServiceConfig.Image
|
|
serviceConfig.WorkingDir = composeServiceConfig.WorkingDir
|
|
serviceConfig.Annotations = map[string]string(composeServiceConfig.Labels)
|
|
serviceConfig.CapAdd = composeServiceConfig.CapAdd
|
|
serviceConfig.CapDrop = composeServiceConfig.CapDrop
|
|
serviceConfig.Expose = composeServiceConfig.Expose
|
|
serviceConfig.Privileged = composeServiceConfig.Privileged
|
|
serviceConfig.User = composeServiceConfig.User
|
|
serviceConfig.Stdin = composeServiceConfig.StdinOpen
|
|
serviceConfig.Tty = composeServiceConfig.Tty
|
|
serviceConfig.TmpFs = composeServiceConfig.Tmpfs
|
|
serviceConfig.Command = composeServiceConfig.Entrypoint
|
|
serviceConfig.Args = composeServiceConfig.Command
|
|
serviceConfig.Labels = composeServiceConfig.Labels
|
|
serviceConfig.DependsON = composeServiceConfig.DependsOn
|
|
serviceConfig.Links = composeServiceConfig.Links
|
|
//
|
|
// Deploy keys
|
|
//
|
|
|
|
// mode:
|
|
serviceConfig.DeployMode = composeServiceConfig.Deploy.Mode
|
|
|
|
if (composeServiceConfig.Deploy.Resources != types.Resources{}) {
|
|
|
|
// memory:
|
|
// TODO: Refactor yaml.MemStringorInt in go to int64
|
|
// Since Deploy.Resources.Limits does not initialize, we must check type Resources before continuing
|
|
serviceConfig.MemLimit = libcomposeyaml.MemStringorInt(composeServiceConfig.Deploy.Resources.Limits.MemoryBytes)
|
|
serviceConfig.MemReservation = libcomposeyaml.MemStringorInt(composeServiceConfig.Deploy.Resources.Reservations.MemoryBytes)
|
|
|
|
// cpu:
|
|
// convert to k8s format, for example: 0.5 = 500m
|
|
// See: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/
|
|
// "The expression 0.1 is equivalent to the expression 100m, which can be read as “one hundred millicpu”."
|
|
|
|
cpuLimit, err := strconv.ParseFloat(composeServiceConfig.Deploy.Resources.Limits.NanoCPUs, 64)
|
|
if err != nil {
|
|
return ComposeObject{}, fmt.Errorf("Unable to convert cpu limits resources value")
|
|
}
|
|
serviceConfig.CPULimit = int64(cpuLimit * 1000)
|
|
|
|
cpuReservation, err := strconv.ParseFloat(composeServiceConfig.Deploy.Resources.Reservations.NanoCPUs, 64)
|
|
if err != nil {
|
|
return ComposeObject{}, fmt.Errorf("Unable to convert cpu limits reservation value")
|
|
}
|
|
serviceConfig.CPUReservation = int64(cpuReservation * 1000)
|
|
|
|
}
|
|
|
|
// restart-policy:
|
|
if composeServiceConfig.Deploy.RestartPolicy != nil {
|
|
serviceConfig.Restart = composeServiceConfig.Deploy.RestartPolicy.Condition
|
|
}
|
|
|
|
// replicas:
|
|
if composeServiceConfig.Deploy.Replicas != nil {
|
|
serviceConfig.Replicas = int(*composeServiceConfig.Deploy.Replicas)
|
|
}
|
|
|
|
// placement:
|
|
// placement := make(map[string]string)
|
|
// for _, j := range composeServiceConfig.Deploy.Placement.Constraints {
|
|
// p := strings.Split(j, " == ")
|
|
// if p[0] == "node.hostname" {
|
|
// placement["kubernetes.io/hostname"] = p[1]
|
|
// } else if p[0] == "engine.labels.operatingsystem" {
|
|
// placement["beta.kubernetes.io/os"] = p[1]
|
|
// } else {
|
|
// logrus.Warn(p[0], " constraints in placement is not supported, only 'node.hostname' and 'engine.labels.operatingsystem' is only supported as a constraint ")
|
|
// }
|
|
// }
|
|
// serviceConfig.Placement = placement
|
|
|
|
// TODO: Build is not yet supported, see:
|
|
// https://github.com/docker/cli/blob/master/cli/compose/types/types.go#L9
|
|
// We will have to *manually* add this / parse.
|
|
// serviceConfig.Build = composeServiceConfig.Build.Context
|
|
// serviceConfig.Dockerfile = composeServiceConfig.Build.Dockerfile
|
|
// serviceConfig.BuildArgs = composeServiceConfig.Build.Args
|
|
|
|
// Gather the environment values
|
|
// DockerCompose uses map[string]*string while we use []string
|
|
// So let's convert that using this hack
|
|
for name, value := range composeServiceConfig.Environment {
|
|
var env EnvVar
|
|
if value != nil {
|
|
env = EnvVar{Name: name, Value: *value}
|
|
} else {
|
|
env = EnvVar{Name: name, Value: ""}
|
|
}
|
|
serviceConfig.Environment = append(serviceConfig.Environment, env)
|
|
}
|
|
|
|
// Get env_file
|
|
serviceConfig.EnvFile = composeServiceConfig.EnvFile
|
|
|
|
// Parse the ports
|
|
// v3 uses a new format called "long syntax" starting in 3.2
|
|
// https://docs.docker.com/compose/compose-file/#ports
|
|
serviceConfig.Port = loadV3Ports(composeServiceConfig.Ports)
|
|
|
|
// Parse the volumes
|
|
// Again, in v3, we use the "long syntax" for volumes in terms of parsing
|
|
// https://docs.docker.com/compose/compose-file/#long-syntax-2
|
|
serviceConfig.VolList = loadV3Volumes(composeServiceConfig.Volumes)
|
|
|
|
// Label handler
|
|
// Labels used to influence conversion of kompose will be handled
|
|
// from here for docker-compose. Each loader will have such handler.
|
|
for key, value := range composeServiceConfig.Labels {
|
|
switch key {
|
|
case "kompose.service.type":
|
|
serviceType, err := handleServiceType(value)
|
|
if err != nil {
|
|
return ComposeObject{}, fmt.Errorf("handleServiceType failed")
|
|
}
|
|
|
|
serviceConfig.ServiceType = serviceType
|
|
case "kompose.service.expose":
|
|
serviceConfig.ExposeService = strings.ToLower(value)
|
|
case "kompose.service.expose.tls-secret":
|
|
serviceConfig.ExposeServiceTLS = value
|
|
}
|
|
}
|
|
|
|
if serviceConfig.ExposeService == "" && serviceConfig.ExposeServiceTLS != "" {
|
|
return ComposeObject{}, fmt.Errorf("kompose.service.expose.tls-secret was specified without kompose.service.expose")
|
|
}
|
|
|
|
// Log if the name will been changed
|
|
if normalizeServiceNames(name) != name {
|
|
logrus.Infof("Service name in docker-compose has been changed from %q to %q", name, normalizeServiceNames(name))
|
|
}
|
|
|
|
// Final step, add to the array!
|
|
co.ServiceConfigs[normalizeServiceNames(name)] = serviceConfig
|
|
}
|
|
handleVolume(&co)
|
|
|
|
return co, nil
|
|
}
|