2018-03-14 14:12:26 +08:00
// Copyright (C) 2014-2018 Goodrain Co., Ltd.
2018-01-17 22:06:58 +08:00
// RAINBOND, Application Management Platform
2018-03-14 14:33:31 +08:00
2018-01-17 22:06:58 +08:00
// 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.
2018-03-14 14:33:31 +08:00
2018-01-17 22:06:58 +08:00
// 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.
2018-03-14 14:33:31 +08:00
2018-01-17 22:06:58 +08:00
// 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"
2020-09-06 11:09:48 +08:00
"github.com/sirupsen/logrus"
2018-01-17 22:06:58 +08:00
libcomposeyaml "github.com/docker/libcompose/yaml"
"github.com/twinj/uuid"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/types"
"os"
2018-04-24 16:44:59 +08:00
"github.com/goodrain/rainbond/util"
2018-01-17 22:06:58 +08:00
)
// 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 , "=" ) {
2018-01-22 16:50:23 +08:00
return result , fmt . Errorf ( "unexpected environment %q" , s )
2018-01-17 22:06:58 +08:00
}
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 {
2018-01-22 16:50:23 +08:00
return ComposeObject { } , fmt . Errorf ( "cannot build environment variables" )
2018-01-17 22:06:58 +08:00
}
// 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 : "" ,
2018-01-22 16:50:23 +08:00
Protocol : strings . ToUpper ( string ( port . Protocol ) ) ,
2018-01-17 22:06:58 +08:00
} )
}
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
2018-02-26 16:16:22 +08:00
logrus . Debugf ( "compose 3 service conf name is %s" , name )
2018-01-17 22:06:58 +08:00
serviceConfig := ServiceConfig { }
2018-02-26 14:59:28 +08:00
serviceConfig . ContainerName = composeServiceConfig . ContainerName
2018-02-26 16:16:22 +08:00
if serviceConfig . ContainerName == "" {
serviceConfig . ContainerName = name
}
2018-01-17 22:06:58 +08:00
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 {
2018-01-22 16:50:23 +08:00
return ComposeObject { } , fmt . Errorf ( "Unable to convert cpu limits resources value" )
2018-01-17 22:06:58 +08:00
}
serviceConfig . CPULimit = int64 ( cpuLimit * 1000 )
cpuReservation , err := strconv . ParseFloat ( composeServiceConfig . Deploy . Resources . Reservations . NanoCPUs , 64 )
if err != nil {
2018-01-22 16:50:23 +08:00
return ComposeObject { } , fmt . Errorf ( "Unable to convert cpu limits reservation value" )
2018-01-17 22:06:58 +08:00
}
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 {
env := EnvVar { Name : name , Value : * 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 {
2018-01-22 16:50:23 +08:00
return ComposeObject { } , fmt . Errorf ( "handleServiceType failed" )
2018-01-17 22:06:58 +08:00
}
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 != "" {
2019-02-12 16:41:14 +08:00
return ComposeObject { } , fmt . Errorf ( "kompose.service.expose.tls-secret was specified without kompose.service.expose" )
2018-01-17 22:06:58 +08:00
}
// 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
}