Initial commit

This commit is contained in:
Ria Bhatia
2017-12-04 13:32:57 -06:00
committed by Erik St. Martin
commit 0075e5b0f3
9056 changed files with 2523100 additions and 0 deletions

2
vendor/github.com/hyperhq/libcompose/.dockerignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
bundles/
**/*.test

8
vendor/github.com/hyperhq/libcompose/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
.DS_Store
/main/main
/docker-compose*
/libcompose-cli*
*.log
*.swp
bundles
.gopath

43
vendor/github.com/hyperhq/libcompose/config/convert.go generated vendored Normal file
View File

@@ -0,0 +1,43 @@
package config
import "github.com/hyperhq/libcompose/utils"
// ConvertV1toV2 converts a v1 service config to a v2 service config
func ConvertV1toV2(v1Services map[string]*ServiceConfigV1, environmentLookup EnvironmentLookup, resourceLookup ResourceLookup) (map[string]*ServiceConfig, error) {
v2Services := make(map[string]*ServiceConfig)
/*
builds := make(map[string]Build)
logs := make(map[string]Log)
for name, service := range v1Services {
builds[name] = Build{
Context: service.Build,
Dockerfile: service.Dockerfile,
}
v1Services[name].Build = ""
v1Services[name].Dockerfile = ""
logs[name] = Log{
Driver: service.LogDriver,
Options: service.LogOpt,
}
v1Services[name].LogDriver = ""
v1Services[name].LogOpt = nil
}
*/
if err := utils.Convert(v1Services, &v2Services); err != nil {
return nil, err
}
/*
for name := range v2Services {
v2Services[name].Build = builds[name]
}
*/
return v2Services, nil
}

95
vendor/github.com/hyperhq/libcompose/config/hash.go generated vendored Normal file
View File

@@ -0,0 +1,95 @@
package config
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"reflect"
"sort"
"github.com/hyperhq/libcompose/yaml"
)
// GetServiceHash computes and returns a hash that will identify a service.
// This hash will be then used to detect if the service definition/configuration
// have changed and needs to be recreated.
func GetServiceHash(name string, config *ServiceConfig) string {
hash := sha1.New()
io.WriteString(hash, name)
//Get values of Service through reflection
val := reflect.ValueOf(config).Elem()
//Create slice to sort the keys in Service Config, which allow constant hash ordering
serviceKeys := []string{}
//Create a data structure of map of values keyed by a string
unsortedKeyValue := make(map[string]interface{})
//Get all keys and values in Service Configuration
for i := 0; i < val.NumField(); i++ {
valueField := val.Field(i)
keyField := val.Type().Field(i)
serviceKeys = append(serviceKeys, keyField.Name)
unsortedKeyValue[keyField.Name] = valueField.Interface()
}
//Sort serviceKeys alphabetically
sort.Strings(serviceKeys)
//Go through keys and write hash
for _, serviceKey := range serviceKeys {
serviceValue := unsortedKeyValue[serviceKey]
io.WriteString(hash, fmt.Sprintf("\n %v: ", serviceKey))
switch s := serviceValue.(type) {
case yaml.SliceorMap:
sliceKeys := []string{}
for lkey := range s {
sliceKeys = append(sliceKeys, lkey)
}
sort.Strings(sliceKeys)
for _, sliceKey := range sliceKeys {
io.WriteString(hash, fmt.Sprintf("%s=%v, ", sliceKey, s[sliceKey]))
}
case yaml.MaporEqualSlice:
for _, sliceKey := range s {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case yaml.MaporColonSlice:
for _, sliceKey := range s {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case yaml.MaporSpaceSlice:
for _, sliceKey := range s {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case yaml.Command:
for _, sliceKey := range s {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case yaml.Stringorslice:
sort.Strings(s)
for _, sliceKey := range s {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case []string:
sliceKeys := s
sort.Strings(sliceKeys)
for _, sliceKey := range sliceKeys {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
default:
io.WriteString(hash, fmt.Sprintf("%v", serviceValue))
}
}
return hex.EncodeToString(hash.Sum(nil))
}

View File

@@ -0,0 +1,169 @@
package config
import (
"bytes"
"fmt"
"strings"
"github.com/Sirupsen/logrus"
)
func isNum(c uint8) bool {
return c >= '0' && c <= '9'
}
func validVariableNameChar(c uint8) bool {
return c == '_' ||
c >= 'A' && c <= 'Z' ||
c >= 'a' && c <= 'z' ||
isNum(c)
}
func parseVariable(line string, pos int, mapping func(string) string) (string, int, bool) {
var buffer bytes.Buffer
for ; pos < len(line); pos++ {
c := line[pos]
switch {
case validVariableNameChar(c):
buffer.WriteByte(c)
default:
return mapping(buffer.String()), pos - 1, true
}
}
return mapping(buffer.String()), pos, true
}
func parseVariableWithBraces(line string, pos int, mapping func(string) string) (string, int, bool) {
var buffer bytes.Buffer
for ; pos < len(line); pos++ {
c := line[pos]
switch {
case c == '}':
bufferString := buffer.String()
if bufferString == "" {
return "", 0, false
}
return mapping(buffer.String()), pos, true
case validVariableNameChar(c):
buffer.WriteByte(c)
default:
return "", 0, false
}
}
return "", 0, false
}
func parseInterpolationExpression(line string, pos int, mapping func(string) string) (string, int, bool) {
c := line[pos]
switch {
case c == '$':
return "$", pos, true
case c == '{':
return parseVariableWithBraces(line, pos+1, mapping)
case !isNum(c) && validVariableNameChar(c):
// Variables can't start with a number
return parseVariable(line, pos, mapping)
default:
return "", 0, false
}
}
func parseLine(line string, mapping func(string) string) (string, bool) {
var buffer bytes.Buffer
for pos := 0; pos < len(line); pos++ {
c := line[pos]
switch {
case c == '$':
var replaced string
var success bool
replaced, pos, success = parseInterpolationExpression(line, pos+1, mapping)
if !success {
return "", false
}
buffer.WriteString(replaced)
default:
buffer.WriteByte(c)
}
}
return buffer.String(), true
}
func parseConfig(option, service string, data *interface{}, mapping func(string) string) error {
switch typedData := (*data).(type) {
case string:
var success bool
*data, success = parseLine(typedData, mapping)
if !success {
return fmt.Errorf("Invalid interpolation format for \"%s\" option in service \"%s\": \"%s\"", option, service, typedData)
}
case []interface{}:
for k, v := range typedData {
err := parseConfig(option, service, &v, mapping)
if err != nil {
return err
}
typedData[k] = v
}
case map[interface{}]interface{}:
for k, v := range typedData {
err := parseConfig(option, service, &v, mapping)
if err != nil {
return err
}
typedData[k] = v
}
}
return nil
}
// Interpolate replaces variables in the raw map representation of the project file
func Interpolate(environmentLookup EnvironmentLookup, config *RawServiceMap) error {
for k, v := range *config {
for k2, v2 := range v {
err := parseConfig(k2, k, &v2, func(s string) string {
values := environmentLookup.Lookup(s, k, nil)
if len(values) == 0 {
logrus.Warnf("The %s variable is not set. Substituting a blank string.", s)
return ""
}
// Use first result if many are given
value := values[0]
// Environment variables come in key=value format
// Return everything past first '='
return strings.SplitN(value, "=", 2)[1]
})
if err != nil {
return err
}
(*config)[k][k2] = v2
}
}
return nil
}

View File

@@ -0,0 +1,226 @@
package config
import (
"fmt"
"os"
"testing"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/stretchr/testify/assert"
)
func testInterpolatedLine(t *testing.T, expectedLine, interpolatedLine string, envVariables map[string]string) {
interpolatedLine, _ = parseLine(interpolatedLine, func(s string) string {
return envVariables[s]
})
assert.Equal(t, expectedLine, interpolatedLine)
}
func testInvalidInterpolatedLine(t *testing.T, line string) {
_, success := parseLine(line, func(string) string {
return ""
})
assert.Equal(t, false, success)
}
func TestParseLine(t *testing.T) {
variables := map[string]string{
"A": "ABC",
"X": "XYZ",
"E": "",
"lower": "WORKED",
"MiXeD": "WORKED",
"split_VaLue": "WORKED",
"9aNumber": "WORKED",
"a9Number": "WORKED",
}
testInterpolatedLine(t, "WORKED", "$lower", variables)
testInterpolatedLine(t, "WORKED", "${MiXeD}", variables)
testInterpolatedLine(t, "WORKED", "${split_VaLue}", variables)
// Starting with a number isn't valid
testInterpolatedLine(t, "", "$9aNumber", variables)
testInterpolatedLine(t, "WORKED", "$a9Number", variables)
testInterpolatedLine(t, "ABC", "$A", variables)
testInterpolatedLine(t, "ABC", "${A}", variables)
testInterpolatedLine(t, "ABC DE", "$A DE", variables)
testInterpolatedLine(t, "ABCDE", "${A}DE", variables)
testInterpolatedLine(t, "$A", "$$A", variables)
testInterpolatedLine(t, "${A}", "$${A}", variables)
testInterpolatedLine(t, "$ABC", "$$${A}", variables)
testInterpolatedLine(t, "$ABC", "$$$A", variables)
testInterpolatedLine(t, "ABC XYZ", "$A $X", variables)
testInterpolatedLine(t, "ABCXYZ", "$A$X", variables)
testInterpolatedLine(t, "ABCXYZ", "${A}${X}", variables)
testInterpolatedLine(t, "", "$B", variables)
testInterpolatedLine(t, "", "${B}", variables)
testInterpolatedLine(t, "", "$ADE", variables)
testInterpolatedLine(t, "", "$E", variables)
testInterpolatedLine(t, "", "${E}", variables)
testInvalidInterpolatedLine(t, "${")
testInvalidInterpolatedLine(t, "$}")
testInvalidInterpolatedLine(t, "${}")
testInvalidInterpolatedLine(t, "${ }")
testInvalidInterpolatedLine(t, "${A }")
testInvalidInterpolatedLine(t, "${ A}")
testInvalidInterpolatedLine(t, "${A!}")
testInvalidInterpolatedLine(t, "$!")
}
type MockEnvironmentLookup struct {
Variables map[string]string
}
func (m MockEnvironmentLookup) Lookup(key, serviceName string, config *ServiceConfig) []string {
return []string{fmt.Sprintf("%s=%s", key, m.Variables[key])}
}
func testInterpolatedConfig(t *testing.T, expectedConfig, interpolatedConfig string, envVariables map[string]string) {
for k, v := range envVariables {
os.Setenv(k, v)
}
expectedConfigBytes := []byte(expectedConfig)
interpolatedConfigBytes := []byte(interpolatedConfig)
expectedData := make(RawServiceMap)
interpolatedData := make(RawServiceMap)
yaml.Unmarshal(expectedConfigBytes, &expectedData)
yaml.Unmarshal(interpolatedConfigBytes, &interpolatedData)
_ = Interpolate(MockEnvironmentLookup{envVariables}, &interpolatedData)
for k := range envVariables {
os.Unsetenv(k)
}
assert.Equal(t, expectedData, interpolatedData)
}
func testInvalidInterpolatedConfig(t *testing.T, interpolatedConfig string) {
interpolatedConfigBytes := []byte(interpolatedConfig)
interpolatedData := make(RawServiceMap)
yaml.Unmarshal(interpolatedConfigBytes, &interpolatedData)
err := Interpolate(new(MockEnvironmentLookup), &interpolatedData)
assert.NotNil(t, err)
}
func TestInterpolate(t *testing.T) {
testInterpolatedConfig(t,
`web:
# unbracketed name
image: busybox
# array element
ports:
- "80:8000"
# dictionary item value
labels:
mylabel: "myvalue"
# unset value
hostname: "host-"
# escaped interpolation
command: "${ESCAPED}"`,
`web:
# unbracketed name
image: $IMAGE
# array element
ports:
- "${HOST_PORT}:8000"
# dictionary item value
labels:
mylabel: "${LABEL_VALUE}"
# unset value
hostname: "host-${UNSET_VALUE}"
# escaped interpolation
command: "$${ESCAPED}"`, map[string]string{
"IMAGE": "busybox",
"HOST_PORT": "80",
"LABEL_VALUE": "myvalue",
})
// Same as above, but testing with equal signs in variables
testInterpolatedConfig(t,
`web:
# unbracketed name
image: =busybox
# array element
ports:
- "=:8000"
# dictionary item value
labels:
mylabel: "myvalue=="
# unset value
hostname: "host-"
# escaped interpolation
command: "${ESCAPED}"`,
`web:
# unbracketed name
image: $IMAGE
# array element
ports:
- "${HOST_PORT}:8000"
# dictionary item value
labels:
mylabel: "${LABEL_VALUE}"
# unset value
hostname: "host-${UNSET_VALUE}"
# escaped interpolation
command: "$${ESCAPED}"`, map[string]string{
"IMAGE": "=busybox",
"HOST_PORT": "=",
"LABEL_VALUE": "myvalue==",
})
testInvalidInterpolatedConfig(t,
`web:
image: "${"`)
testInvalidInterpolatedConfig(t,
`web:
image: busybox
# array element
ports:
- "${}:8000"`)
testInvalidInterpolatedConfig(t,
`web:
image: busybox
# array element
ports:
- "80:8000"
# dictionary item value
labels:
mylabel: "${ LABEL_VALUE}"`)
}

View File

@@ -0,0 +1,90 @@
package config
import (
"testing"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
yamlTypes "github.com/hyperhq/libcompose/yaml"
"github.com/stretchr/testify/assert"
)
type TestConfig struct {
SystemContainers map[string]*ServiceConfig
}
func newTestConfig() TestConfig {
return TestConfig{
SystemContainers: map[string]*ServiceConfig{
"udev": {
Image: "udev",
Restart: "always",
// NetworkMode: "host",
// Privileged: true,
// DNS: []string{"8.8.8.8", "8.8.4.4"},
Environment: yamlTypes.MaporEqualSlice([]string{
"DAEMON=true",
}),
Labels: yamlTypes.SliceorMap{
"io.rancher.os.detach": "true",
"io.rancher.os.scope": "system",
},
// VolumesFrom: []string{
// "system-volumes",
// },
// Ulimits: yamlTypes.Ulimits{
// Elements: []yamlTypes.Ulimit{
// yamlTypes.NewUlimit("nproc", 65557, 65557),
// },
// },
},
"system-volumes": {
Image: "state",
// NetworkMode: "none",
// ReadOnly: true,
// Privileged: true,
Labels: yamlTypes.SliceorMap{
"io.rancher.os.createonly": "true",
"io.rancher.os.scope": "system",
},
Volumes: []string{
"/dev:/host/dev",
"/var/lib/rancher/conf:/var/lib/rancher/conf",
"/etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt.rancher",
"/lib/modules:/lib/modules",
"/lib/firmware:/lib/firmware",
"/var/run:/var/run",
"/var/log:/var/log",
},
// Logging: Log{
// Driver: "json-file",
//},
},
},
}
}
func TestMarshalConfig(t *testing.T) {
config := newTestConfig()
bytes, err := yaml.Marshal(config)
assert.Nil(t, err)
config2 := TestConfig{}
err = yaml.Unmarshal(bytes, &config2)
assert.Nil(t, err)
assert.Equal(t, config, config2)
}
func TestMarshalServiceConfig(t *testing.T) {
configPtr := newTestConfig().SystemContainers["udev"]
bytes, err := yaml.Marshal(configPtr)
assert.Nil(t, err)
configPtr2 := &ServiceConfig{}
err = yaml.Unmarshal(bytes, configPtr2)
assert.Nil(t, err)
assert.Equal(t, configPtr, configPtr2)
}

150
vendor/github.com/hyperhq/libcompose/config/merge.go generated vendored Normal file
View File

@@ -0,0 +1,150 @@
package config
import (
"bufio"
"bytes"
"fmt"
"strings"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/hyperhq/hypercli/pkg/urlutil"
)
var (
noMerge = []string{
"links",
"volumes_from",
}
)
// Merge merges a compose file into an existing set of service configs
func Merge(existingServices *ServiceConfigs, environmentLookup EnvironmentLookup, resourceLookup ResourceLookup, file string, bytes []byte) (map[string]*ServiceConfig, map[string]*VolumeConfig, map[string]*NetworkConfig, error) {
var config Config
if err := yaml.Unmarshal(bytes, &config); err != nil {
return nil, nil, nil, err
}
var serviceConfigs map[string]*ServiceConfig
var volumeConfigs map[string]*VolumeConfig
var networkConfigs map[string]*NetworkConfig
if config.Version == "2" {
var err error
serviceConfigs, err = MergeServicesV2(existingServices, environmentLookup, resourceLookup, file, bytes)
if err != nil {
return nil, nil, nil, err
}
volumeConfigs, err = ParseVolumes(environmentLookup, resourceLookup, file, bytes)
if err != nil {
return nil, nil, nil, err
}
networkConfigs, err = ParseNetworks(environmentLookup, resourceLookup, file, bytes)
if err != nil {
return nil, nil, nil, err
}
} else {
serviceConfigsV1, err := MergeServicesV1(existingServices, environmentLookup, resourceLookup, file, bytes)
if err != nil {
return nil, nil, nil, err
}
serviceConfigs, err = ConvertV1toV2(serviceConfigsV1, environmentLookup, resourceLookup)
if err != nil {
return nil, nil, nil, err
}
}
adjustValues(serviceConfigs)
return serviceConfigs, volumeConfigs, networkConfigs, nil
}
func adjustValues(configs map[string]*ServiceConfig) {
// yaml parser turns "no" into "false" but that is not valid for a restart policy
for _, v := range configs {
if v.Restart == "false" {
v.Restart = "no"
}
}
}
func readEnvFile(resourceLookup ResourceLookup, inFile string, serviceData RawService) (RawService, error) {
if _, ok := serviceData["env_file"]; !ok {
return serviceData, nil
}
envFiles := serviceData["env_file"].([]interface{})
if len(envFiles) == 0 {
return serviceData, nil
}
if resourceLookup == nil {
return nil, fmt.Errorf("Can not use env_file in file %s no mechanism provided to load files", inFile)
}
var vars []interface{}
if _, ok := serviceData["environment"]; ok {
vars = serviceData["environment"].([]interface{})
}
for i := len(envFiles) - 1; i >= 0; i-- {
envFile := envFiles[i].(string)
content, _, err := resourceLookup.Lookup(envFile, inFile)
if err != nil {
return nil, err
}
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(bytes.NewBuffer(content))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
key := strings.SplitAfter(line, "=")[0]
found := false
for _, v := range vars {
if strings.HasPrefix(v.(string), key) {
found = true
break
}
}
if !found {
vars = append(vars, line)
}
}
if scanner.Err() != nil {
return nil, scanner.Err()
}
}
serviceData["environment"] = vars
delete(serviceData, "env_file")
return serviceData, nil
}
func mergeConfig(baseService, serviceData RawService) RawService {
for k, v := range serviceData {
// Image and build are mutually exclusive in merge
if k == "image" {
delete(baseService, "build")
} else if k == "build" {
delete(baseService, "image")
}
existing, ok := baseService[k]
if ok {
baseService[k] = merge(existing, v)
} else {
baseService[k] = v
}
}
return baseService
}
// IsValidRemote checks if the specified string is a valid remote (for builds)
func IsValidRemote(remote string) bool {
return urlutil.IsGitURL(remote) || urlutil.IsURL(remote)
}

View File

@@ -0,0 +1,292 @@
package config
import "testing"
type NullLookup struct {
}
func (n *NullLookup) Lookup(file, relativeTo string) ([]byte, string, error) {
return nil, "", nil
}
func (n *NullLookup) ResolvePath(path, inFile string) string {
return ""
}
func TestExtendsInheritImage(t *testing.T) {
configV1, _, _, err := Merge(NewServiceConfigs(), nil, &NullLookup{}, "", []byte(`
parent:
image: foo
child:
extends:
service: parent
`))
if err != nil {
t.Fatal(err)
}
configV2, _, _, err := Merge(NewServiceConfigs(), nil, &NullLookup{}, "", []byte(`
version: '2'
services:
parent:
image: foo
child:
extends:
service: parent
`))
if err != nil {
t.Fatal(err)
}
for _, config := range []map[string]*ServiceConfig{configV1, configV2} {
parent := config["parent"]
child := config["child"]
if parent.Image != "foo" {
t.Fatal("Invalid image", parent.Image)
}
// if child.Build.Context != "" {
// t.Fatal("Invalid build", child.Build)
// }
if child.Image != "foo" {
t.Fatal("Invalid image", child.Image)
}
}
}
/*
func TestExtendsInheritBuild(t *testing.T) {
configV1, _, _, err := Merge(NewServiceConfigs(), nil, &NullLookup{}, "", []byte(`
parent:
build: .
child:
extends:
service: parent
`))
if err != nil {
t.Fatal(err)
}
configV2, _, _, err := Merge(NewServiceConfigs(), nil, &NullLookup{}, "", []byte(`
version: '2'
services:
parent:
build:
context: .
child:
extends:
service: parent
`))
if err != nil {
t.Fatal(err)
}
for _, config := range []map[string]*ServiceConfig{configV1, configV2} {
parent := config["parent"]
child := config["child"]
if parent.Build.Context != "." {
t.Fatal("Invalid build", parent.Build)
}
if child.Build.Context != "." {
t.Fatal("Invalid build", child.Build)
}
if child.Image != "" {
t.Fatal("Invalid image", child.Image)
}
}
}
func TestExtendBuildOverImage(t *testing.T) {
configV1, _, _, err := Merge(NewServiceConfigs(), nil, &NullLookup{}, "", []byte(`
parent:
image: foo
child:
build: .
extends:
service: parent
`))
if err != nil {
t.Fatal(err)
}
configV2, _, _, err := Merge(NewServiceConfigs(), nil, &NullLookup{}, "", []byte(`
version: '2'
services:
parent:
image: foo
child:
build:
context: .
extends:
service: parent
`))
if err != nil {
t.Fatal(err)
}
for _, config := range []map[string]*ServiceConfig{configV1, configV2} {
parent := config["parent"]
child := config["child"]
if parent.Image != "foo" {
t.Fatal("Invalid image", parent.Image)
}
if child.Build.Context != "." {
t.Fatal("Invalid build", child.Build)
}
if child.Image != "" {
t.Fatal("Invalid image", child.Image)
}
}
}
func TestExtendImageOverBuild(t *testing.T) {
configV1, _, _, err := Merge(NewServiceConfigs(), nil, &NullLookup{}, "", []byte(`
parent:
build: .
child:
image: foo
extends:
service: parent
`))
if err != nil {
t.Fatal(err)
}
configV2, _, _, err := Merge(NewServiceConfigs(), nil, &NullLookup{}, "", []byte(`
version: '2'
services:
parent:
build:
context: .
child:
image: foo
extends:
service: parent
`))
if err != nil {
t.Fatal(err)
}
for _, config := range []map[string]*ServiceConfig{configV1, configV2} {
parent := config["parent"]
child := config["child"]
if parent.Image != "" {
t.Fatal("Invalid image", parent.Image)
}
if parent.Build.Context != "." {
t.Fatal("Invalid build", parent.Build)
}
if child.Build.Context != "" {
t.Fatal("Invalid build", child.Build)
}
if child.Image != "foo" {
t.Fatal("Invalid image", child.Image)
}
}
}
*/
func TestRestartNo(t *testing.T) {
configV1, _, _, err := Merge(NewServiceConfigs(), nil, &NullLookup{}, "", []byte(`
test:
restart: "no"
image: foo
`))
if err != nil {
t.Fatal(err)
}
configV2, _, _, err := Merge(NewServiceConfigs(), nil, &NullLookup{}, "", []byte(`
version: '2'
services:
test:
restart: "no"
image: foo
`))
if err != nil {
t.Fatal(err)
}
for _, config := range []map[string]*ServiceConfig{configV1, configV2} {
test := config["test"]
if test.Restart != "no" {
t.Fatal("Invalid restart policy", test.Restart)
}
}
}
func TestRestartAlways(t *testing.T) {
configV1, _, _, err := Merge(NewServiceConfigs(), nil, &NullLookup{}, "", []byte(`
test:
restart: always
image: foo
`))
if err != nil {
t.Fatal(err)
}
configV2, _, _, err := Merge(NewServiceConfigs(), nil, &NullLookup{}, "", []byte(`
version: '2'
services:
test:
restart: always
image: foo
`))
if err != nil {
t.Fatal(err)
}
for _, config := range []map[string]*ServiceConfig{configV1, configV2} {
test := config["test"]
if test.Restart != "always" {
t.Fatal("Invalid restart policy", test.Restart)
}
}
}
func TestIsValidRemote(t *testing.T) {
gitUrls := []string{
"git://github.com/docker/docker",
"git@github.com:docker/docker.git",
"git@bitbucket.org:atlassianlabs/atlassian-docker.git",
"https://github.com/docker/docker.git",
"http://github.com/docker/docker.git",
"http://github.com/docker/docker.git#branch",
"http://github.com/docker/docker.git#:dir",
}
incompleteGitUrls := []string{
"github.com/docker/docker",
}
invalidGitUrls := []string{
"http://github.com/docker/docker.git:#branch",
}
for _, url := range gitUrls {
if !IsValidRemote(url) {
t.Fatalf("%q should have been a valid remote", url)
}
}
for _, url := range incompleteGitUrls {
if !IsValidRemote(url) {
t.Fatalf("%q should have been a valid remote", url)
}
}
for _, url := range invalidGitUrls {
if !IsValidRemote(url) {
t.Fatalf("%q should have been a valid remote", url)
}
}
}

173
vendor/github.com/hyperhq/libcompose/config/merge_v1.go generated vendored Normal file
View File

@@ -0,0 +1,173 @@
package config
import (
"fmt"
"path"
"github.com/Sirupsen/logrus"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/hyperhq/libcompose/utils"
)
// MergeServicesV1 merges a v1 compose file into an existing set of service configs
func MergeServicesV1(existingServices *ServiceConfigs, environmentLookup EnvironmentLookup, resourceLookup ResourceLookup, file string, bytes []byte) (map[string]*ServiceConfigV1, error) {
datas := make(RawServiceMap)
if err := yaml.Unmarshal(bytes, &datas); err != nil {
return nil, err
}
if err := Interpolate(environmentLookup, &datas); err != nil {
return nil, err
}
if err := validate(datas, "v1"); err != nil {
return nil, err
}
for name, data := range datas {
data, err := parseV1(resourceLookup, environmentLookup, file, data, datas)
if err != nil {
logrus.Errorf("Failed to parse service %s: %v", name, err)
return nil, err
}
if serviceConfig, ok := existingServices.Get(name); ok {
var rawExistingService RawService
if err := utils.Convert(serviceConfig, &rawExistingService); err != nil {
return nil, err
}
data = mergeConfig(rawExistingService, data)
}
datas[name] = data
}
for name, data := range datas {
err := validateServiceConstraints(data, name)
if err != nil {
return nil, err
}
}
serviceConfigs := make(map[string]*ServiceConfigV1)
if err := utils.Convert(datas, &serviceConfigs); err != nil {
return nil, err
}
return serviceConfigs, nil
}
func parseV1(resourceLookup ResourceLookup, environmentLookup EnvironmentLookup, inFile string, serviceData RawService, datas RawServiceMap) (RawService, error) {
serviceData, err := readEnvFile(resourceLookup, inFile, serviceData)
if err != nil {
return nil, err
}
//serviceData = resolveContextV1(inFile, serviceData)
value, ok := serviceData["extends"]
if !ok {
return serviceData, nil
}
mapValue, ok := value.(map[interface{}]interface{})
if !ok {
return serviceData, nil
}
if resourceLookup == nil {
return nil, fmt.Errorf("Can not use extends in file %s no mechanism provided to files", inFile)
}
file := asString(mapValue["file"])
service := asString(mapValue["service"])
if service == "" {
return serviceData, nil
}
var baseService RawService
if file == "" {
if serviceData, ok := datas[service]; ok {
baseService, err = parseV1(resourceLookup, environmentLookup, inFile, serviceData, datas)
} else {
return nil, fmt.Errorf("Failed to find service %s to extend", service)
}
} else {
bytes, resolved, err := resourceLookup.Lookup(file, inFile)
if err != nil {
logrus.Errorf("Failed to lookup file %s: %v", file, err)
return nil, err
}
var baseRawServices RawServiceMap
if err := yaml.Unmarshal(bytes, &baseRawServices); err != nil {
return nil, err
}
err = Interpolate(environmentLookup, &baseRawServices)
if err != nil {
return nil, err
}
if err := validate(baseRawServices, "v1"); err != nil {
return nil, err
}
baseService, ok = baseRawServices[service]
if !ok {
return nil, fmt.Errorf("Failed to find service %s in file %s", service, file)
}
baseService, err = parseV1(resourceLookup, environmentLookup, resolved, baseService, baseRawServices)
}
if err != nil {
return nil, err
}
baseService = clone(baseService)
logrus.Debugf("Merging %#v, %#v", baseService, serviceData)
for _, k := range noMerge {
if _, ok := baseService[k]; ok {
source := file
if source == "" {
source = inFile
}
return nil, fmt.Errorf("Cannot extend service '%s' in %s: services with '%s' cannot be extended", service, source, k)
}
}
baseService = mergeConfig(baseService, serviceData)
logrus.Debugf("Merged result %#v", baseService)
return baseService, nil
}
func resolveContextV1(inFile string, serviceData RawService) RawService {
context := asString(serviceData["build"])
if context == "" {
return serviceData
}
if IsValidRemote(context) {
return serviceData
}
current := path.Dir(inFile)
if context == "." {
context = current
} else {
current = path.Join(current, context)
}
serviceData["build"] = current
return serviceData
}

211
vendor/github.com/hyperhq/libcompose/config/merge_v2.go generated vendored Normal file
View File

@@ -0,0 +1,211 @@
package config
import (
"fmt"
"path"
"github.com/Sirupsen/logrus"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/hyperhq/libcompose/utils"
)
// MergeServicesV2 merges a v2 compose file into an existing set of service configs
func MergeServicesV2(existingServices *ServiceConfigs, environmentLookup EnvironmentLookup, resourceLookup ResourceLookup, file string, bytes []byte) (map[string]*ServiceConfig, error) {
var config Config
if err := yaml.Unmarshal(bytes, &config); err != nil {
return nil, err
}
datas := config.Services
if err := Interpolate(environmentLookup, &datas); err != nil {
return nil, err
}
/*
data := RawServiceMap{
"services": map[string]interface{}{},
}
for k, v := range datas {
data["services"][k] = v
}
*/
if err := validate(datas, "v2"); err != nil {
return nil, err
}
for name, data := range datas {
data, err := parseV2(resourceLookup, environmentLookup, file, data, datas)
if err != nil {
logrus.Errorf("Failed to parse service %s: %v", name, err)
return nil, err
}
if serviceConfig, ok := existingServices.Get(name); ok {
var rawExistingService RawService
if err := utils.Convert(serviceConfig, &rawExistingService); err != nil {
return nil, err
}
data = mergeConfig(rawExistingService, data)
}
datas[name] = data
}
serviceConfigs := make(map[string]*ServiceConfig)
if err := utils.Convert(datas, &serviceConfigs); err != nil {
return nil, err
}
return serviceConfigs, nil
}
// ParseVolumes parses volumes in a compose file
func ParseVolumes(environmentLookup EnvironmentLookup, resourceLookup ResourceLookup, file string, bytes []byte) (map[string]*VolumeConfig, error) {
volumeConfigs := make(map[string]*VolumeConfig)
var config Config
if err := yaml.Unmarshal(bytes, &config); err != nil {
return nil, err
}
if err := utils.Convert(config.Volumes, &volumeConfigs); err != nil {
return nil, err
}
return volumeConfigs, nil
}
// ParseNetworks parses networks in a compose file
func ParseNetworks(environmentLookup EnvironmentLookup, resourceLookup ResourceLookup, file string, bytes []byte) (map[string]*NetworkConfig, error) {
networkConfigs := make(map[string]*NetworkConfig)
var config Config
if err := yaml.Unmarshal(bytes, &config); err != nil {
return nil, err
}
if err := utils.Convert(config.Networks, &networkConfigs); err != nil {
return nil, err
}
return networkConfigs, nil
}
func parseV2(resourceLookup ResourceLookup, environmentLookup EnvironmentLookup, inFile string, serviceData RawService, datas RawServiceMap) (RawService, error) {
serviceData, err := readEnvFile(resourceLookup, inFile, serviceData)
if err != nil {
return nil, err
}
//serviceData = resolveContextV2(inFile, serviceData)
value, ok := serviceData["extends"]
if !ok {
return serviceData, nil
}
mapValue, ok := value.(map[interface{}]interface{})
if !ok {
return serviceData, nil
}
if resourceLookup == nil {
return nil, fmt.Errorf("Can not use extends in file %s no mechanism provided to files", inFile)
}
file := asString(mapValue["file"])
service := asString(mapValue["service"])
if service == "" {
return serviceData, nil
}
var baseService RawService
if file == "" {
if serviceData, ok := datas[service]; ok {
baseService, err = parseV2(resourceLookup, environmentLookup, inFile, serviceData, datas)
} else {
return nil, fmt.Errorf("Failed to find service %s to extend", service)
}
} else {
bytes, resolved, err := resourceLookup.Lookup(file, inFile)
if err != nil {
logrus.Errorf("Failed to lookup file %s: %v", file, err)
return nil, err
}
var config Config
if err := yaml.Unmarshal(bytes, &config); err != nil {
return nil, err
}
baseRawServices := config.Services
err = Interpolate(environmentLookup, &baseRawServices)
if err != nil {
return nil, err
}
if err = validate(datas, "v2"); err != nil {
return nil, err
}
baseService, ok = baseRawServices[service]
if !ok {
return nil, fmt.Errorf("Failed to find service %s in file %s", service, file)
}
baseService, err = parseV2(resourceLookup, environmentLookup, resolved, baseService, baseRawServices)
}
if err != nil {
return nil, err
}
baseService = clone(baseService)
logrus.Debugf("Merging %#v, %#v", baseService, serviceData)
for _, k := range noMerge {
if _, ok := baseService[k]; ok {
source := file
if source == "" {
source = inFile
}
return nil, fmt.Errorf("Cannot extend service '%s' in %s: services with '%s' cannot be extended", service, source, k)
}
}
baseService = mergeConfig(baseService, serviceData)
logrus.Debugf("Merged result %#v", baseService)
return baseService, nil
}
func resolveContextV2(inFile string, serviceData RawService) RawService {
if _, ok := serviceData["build"]; !ok {
return serviceData
}
build := serviceData["build"].(map[interface{}]interface{})
context := asString(build["context"])
if context == "" {
return serviceData
}
if IsValidRemote(context) {
return serviceData
}
current := path.Dir(inFile)
if context == "." {
context = current
} else {
current = path.Join(current, context)
}
build["context"] = current
return serviceData
}

339
vendor/github.com/hyperhq/libcompose/config/schema.go generated vendored Normal file
View File

@@ -0,0 +1,339 @@
package config
var schemaV1 = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v1.json",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/service"
}
},
"additionalProperties": false,
"definitions": {
"service": {
"id": "#/definitions/service",
"type": "object",
"properties": {
"cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cgroup_parent": {"type": "string"},
"command": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"container_name": {"type": "string"},
"cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]},
"cpuset": {"type": "string"},
"dns": {"$ref": "#/definitions/string_or_list"},
"dns_search": {"$ref": "#/definitions/string_or_list"},
"domainname": {"type": "string"},
"entrypoint": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"env_file": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"environment": {"$ref": "#/definitions/list_or_dict"},
"extends": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"service": {"type": "string"},
"file": {"type": "string"}
},
"required": ["service"],
"additionalProperties": false
}
]
},
"expose": {
"type": "array",
"items": {
"type": ["string", "number"]
},
"uniqueItems": true
},
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"hostname": {"type": "string"},
"image": {"type": "string"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"mem_limit": {"type": ["number", "string"]},
"memswap_limit": {"type": ["number", "string"]},
"noauto_volume": {"type": "boolean"},
"ports": {
"type": "array",
"items": {
"type": ["string", "number"]
},
"uniqueItems": true
},
"stop_signal": {"type": "string"},
"security_groups": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"restart": {"type": "string"},
"stdin_open": {"type": "boolean"},
"tty": {"type": "boolean"},
"volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"working_dir": {"type": "string"},
"user": {"type": "string"},
"size": {"type": "string"},
"fip": {"type": "string"}
},
"dependencies": {
},
"additionalProperties": false
},
"string_or_list": {
"oneOf": [
{"type": "string"},
{"$ref": "#/definitions/list_of_strings"}
]
},
"list_of_strings": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
},
"list_or_dict": {
"oneOf": [
{
"type": "object",
"patternProperties": {
".+": {
"type": ["string", "number", "null"]
}
},
"additionalProperties": false
},
{"type": "array", "items": {"type": "string"}, "uniqueItems": true}
]
},
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
"anyOf": [
{
"required": ["image"]
}
]
}
}
}
}
`
var schemaV2 = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v2.0.json",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/service"
}
},
"additionalProperties": false,
"definitions": {
"service": {
"id": "#/definitions/service",
"type": "object",
"properties": {
"cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cgroup_parent": {"type": "string"},
"command": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"container_name": {"type": "string"},
"cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]},
"cpuset": {"type": "string"},
"depends_on": {"$ref": "#/definitions/list_of_strings"},
"dns": {"$ref": "#/definitions/string_or_list"},
"dns_search": {"$ref": "#/definitions/string_or_list"},
"domainname": {"type": "string"},
"entrypoint": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"env_file": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"environment": {"$ref": "#/definitions/list_or_dict"},
"extends": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"service": {"type": "string"},
"file": {"type": "string"}
},
"required": ["service"],
"additionalProperties": false
}
]
},
"expose": {
"type": "array",
"items": {
"type": ["string", "number"]
},
"uniqueItems": true
},
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"hostname": {"type": "string"},
"image": {"type": "string"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"mem_limit": {"type": ["number", "string"]},
"memswap_limit": {"type": ["number", "string"]},
"network_mode": {"type": "string"},
"noauto_volume": {"type": "boolean"},
"networks": {
"oneOf": [
{"$ref": "#/definitions/list_of_strings"},
{
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"oneOf": [
{
"type": "object",
"properties": {
"aliases": {"$ref": "#/definitions/list_of_strings"},
"ipv4_address": {"type": "string"},
"ipv6_address": {"type": "string"}
},
"additionalProperties": false
},
{"type": "null"}
]
}
},
"additionalProperties": false
}
]
},
"ports": {
"type": "array",
"items": {
"type": ["string", "number"]
},
"uniqueItems": true
},
"restart": {"type": "string"},
"stdin_open": {"type": "boolean"},
"stop_signal": {"type": "string"},
"security_groups": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"tty": {"type": "boolean"},
"user": {"type": "string"},
"volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"working_dir": {"type": "string"},
"size": {"type": "string"},
"fip": {"type": "string"}
},
"additionalProperties": false
},
"volume": {
"id": "#/definitions/volume",
"type": ["object", "null"],
"properties": {
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
}
},
"additionalProperties": false
},
"additionalProperties": false
},
"string_or_list": {
"oneOf": [
{"type": "string"},
{"$ref": "#/definitions/list_of_strings"}
]
},
"list_of_strings": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
},
"list_or_dict": {
"oneOf": [
{
"type": "object",
"patternProperties": {
".+": {
"type": ["string", "number", "null"]
}
},
"additionalProperties": false
},
{"type": "array", "items": {"type": "string"}, "uniqueItems": true}
]
},
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
"anyOf": [
{"required": ["image"]}
]
}
}
}
}
`

View File

@@ -0,0 +1,96 @@
package config
import (
"encoding/json"
"strings"
"github.com/docker/go-connections/nat"
"github.com/xeipuuv/gojsonschema"
)
var (
schemaLoader gojsonschema.JSONLoader
constraintSchemaLoader gojsonschema.JSONLoader
schema map[string]interface{}
)
type (
environmentFormatChecker struct{}
portsFormatChecker struct{}
)
func (checker environmentFormatChecker) IsFormat(input string) bool {
// If the value is a boolean, a warning should be given
// However, we can't determine type since gojsonschema converts the value to a string
// Adding a function with an interface{} parameter to gojsonschema is probably the best way to handle this
return true
}
func (checker portsFormatChecker) IsFormat(input string) bool {
_, _, err := nat.ParsePortSpecs([]string{input})
return err == nil
}
func setupSchemaLoaders(version string) error {
if schema != nil {
return nil
}
var schemaRaw interface{}
var schemaStr string = schemaV1
if version == "v2" {
schemaStr = schemaV2
}
err := json.Unmarshal([]byte(schemaStr), &schemaRaw)
if err != nil {
return err
}
schema = schemaRaw.(map[string]interface{})
gojsonschema.FormatCheckers.Add("environment", environmentFormatChecker{})
//gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{})
//gojsonschema.FormatCheckers.Add("expose", portsFormatChecker{})
schemaLoader = gojsonschema.NewGoLoader(schemaRaw)
definitions := schema["definitions"].(map[string]interface{})
constraints := definitions["constraints"].(map[string]interface{})
service := constraints["service"].(map[string]interface{})
constraintSchemaLoader = gojsonschema.NewGoLoader(service)
return nil
}
// gojsonschema doesn't provide a list of valid types for a property
// This parses the schema manually to find all valid types
func parseValidTypesFromSchema(schema map[string]interface{}, context string) []string {
contextSplit := strings.Split(context, ".")
key := contextSplit[len(contextSplit)-1]
definitions := schema["definitions"].(map[string]interface{})
service := definitions["service"].(map[string]interface{})
properties := service["properties"].(map[string]interface{})
property := properties[key].(map[string]interface{})
var validTypes []string
if val, ok := property["oneOf"]; ok {
validConditions := val.([]interface{})
for _, validCondition := range validConditions {
condition := validCondition.(map[string]interface{})
validTypes = append(validTypes, condition["type"].(string))
}
} else if val, ok := property["$ref"]; ok {
reference := val.(string)
if reference == "#/definitions/string_or_list" {
return []string{"string", "array"}
} else if reference == "#/definitions/list_of_strings" {
return []string{"array"}
} else if reference == "#/definitions/list_or_dict" {
return []string{"array", "object"}
}
}
return validTypes
}

238
vendor/github.com/hyperhq/libcompose/config/types.go generated vendored Normal file
View File

@@ -0,0 +1,238 @@
package config
import (
"sync"
"github.com/hyperhq/libcompose/yaml"
)
// EnvironmentLookup defines methods to provides environment variable loading.
type EnvironmentLookup interface {
Lookup(key, serviceName string, config *ServiceConfig) []string
}
// ResourceLookup defines methods to provides file loading.
type ResourceLookup interface {
Lookup(file, relativeTo string) ([]byte, string, error)
ResolvePath(path, inFile string) string
}
// ServiceConfigV1 holds version 1 of libcompose service configuration
type ServiceConfigV1 struct {
/*
Build string `yaml:"build,omitempty"`
CapAdd []string `yaml:"cap_add,omitempty"`
CapDrop []string `yaml:"cap_drop,omitempty"`
CgroupParent string `yaml:"cgroup_parent,omitempty"`
CPUQuota int64 `yaml:"cpu_quota,omitempty"`
CPUSet string `yaml:"cpuset,omitempty"`
CPUShares int64 `yaml:"cpu_shares,omitempty"`
Devices []string `yaml:"devices,omitempty"`
DNS yaml.Stringorslice `yaml:"dns,omitempty"`
DNSSearch yaml.Stringorslice `yaml:"dns_search,omitempty"`
Dockerfile string `yaml:"dockerfile,omitempty"`
LogDriver string `yaml:"log_driver,omitempty"`
MacAddress string `yaml:"mac_address,omitempty"`
MemLimit int64 `yaml:"mem_limit,omitempty"`
MemSwapLimit int64 `yaml:"memswap_limit,omitempty"`
Name string `yaml:"name,omitempty"`
Net string `yaml:"net,omitempty"`
Pid string `yaml:"pid,omitempty"`
Uts string `yaml:"uts,omitempty"`
Ipc string `yaml:"ipc,omitempty"`
Ports []string `yaml:"ports,omitempty"`
Privileged bool `yaml:"privileged,omitempty"`
ReadOnly bool `yaml:"read_only,omitempty"`
SecurityOpt []string `yaml:"security_opt,omitempty"`
User string `yaml:"user,omitempty"`
VolumeDriver string `yaml:"volume_driver,omitempty"`
VolumesFrom []string `yaml:"volumes_from,omitempty"`
Expose []string `yaml:"expose,omitempty"`
LogOpt map[string]string `yaml:"log_opt,omitempty"`
ExtraHosts []string `yaml:"extra_hosts,omitempty"`
Ulimits yaml.Ulimits `yaml:"ulimits,omitempty"`
*/
Command yaml.Command `yaml:"command,flow,omitempty" json:"command,omitempty"`
ContainerName string `yaml:"container_name,omitempty" json:"container_name,omitempty"`
DomainName string `yaml:"domainname,omitempty" json:"domainname,omitempty"`
Entrypoint yaml.Command `yaml:"entrypoint,flow,omitempty" json:"entrypoint,omitempty"`
EnvFile yaml.Stringorslice `yaml:"env_file,omitempty" json:"env_file,omitempty"`
Environment yaml.MaporEqualSlice `yaml:"environment,omitempty" json:"environment,omitempty"`
Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`
Image string `yaml:"image,omitempty" json:"image,omitempty"`
Labels yaml.SliceorMap `yaml:"labels,omitempty" json:"labels,omitempty"`
Links yaml.MaporColonSlice `yaml:"links,omitempty" json:"links,omitempty"`
Restart string `yaml:"restart,omitempty" json:"restart,omitempty"`
StdinOpen bool `yaml:"stdin_open,omitempty" json:"stdin_open,omitempty"`
Tty bool `yaml:"tty,omitempty" json:"tty,omitempty"`
Volumes []string `yaml:"volumes,omitempty" json:"volumes,omitempty"`
WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
ExternalLinks []string `yaml:"external_links,omitempty" json:"external_links,omitempty"`
Size string `yaml:"size,omitempty" json:"size,omitempty"`
Fip string `yaml:"fip,omitempty" json:"fip,omitempty"`
SecurityGroups []string `yaml:"security_groups,omitempty" json:"security_groups,omitempty"`
NoAutoVolume bool `yaml:"noauto_volume,omitempty" json:"noauto_volume,omitempty"`
}
// Build holds v2 build information
type Build struct {
Context string `yaml:"context,omitempty"`
Dockerfile string `yaml:"dockerfile,omitempty"`
Args yaml.MaporEqualSlice `yaml:"args,omitempty"`
}
// Log holds v2 logging information
type Log struct {
Driver string `yaml:"driver,omitempty"`
Options map[string]string `yaml:"options,omitempty"`
}
// ServiceConfig holds version 2 of libcompose service configuration
type ServiceConfig struct {
/*
Build Build `yaml:"build,omitempty"`
CapAdd []string `yaml:"cap_add,omitempty"`
CapDrop []string `yaml:"cap_drop,omitempty"`
CPUSet string `yaml:"cpuset,omitempty"`
CPUShares int64 `yaml:"cpu_shares,omitempty"`
CPUQuota int64 `yaml:"cpu_quota,omitempty"`
CgroupParent string `yaml:"cgroup_parrent,omitempty"`
Devices []string `yaml:"devices,omitempty"`
DNS yaml.Stringorslice `yaml:"dns,omitempty"`
DNSSearch yaml.Stringorslice `yaml:"dns_search,omitempty"`
Expose []string `yaml:"expose,omitempty"`
Ipc string `yaml:"ipc,omitempty"`
Logging Log `yaml:"logging,omitempty"`
MacAddress string `yaml:"mac_address,omitempty"`
MemLimit int64 `yaml:"mem_limit,omitempty"`
MemSwapLimit int64 `yaml:"memswap_limit,omitempty"`
NetworkMode string `yaml:"network_mode,omitempty"`
Networks []string `yaml:"networks,omitempty"`
Pid string `yaml:"pid,omitempty"`
Ports []string `yaml:"ports,omitempty"`
Privileged bool `yaml:"privileged,omitempty"`
SecurityOpt []string `yaml:"security_opt,omitempty"`
StopSignal string `yaml:"stop_signal,omitempty"`
VolumeDriver string `yaml:"volume_driver,omitempty"`
VolumesFrom []string `yaml:"volumes_from,omitempty"`
Uts string `yaml:"uts,omitempty"`
ReadOnly bool `yaml:"read_only,omitempty"`
User string `yaml:"user,omitempty"`
Ulimits yaml.Ulimits `yaml:"ulimits,omitempty"`
*/
Expose []string `yaml:"expose,omitempty" json:"expose,omitempty"`
Ports []string `yaml:"ports,omitempty" json:"ports,omitempty"`
Command yaml.Command `yaml:"command,flow,omitempty" json:"command,omitempty"`
ContainerName string `yaml:"container_name,omitempty" json:"container_name,omitempty"`
DomainName string `yaml:"domainname,omitempty" json:"domainname,omitempty"`
DependsOn []string `yaml:"depends_on,omitempty" json:"depends_on,omitempty"`
Entrypoint yaml.Command `yaml:"entrypoint,flow,omitempty" json:"entrypoint,omitempty"`
EnvFile yaml.Stringorslice `yaml:"env_file,omitempty" json:"env_file,omitempty"`
Environment yaml.MaporEqualSlice `yaml:"environment,omitempty" json:"environment,omitempty"`
Extends yaml.MaporEqualSlice `yaml:"extends,omitempty" json:"extends,omitempty"`
ExternalLinks []string `yaml:"external_links,omitempty" json:"external_links"`
Image string `yaml:"image,omitempty" json:"image,omitempty"`
Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`
Labels yaml.SliceorMap `yaml:"labels,omitempty" json:"labels,omitempty"`
Links yaml.MaporColonSlice `yaml:"links,omitempty" json:"links,omitempty"`
Volumes []string `yaml:"volumes,omitempty" json:"volumes,omitempty"`
Restart string `yaml:"restart,omitempty" json:"restart,omitempty"`
StdinOpen bool `yaml:"stdin_open,omitempty" json:"stdin_open,omitempty"`
Tty bool `yaml:"tty,omitempty" json:"tty,omitempty"`
WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
Size string `yaml:"size,omitempty" json:"size,omitempty"`
Fip string `yaml:"fip,omitempty" json:"fip,omitempty"`
SecurityGroups []string `yaml:"security_groups,omitempty" json:"security_groups,omitempty"`
NoAutoVolume bool `yaml:"noauto_volume,omitempty" json:"noauto_volume,omitempty"`
}
// VolumeConfig holds v2 volume configuration
type VolumeConfig struct {
Driver string `yaml:"driver,omitempty"`
DriverOpts map[string]string `yaml:"driver_opts,omitempty"`
External bool `yaml:"external,omitempty"`
}
// Ipam holds v2 network IPAM information
type Ipam struct {
Driver string `yaml:"driver,omitempty"`
Config []string `yaml:"config,omitempty"`
}
// NetworkConfig holds v2 network configuration
type NetworkConfig struct {
Driver string `yaml:"driver,omitempty"`
DriverOpts map[string]string `yaml:"driver_opts,omitempty"`
External bool `yaml:"external,omitempty"`
Ipam Ipam `yaml:"ipam,omitempty"`
}
// Config holds libcompose top level configuration
type Config struct {
Version string `yaml:"version,omitempty"`
Services RawServiceMap `yaml:"services,omitempty"`
Volumes map[string]*VolumeConfig `yaml:"volumes,omitempty"`
Networks map[string]*NetworkConfig `yaml:"networks,omitempty"`
}
// NewServiceConfigs initializes a new Configs struct
func NewServiceConfigs() *ServiceConfigs {
return &ServiceConfigs{
M: make(map[string]*ServiceConfig),
}
}
// ServiceConfigs holds a concurrent safe map of ServiceConfig
type ServiceConfigs struct {
M map[string]*ServiceConfig
mu sync.RWMutex
}
// Has checks if the config map has the specified name
func (c *ServiceConfigs) Has(name string) bool {
c.mu.RLock()
defer c.mu.RUnlock()
_, ok := c.M[name]
return ok
}
// Get returns the config and the presence of the specified name
func (c *ServiceConfigs) Get(name string) (*ServiceConfig, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
service, ok := c.M[name]
return service, ok
}
// Add add the specifed config with the specified name
func (c *ServiceConfigs) Add(name string, service *ServiceConfig) {
c.mu.Lock()
c.M[name] = service
c.mu.Unlock()
}
// Len returns the len of the configs
func (c *ServiceConfigs) Len() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.M)
}
// Keys returns the names of the config
func (c *ServiceConfigs) Keys() []string {
keys := []string{}
c.mu.RLock()
defer c.mu.RUnlock()
for name := range c.M {
keys = append(keys, name)
}
return keys
}
// RawService is represent a Service in map form unparsed
type RawService map[string]interface{}
// RawServiceMap is a collection of RawServices
type RawServiceMap map[string]RawService

42
vendor/github.com/hyperhq/libcompose/config/utils.go generated vendored Normal file
View File

@@ -0,0 +1,42 @@
package config
func merge(existing, value interface{}) interface{} {
// append strings
if left, lok := existing.([]interface{}); lok {
if right, rok := value.([]interface{}); rok {
return append(left, right...)
}
}
//merge maps
if left, lok := existing.(map[interface{}]interface{}); lok {
if right, rok := value.(map[interface{}]interface{}); rok {
newLeft := make(map[interface{}]interface{})
for k, v := range left {
newLeft[k] = v
}
for k, v := range right {
newLeft[k] = v
}
return newLeft
}
}
return value
}
func clone(in RawService) RawService {
result := RawService{}
for k, v := range in {
result[k] = v
}
return result
}
func asString(obj interface{}) string {
if v, ok := obj.(string); ok {
return v
}
return ""
}

View File

@@ -0,0 +1,309 @@
package config
import (
"fmt"
"strconv"
"strings"
"github.com/Sirupsen/logrus"
"github.com/xeipuuv/gojsonschema"
)
func serviceNameFromErrorField(field string) string {
splitKeys := strings.Split(field, ".")
return splitKeys[0]
}
func keyNameFromErrorField(field string) string {
splitKeys := strings.Split(field, ".")
if len(splitKeys) > 0 {
return splitKeys[len(splitKeys)-1]
}
return ""
}
func containsTypeError(resultError gojsonschema.ResultError) bool {
contextSplit := strings.Split(resultError.Context().String(), ".")
_, err := strconv.Atoi(contextSplit[len(contextSplit)-1])
return err == nil
}
func addArticle(s string) string {
switch s[0] {
case 'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U':
return "an " + s
default:
return "a " + s
}
}
// Gets the value in a service map at a given error context
func getValue(val interface{}, context string) string {
keys := strings.Split(context, ".")
if keys[0] == "(root)" {
keys = keys[1:]
}
for i, k := range keys {
switch typedVal := (val).(type) {
case string:
return typedVal
case []interface{}:
if index, err := strconv.Atoi(k); err == nil {
val = typedVal[index]
}
case RawServiceMap:
val = typedVal[k]
case RawService:
val = typedVal[k]
case map[interface{}]interface{}:
val = typedVal[k]
}
if i == len(keys)-1 {
return fmt.Sprint(val)
}
}
return ""
}
// Converts map[interface{}]interface{} to map[string]interface{} recursively
// gojsonschema only accepts map[string]interface{}
func convertServiceMapKeysToStrings(serviceMap RawServiceMap) RawServiceMap {
newServiceMap := make(RawServiceMap)
for k, v := range serviceMap {
newServiceMap[k] = convertServiceKeysToStrings(v)
}
return newServiceMap
}
func convertServiceKeysToStrings(service RawService) RawService {
newService := make(RawService)
for k, v := range service {
newService[k] = convertKeysToStrings(v)
}
return newService
}
func convertKeysToStrings(item interface{}) interface{} {
switch typedDatas := item.(type) {
case map[interface{}]interface{}:
newMap := make(map[string]interface{})
for key, value := range typedDatas {
stringKey := key.(string)
newMap[stringKey] = convertKeysToStrings(value)
}
return newMap
case []interface{}:
// newArray := make([]interface{}, 0) will cause golint to complain
var newArray []interface{}
newArray = make([]interface{}, 0)
for _, value := range typedDatas {
newArray = append(newArray, convertKeysToStrings(value))
}
return newArray
default:
return item
}
}
var dockerConfigHints = map[string]string{
/*
"cpu_share": "cpu_shares",
"add_host": "extra_hosts",
"hosts": "extra_hosts",
"extra_host": "extra_hosts",
"device": "devices",
*/
"link": "links",
/*
"memory_swap": "memswap_limit",
"port": "ports",
"privilege": "privileged",
"priviliged": "privileged",
"privilige": "privileged",
*/
"volume": "volumes",
"workdir": "working_dir",
}
func unsupportedConfigMessage(key string, nextErr gojsonschema.ResultError) string {
service := serviceNameFromErrorField(nextErr.Field())
message := fmt.Sprintf("Unsupported config option for %s service: '%s'", service, key)
if val, ok := dockerConfigHints[key]; ok {
message += fmt.Sprintf(" (did you mean '%s'?)", val)
}
return message
}
func oneOfMessage(serviceMap RawServiceMap, schema map[string]interface{}, err, nextErr gojsonschema.ResultError) string {
switch nextErr.Type() {
case "additional_property_not_allowed":
property := nextErr.Details()["property"]
return fmt.Sprintf("contains unsupported option: '%s'", property)
case "invalid_type":
if containsTypeError(nextErr) {
expectedType := addArticle(nextErr.Details()["expected"].(string))
return fmt.Sprintf("contains %s, which is an invalid type, it should be %s", getValue(serviceMap, nextErr.Context().String()), expectedType)
}
validTypes := parseValidTypesFromSchema(schema, err.Context().String())
validTypesMsg := addArticle(strings.Join(validTypes, " or "))
return fmt.Sprintf("contains an invalid type, it should be %s", validTypesMsg)
case "unique":
contextWithDuplicates := getValue(serviceMap, nextErr.Context().String())
return fmt.Sprintf("contains non unique items, please remove duplicates from %s", contextWithDuplicates)
}
return ""
}
func invalidTypeMessage(service, key string, err gojsonschema.ResultError) string {
expectedTypesString := err.Details()["expected"].(string)
var expectedTypes []string
if strings.Contains(expectedTypesString, ",") {
expectedTypes = strings.Split(expectedTypesString[1:len(expectedTypesString)-1], ",")
} else {
expectedTypes = []string{expectedTypesString}
}
validTypesMsg := addArticle(strings.Join(expectedTypes, " or "))
return fmt.Sprintf("Service '%s' configuration key '%s' contains an invalid type, it should be %s.", service, key, validTypesMsg)
}
func validate(serviceMap RawServiceMap, version string) error {
if err := setupSchemaLoaders(version); err != nil {
return err
}
serviceMap = convertServiceMapKeysToStrings(serviceMap)
var validationErrors []string
dataLoader := gojsonschema.NewGoLoader(serviceMap)
result, err := gojsonschema.Validate(schemaLoader, dataLoader)
if err != nil {
return err
}
// gojsonschema can create extraneous "additional_property_not_allowed" errors in some cases
// If this is set, and the error is at root level, skip over that error
skipRootAdditionalPropertyError := false
if !result.Valid() {
for i := 0; i < len(result.Errors()); i++ {
err := result.Errors()[i]
if skipRootAdditionalPropertyError && err.Type() == "additional_property_not_allowed" && err.Context().String() == "(root)" {
skipRootAdditionalPropertyError = false
continue
}
if err.Context().String() == "(root)" {
switch err.Type() {
case "additional_property_not_allowed":
validationErrors = append(validationErrors, fmt.Sprintf("Invalid service name '%s' - only [a-zA-Z0-9\\._\\-] characters are allowed", err.Field()))
default:
validationErrors = append(validationErrors, err.Description())
}
} else {
skipRootAdditionalPropertyError = true
serviceName := serviceNameFromErrorField(err.Field())
key := keyNameFromErrorField(err.Field())
switch err.Type() {
case "additional_property_not_allowed":
logrus.Infof("%s %s", serviceName, key)
validationErrors = append(validationErrors, unsupportedConfigMessage(key, result.Errors()[i+1]))
case "number_one_of":
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' configuration key '%s' %s", serviceName, key, oneOfMessage(serviceMap, schema, err, result.Errors()[i+1])))
// Next error handled in oneOfMessage, skip over it
i++
case "invalid_type":
validationErrors = append(validationErrors, invalidTypeMessage(serviceName, key, err))
case "required":
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' option '%s' is invalid, %s", serviceName, key, err.Description()))
case "missing_dependency":
dependency := err.Details()["dependency"].(string)
validationErrors = append(validationErrors, fmt.Sprintf("Invalid configuration for '%s' service: dependency '%s' is not satisfied", serviceName, dependency))
case "unique":
contextWithDuplicates := getValue(serviceMap, err.Context().String())
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' configuration key '%s' value %s has non-unique elements", serviceName, key, contextWithDuplicates))
default:
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' configuration key %s value %s", serviceName, key, err.Description()))
}
}
}
if len(validationErrors) > 0 {
return fmt.Errorf(strings.Join(validationErrors, "\n"))
}
}
return nil
}
func validateServiceConstraints(service RawService, serviceName string) error {
if err := setupSchemaLoaders("v1"); err != nil {
return err
}
service = convertServiceKeysToStrings(service)
var validationErrors []string
dataLoader := gojsonschema.NewGoLoader(service)
result, err := gojsonschema.Validate(constraintSchemaLoader, dataLoader)
if err != nil {
return err
}
if !result.Valid() {
for _, err := range result.Errors() {
if err.Type() == "number_any_of" {
_, containsImage := service["image"]
_, containsBuild := service["build"]
_, containsDockerfile := service["dockerfile"]
if containsImage && containsBuild {
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' has both an image and build path specified. A service can either be built to image or use an existing image, not both.", serviceName))
} else if !containsImage && !containsBuild {
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' has neither an image nor a build path specified. Exactly one must be provided.", serviceName))
} else if containsImage && containsDockerfile {
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' has both an image and alternate Dockerfile. A service can either be built to image or use an existing image, not both.", serviceName))
}
}
}
return fmt.Errorf(strings.Join(validationErrors, "\n"))
}
return nil
}

View File

@@ -0,0 +1,360 @@
package config
import (
"bytes"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func testValidSchema(t *testing.T, serviceMap RawServiceMap) {
err := validate(serviceMap, "v1")
assert.Nil(t, err)
for name, service := range serviceMap {
err := validateServiceConstraints(service, name)
assert.Nil(t, err)
}
}
func testInvalidSchema(t *testing.T, serviceMap RawServiceMap, errMsgs []string, errCount int) {
var combinedErrMsg bytes.Buffer
err := validate(serviceMap, "v2")
if err != nil {
combinedErrMsg.WriteString(err.Error())
combinedErrMsg.WriteRune('\n')
}
for name, service := range serviceMap {
err := validateServiceConstraints(service, name)
if err != nil {
combinedErrMsg.WriteString(err.Error())
combinedErrMsg.WriteRune('\n')
}
}
for _, errMsg := range errMsgs {
assert.True(t, strings.Contains(combinedErrMsg.String(), errMsg))
}
// gojsonschema has bugs that can cause extraneous errors
// This makes sure we don't have more errors than expected
assert.True(t, strings.Count(combinedErrMsg.String(), "\n") == errCount)
}
func TestInvalidServiceNames(t *testing.T) {
invalidServiceNames := []string{"?not?allowed", " ", "", "!", "/"}
for _, invalidServiceName := range invalidServiceNames {
testInvalidSchema(t, RawServiceMap{
invalidServiceName: map[string]interface{}{
"image": "busybox",
},
}, []string{fmt.Sprintf("Invalid service name '%s' - only [a-zA-Z0-9\\._\\-] characters are allowed", invalidServiceName)}, 1)
}
}
func TestValidServiceNames(t *testing.T) {
validServiceNames := []string{"_", "-", ".__.", "_what-up.", "what_.up----", "whatup"}
for _, validServiceName := range validServiceNames {
testValidSchema(t, RawServiceMap{
validServiceName: map[string]interface{}{
"image": "busybox",
},
})
}
}
/*
func TestConfigInvalidPorts(t *testing.T) {
portsValues := []interface{}{
map[string]interface{}{
"1": "8000",
},
false,
0,
"8000",
}
for _, portsValue := range portsValues {
testInvalidSchema(t, RawServiceMap{
"web": map[string]interface{}{
"image": "busybox",
"ports": portsValue,
},
}, []string{"Service 'web' configuration key 'ports' contains an invalid type, it should be an array"}, 1)
}
testInvalidSchema(t, RawServiceMap{
"web": map[string]interface{}{
"image": "busybox",
"ports": []interface{}{
"8000",
"8000",
},
},
}, []string{"Service 'web' configuration key 'ports' value [8000 8000] has non-unique elements"}, 1)
}
*/
/*
func TestConfigValidPorts(t *testing.T) {
portsValues := []interface{}{
[]interface{}{"8000", "9000"},
[]interface{}{"8000"},
[]interface{}{8000},
[]interface{}{"127.0.0.1::8000"},
[]interface{}{"49153-49154:3002-3003"},
}
for _, portsValue := range portsValues {
testValidSchema(t, RawServiceMap{
"web": map[string]interface{}{
"image": "busybox",
"ports": portsValue,
},
})
}
}
*/
func TestConfigHint(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
"foo": map[string]interface{}{
"image": "busybox",
"privilege": "something",
},
}, []string{"Unsupported config option for foo service: 'privilege'"}, 1)
}
func TestTypeShouldBeAnArray(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
"foo": map[string]interface{}{
"image": "busybox",
"links": "an_link",
},
}, []string{"Service 'foo' configuration key 'links' contains an invalid type, it should be an array"}, 1)
}
func TestInvalidTypeWithMultipleValidTypes(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
"web": map[string]interface{}{
"image": "busybox",
"mem_limit": []interface{}{
"array_elem",
},
},
}, []string{"Unsupported config option for web service: 'mem_limit'"}, 1)
}
func TestInvalidNotUniqueItems(t *testing.T) {
// Test property with array as only valid type
testInvalidSchema(t, RawServiceMap{
"foo": map[string]interface{}{
"image": "busybox",
"devices": []string{
"/dev/foo:/dev/foo",
"/dev/foo:/dev/foo",
},
},
}, []string{"Unsupported config option for foo service: 'devices'"}, 1)
// Test property with multiple valid types
testInvalidSchema(t, RawServiceMap{
"foo": map[string]interface{}{
"image": "busybox",
"environment": []string{
"KEY=VAL",
"KEY=VAL",
},
},
}, []string{"Service 'foo' configuration key 'environment' contains non unique items, please remove duplicates from [KEY=VAL KEY=VAL]"}, 1)
}
func TestInvalidListOfStringsFormat(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
"web": map[string]interface{}{
"image": "busybox",
"command": []interface{}{
1,
},
},
}, []string{"Service 'web' configuration key 'command' contains 1, which is an invalid type, it should be a string"}, 1)
}
func TestInvalidExtraHostsString(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
"web": map[string]interface{}{
"image": "busybox",
"extra_hosts": "somehost:162.242.195.82",
},
}, []string{"Unsupported config option for web service: 'extra_hosts'"}, 1)
}
/*
func TestValidConfigWhichAllowsTwoTypeDefinitions(t *testing.T) {
for _, exposeValue := range []interface{}{"8000", 9000} {
testValidSchema(t, RawServiceMap{
"web": map[string]interface{}{
"image": "busybox",
"expose": []interface{}{
exposeValue,
},
},
})
}
}
*/
func TestValidConfigOneOfStringOrList(t *testing.T) {
entrypointValues := []interface{}{
[]interface{}{
"sh",
},
"sh",
}
for _, entrypointValue := range entrypointValues {
testValidSchema(t, RawServiceMap{
"web": map[string]interface{}{
"image": "busybox",
"entrypoint": entrypointValue,
},
})
}
}
func TestInvalidServiceProperty(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
"web": map[string]interface{}{
"image": "busybox",
"invalid_property": "value",
},
}, []string{"Unsupported config option for web service: 'invalid_property'"}, 1)
}
func TestServiceInvalidMissingImageAndBuild(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
"web": map[string]interface{}{},
}, []string{"Service 'web' has neither an image nor a build path specified. Exactly one must be provided."}, 1)
}
func TestServiceInvalidSpecifiesImageAndBuild(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
"web": map[string]interface{}{
"image": "busybox",
"build": ".",
},
}, []string{"Unsupported config option for web service: 'build'"}, 1)
}
func TestServiceInvalidSpecifiesImageAndDockerfile(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
"web": map[string]interface{}{
"image": "busybox",
"dockerfile": "Dockerfile",
},
}, []string{"Unsupported config option for web service: 'dockerfile'"}, 1)
}
func TestInvalidServiceForMultipleErrors(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
"foo": map[string]interface{}{
"image": "busybox",
// "ports": "invalid_type",
"links": "an_type",
"environment": []string{
"KEY=VAL",
"KEY=VAL",
},
},
}, []string{
// "Service 'foo' configuration key 'ports' contains an invalid type, it should be an array",
"Service 'foo' configuration key 'links' contains an invalid type, it should be an array",
"Service 'foo' configuration key 'environment' contains non unique items, please remove duplicates from [KEY=VAL KEY=VAL]",
}, 2)
}
func TestInvalidServiceWithAdditionalProperties(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
"foo": map[string]interface{}{
"image": "busybox",
// "ports": "invalid_type",
"---": "nope",
"environment": []string{
"KEY=VAL",
"KEY=VAL",
},
},
}, []string{
// "Service 'foo' configuration key 'ports' contains an invalid type, it should be an array",
"Unsupported config option for foo service: '---'",
"Service 'foo' configuration key 'environment' contains non unique items, please remove duplicates from [KEY=VAL KEY=VAL]",
}, 2)
}
func TestMultipleInvalidServices(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
"foo1": map[string]interface{}{
"image": "busybox",
// "ports": "invalid_type",
},
"foo2": map[string]interface{}{
"image": "busybox",
// "ports": "invalid_type",
},
}, []string{
// "Service 'foo1' configuration key 'ports' contains an invalid type, it should be an array",
// "Service 'foo2' configuration key 'ports' contains an invalid type, it should be an array",
}, 0)
}
func TestMixedInvalidServicesAndInvalidServiceNames(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
//"foo1": map[string]interface{}{
// "image": "busybox",
// "ports": "invalid_type",
// },
"???": map[string]interface{}{
"image": "busybox",
},
// "foo2": map[string]interface{}{
// "image": "busybox",
// "ports": "invalid_type",
// },
}, []string{
// "Service 'foo1' configuration key 'ports' contains an invalid type, it should be an array",
"Invalid service name '???' - only [a-zA-Z0-9\\._\\-] characters are allowed",
// "Service 'foo2' configuration key 'ports' contains an invalid type, it should be an array",
}, 1)
}
func TestMultipleInvalidServicesForMultipleErrors(t *testing.T) {
testInvalidSchema(t, RawServiceMap{
"foo1": map[string]interface{}{
"image": "busybox",
// "ports": "invalid_type",
"environment": []string{
"KEY=VAL",
"KEY=VAL",
},
},
"foo2": map[string]interface{}{
"image": "busybox",
// "ports": "invalid_type",
"environment": []string{
"KEY=VAL",
"KEY=VAL",
},
},
}, []string{
// "Service 'foo1' configuration key 'ports' contains an invalid type, it should be an array",
"Service 'foo1' configuration key 'environment' contains non unique items, please remove duplicates from [KEY=VAL KEY=VAL]",
// "Service 'foo2' configuration key 'ports' contains an invalid type, it should be an array",
"Service 'foo2' configuration key 'environment' contains non unique items, please remove duplicates from [KEY=VAL KEY=VAL]",
}, 2)
}

33
vendor/github.com/hyperhq/libcompose/docker/auth.go generated vendored Normal file
View File

@@ -0,0 +1,33 @@
package docker
import (
"github.com/docker/engine-api/types"
"github.com/hyperhq/hypercli/registry"
)
// AuthLookup defines a method for looking up authentication information
type AuthLookup interface {
All() map[string]types.AuthConfig
Lookup(repoInfo *registry.RepositoryInfo) types.AuthConfig
}
// ConfigAuthLookup implements AuthLookup by reading a Docker config file
type ConfigAuthLookup struct {
context *Context
}
// Lookup uses a Docker config file to lookup authentication information
func (c *ConfigAuthLookup) Lookup(repoInfo *registry.RepositoryInfo) types.AuthConfig {
if c.context.ConfigFile == nil || repoInfo == nil || repoInfo.Index == nil {
return types.AuthConfig{}
}
return registry.ResolveAuthConfig(c.context.ConfigFile.AuthConfigs, repoInfo.Index)
}
// All uses a Docker config file to get all authentication information
func (c *ConfigAuthLookup) All() map[string]types.AuthConfig {
if c.context.ConfigFile == nil {
return map[string]types.AuthConfig{}
}
return c.context.ConfigFile.AuthConfigs
}

View File

@@ -0,0 +1,729 @@
package docker
import (
"fmt"
"io"
"math"
"os"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/container"
"github.com/docker/go-connections/nat"
"github.com/hyperhq/hypercli/pkg/promise"
"github.com/hyperhq/hypercli/pkg/stdcopy"
"github.com/hyperhq/hypercli/pkg/stringid"
"github.com/hyperhq/hypercli/pkg/term"
"github.com/hyperhq/libcompose/config"
"github.com/hyperhq/libcompose/labels"
"github.com/hyperhq/libcompose/logger"
"github.com/hyperhq/libcompose/project"
"github.com/hyperhq/libcompose/project/events"
util "github.com/hyperhq/libcompose/utils"
"golang.org/x/net/context"
)
// Container holds information about a docker container and the service it is tied on.
type Container struct {
name string
serviceName string
projectName string
containerNumber int
oneOff bool
eventNotifier events.Notifier
loggerFactory logger.Factory
client client.APIClient
// FIXME(vdemeester) Remove this dependency
service *Service
}
// NewContainer creates a container struct with the specified docker client, name and service.
func NewContainer(client client.APIClient, name string, containerNumber int, service *Service) *Container {
return &Container{
client: client,
name: name,
containerNumber: containerNumber,
// TODO(vdemeester) Move these to arguments
serviceName: service.name,
projectName: service.context.Project.Name,
eventNotifier: service.context.Project,
loggerFactory: service.context.LoggerFactory,
// TODO(vdemeester) Remove this dependency
service: service,
}
}
// NewOneOffContainer creates a "oneoff" container struct with the specified docker client, name and service.
func NewOneOffContainer(client client.APIClient, name string, containerNumber int, service *Service) *Container {
c := NewContainer(client, name, containerNumber, service)
c.oneOff = true
return c
}
func (c *Container) findExisting() (*types.ContainerJSON, error) {
return GetContainer(c.client, c.name)
}
// Info returns info about the container, like name, command, state or ports.
func (c *Container) Info(qFlag bool) (project.Info, error) {
container, err := c.findExisting()
if err != nil || container == nil {
return nil, err
}
infos, err := GetContainersByFilter(c.client, map[string][]string{
"name": {container.Name},
})
if err != nil || len(infos) == 0 {
return nil, err
}
info := infos[0]
result := project.Info{}
if qFlag {
result = append(result, project.InfoPart{Key: "Id", Value: container.ID})
} else {
result = append(result, project.InfoPart{Key: "Name", Value: name(info.Names)})
result = append(result, project.InfoPart{Key: "Command", Value: info.Command})
result = append(result, project.InfoPart{Key: "State", Value: info.Status})
result = append(result, project.InfoPart{Key: "Ports", Value: portString(info.Ports)})
}
return result, nil
}
func portString(ports []types.Port) string {
result := []string{}
for _, port := range ports {
if port.PublicPort > 0 {
result = append(result, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type))
} else {
result = append(result, fmt.Sprintf("%d/%s", port.PrivatePort, port.Type))
}
}
return strings.Join(result, ", ")
}
func name(names []string) string {
max := math.MaxInt32
var current string
for _, v := range names {
if len(v) < max {
max = len(v)
current = v
}
}
return current[1:]
}
// Recreate will not refresh the container by means of relaxation and enjoyment,
// just delete it and create a new one with the current configuration
func (c *Container) Recreate(imageName string) (*types.ContainerJSON, error) {
container, err := c.findExisting()
if err != nil || container == nil {
return nil, err
}
hash := container.Config.Labels[labels.HASH.Str()]
if hash == "" {
return nil, fmt.Errorf("Failed to find hash on old container: %s", container.Name)
}
name := container.Name[1:]
newName := fmt.Sprintf("%s-%s", name, container.ID[:12])
logrus.Debugf("Renaming %s => %s", name, newName)
if err := c.client.ContainerRename(context.Background(), container.ID, newName); err != nil {
logrus.Errorf("Failed to rename old container %s", c.name)
return nil, err
}
newContainer, err := c.createContainer(imageName, container.ID, nil)
if err != nil {
return nil, err
}
logrus.Debugf("Created replacement container %s", newContainer.ID)
if _, err := c.client.ContainerRemove(context.Background(), container.ID, types.ContainerRemoveOptions{
Force: true,
RemoveVolumes: false,
}); err != nil {
logrus.Errorf("Failed to remove old container %s", c.name)
return nil, err
}
logrus.Debugf("Removed old container %s %s", c.name, container.ID)
return newContainer, nil
}
// Create creates the container based on the specified image name and send an event
// to notify the container has been created. If the container already exists, does
// nothing.
func (c *Container) Create(imageName string) (*types.ContainerJSON, error) {
return c.CreateWithOverride(imageName, nil)
}
// CreateWithOverride create container and override parts of the config to
// allow special situations to override the config generated from the compose
// file
func (c *Container) CreateWithOverride(imageName string, configOverride *config.ServiceConfig) (*types.ContainerJSON, error) {
container, err := c.findExisting()
if err != nil {
return nil, err
}
if container == nil {
container, err = c.createContainer(imageName, "", configOverride)
if err != nil {
return nil, err
}
c.eventNotifier.Notify(events.ContainerCreated, c.serviceName, map[string]string{
"name": c.Name(),
})
}
return container, err
}
// Stop stops the container.
func (c *Container) Stop(timeout int) error {
return c.withContainer(func(container *types.ContainerJSON) error {
return c.client.ContainerStop(context.Background(), container.ID, timeout)
})
}
// Pause pauses the container. If the containers are already paused, don't fail.
func (c *Container) Pause() error {
return c.withContainer(func(container *types.ContainerJSON) error {
if !container.State.Paused {
return c.client.ContainerPause(context.Background(), container.ID)
}
return nil
})
}
// Unpause unpauses the container. If the containers are not paused, don't fail.
func (c *Container) Unpause() error {
return c.withContainer(func(container *types.ContainerJSON) error {
if container.State.Paused {
return c.client.ContainerUnpause(context.Background(), container.ID)
}
return nil
})
}
// Kill kill the container.
func (c *Container) Kill(signal string) error {
return c.withContainer(func(container *types.ContainerJSON) error {
return c.client.ContainerKill(context.Background(), container.ID, signal)
})
}
// Delete removes the container if existing. If the container is running, it tries
// to stop it first.
func (c *Container) Delete(removeVolume bool) error {
container, err := c.findExisting()
if err != nil || container == nil {
return err
}
info, err := c.client.ContainerInspect(context.Background(), container.ID)
if err != nil {
return err
}
if !info.State.Running {
_, err := c.client.ContainerRemove(context.Background(), container.ID, types.ContainerRemoveOptions{
Force: true,
RemoveVolumes: removeVolume,
})
return err
}
return nil
}
// IsRunning returns the running state of the container.
func (c *Container) IsRunning() (bool, error) {
container, err := c.findExisting()
if err != nil || container == nil {
return false, err
}
info, err := c.client.ContainerInspect(context.Background(), container.ID)
if err != nil {
return false, err
}
return info.State.Running, nil
}
// Run creates, start and attach to the container based on the image name,
// the specified configuration.
// It will always create a new container.
func (c *Container) Run(ctx context.Context, imageName string, configOverride *config.ServiceConfig) (int, error) {
var (
errCh chan error
out, stderr io.Writer
in io.ReadCloser
)
container, err := c.createContainer(imageName, "", configOverride)
if err != nil {
return -1, err
}
if configOverride.StdinOpen {
in = os.Stdin
}
if configOverride.Tty {
out = os.Stdout
}
if configOverride.Tty {
stderr = os.Stderr
}
options := types.ContainerAttachOptions{
Stream: true,
Stdin: configOverride.StdinOpen,
Stdout: configOverride.Tty,
Stderr: configOverride.Tty,
}
resp, err := c.client.ContainerAttach(ctx, container.ID, options)
if err != nil {
return -1, err
}
// set raw terminal
inFd, _ := term.GetFdInfo(in)
state, err := term.SetRawTerminal(inFd)
if err != nil {
return -1, err
}
// restore raw terminal
defer term.RestoreTerminal(inFd, state)
// holdHijackedConnection (in goroutine)
errCh = promise.Go(func() error {
return holdHijackedConnection(configOverride.Tty, in, out, stderr, resp)
})
if err := c.client.ContainerStart(ctx, container.ID, ""); err != nil {
return -1, err
}
if err := <-errCh; err != nil {
logrus.Debugf("Error hijack: %s", err)
return -1, err
}
var status int
// Attached mode
if c.service.context.Autoremove {
// Warn user if they detached us
js, err := c.client.ContainerInspect(ctx, container.ID)
if err != nil {
return -1, err
}
if js.State.Running == true || js.State.Paused == true {
logrus.Infof("Detached from %s, awaiting its termination in order to uphold \"--rm\".\n",
stringid.TruncateID(container.ID))
}
// Autoremove: wait for the container to finish, retrieve
// the exit code and remove the container
if status, err = c.client.ContainerWait(ctx, container.ID); err != nil {
return -1, err
}
exitedContainer, err := c.client.ContainerInspect(ctx, container.ID)
if err != nil {
return -1, err
}
status = exitedContainer.State.ExitCode
} else {
// No Autoremove: Simply retrieve the exit code
if !configOverride.Tty {
// In non-TTY mode, we can't detach, so we must wait for container exit
if status, err = c.client.ContainerWait(ctx, container.ID); err != nil {
return -1, err
}
} else {
// In TTY mode, there is a race: if the process dies too slowly, the state could
// be updated after the getExitCode call and result in the wrong exit code being reported
exitedContainer, err := c.client.ContainerInspect(ctx, container.ID)
if err != nil {
return -1, err
}
status = exitedContainer.State.ExitCode
}
}
return status, nil
}
func holdHijackedConnection(tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error {
var err error
receiveStdout := make(chan error, 1)
if outputStream != nil || errorStream != nil {
go func() {
// When TTY is ON, use regular copy
if tty && outputStream != nil {
_, err = io.Copy(outputStream, resp.Reader)
} else {
_, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader)
}
logrus.Debugf("[hijack] End of stdout")
receiveStdout <- err
}()
}
stdinDone := make(chan struct{})
go func() {
if inputStream != nil {
io.Copy(resp.Conn, inputStream)
logrus.Debugf("[hijack] End of stdin")
}
if err := resp.CloseWrite(); err != nil {
logrus.Debugf("Couldn't send EOF: %s", err)
}
close(stdinDone)
}()
select {
case err := <-receiveStdout:
if err != nil {
logrus.Debugf("Error receiveStdout: %s", err)
return err
}
case <-stdinDone:
if outputStream != nil || errorStream != nil {
if err := <-receiveStdout; err != nil {
logrus.Debugf("Error receiveStdout: %s", err)
return err
}
}
}
return nil
}
// Up creates and start the container based on the image name and send an event
// to notify the container has been created. If the container exists but is stopped
// it tries to start it.
func (c *Container) Up(imageName string) error {
var err error
container, err := c.Create(imageName)
if err != nil {
return err
}
if !container.State.Running {
c.Start(container)
}
return nil
}
// Start the specified container with the specified host config
func (c *Container) Start(container *types.ContainerJSON) error {
logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Starting container")
if err := c.client.ContainerStart(context.Background(), container.ID, ""); err != nil {
logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Failed to start container")
return err
}
c.eventNotifier.Notify(events.ContainerStarted, c.serviceName, map[string]string{
"name": c.Name(),
})
return nil
}
// OutOfSync checks if the container is out of sync with the service definition.
// It looks if the the service hash container label is the same as the computed one.
func (c *Container) OutOfSync(imageName string) (bool, error) {
container, err := c.findExisting()
if err != nil || container == nil {
return false, err
}
if container.Config.Image != imageName {
logrus.Debugf("Images for %s do not match %s!=%s", c.name, container.Config.Image, imageName)
return true, nil
}
if container.Config.Labels[labels.HASH.Str()] != c.getHash() {
logrus.Debugf("Hashes for %s do not match %s!=%s", c.name, container.Config.Labels[labels.HASH.Str()], c.getHash())
return true, nil
}
image, _, err := c.client.ImageInspectWithRaw(context.Background(), container.Config.Image, false)
if err != nil {
if client.IsErrImageNotFound(err) {
logrus.Debugf("Image %s do not exist, do not know if it's out of sync", container.Config.Image)
return false, nil
}
return false, err
}
logrus.Debugf("Checking existing image name vs id: %s == %s", image.ID, container.Image)
return image.ID != container.Image, err
}
func (c *Container) getHash() string {
return config.GetServiceHash(c.serviceName, c.service.Config())
}
func volumeBinds(volumes map[string]struct{}, container *types.ContainerJSON) []string {
result := make([]string, 0, len(container.Mounts))
for _, mount := range container.Mounts {
if _, ok := volumes[mount.Destination]; ok {
result = append(result, fmt.Sprint(mount.Source, ":", mount.Destination))
}
}
return result
}
func (c *Container) createContainer(imageName, oldContainer string, configOverride *config.ServiceConfig) (*types.ContainerJSON, error) {
serviceConfig := c.service.serviceConfig
if configOverride != nil {
serviceConfig.Command = configOverride.Command
serviceConfig.Tty = configOverride.Tty
serviceConfig.StdinOpen = configOverride.StdinOpen
}
configWrapper, err := ConvertToAPI(c.service)
if err != nil {
return nil, err
}
configWrapper.Config.Image = imageName
if configWrapper.Config.Labels == nil {
configWrapper.Config.Labels = map[string]string{}
}
oneOffString := "False"
if c.oneOff {
oneOffString = "True"
}
configWrapper.Config.Labels[labels.SERVICE.Str()] = c.serviceName
configWrapper.Config.Labels[labels.PROJECT.Str()] = c.projectName
configWrapper.Config.Labels[labels.HASH.Str()] = c.getHash()
configWrapper.Config.Labels[labels.ONEOFF.Str()] = oneOffString
configWrapper.Config.Labels[labels.NUMBER.Str()] = fmt.Sprint(c.containerNumber)
configWrapper.Config.Labels[labels.VERSION.Str()] = ComposeVersion
size := "s4"
if serviceConfig.Size != "" {
size = serviceConfig.Size
}
configWrapper.Config.Labels["sh_hyper_instancetype"] = size
err = c.populateAdditionalHostConfig(configWrapper.HostConfig)
if err != nil {
return nil, err
}
if oldContainer != "" {
info, err := c.client.ContainerInspect(context.Background(), oldContainer)
if err != nil {
return nil, err
}
configWrapper.HostConfig.Binds = util.Merge(configWrapper.HostConfig.Binds, volumeBinds(configWrapper.Config.Volumes, &info))
}
logrus.Debugf("Creating container %s %#v", c.name, configWrapper)
container, err := c.client.ContainerCreate(context.Background(), configWrapper.Config, configWrapper.HostConfig, configWrapper.NetworkingConfig, c.name)
if err != nil {
logrus.Debugf("Failed to create container %s: %v", c.name, err)
return nil, err
}
return GetContainer(c.client, container.ID)
}
func (c *Container) populateAdditionalHostConfig(hostConfig *container.HostConfig) error {
links := map[string]string{}
for _, link := range c.service.DependentServices() {
if !c.service.context.Project.ServiceConfigs.Has(link.Target) {
continue
}
service, err := c.service.context.Project.CreateService(link.Target)
if err != nil {
return err
}
containers, err := service.Containers()
if err != nil {
return err
}
if link.Type == project.RelTypeLink {
c.addLinks(links, service, link, containers)
} else if link.Type == project.RelTypeIpcNamespace {
hostConfig, err = c.addIpc(hostConfig, service, containers)
} else if link.Type == project.RelTypeNetNamespace {
hostConfig, err = c.addNetNs(hostConfig, service, containers)
}
if err != nil {
return err
}
}
hostConfig.Links = []string{}
for k, v := range links {
hostConfig.Links = append(hostConfig.Links, strings.Join([]string{v, k}, ":"))
}
for _, v := range c.service.Config().ExternalLinks {
hostConfig.Links = append(hostConfig.Links, v)
}
return nil
}
func (c *Container) addLinks(links map[string]string, service project.Service, rel project.ServiceRelationship, containers []project.Container) {
for _, container := range containers {
if _, ok := links[rel.Alias]; !ok {
links[rel.Alias] = container.Name()
}
links[container.Name()] = container.Name()
}
}
func (c *Container) addIpc(config *container.HostConfig, service project.Service, containers []project.Container) (*container.HostConfig, error) {
/*
if len(containers) == 0 {
return nil, fmt.Errorf("Failed to find container for IPC %v", c.service.Config().Ipc)
}
id, err := containers[0].ID()
if err != nil {
return nil, err
}
config.IpcMode = container.IpcMode("container:" + id)
*/
return config, nil
}
func (c *Container) addNetNs(config *container.HostConfig, service project.Service, containers []project.Container) (*container.HostConfig, error) {
/*
if len(containers) == 0 {
return nil, fmt.Errorf("Failed to find container for networks ns %v", c.service.Config().NetworkMode)
}
id, err := containers[0].ID()
if err != nil {
return nil, err
}
config.NetworkMode = container.NetworkMode("container:" + id)
*/
return config, nil
}
// ID returns the container Id.
func (c *Container) ID() (string, error) {
container, err := c.findExisting()
if container == nil {
return "", err
}
return container.ID, err
}
// Name returns the container name.
func (c *Container) Name() string {
return c.name
}
// Restart restarts the container if existing, does nothing otherwise.
func (c *Container) Restart(timeout int) error {
container, err := c.findExisting()
if err != nil || container == nil {
return err
}
return c.client.ContainerRestart(context.Background(), container.ID, timeout)
}
// Log forwards container logs to the project configured logger.
func (c *Container) Log(follow bool) error {
container, err := c.findExisting()
if container == nil || err != nil {
return err
}
info, err := c.client.ContainerInspect(context.Background(), container.ID)
if err != nil {
return err
}
// FIXME(vdemeester) update container struct to do less API calls
name := fmt.Sprintf("%s-%d", c.service.name, c.containerNumber)
l := c.loggerFactory.Create(name)
options := types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: follow,
Tail: "all",
}
responseBody, err := c.client.ContainerLogs(context.Background(), container.ID, options)
if err != nil {
return err
}
defer responseBody.Close()
if info.Config.Tty {
_, err = io.Copy(&logger.Wrapper{Logger: l}, responseBody)
} else {
_, err = stdcopy.StdCopy(&logger.Wrapper{Logger: l}, &logger.Wrapper{Logger: l, Err: true}, responseBody)
}
logrus.WithFields(logrus.Fields{"Logger": l, "err": err}).Debug("c.client.Logs() returned error")
return err
}
func (c *Container) withContainer(action func(*types.ContainerJSON) error) error {
container, err := c.findExisting()
if err != nil {
return err
}
if container != nil {
return action(container)
}
return nil
}
// Port returns the host port the specified port is mapped on.
func (c *Container) Port(port string) (string, error) {
container, err := c.findExisting()
if err != nil {
return "", err
}
if bindings, ok := container.NetworkSettings.Ports[nat.Port(port)]; ok {
result := []string{}
for _, binding := range bindings {
result = append(result, binding.HostIP+":"+binding.HostPort)
}
return strings.Join(result, "\n"), nil
}
return "", nil
}

36
vendor/github.com/hyperhq/libcompose/docker/context.go generated vendored Normal file
View File

@@ -0,0 +1,36 @@
package docker
import (
"github.com/hyperhq/hypercli/cliconfig"
"github.com/hyperhq/libcompose/project"
)
// Context holds context meta information about a libcompose project and docker
// client information (like configuration file, builder to use, …)
type Context struct {
project.Context
ClientFactory project.ClientFactory
ConfigDir string
ConfigFile *cliconfig.ConfigFile
AuthLookup AuthLookup
}
func (c *Context) open() error {
return c.LookupConfig()
}
// LookupConfig tries to load the docker configuration files, if any.
func (c *Context) LookupConfig() error {
if c.ConfigFile != nil {
return nil
}
config, err := cliconfig.Load(c.ConfigDir)
if err != nil {
return err
}
c.ConfigFile = config
return nil
}

257
vendor/github.com/hyperhq/libcompose/docker/convert.go generated vendored Normal file
View File

@@ -0,0 +1,257 @@
package docker
import (
"strings"
"github.com/docker/engine-api/types/container"
"github.com/docker/engine-api/types/network"
"github.com/docker/engine-api/types/strslice"
"github.com/hyperhq/libcompose/config"
"github.com/hyperhq/libcompose/project"
"github.com/hyperhq/libcompose/utils"
"github.com/hyperhq/hypercli/runconfig/opts"
)
// ConfigWrapper wraps Config, HostConfig and NetworkingConfig for a container.
type ConfigWrapper struct {
Config *container.Config
HostConfig *container.HostConfig
NetworkingConfig *network.NetworkingConfig
}
// Filter filters the specified string slice with the specified function.
func Filter(vs []string, f func(string) bool) []string {
r := make([]string, 0, len(vs))
for _, v := range vs {
if f(v) {
r = append(r, v)
}
}
return r
}
func isBind(s string) bool {
return strings.ContainsRune(s, ':')
}
func isVolume(s string) bool {
return !isBind(s)
}
// ConvertToAPI converts a service configuration to a docker API container configuration.
func ConvertToAPI(s *Service) (*ConfigWrapper, error) {
config, hostConfig, err := Convert(s.serviceConfig, s.context.Context)
if err != nil {
return nil, err
}
result := ConfigWrapper{
Config: config,
HostConfig: hostConfig,
NetworkingConfig: &network.NetworkingConfig{},
}
return &result, nil
}
func isNamedVolume(volume string) bool {
return !strings.HasPrefix(volume, ".") && !strings.HasPrefix(volume, "/") && !strings.HasPrefix(volume, "~")
}
func volumes(c *config.ServiceConfig, ctx project.Context) map[string]struct{} {
volumes := make(map[string]struct{}, len(c.Volumes))
for k, v := range c.Volumes {
if len(ctx.ComposeFiles) > 0 && !isNamedVolume(v) {
v = ctx.ResourceLookup.ResolvePath(v, ctx.ComposeFiles[0])
}
c.Volumes[k] = v
if isVolume(v) {
volumes[v] = struct{}{}
}
}
return volumes
}
func restartPolicy(c *config.ServiceConfig) (*container.RestartPolicy, error) {
restart, err := opts.ParseRestartPolicy(c.Restart)
if err != nil {
return nil, err
}
return &container.RestartPolicy{Name: restart.Name, MaximumRetryCount: restart.MaximumRetryCount}, nil
}
/*
func ports(c *config.ServiceConfig) (map[nat.Port]struct{}, nat.PortMap, error) {
ports, binding, err := nat.ParsePortSpecs(c.Ports)
if err != nil {
return nil, nil, err
}
exPorts, _, err := nat.ParsePortSpecs(c.Expose)
if err != nil {
return nil, nil, err
}
for k, v := range exPorts {
ports[k] = v
}
exposedPorts := map[nat.Port]struct{}{}
for k, v := range ports {
exposedPorts[nat.Port(k)] = v
}
portBindings := nat.PortMap{}
for k, bv := range binding {
dcbs := make([]nat.PortBinding, len(bv))
for k, v := range bv {
dcbs[k] = nat.PortBinding{HostIP: v.HostIP, HostPort: v.HostPort}
}
portBindings[nat.Port(k)] = dcbs
}
return exposedPorts, portBindings, nil
}
*/
// Convert converts a service configuration to an docker API structures (Config and HostConfig)
func Convert(c *config.ServiceConfig, ctx project.Context) (*container.Config, *container.HostConfig, error) {
restartPolicy, err := restartPolicy(c)
if err != nil {
return nil, nil, err
}
/*
exposedPorts, portBindings, err := ports(c)
if err != nil {
return nil, nil, err
}
deviceMappings, err := parseDevices(c.Devices)
if err != nil {
return nil, nil, err
}
var volumesFrom []string
if c.VolumesFrom != nil {
volumesFrom, err = getVolumesFrom(c.VolumesFrom, ctx.Project.ServiceConfigs, ctx.ProjectName)
if err != nil {
return nil, nil, err
}
}
*/
config := &container.Config{
Entrypoint: strslice.StrSlice(utils.CopySlice(c.Entrypoint)),
Hostname: c.Hostname,
Domainname: c.DomainName,
//User: c.User,
Env: utils.CopySlice(c.Environment),
Cmd: strslice.StrSlice(utils.CopySlice(c.Command)),
Image: c.Image,
Labels: utils.CopyMap(c.Labels),
//ExposedPorts: exposedPorts,
Tty: c.Tty,
OpenStdin: c.StdinOpen,
WorkingDir: c.WorkingDir,
Volumes: volumes(c, ctx),
//MacAddress: c.MacAddress,
}
/*
ulimits := []*units.Ulimit{}
if c.Ulimits.Elements != nil {
for _, ulimit := range c.Ulimits.Elements {
ulimits = append(ulimits, &units.Ulimit{
Name: ulimit.Name,
Soft: ulimit.Soft,
Hard: ulimit.Hard,
})
}
}
resources := container.Resources{
CgroupParent: c.CgroupParent,
Memory: c.MemLimit,
MemorySwap: c.MemSwapLimit,
CPUShares: c.CPUShares,
CPUQuota: c.CPUQuota,
CpusetCpus: c.CPUSet,
Ulimits: ulimits,
Devices: deviceMappings,
}
dns := strslice.StrSlice(c.DNS)
dnsSearch := strslice.StrSlice(c.DNSSearch)
*/
hostConfig := &container.HostConfig{
/*
VolumesFrom: volumesFrom,
CapAdd: strslice.New(c.CapAdd...),
CapDrop: strslice.New(c.CapDrop...),
ExtraHosts: utils.CopySlice(c.ExtraHosts),
Privileged: c.Privileged,
*/
Binds: Filter(c.Volumes, isBind),
/*
DNS: (&dns).Slice(),
DNSSearch: (&dnsSearch).Slice(),
LogConfig: container.LogConfig{
Type: c.Logging.Driver,
Config: utils.CopyMap(c.Logging.Options),
},
*/
NetworkMode: "bridge",
/*
ReadonlyRootfs: c.ReadOnly,
PidMode: container.PidMode(c.Pid),
UTSMode: container.UTSMode(c.Uts),
IpcMode: container.IpcMode(c.Ipc),
PortBindings: portBindings,
*/
RestartPolicy: *restartPolicy,
/*
SecurityOpt: utils.CopySlice(c.SecurityOpt),
VolumeDriver: c.VolumeDriver,
Resources: resources,
*/
}
return config, hostConfig, nil
}
/*
func getVolumesFrom(volumesFrom []string, serviceConfigs *config.ServiceConfigs, projectName string) ([]string, error) {
volumes := []string{}
for _, volumeFrom := range volumesFrom {
if serviceConfig, ok := serviceConfigs.Get(volumeFrom); ok {
// It's a service - Use the first one
name := fmt.Sprintf("%s_%s_1", projectName, volumeFrom)
// If a container name is specified, use that instead
if serviceConfig.ContainerName != "" {
name = serviceConfig.ContainerName
}
volumes = append(volumes, name)
} else {
volumes = append(volumes, volumeFrom)
}
}
return volumes, nil
}
func parseDevices(devices []string) ([]container.DeviceMapping, error) {
// parse device mappings
deviceMappings := []container.DeviceMapping{}
for _, device := range devices {
v, err := opts.ParseDevice(device)
if err != nil {
return nil, err
}
deviceMappings = append(deviceMappings, container.DeviceMapping{
PathOnHost: v.PathOnHost,
PathInContainer: v.PathInContainer,
CgroupPermissions: v.CgroupPermissions,
})
}
return deviceMappings, nil
}
*/

View File

@@ -0,0 +1,58 @@
package docker
import (
"path/filepath"
"testing"
shlex "github.com/flynn/go-shlex"
"github.com/hyperhq/libcompose/config"
"github.com/hyperhq/libcompose/lookup"
"github.com/hyperhq/libcompose/yaml"
"github.com/stretchr/testify/assert"
)
func TestParseCommand(t *testing.T) {
exp := []string{"sh", "-c", "exec /opt/bin/flanneld -logtostderr=true -iface=${NODE_IP}"}
cmd, err := shlex.Split("sh -c 'exec /opt/bin/flanneld -logtostderr=true -iface=${NODE_IP}'")
assert.Nil(t, err)
assert.Equal(t, exp, cmd)
}
func TestParseBindsAndVolumes(t *testing.T) {
ctx := &Context{}
ctx.ComposeFiles = []string{"foo/docker-compose.yml"}
ctx.ResourceLookup = &lookup.FileConfigLookup{}
abs, err := filepath.Abs(".")
assert.Nil(t, err)
cfg, hostCfg, err := Convert(&config.ServiceConfig{
Volumes: []string{"/foo", "/home:/home", "/bar/baz", ".:/home", "/usr/lib:/usr/lib:ro"},
}, ctx.Context)
assert.Nil(t, err)
assert.Equal(t, map[string]struct{}{"/foo": {}, "/bar/baz": {}}, cfg.Volumes)
assert.Equal(t, []string{"/home:/home", abs + "/foo:/home", "/usr/lib:/usr/lib:ro"}, hostCfg.Binds)
}
func TestParseLabels(t *testing.T) {
ctx := &Context{}
ctx.ComposeFiles = []string{"foo/docker-compose.yml"}
ctx.ResourceLookup = &lookup.FileConfigLookup{}
bashCmd := "bash"
fooLabel := "foo.label"
fooLabelValue := "service.config.value"
sc := &config.ServiceConfig{
Entrypoint: yaml.Command([]string{bashCmd}),
Labels: yaml.SliceorMap{fooLabel: "service.config.value"},
}
cfg, _, err := Convert(sc, ctx.Context)
assert.Nil(t, err)
cfg.Labels[fooLabel] = "FUN"
cfg.Entrypoint[0] = "less"
assert.Equal(t, fooLabelValue, sc.Labels[fooLabel])
assert.Equal(t, "FUN", cfg.Labels[fooLabel])
assert.Equal(t, yaml.Command{bashCmd}, sc.Entrypoint)
assert.Equal(t, []string{"less"}, []string(cfg.Entrypoint))
}

View File

@@ -0,0 +1,41 @@
package docker
import (
"github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/filters"
"golang.org/x/net/context"
)
// GetContainersByFilter looks up the hosts containers with the specified filters and
// returns a list of container matching it, or an error.
func GetContainersByFilter(clientInstance client.APIClient, containerFilters ...map[string][]string) ([]types.Container, error) {
filterArgs := filters.NewArgs()
// FIXME(vdemeester) I don't like 3 for loops >_<
for _, filter := range containerFilters {
for key, filterValue := range filter {
for _, value := range filterValue {
filterArgs.Add(key, value)
}
}
}
return clientInstance.ContainerList(context.Background(), types.ContainerListOptions{
All: true,
Filter: filterArgs,
})
}
// GetContainer looks up the hosts containers with the specified ID
// or name and returns it, or an error.
func GetContainer(clientInstance client.APIClient, id string) (*types.ContainerJSON, error) {
container, err := clientInstance.ContainerInspect(context.Background(), id)
if err != nil {
if client.IsErrContainerNotFound(err) {
return nil, nil
}
return nil, err
}
return &container, nil
}

80
vendor/github.com/hyperhq/libcompose/docker/image.go generated vendored Normal file
View File

@@ -0,0 +1,80 @@
package docker
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"golang.org/x/net/context"
"github.com/Sirupsen/logrus"
"github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
"github.com/hyperhq/hypercli/pkg/jsonmessage"
"github.com/hyperhq/hypercli/pkg/term"
"github.com/hyperhq/hypercli/reference"
"github.com/hyperhq/hypercli/registry"
)
func removeImage(client client.APIClient, image string) error {
_, err := client.ImageRemove(context.Background(), image, types.ImageRemoveOptions{})
return err
}
func pullImage(client client.APIClient, service *Service, image string) error {
fmt.Fprintf(os.Stderr, "Pulling %s (%s)...\n", service.name, image)
distributionRef, err := reference.ParseNamed(image)
if err != nil {
return err
}
repoInfo, err := registry.ParseRepositoryInfo(distributionRef)
if err != nil {
return err
}
authConfig := service.context.AuthLookup.Lookup(repoInfo)
encodedAuth, err := encodeAuthToBase64(authConfig)
if err != nil {
return err
}
options := types.ImagePullOptions{
RegistryAuth: encodedAuth,
}
responseBody, err := client.ImagePull(context.Background(), distributionRef.String(), options)
if err != nil {
logrus.Errorf("Failed to pull image %s: %v", image, err)
return err
}
defer responseBody.Close()
var writeBuff io.Writer = os.Stderr
outFd, isTerminalOut := term.GetFdInfo(os.Stderr)
err = jsonmessage.DisplayJSONMessagesStream(responseBody, writeBuff, outFd, isTerminalOut, nil)
if err != nil {
if jerr, ok := err.(*jsonmessage.JSONError); ok {
// If no error code is set, default to 1
if jerr.Code == 0 {
jerr.Code = 1
}
fmt.Fprintf(os.Stderr, "%s", writeBuff)
return fmt.Errorf("Status: %s, Code: %d", jerr.Message, jerr.Code)
}
}
return err
}
// encodeAuthToBase64 serializes the auth configuration as JSON base64 payload
func encodeAuthToBase64(authConfig types.AuthConfig) (string, error) {
buf, err := json.Marshal(authConfig)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(buf), nil
}

92
vendor/github.com/hyperhq/libcompose/docker/name.go generated vendored Normal file
View File

@@ -0,0 +1,92 @@
package docker
import (
"fmt"
"strconv"
"golang.org/x/net/context"
"github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/filters"
"github.com/hyperhq/libcompose/labels"
)
const format = "%s-%s-%d"
// Namer defines method to provide container name.
type Namer interface {
Next() (string, int)
}
type defaultNamer struct {
project string
service string
oneOff bool
currentNumber int
}
type singleNamer struct {
name string
}
// NewSingleNamer returns a namer that only allows a single name.
func NewSingleNamer(name string) Namer {
return &singleNamer{name}
}
// NewNamer returns a namer that returns names based on the specified project and
// service name and an inner counter, e.g. project_service_1, project_service_2…
func NewNamer(client client.APIClient, project, service string, oneOff bool) (Namer, error) {
namer := &defaultNamer{
project: project,
service: service,
oneOff: oneOff,
}
filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", labels.PROJECT.Str(), project))
filter.Add("label", fmt.Sprintf("%s=%s", labels.SERVICE.Str(), service))
if oneOff {
filter.Add("label", fmt.Sprintf("%s=%s", labels.ONEOFF.Str(), "True"))
} else {
filter.Add("label", fmt.Sprintf("%s=%s", labels.ONEOFF.Str(), "False"))
}
containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{
All: true,
Filter: filter,
})
if err != nil {
return nil, err
}
maxNumber := 0
for _, container := range containers {
number, err := strconv.Atoi(container.Labels[labels.NUMBER.Str()])
if err != nil {
return nil, err
}
if number > maxNumber {
maxNumber = number
}
}
namer.currentNumber = maxNumber + 1
return namer, nil
}
func (i *defaultNamer) Next() (string, int) {
service := i.service
if i.oneOff {
service = i.service + "-run"
}
name := fmt.Sprintf(format, i.project, service, i.currentNumber)
number := i.currentNumber
i.currentNumber = i.currentNumber + 1
return name, number
}
func (s *singleNamer) Next() (string, int) {
return s.name, 1
}

View File

@@ -0,0 +1,177 @@
package docker
import (
"fmt"
"testing"
"github.com/docker/engine-api/types"
"github.com/hyperhq/libcompose/labels"
"github.com/hyperhq/libcompose/test"
"golang.org/x/net/context"
)
func TestSingleNamer(t *testing.T) {
expectedName := "myName"
expectedNumber := 1
namer := NewSingleNamer("myName")
for i := 0; i < 10; i++ {
name, number := namer.Next()
if name != expectedName {
t.Fatalf("expected %s, got %s", expectedName, name)
}
if number != expectedNumber {
t.Fatalf("expected %d, got %d", expectedNumber, number)
}
}
}
type NamerClient struct {
test.NopClient
expectedLabelFilters []string
containers []types.Container
}
func (client *NamerClient) ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) {
if len(client.expectedLabelFilters) > 1 {
labelFilters := options.Filter.Get("label")
if len(labelFilters) != len(client.expectedLabelFilters) {
return []types.Container{}, fmt.Errorf("expected filters %v, got %v", client.expectedLabelFilters, labelFilters)
}
for _, expectedLabelFilter := range client.expectedLabelFilters {
found := false
for _, labelFilter := range labelFilters {
if labelFilter == expectedLabelFilter {
found = true
break
}
}
if !found {
return []types.Container{}, fmt.Errorf("expected to find filter %s, did not in %v", expectedLabelFilter, labelFilters)
}
}
}
return client.containers, nil
}
func TestDefaultNamerClientError(t *testing.T) {
client := test.NewNopClient()
_, err := NewNamer(client, "project", "service", false)
if err == nil || err.Error() != "Engine no longer exists" {
t.Fatalf("expected an error 'Engine no longer exists', got %s", err)
}
}
func TestDefaultNamerLabelNotANumber(t *testing.T) {
client := &NamerClient{
containers: []types.Container{
{
Labels: map[string]string{
labels.ONEOFF.Str(): "IAmAString",
},
},
},
}
_, err := NewNamer(client, "project", "service", false)
if err == nil {
t.Fatal("expected an error, got nothing")
}
}
func TestDefaultNamer(t *testing.T) {
cases := []struct {
projectName string
serviceName string
oneOff bool
containers []types.Container
expectedLabels []string
expectedName string
expectedNumber int
}{
{
projectName: "",
serviceName: "",
oneOff: false,
containers: []types.Container{},
expectedLabels: []string{
fmt.Sprintf("%s=", labels.PROJECT.Str()),
fmt.Sprintf("%s=", labels.SERVICE.Str()),
fmt.Sprintf("%s=False", labels.ONEOFF.Str()),
},
expectedName: "--1",
expectedNumber: 1,
},
{
projectName: "project",
serviceName: "service",
oneOff: false,
containers: []types.Container{},
expectedLabels: []string{
fmt.Sprintf("%s=project", labels.PROJECT.Str()),
fmt.Sprintf("%s=service", labels.SERVICE.Str()),
fmt.Sprintf("%s=False", labels.ONEOFF.Str()),
},
expectedName: "project-service-1",
expectedNumber: 1,
},
{
projectName: "project",
serviceName: "service",
oneOff: false,
containers: []types.Container{
{
Labels: map[string]string{
labels.NUMBER.Str(): "1",
},
},
},
expectedLabels: []string{
fmt.Sprintf("%s=project", labels.PROJECT.Str()),
fmt.Sprintf("%s=service", labels.SERVICE.Str()),
fmt.Sprintf("%s=False", labels.ONEOFF.Str()),
},
expectedName: "project-service-2",
expectedNumber: 2,
},
{
projectName: "project",
serviceName: "anotherservice",
oneOff: false,
containers: []types.Container{
{
Labels: map[string]string{
labels.NUMBER.Str(): "10",
},
},
},
expectedLabels: []string{
fmt.Sprintf("%s=project", labels.PROJECT.Str()),
fmt.Sprintf("%s=anotherservice", labels.SERVICE.Str()),
fmt.Sprintf("%s=False", labels.ONEOFF.Str()),
},
expectedName: "project-anotherservice-11",
expectedNumber: 11,
},
}
for _, c := range cases {
client := &NamerClient{
expectedLabelFilters: c.expectedLabels,
containers: c.containers,
}
namer, err := NewNamer(client, c.projectName, c.serviceName, c.oneOff)
if err != nil {
t.Error(err)
}
name, number := namer.Next()
if name != c.expectedName {
t.Errorf("Expected %s, got %s for %v", c.expectedName, name, c)
}
if number != c.expectedNumber {
t.Errorf("Expected %d, got %d for %v", c.expectedNumber, number, c)
}
_, number = namer.Next()
if number != c.expectedNumber+1 {
t.Errorf("Expected a 2nd call to increment numbre to %d, got %d for %v", c.expectedNumber+1, number, c)
}
}
}

66
vendor/github.com/hyperhq/libcompose/docker/project.go generated vendored Normal file
View File

@@ -0,0 +1,66 @@
package docker
import (
"fmt"
"os"
"path/filepath"
"github.com/Sirupsen/logrus"
"github.com/hyperhq/libcompose/config"
"github.com/hyperhq/libcompose/lookup"
"github.com/hyperhq/libcompose/project"
)
// ComposeVersion is name of docker-compose.yml file syntax supported version
const ComposeVersion = "1.5.0"
// NewProject creates a Project with the specified context.
func NewProject(context *Context) (project.APIProject, error) {
if context.ResourceLookup == nil {
context.ResourceLookup = &lookup.FileConfigLookup{}
}
if context.EnvironmentLookup == nil {
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
context.EnvironmentLookup = &lookup.ComposableEnvLookup{
Lookups: []config.EnvironmentLookup{
&lookup.EnvfileLookup{
Path: filepath.Join(cwd, ".env"),
},
&lookup.OsEnvLookup{},
},
}
}
if context.AuthLookup == nil {
context.AuthLookup = &ConfigAuthLookup{context}
}
if context.ServiceFactory == nil {
context.ServiceFactory = &ServiceFactory{
context: context,
}
}
if context.ClientFactory == nil {
return nil, fmt.Errorf("please provide the client to operate the Hyper.sh")
}
// FIXME(vdemeester) Remove the context duplication ?
p := project.NewProject(context.ClientFactory, &context.Context)
err := p.Parse()
if err != nil {
return nil, err
}
if err = context.open(); err != nil {
logrus.Errorf("Failed to open project %s: %v", p.Name, err)
return nil, err
}
return p, err
}

480
vendor/github.com/hyperhq/libcompose/docker/service.go generated vendored Normal file
View File

@@ -0,0 +1,480 @@
package docker
import (
"fmt"
"strconv"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/engine-api/client"
"github.com/docker/go-connections/nat"
"github.com/hyperhq/libcompose/config"
"github.com/hyperhq/libcompose/labels"
"github.com/hyperhq/libcompose/project"
"github.com/hyperhq/libcompose/project/options"
"github.com/hyperhq/libcompose/utils"
"golang.org/x/net/context"
)
// Service is a project.Service implementations.
type Service struct {
name string
serviceConfig *config.ServiceConfig
context *Context
}
// NewService creates a service
func NewService(name string, serviceConfig *config.ServiceConfig, context *Context) *Service {
return &Service{
name: name,
serviceConfig: serviceConfig,
context: context,
}
}
// Name returns the service name.
func (s *Service) Name() string {
return s.name
}
// Config returns the configuration of the service (config.ServiceConfig).
func (s *Service) Config() *config.ServiceConfig {
return s.serviceConfig
}
// DependentServices returns the dependent services (as an array of ServiceRelationship) of the service.
func (s *Service) DependentServices() []project.ServiceRelationship {
return project.DefaultDependentServices(s.context.Project, s)
}
// Create implements Service.Create. It ensures the image exists or build it
// if it can and then create a container.
func (s *Service) Create(options options.Create) error {
containers, err := s.collectContainers()
if err != nil {
return err
}
imageName, err := s.ensureImageExists(options.NoBuild)
if err != nil {
return err
}
if len(containers) != 0 {
return s.eachContainer(func(c *Container) error {
return s.recreateIfNeeded(imageName, c, options.NoRecreate, options.ForceRecreate)
})
}
_, err = s.createOne(imageName)
return err
}
func (s *Service) collectContainers() ([]*Container, error) {
client := s.context.ClientFactory.Create(s)
containers, err := GetContainersByFilter(client, labels.SERVICE.Eq(s.name), labels.PROJECT.Eq(s.context.Project.Name))
if err != nil {
return nil, err
}
result := []*Container{}
for _, container := range containers {
containerNumber, err := strconv.Atoi(container.Labels[labels.NUMBER.Str()])
if err != nil {
return nil, err
}
// Compose add "/" before name, so Name[1] will store actaul name.
name := strings.SplitAfter(container.Names[0], "/")
result = append(result, NewContainer(client, name[1], containerNumber, s))
}
return result, nil
}
func (s *Service) createOne(imageName string) (*Container, error) {
containers, err := s.constructContainers(imageName, 1)
if err != nil {
return nil, err
}
return containers[0], err
}
func (s *Service) ensureImageExists(noBuild bool) (string, error) {
err := s.imageExists()
if err == nil {
return s.imageName(), nil
}
if err != nil && !client.IsErrImageNotFound(err) {
return "", err
}
/*
if s.Config().Build.Context != "" {
if noBuild {
return "", fmt.Errorf("Service %q needs to be built, but no-build was specified", s.name)
}
return s.imageName(), s.build(options.Build{})
}
*/
return s.imageName(), s.Pull()
}
func (s *Service) imageExists() error {
client := s.context.ClientFactory.Create(s)
_, _, err := client.ImageInspectWithRaw(context.Background(), s.imageName(), false)
return err
}
func (s *Service) imageName() string {
if s.Config().Image != "" {
return s.Config().Image
}
return fmt.Sprintf("%s_%s", s.context.ProjectName, s.Name())
}
// Build implements Service.Build. If an imageName is specified or if the context has
// no build to work with it will do nothing. Otherwise it will try to build
// the image and returns an error if any.
func (s *Service) Build(buildOptions options.Build) error {
if s.Config().Image != "" {
return nil
}
return s.build(buildOptions)
}
func (s *Service) build(buildOptions options.Build) error {
return nil
}
func (s *Service) constructContainers(imageName string, count int) ([]*Container, error) {
result, err := s.collectContainers()
if err != nil {
return nil, err
}
client := s.context.ClientFactory.Create(s)
var namer Namer
if s.serviceConfig.ContainerName != "" {
if count > 1 {
logrus.Warnf(`The "%s" service is using the custom container name "%s". Docker requires each container to have a unique name. Remove the custom name to scale the service.`, s.name, s.serviceConfig.ContainerName)
}
namer = NewSingleNamer(s.serviceConfig.ContainerName)
} else {
namer, err = NewNamer(client, s.context.Project.Name, s.name, false)
if err != nil {
return nil, err
}
}
for i := len(result); i < count; i++ {
containerName, containerNumber := namer.Next()
c := NewContainer(client, containerName, containerNumber, s)
dockerContainer, err := c.Create(imageName)
if err != nil {
return nil, err
}
logrus.Debugf("Created container %s: %v", dockerContainer.ID, dockerContainer.Name)
result = append(result, NewContainer(client, containerName, containerNumber, s))
}
return result, nil
}
// Up implements Service.Up. It builds the image if needed, creates a container
// and start it.
func (s *Service) Up(options options.Up) error {
containers, err := s.collectContainers()
if err != nil {
return err
}
var imageName = s.imageName()
if len(containers) == 0 || !options.NoRecreate {
imageName, err = s.ensureImageExists(options.NoBuild)
if err != nil {
return err
}
}
return s.up(imageName, true, options)
}
// Run implements Service.Run. It runs a one of command within the service container.
func (s *Service) Run(ctx context.Context, commandParts []string) (int, error) {
imageName, err := s.ensureImageExists(false)
if err != nil {
return -1, err
}
client := s.context.ClientFactory.Create(s)
namer, err := NewNamer(client, s.context.Project.Name, s.name, true)
if err != nil {
return -1, err
}
containerName, containerNumber := namer.Next()
c := NewOneOffContainer(client, containerName, containerNumber, s)
return c.Run(ctx, imageName, &config.ServiceConfig{Command: commandParts, Tty: true, StdinOpen: true})
}
// Info implements Service.Info. It returns an project.InfoSet with the containers
// related to this service (can be multiple if using the scale command).
func (s *Service) Info(qFlag bool) (project.InfoSet, error) {
result := project.InfoSet{}
containers, err := s.collectContainers()
if err != nil {
return nil, err
}
for _, c := range containers {
info, err := c.Info(qFlag)
if err != nil {
return nil, err
}
result = append(result, info)
}
return result, nil
}
// Start implements Service.Start. It tries to start a container without creating it.
func (s *Service) Start() error {
return s.up("", false, options.Up{})
}
func (s *Service) up(imageName string, create bool, options options.Up) error {
containers, err := s.collectContainers()
if err != nil {
return err
}
logrus.Debugf("Found %d existing containers for service %s", len(containers), s.name)
if len(containers) == 0 && create {
c, err := s.createOne(imageName)
if err != nil {
return err
}
containers = []*Container{c}
}
return s.eachContainer(func(c *Container) error {
if create {
if err := s.recreateIfNeeded(imageName, c, options.NoRecreate, options.ForceRecreate); err != nil {
return err
}
}
return c.Up(imageName)
})
}
func (s *Service) recreateIfNeeded(imageName string, c *Container, noRecreate, forceRecreate bool) error {
if noRecreate {
return nil
}
outOfSync, err := c.OutOfSync(imageName)
if err != nil {
return err
}
logrus.WithFields(logrus.Fields{
"outOfSync": outOfSync,
"ForceRecreate": forceRecreate,
"NoRecreate": noRecreate}).Debug("Going to decide if recreate is needed")
if forceRecreate || outOfSync {
logrus.Infof("Recreating %s", s.name)
if _, err := c.Recreate(imageName); err != nil {
return err
}
}
return nil
}
func (s *Service) eachContainer(action func(*Container) error) error {
containers, err := s.collectContainers()
if err != nil {
return err
}
tasks := utils.InParallel{}
for _, container := range containers {
task := func(container *Container) func() error {
return func() error {
return action(container)
}
}(container)
tasks.Add(task)
}
return tasks.Wait()
}
// Stop implements Service.Stop. It stops any containers related to the service.
func (s *Service) Stop(timeout int) error {
return s.eachContainer(func(c *Container) error {
return c.Stop(timeout)
})
}
// Restart implements Service.Restart. It restarts any containers related to the service.
func (s *Service) Restart(timeout int) error {
return s.eachContainer(func(c *Container) error {
return c.Restart(timeout)
})
}
// Kill implements Service.Kill. It kills any containers related to the service.
func (s *Service) Kill(signal string) error {
return s.eachContainer(func(c *Container) error {
return c.Kill(signal)
})
}
// Delete implements Service.Delete. It removes any containers related to the service.
func (s *Service) Delete(options options.Delete) error {
return s.eachContainer(func(c *Container) error {
return c.Delete(options.RemoveVolume)
})
}
// Log implements Service.Log. It returns the docker logs for each container related to the service.
func (s *Service) Log(follow bool) error {
return s.eachContainer(func(c *Container) error {
return c.Log(follow)
})
}
// Scale implements Service.Scale. It creates or removes containers to have the specified number
// of related container to the service to run.
func (s *Service) Scale(scale int, timeout int) error {
if s.specificiesHostPort() {
logrus.Warnf("The \"%s\" service specifies a port on the host. If multiple containers for this service are created on a single host, the port will clash.", s.Name())
}
foundCount := 0
err := s.eachContainer(func(c *Container) error {
foundCount++
if foundCount > scale {
err := c.Stop(timeout)
if err != nil {
return err
}
// FIXME(vdemeester) remove volume in scale by default ?
return c.Delete(false)
}
return nil
})
if err != nil {
return err
}
if foundCount != scale {
imageName, err := s.ensureImageExists(false)
if err != nil {
return err
}
if _, err = s.constructContainers(imageName, scale); err != nil {
return err
}
}
return s.up("", false, options.Up{})
}
// Pull implements Service.Pull. It pulls the image of the service and skip the service that
// would need to be built.
func (s *Service) Pull() error {
if s.Config().Image == "" {
return nil
}
return pullImage(s.context.ClientFactory.Create(s), s, s.Config().Image)
}
// Pause implements Service.Pause. It puts into pause the container(s) related
// to the service.
func (s *Service) Pause() error {
return s.eachContainer(func(c *Container) error {
return c.Pause()
})
}
// Unpause implements Service.Pause. It brings back from pause the container(s)
// related to the service.
func (s *Service) Unpause() error {
return s.eachContainer(func(c *Container) error {
return c.Unpause()
})
}
// RemoveImage implements Service.RemoveImage. It removes images used for the service
// depending on the specified type.
func (s *Service) RemoveImage(imageType options.ImageType) error {
switch imageType {
case "local":
if s.Config().Image != "" {
return nil
}
return removeImage(s.context.ClientFactory.Create(s), s.imageName())
case "all":
return removeImage(s.context.ClientFactory.Create(s), s.imageName())
default:
// Don't do a thing, should be validated up-front
return nil
}
}
// Containers implements Service.Containers. It returns the list of containers
// that are related to the service.
func (s *Service) Containers() ([]project.Container, error) {
result := []project.Container{}
containers, err := s.collectContainers()
if err != nil {
return nil, err
}
for _, c := range containers {
result = append(result, c)
}
return result, nil
}
func (s *Service) specificiesHostPort() bool {
_, bindings, err := nat.ParsePortSpecs(s.Config().Ports)
if err != nil {
fmt.Println(err)
}
for _, portBindings := range bindings {
for _, portBinding := range portBindings {
if portBinding.HostPort != "" {
return true
}
}
}
return false
}

View File

@@ -0,0 +1,16 @@
package docker
import (
"github.com/hyperhq/libcompose/config"
"github.com/hyperhq/libcompose/project"
)
// ServiceFactory is an implementation of project.ServiceFactory.
type ServiceFactory struct {
context *Context
}
// Create creates a Service based on the specified project, name and service configuration.
func (s *ServiceFactory) Create(project *project.Project, name string, serviceConfig *config.ServiceConfig) (project.Service, error) {
return NewService(name, serviceConfig, s.context), nil
}

View File

@@ -0,0 +1,23 @@
package docker
/*
func TestSpecifiesHostPort(t *testing.T) {
servicesWithHostPort := []Service{
{serviceConfig: &config.ServiceConfig{Ports: []string{"8000:8000"}}},
{serviceConfig: &config.ServiceConfig{Ports: []string{"127.0.0.1:8000:8000"}}},
}
for _, service := range servicesWithHostPort {
assert.True(t, service.specificiesHostPort())
}
servicesWithoutHostPort := []Service{
{serviceConfig: &config.ServiceConfig{Ports: []string{"8000"}}},
{serviceConfig: &config.ServiceConfig{Ports: []string{"127.0.0.1::8000"}}},
}
for _, service := range servicesWithoutHostPort {
assert.False(t, service.specificiesHostPort())
}
}
*/

94
vendor/github.com/hyperhq/libcompose/labels/labels.go generated vendored Normal file
View File

@@ -0,0 +1,94 @@
package labels
import (
"encoding/json"
"fmt"
"github.com/hyperhq/libcompose/utils"
)
// Label represents a docker label.
type Label string
// Libcompose default labels.
const (
NUMBER = Label("sh.hyper.compose.container-number")
ONEOFF = Label("sh.hyper.compose.oneoff")
PROJECT = Label("sh.hyper.compose.project")
SERVICE = Label("sh.hyper.compose.service")
HASH = Label("sh.hyper.compose.config-hash")
VERSION = Label("sh.hyper.compose.version")
)
// EqString returns a label json string representation with the specified value.
func (f Label) EqString(value string) string {
return LabelFilterString(string(f), value)
}
// Eq returns a label map representation with the specified value.
func (f Label) Eq(value string) map[string][]string {
return LabelFilter(string(f), value)
}
// AndString returns a json list of labels by merging the two specified values (left and right) serialized as string.
func AndString(left, right string) string {
leftMap := map[string][]string{}
rightMap := map[string][]string{}
// Ignore errors
json.Unmarshal([]byte(left), &leftMap)
json.Unmarshal([]byte(right), &rightMap)
for k, v := range rightMap {
existing, ok := leftMap[k]
if ok {
leftMap[k] = append(existing, v...)
} else {
leftMap[k] = v
}
}
result, _ := json.Marshal(leftMap)
return string(result)
}
// And returns a map of labels by merging the two specified values (left and right).
func And(left, right map[string][]string) map[string][]string {
result := map[string][]string{}
for k, v := range left {
result[k] = v
}
for k, v := range right {
existing, ok := result[k]
if ok {
result[k] = append(existing, v...)
} else {
result[k] = v
}
}
return result
}
// Str returns the label name.
func (f Label) Str() string {
return string(f)
}
// LabelFilterString returns a label json string representation of the specifed couple (key,value)
// that is used as filter for docker.
func LabelFilterString(key, value string) string {
return utils.FilterString(map[string][]string{
"label": {fmt.Sprintf("%s=%s", key, value)},
})
}
// LabelFilter returns a label map representation of the specifed couple (key,value)
// that is used as filter for docker.
func LabelFilter(key, value string) map[string][]string {
return map[string][]string{
"label": {fmt.Sprintf("%s=%s", key, value)},
}
}

View File

@@ -0,0 +1,50 @@
package labels
import (
"testing"
)
func TestLabelEq(t *testing.T) {
label := Label("labelName")
m := label.Eq("value")
values, ok := m["label"]
if !ok {
t.Fatalf("expected a label key, got %v", m)
}
if len(values) != 1 {
t.Fatalf("expected only one value, got %v", values)
}
if values[0] != "labelName=value" {
t.Fatalf("expected 'labelName=value', got %s", values)
}
}
func TestLabelEqString(t *testing.T) {
label := Label("labelName")
value := label.EqString("value")
if value != `{"label":["labelName=value"]}` {
t.Fatalf("expected '{labelName=value}', got %s", value)
}
}
func TestLabelFilter(t *testing.T) {
filters := []struct {
key string
value string
expected string
}{
{
"key", "value", `{"label":["key=value"]}`,
}, {
"key", "", `{"label":["key="]}`,
}, {
"", "", `{"label":["="]}`,
},
}
for _, filter := range filters {
actual := LabelFilterString(filter.key, filter.value)
if actual != filter.expected {
t.Fatalf("Expected '%s for key=%s and value=%s, got %s", filter.expected, filter.key, filter.value, actual)
}
}
}

34
vendor/github.com/hyperhq/libcompose/logger/color.go generated vendored Normal file
View File

@@ -0,0 +1,34 @@
package logger
import "fmt"
var (
colorPrefix = make(chan string)
)
func generateColors() {
i := 0
colorOrder := []string{
"36", // cyan
"33", // yellow
"32", // green
"35", // magenta
"31", // red
"34", // blue
"36;1", // intense cyan
"33;1", // intense yellow
"32;1", // intense green
"35;1", // intense magenta
"31;1", // intense red
"34;1", // intense blue
}
for {
colorPrefix <- fmt.Sprintf("\033[%sm%%s |\033[0m", colorOrder[i])
i = (i + 1) % len(colorOrder)
}
}
func init() {
go generateColors()
}

View File

@@ -0,0 +1,75 @@
package logger
import (
"fmt"
"os"
"strconv"
"golang.org/x/crypto/ssh/terminal"
)
// ColorLoggerFactory implements logger.Factory interface using ColorLogger.
type ColorLoggerFactory struct {
maxLength int
tty bool
}
// ColorLogger implements logger.Logger interface with color support.
type ColorLogger struct {
name string
colorPrefix string
factory *ColorLoggerFactory
}
// NewColorLoggerFactory creates a new ColorLoggerFactory.
func NewColorLoggerFactory() *ColorLoggerFactory {
return &ColorLoggerFactory{
tty: terminal.IsTerminal(int(os.Stdout.Fd())),
}
}
// Create implements logger.Factory.Create.
func (c *ColorLoggerFactory) Create(name string) Logger {
if c.maxLength < len(name) {
c.maxLength = len(name)
}
return &ColorLogger{
name: name,
factory: c,
colorPrefix: <-colorPrefix,
}
}
// Out implements logger.Logger.Out.
func (c *ColorLogger) Out(bytes []byte) {
if len(bytes) == 0 {
return
}
logFmt, name := c.getLogFmt()
message := fmt.Sprintf(logFmt, name, string(bytes))
fmt.Print(message)
}
// Err implements logger.Logger.Err.
func (c *ColorLogger) Err(bytes []byte) {
if len(bytes) == 0 {
return
}
logFmt, name := c.getLogFmt()
message := fmt.Sprintf(logFmt, name, string(bytes))
fmt.Fprint(os.Stderr, message)
}
func (c *ColorLogger) getLogFmt() (string, string) {
pad := c.factory.maxLength
logFmt := "%s | %s"
if c.factory.tty {
logFmt = c.colorPrefix + " %s"
}
name := fmt.Sprintf("%-"+strconv.Itoa(pad)+"s", c.name)
return logFmt, name
}

18
vendor/github.com/hyperhq/libcompose/logger/null.go generated vendored Normal file
View File

@@ -0,0 +1,18 @@
package logger
// NullLogger is a logger.Logger and logger.Factory implementation that does nothing.
type NullLogger struct {
}
// Out is a no-op function.
func (n *NullLogger) Out(_ []byte) {
}
// Err is a no-op function.
func (n *NullLogger) Err(_ []byte) {
}
// Create implements logger.Factory and returns a NullLogger.
func (n *NullLogger) Create(_ string) Logger {
return &NullLogger{}
}

29
vendor/github.com/hyperhq/libcompose/logger/types.go generated vendored Normal file
View File

@@ -0,0 +1,29 @@
package logger
// Factory defines methods a factory should implement, to create a Logger
// based on the specified name.
type Factory interface {
Create(name string) Logger
}
// Logger defines methods to implement for being a logger.
type Logger interface {
Out(bytes []byte)
Err(bytes []byte)
}
// Wrapper is a wrapper around Logger that implements the Writer interface,
// mainly use by docker/pkg/stdcopy functions.
type Wrapper struct {
Err bool
Logger Logger
}
func (l *Wrapper) Write(bytes []byte) (int, error) {
if l.Err {
l.Logger.Err(bytes)
} else {
l.Logger.Out(bytes)
}
return len(bytes), nil
}

View File

@@ -0,0 +1,25 @@
package lookup
import (
"github.com/hyperhq/libcompose/config"
)
// ComposableEnvLookup is a structure that implements the project.EnvironmentLookup interface.
// It holds an ordered list of EnvironmentLookup to call to look for the environment value.
type ComposableEnvLookup struct {
Lookups []config.EnvironmentLookup
}
// Lookup creates a string slice of string containing a "docker-friendly" environment string
// in the form of 'key=value'. It loop through the lookups and returns the latest value if
// more than one lookup return a result.
func (l *ComposableEnvLookup) Lookup(key, serviceName string, config *config.ServiceConfig) []string {
result := []string{}
for _, lookup := range l.Lookups {
env := lookup.Lookup(key, serviceName, config)
if len(env) == 1 {
result = env
}
}
return result
}

View File

@@ -0,0 +1,47 @@
package lookup
import (
"testing"
"github.com/hyperhq/libcompose/config"
)
type simpleEnvLookup struct {
value []string
}
func (l *simpleEnvLookup) Lookup(key, serviceName string, config *config.ServiceConfig) []string {
return l.value
}
func TestComposableLookupWithoutAnyLookup(t *testing.T) {
envLookup := &ComposableEnvLookup{}
actuals := envLookup.Lookup("any", "", nil)
if len(actuals) != 0 {
t.Fatalf("expected an empty slice, got %v", actuals)
}
}
func TestComposableLookupReturnsTheLastValue(t *testing.T) {
envLookup1 := &simpleEnvLookup{
value: []string{"value=1"},
}
envLookup2 := &simpleEnvLookup{
value: []string{"value=2"},
}
envLookup := &ComposableEnvLookup{
[]config.EnvironmentLookup{
envLookup1,
envLookup2,
},
}
validateLookup(t, "value=2", envLookup.Lookup("value", "", nil))
envLookup = &ComposableEnvLookup{
[]config.EnvironmentLookup{
envLookup2,
envLookup1,
},
}
validateLookup(t, "value=1", envLookup.Lookup("value", "", nil))
}

31
vendor/github.com/hyperhq/libcompose/lookup/envfile.go generated vendored Normal file
View File

@@ -0,0 +1,31 @@
package lookup
import (
"strings"
"github.com/hyperhq/libcompose/config"
"github.com/hyperhq/hypercli/runconfig/opts"
)
// EnvfileLookup is a structure that implements the project.EnvironmentLookup interface.
// It holds the path of the file where to lookup environment values.
type EnvfileLookup struct {
Path string
}
// Lookup creates a string slice of string containing a "docker-friendly" environment string
// in the form of 'key=value'. It gets environment values using a '.env' file in the specified
// path.
func (l *EnvfileLookup) Lookup(key, serviceName string, config *config.ServiceConfig) []string {
envs, err := opts.ParseEnvFile(l.Path)
if err != nil {
return []string{}
}
for _, env := range envs {
e := strings.Split(env, "=")
if e[0] == key {
return []string{env}
}
}
return []string{}
}

View File

@@ -0,0 +1,57 @@
package lookup
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestEnvfileLookupReturnsEmptyIfError(t *testing.T) {
envfileLookup := &EnvfileLookup{
Path: "anything/file.env",
}
actuals := envfileLookup.Lookup("any", "", nil)
if len(actuals) != 0 {
t.Fatalf("expected an empty slice, got %v", actuals)
}
}
func TestEnvfileLookupWithGoodFile(t *testing.T) {
content := `foo=bar
baz=quux
# comment
_foobar=foobaz
with.dots=working
and_underscore=working too
`
tmpFolder, err := ioutil.TempDir("", "test-envfile")
if err != nil {
t.Fatal(err)
}
envfile := filepath.Join(tmpFolder, ".env")
if err := ioutil.WriteFile(envfile, []byte(content), 0700); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpFolder)
envfileLookup := &EnvfileLookup{
Path: envfile,
}
validateLookup(t, "baz=quux", envfileLookup.Lookup("baz", "", nil))
validateLookup(t, "foo=bar", envfileLookup.Lookup("foo", "", nil))
validateLookup(t, "_foobar=foobaz", envfileLookup.Lookup("_foobar", "", nil))
validateLookup(t, "with.dots=working", envfileLookup.Lookup("with.dots", "", nil))
validateLookup(t, "and_underscore=working too", envfileLookup.Lookup("and_underscore", "", nil))
}
func validateLookup(t *testing.T, expected string, actuals []string) {
if len(actuals) != 1 {
t.Fatalf("expected 1 result, got %v", actuals)
}
if actuals[0] != expected {
t.Fatalf("expected %s, got %s", expected, actuals[0])
}
}

66
vendor/github.com/hyperhq/libcompose/lookup/file.go generated vendored Normal file
View File

@@ -0,0 +1,66 @@
package lookup
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"github.com/Sirupsen/logrus"
)
// relativePath returns the proper relative path for the given file path. If
// the relativeTo string equals "-", then it means that it's from the stdin,
// and the returned path will be the current working directory. Otherwise, if
// file is really an absolute path, then it will be returned without any
// changes. Otherwise, the returned path will be a combination of relativeTo
// and file.
func relativePath(file, relativeTo string) string {
// stdin: return the current working directory if possible.
if relativeTo == "-" {
if cwd, err := os.Getwd(); err == nil {
return cwd
}
}
// If the given file is already an absolute path, just return it.
// Otherwise, the returned path will be relative to the given relativeTo
// path.
if filepath.IsAbs(file) {
return file
}
abs, err := filepath.Abs(filepath.Join(path.Dir(relativeTo), file))
if err != nil {
logrus.Errorf("Failed to get absolute directory: %s", err)
return file
}
return abs
}
// FileConfigLookup is a "bare" structure that implements the project.ResourceLookup interface
type FileConfigLookup struct {
}
// Lookup returns the content and the actual filename of the file that is "built" using the
// specified file and relativeTo string. file and relativeTo are supposed to be file path.
// If file starts with a slash ('/'), it tries to load it, otherwise it will build a
// filename using the folder part of relativeTo joined with file.
func (f *FileConfigLookup) Lookup(file, relativeTo string) ([]byte, string, error) {
file = relativePath(file, relativeTo)
logrus.Debugf("Reading file %s", file)
bytes, err := ioutil.ReadFile(file)
return bytes, file, err
}
// ResolvePath returns the path to be used for the given path volume. This
// function already takes care of relative paths.
func (f *FileConfigLookup) ResolvePath(path, inFile string) string {
vs := strings.SplitN(path, ":", 2)
if len(vs) != 2 || filepath.IsAbs(vs[0]) {
return path
}
vs[0] = relativePath(vs[0], inFile)
return strings.Join(vs, ":")
}

View File

@@ -0,0 +1,70 @@
package lookup
import (
"fmt"
"io/ioutil"
"path/filepath"
"testing"
)
type input struct {
file string
relativeTo string
}
func TestLookupError(t *testing.T) {
abs, err := filepath.Abs(".")
if err != nil {
t.Fatalf("Failed to get absolute directory: %s", err)
}
invalids := map[input]string{
input{"", ""}: fmt.Sprintf("read %s: is a directory", abs),
input{"", "/tmp/"}: "read /tmp: is a directory",
input{"file", "/does/not/exists/"}: "open /does/not/exists/file: no such file or directory",
input{"file", "/does/not/something"}: "open /does/not/file: no such file or directory",
input{"file", "/does/not/exists/another"}: "open /does/not/exists/file: no such file or directory",
input{"/does/not/exists/file", "/tmp/"}: "open /does/not/exists/file: no such file or directory",
input{"does/not/exists/file", "/tmp/"}: "open /tmp/does/not/exists/file: no such file or directory",
}
fileConfigLookup := FileConfigLookup{}
for invalid, expectedError := range invalids {
_, _, err := fileConfigLookup.Lookup(invalid.file, invalid.relativeTo)
if err == nil || err.Error() != expectedError {
t.Fatalf("Expected error with '%s', got '%v'", expectedError, err)
}
}
}
func TestLookupOK(t *testing.T) {
tmpFolder, err := ioutil.TempDir("", "lookup-tests")
if err != nil {
t.Fatal(err)
}
tmpFile1 := filepath.Join(tmpFolder, "file1")
tmpFile2 := filepath.Join(tmpFolder, "file2")
if err = ioutil.WriteFile(tmpFile1, []byte("content1"), 0755); err != nil {
t.Fatal(err)
}
if err = ioutil.WriteFile(tmpFile2, []byte("content2"), 0755); err != nil {
t.Fatal(err)
}
fileConfigLookup := FileConfigLookup{}
valids := map[input]string{
input{"file1", tmpFolder + "/"}: "content1",
input{"file2", tmpFolder + "/"}: "content2",
input{tmpFile1, tmpFolder}: "content1",
input{tmpFile1, "/does/not/exists"}: "content1",
input{"file2", tmpFile1}: "content2",
}
for valid, expectedContent := range valids {
out, _, err := fileConfigLookup.Lookup(valid.file, valid.relativeTo)
if err != nil || string(out) != expectedContent {
t.Fatalf("Expected %s to contains '%s', got %s, %v.", valid.file, expectedContent, out, err)
}
}
}

View File

@@ -0,0 +1,24 @@
package lookup
import (
"fmt"
"os"
"github.com/hyperhq/libcompose/config"
)
// OsEnvLookup is a "bare" structure that implements the project.EnvironmentLookup interface
type OsEnvLookup struct {
}
// Lookup creates a string slice of string containing a "docker-friendly" environment string
// in the form of 'key=value'. It gets environment values using os.Getenv.
// If the os environment variable does not exists, the slice is empty. serviceName and config
// are not used at all in this implementation.
func (o *OsEnvLookup) Lookup(key, serviceName string, config *config.ServiceConfig) []string {
ret := os.Getenv(key)
if ret == "" {
return []string{}
}
return []string{fmt.Sprintf("%s=%s", key, ret)}
}

View File

@@ -0,0 +1,28 @@
package lookup
import (
"testing"
)
func TestOsEnvLookup(t *testing.T) {
// Putting bare minimun value for serviceName and config as there are
// not important on this test.
serviceName := "anything"
osEnvLookup := &OsEnvLookup{}
envs := osEnvLookup.Lookup("PATH", serviceName, nil)
if len(envs) != 1 {
t.Fatalf("Expected envs to contains one element, but was %v", envs)
}
envs = osEnvLookup.Lookup("path", serviceName, nil)
if len(envs) != 0 {
t.Fatalf("Expected envs to be empty, but was %v", envs)
}
envs = osEnvLookup.Lookup("DOES_NOT_EXIST", serviceName, nil)
if len(envs) != 0 {
t.Fatalf("Expected envs to be empty, but was %v", envs)
}
}

1
vendor/github.com/hyperhq/libcompose/package.go generated vendored Normal file
View File

@@ -0,0 +1 @@
package libcompose

View File

@@ -0,0 +1,11 @@
package project
import "github.com/docker/engine-api/client"
// ClientFactory is a factory to create docker clients.
type ClientFactory interface {
// Create constructs a Docker client for the given service. The passed in
// config may be nil in which case a generic client for the project should
// be returned.
Create(service Service) client.APIClient
}

View File

@@ -0,0 +1,9 @@
package project
// Container defines what a libcompose container provides.
type Container interface {
ID() (string, error)
Name() string
Port(port string) (string, error)
IsRunning() (bool, error)
}

141
vendor/github.com/hyperhq/libcompose/project/context.go generated vendored Normal file
View File

@@ -0,0 +1,141 @@
package project
import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/Sirupsen/logrus"
"github.com/hyperhq/libcompose/config"
"github.com/hyperhq/libcompose/logger"
)
var projectRegexp = regexp.MustCompile("[^a-zA-Z0-9_.-]")
// Context holds context meta information about a libcompose project, like
// the project name, the compose file, etc.
type Context struct {
ComposeFiles []string
ComposeBytes [][]byte
ProjectName string
isOpen bool
ServiceFactory ServiceFactory
EnvironmentLookup config.EnvironmentLookup
ResourceLookup config.ResourceLookup
LoggerFactory logger.Factory
IgnoreMissingConfig bool
Project *Project
Autoremove bool
}
func (c *Context) readComposeFiles() error {
if c.ComposeBytes != nil {
return nil
}
logrus.Debugf("Opening compose files: %s", strings.Join(c.ComposeFiles, ","))
// Handle STDIN (`-f -`)
if len(c.ComposeFiles) == 1 && c.ComposeFiles[0] == "-" {
composeBytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
logrus.Errorf("Failed to read compose file from stdin: %v", err)
return err
}
c.ComposeBytes = [][]byte{composeBytes}
return nil
}
for _, composeFile := range c.ComposeFiles {
composeBytes, err := ioutil.ReadFile(composeFile)
if err != nil && !os.IsNotExist(err) {
logrus.Errorf("Failed to open the compose file: %s", composeFile)
return err
}
if err != nil && !c.IgnoreMissingConfig {
logrus.Errorf("Failed to find the compose file: %s", composeFile)
return err
}
c.ComposeBytes = append(c.ComposeBytes, composeBytes)
}
return nil
}
func (c *Context) determineProject() error {
name, err := c.lookupProjectName()
if err != nil {
return err
}
c.ProjectName = normalizeName(name)
if c.ProjectName == "" {
return fmt.Errorf("Falied to determine project name")
}
return nil
}
func (c *Context) lookupProjectName() (string, error) {
if c.ProjectName != "" {
return c.ProjectName, nil
}
if envProject := os.Getenv("COMPOSE_PROJECT_NAME"); envProject != "" {
return envProject, nil
}
file := "."
if len(c.ComposeFiles) > 0 {
file = c.ComposeFiles[0]
}
f, err := filepath.Abs(file)
if err != nil {
logrus.Errorf("Failed to get absolute directory for: %s", file)
return "", err
}
f = toUnixPath(f)
parent := path.Base(path.Dir(f))
if parent != "" && parent != "." {
return parent, nil
} else if wd, err := os.Getwd(); err != nil {
return "", err
} else {
return path.Base(toUnixPath(wd)), nil
}
}
func normalizeName(name string) string {
r := regexp.MustCompile("[^a-z0-9]+")
return r.ReplaceAllString(strings.ToLower(name), "")
}
func toUnixPath(p string) string {
return strings.Replace(p, "\\", "/", -1)
}
func (c *Context) open() error {
if c.isOpen {
return nil
}
if err := c.readComposeFiles(); err != nil {
return err
}
if err := c.determineProject(); err != nil {
return err
}
c.isOpen = true
return nil
}

94
vendor/github.com/hyperhq/libcompose/project/empty.go generated vendored Normal file
View File

@@ -0,0 +1,94 @@
package project
import (
"github.com/hyperhq/libcompose/project/options"
)
// EmptyService is a struct that implements Service but does nothing.
type EmptyService struct {
}
// Create implements Service.Create but does nothing.
func (e *EmptyService) Create(options options.Create) error {
return nil
}
// Build implements Service.Build but does nothing.
func (e *EmptyService) Build(buildOptions options.Build) error {
return nil
}
// Up implements Service.Up but does nothing.
func (e *EmptyService) Up(options options.Up) error {
return nil
}
// Start implements Service.Start but does nothing.
func (e *EmptyService) Start() error {
return nil
}
// Stop implements Service.Stop() but does nothing.
func (e *EmptyService) Stop(timeout int) error {
return nil
}
// Delete implements Service.Delete but does nothing.
func (e *EmptyService) Delete(options options.Delete) error {
return nil
}
// Restart implements Service.Restart but does nothing.
func (e *EmptyService) Restart(timeout int) error {
return nil
}
// Log implements Service.Log but does nothing.
func (e *EmptyService) Log(follow bool) error {
return nil
}
// Pull implements Service.Pull but does nothing.
func (e *EmptyService) Pull() error {
return nil
}
// Kill implements Service.Kill but does nothing.
func (e *EmptyService) Kill(signal string) error {
return nil
}
// Containers implements Service.Containers but does nothing.
func (e *EmptyService) Containers() ([]Container, error) {
return []Container{}, nil
}
// Scale implements Service.Scale but does nothing.
func (e *EmptyService) Scale(count int, timeout int) error {
return nil
}
// Info implements Service.Info but does nothing.
func (e *EmptyService) Info(qFlag bool) (InfoSet, error) {
return InfoSet{}, nil
}
// Pause implements Service.Pause but does nothing.
func (e *EmptyService) Pause() error {
return nil
}
// Unpause implements Service.Pause but does nothing.
func (e *EmptyService) Unpause() error {
return nil
}
// Run implements Service.Run but does nothing.
func (e *EmptyService) Run(commandParts []string) (int, error) {
return 0, nil
}
// RemoveImage implements Service.RemoveImage but does nothing.
func (e *EmptyService) RemoveImage(imageType options.ImageType) error {
return nil
}

View File

@@ -0,0 +1,197 @@
// Package events holds event structures, methods and functions.
package events
import (
"fmt"
)
// Notifier defines the methods an event notifier should have.
type Notifier interface {
Notify(eventType EventType, serviceName string, data map[string]string)
}
// Emitter defines the methods an event emitter should have.
type Emitter interface {
AddListener(c chan<- Event)
}
// Event holds project-wide event informations.
type Event struct {
EventType EventType
ServiceName string
Data map[string]string
}
// EventType defines a type of libcompose event.
type EventType int
// Definitions of libcompose events
const (
NoEvent = EventType(iota)
ContainerCreated = EventType(iota)
ContainerStarted = EventType(iota)
ServiceAdd = EventType(iota)
ServiceUpStart = EventType(iota)
ServiceUpIgnored = EventType(iota)
ServiceUp = EventType(iota)
ServiceCreateStart = EventType(iota)
ServiceCreate = EventType(iota)
ServiceDeleteStart = EventType(iota)
ServiceDelete = EventType(iota)
ServiceDownStart = EventType(iota)
ServiceDown = EventType(iota)
ServiceRestartStart = EventType(iota)
ServiceRestart = EventType(iota)
ServicePullStart = EventType(iota)
ServicePull = EventType(iota)
ServiceKillStart = EventType(iota)
ServiceKill = EventType(iota)
ServiceStartStart = EventType(iota)
ServiceStart = EventType(iota)
ServiceBuildStart = EventType(iota)
ServiceBuild = EventType(iota)
ServicePauseStart = EventType(iota)
ServicePause = EventType(iota)
ServiceUnpauseStart = EventType(iota)
ServiceUnpause = EventType(iota)
ServiceStopStart = EventType(iota)
ServiceStop = EventType(iota)
ServiceRunStart = EventType(iota)
ServiceRun = EventType(iota)
VolumeAdd = EventType(iota)
NetworkAdd = EventType(iota)
ProjectDownStart = EventType(iota)
ProjectDownDone = EventType(iota)
ProjectCreateStart = EventType(iota)
ProjectCreateDone = EventType(iota)
ProjectUpStart = EventType(iota)
ProjectUpDone = EventType(iota)
ProjectDeleteStart = EventType(iota)
ProjectDeleteDone = EventType(iota)
ProjectRestartStart = EventType(iota)
ProjectRestartDone = EventType(iota)
ProjectReload = EventType(iota)
ProjectReloadTrigger = EventType(iota)
ProjectKillStart = EventType(iota)
ProjectKillDone = EventType(iota)
ProjectStartStart = EventType(iota)
ProjectStartDone = EventType(iota)
ProjectBuildStart = EventType(iota)
ProjectBuildDone = EventType(iota)
ProjectPauseStart = EventType(iota)
ProjectPauseDone = EventType(iota)
ProjectUnpauseStart = EventType(iota)
ProjectUnpauseDone = EventType(iota)
ProjectStopStart = EventType(iota)
ProjectStopDone = EventType(iota)
)
func (e EventType) String() string {
var m string
switch e {
case ContainerCreated:
m = "Created container"
case ContainerStarted:
m = "Started container"
case ServiceAdd:
m = "Adding"
case ServiceUpStart:
m = "Starting"
case ServiceUpIgnored:
m = "Ignoring"
case ServiceUp:
m = "Started"
case ServiceCreateStart:
m = "Creating"
case ServiceCreate:
m = "Created"
case ServiceDeleteStart:
m = "Deleting"
case ServiceDelete:
m = "Deleted"
case ServiceStopStart:
m = "Stopping"
case ServiceStop:
m = "Stopped"
case ServiceDownStart:
m = "Stopping"
case ServiceDown:
m = "Stopped"
case ServiceRestartStart:
m = "Restarting"
case ServiceRestart:
m = "Restarted"
case ServicePullStart:
m = "Pulling"
case ServicePull:
m = "Pulled"
case ServiceKillStart:
m = "Killing"
case ServiceKill:
m = "Killed"
case ServiceStartStart:
m = "Starting"
case ServiceStart:
m = "Started"
case ServiceBuildStart:
m = "Building"
case ServiceBuild:
m = "Built"
case ServiceRunStart:
m = "Executing"
case ServiceRun:
m = "Executed"
case ProjectDownStart:
m = "Stopping project"
case ProjectDownDone:
m = "Project stopped"
case ProjectStopStart:
m = "Stopping project"
case ProjectStopDone:
m = "Project stopped"
case ProjectCreateStart:
m = "Creating project"
case ProjectCreateDone:
m = "Project created"
case ProjectUpStart:
m = "Starting project"
case ProjectUpDone:
m = "Project started"
case ProjectDeleteStart:
m = "Deleting project"
case ProjectDeleteDone:
m = "Project deleted"
case ProjectRestartStart:
m = "Restarting project"
case ProjectRestartDone:
m = "Project restarted"
case ProjectReload:
m = "Reloading project"
case ProjectReloadTrigger:
m = "Triggering project reload"
case ProjectKillStart:
m = "Killing project"
case ProjectKillDone:
m = "Project killed"
case ProjectStartStart:
m = "Starting project"
case ProjectStartDone:
m = "Project started"
case ProjectBuildStart:
m = "Building project"
case ProjectBuildDone:
m = "Project built"
}
if m == "" {
m = fmt.Sprintf("EventType: %d", int(e))
}
return m
}

View File

@@ -0,0 +1,21 @@
package events
import (
"fmt"
"testing"
)
func TestEventEquality(t *testing.T) {
if fmt.Sprintf("%s", ServiceStart) != "Started" ||
fmt.Sprintf("%v", ServiceStart) != "Started" {
t.Fatalf("EventServiceStart String() doesn't work: %s %v", ServiceStart, ServiceStart)
}
if fmt.Sprintf("%s", ServiceStart) != fmt.Sprintf("%s", ServiceUp) {
t.Fatal("Event messages do not match")
}
if ServiceStart == ServiceUp {
t.Fatal("Events match")
}
}

53
vendor/github.com/hyperhq/libcompose/project/info.go generated vendored Normal file
View File

@@ -0,0 +1,53 @@
package project
import (
"bytes"
"io"
"text/tabwriter"
)
// InfoPart holds key/value strings.
type InfoPart struct {
Key, Value string
}
// InfoSet holds a list of Info.
type InfoSet []Info
// Info holds a list of InfoPart.
type Info []InfoPart
func (infos InfoSet) String(titleFlag bool) string {
//no error checking, none of this should fail
buffer := bytes.NewBuffer(make([]byte, 0, 1024))
tabwriter := tabwriter.NewWriter(buffer, 4, 4, 2, ' ', 0)
first := true
for _, info := range infos {
if first && titleFlag {
writeLine(tabwriter, true, info)
}
first = false
writeLine(tabwriter, false, info)
}
tabwriter.Flush()
return buffer.String()
}
func writeLine(writer io.Writer, key bool, info Info) {
first := true
for _, part := range info {
if !first {
writer.Write([]byte{'\t'})
}
first = false
if key {
writer.Write([]byte(part.Key))
} else {
writer.Write([]byte(part.Value))
}
}
writer.Write([]byte{'\n'})
}

View File

@@ -0,0 +1,36 @@
package project
import (
"github.com/hyperhq/libcompose/config"
"github.com/hyperhq/libcompose/project/events"
"github.com/hyperhq/libcompose/project/options"
"golang.org/x/net/context"
)
// APIProject is an interface defining the methods a libcompose project should implement.
type APIProject interface {
events.Notifier
events.Emitter
Build(options options.Build, sevice ...string) error
Create(options options.Create, services ...string) error
Delete(options options.Delete, services ...string) error
Down(options options.Down, services ...string) error
Kill(signal string, services ...string) error
Log(follow bool, services ...string) error
Pause(services ...string) error
Ps(onlyID bool, services ...string) (InfoSet, error)
// FIXME(vdemeester) we could use nat.Port instead ?
Port(index int, protocol, serviceName, privatePort string) (string, error)
Pull(services ...string) error
Restart(timeout int, services ...string) error
Run(ctx context.Context, serviceName string, commandParts []string) (int, error)
Scale(timeout int, servicesScale map[string]int) error
Start(services ...string) error
Stop(timeout int, services ...string) error
Unpause(services ...string) error
Up(options options.Up, services ...string) error
Parse() error
GetConfig() (*config.ServiceConfigs, map[string]*config.VolumeConfig, map[string]*config.NetworkConfig)
}

View File

@@ -0,0 +1,77 @@
package project
import (
"bytes"
"github.com/Sirupsen/logrus"
"github.com/hyperhq/libcompose/project/events"
)
var (
infoEvents = map[events.EventType]bool{
events.ProjectDeleteDone: true,
events.ProjectDeleteStart: true,
events.ProjectDownDone: true,
events.ProjectDownStart: true,
events.ProjectRestartDone: true,
events.ProjectRestartStart: true,
events.ProjectUpDone: true,
events.ProjectUpStart: true,
events.ServiceDeleteStart: true,
events.ServiceDelete: true,
events.ServiceDownStart: true,
events.ServiceDown: true,
events.ServiceRestartStart: true,
events.ServiceRestart: true,
events.ServiceUpStart: true,
events.ServiceUp: true,
}
)
type defaultListener struct {
project *Project
listenChan chan events.Event
upCount int
}
// NewDefaultListener create a default listener for the specified project.
func NewDefaultListener(p *Project) chan<- events.Event {
l := defaultListener{
listenChan: make(chan events.Event),
project: p,
}
go l.start()
return l.listenChan
}
func (d *defaultListener) start() {
for event := range d.listenChan {
buffer := bytes.NewBuffer(nil)
if event.Data != nil {
for k, v := range event.Data {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(k)
buffer.WriteString("=")
buffer.WriteString(v)
}
}
if event.EventType == events.ServiceUp {
d.upCount++
}
logf := logrus.Debugf
if infoEvents[event.EventType] {
logf = logrus.Infof
}
if event.ServiceName == "" {
logf("Project [%s]: %s %s", d.project.Name, event.EventType, buffer.Bytes())
} else {
logf("[%d/%d] [%s]: %s %s", d.upCount, d.project.ServiceConfigs.Len(), event.ServiceName, event.EventType, buffer.Bytes())
}
}
}

View File

@@ -0,0 +1,47 @@
package options
// Build holds options of compose build.
type Build struct {
NoCache bool
ForceRemove bool
Pull bool
}
// Delete holds options of compose rm.
type Delete struct {
RemoveVolume bool
BeforeDeleteCallback func([]string) bool
}
// Down holds options of compose down.
type Down struct {
RemoveVolume bool
RemoveImages ImageType
RemoveOrphans bool
}
// Create holds options of compose create.
type Create struct {
NoRecreate bool
ForceRecreate bool
NoBuild bool
// ForceBuild bool
}
// Up holds options of compose up.
type Up struct {
Create
}
// ImageType defines the type of image (local, all)
type ImageType string
// Valid indicates whether the image type is valid.
func (i ImageType) Valid() bool {
switch string(i) {
case "", "local", "all":
return true
default:
return false
}
}

View File

@@ -0,0 +1,39 @@
package options
import (
"testing"
)
func TestImageType(t *testing.T) {
cases := []struct {
imageType string
valid bool
}{
{
imageType: "",
valid: true,
},
{
imageType: " ",
valid: false,
},
{
imageType: "hello",
valid: false,
},
{
imageType: "local",
valid: true,
},
{
imageType: "all",
valid: true,
},
}
for _, c := range cases {
i := ImageType(c.imageType)
if i.Valid() != c.valid {
t.Errorf("expected %v, got %v, for %v", c.valid, i.Valid(), c)
}
}
}

679
vendor/github.com/hyperhq/libcompose/project/project.go generated vendored Normal file
View File

@@ -0,0 +1,679 @@
package project
import (
"errors"
"fmt"
"strings"
"golang.org/x/net/context"
log "github.com/Sirupsen/logrus"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/filters"
"github.com/hyperhq/libcompose/config"
"github.com/hyperhq/libcompose/labels"
"github.com/hyperhq/libcompose/logger"
"github.com/hyperhq/libcompose/project/events"
"github.com/hyperhq/libcompose/project/options"
"github.com/hyperhq/libcompose/utils"
)
type wrapperAction func(*serviceWrapper, map[string]*serviceWrapper)
type serviceAction func(service Service) error
// Project holds libcompose project information.
type Project struct {
Name string
ServiceConfigs *config.ServiceConfigs
VolumeConfigs map[string]*config.VolumeConfig
NetworkConfigs map[string]*config.NetworkConfig
Files []string
ReloadCallback func() error
context *Context
clientFactory ClientFactory
reload []string
upCount int
listeners []chan<- events.Event
hasListeners bool
}
// NewProject creates a new project with the specified context.
func NewProject(clientFactory ClientFactory, context *Context) *Project {
p := &Project{
context: context,
clientFactory: clientFactory,
ServiceConfigs: config.NewServiceConfigs(),
VolumeConfigs: make(map[string]*config.VolumeConfig),
NetworkConfigs: make(map[string]*config.NetworkConfig),
}
if context.LoggerFactory == nil {
context.LoggerFactory = &logger.NullLogger{}
}
context.Project = p
p.listeners = []chan<- events.Event{NewDefaultListener(p)}
return p
}
func (p *Project) GetConfig() (*config.ServiceConfigs, map[string]*config.VolumeConfig, map[string]*config.NetworkConfig) {
return p.ServiceConfigs, p.VolumeConfigs, p.NetworkConfigs
}
// Parse populates project information based on its context. It sets up the name,
// the composefile and the composebytes (the composefile content).
func (p *Project) Parse() error {
err := p.context.open()
if err != nil {
return err
}
p.Name = p.context.ProjectName
p.Files = p.context.ComposeFiles
if len(p.Files) == 1 && p.Files[0] == "-" {
p.Files = []string{"."}
}
if p.context.ComposeBytes != nil {
for i, composeBytes := range p.context.ComposeBytes {
file := ""
if i < len(p.context.ComposeFiles) {
file = p.Files[i]
}
if err := p.load(file, composeBytes); err != nil {
return err
}
}
}
return nil
}
// CreateService creates a service with the specified name based. If there
// is no config in the project for this service, it will return an error.
func (p *Project) CreateService(name string) (Service, error) {
existing, ok := p.ServiceConfigs.Get(name)
if !ok {
return nil, fmt.Errorf("Failed to find service: %s", name)
}
// Copy because we are about to modify the environment
config := *existing
if p.context.EnvironmentLookup != nil {
parsedEnv := make([]string, 0, len(config.Environment))
for _, env := range config.Environment {
parts := strings.SplitN(env, "=", 2)
if len(parts) > 1 && parts[1] != "" {
parsedEnv = append(parsedEnv, env)
continue
} else {
env = parts[0]
}
for _, value := range p.context.EnvironmentLookup.Lookup(env, name, &config) {
parsedEnv = append(parsedEnv, value)
}
}
config.Environment = parsedEnv
}
return p.context.ServiceFactory.Create(p, name, &config)
}
// AddConfig adds the specified service config for the specified name.
func (p *Project) AddConfig(name string, config *config.ServiceConfig) error {
p.Notify(events.ServiceAdd, name, nil)
p.ServiceConfigs.Add(name, config)
p.reload = append(p.reload, name)
return nil
}
// AddVolumeConfig adds the specified volume config for the specified name.
func (p *Project) AddVolumeConfig(name string, config *config.VolumeConfig) error {
p.Notify(events.VolumeAdd, name, nil)
p.VolumeConfigs[name] = config
return nil
}
// AddNetworkConfig adds the specified network config for the specified name.
func (p *Project) AddNetworkConfig(name string, config *config.NetworkConfig) error {
p.Notify(events.NetworkAdd, name, nil)
p.NetworkConfigs[name] = config
return nil
}
// Load loads the specified byte array (the composefile content) and adds the
// service configuration to the project.
// FIXME is it needed ?
func (p *Project) Load(bytes []byte) error {
return p.load("", bytes)
}
func (p *Project) load(file string, bytes []byte) error {
serviceConfigs, volumeConfigs, networkConfigs, err := config.Merge(p.ServiceConfigs, p.context.EnvironmentLookup, p.context.ResourceLookup, file, bytes)
if err != nil {
log.Errorf("Could not parse config for project %s : %v", p.Name, err)
return err
}
for name, config := range serviceConfigs {
err := p.AddConfig(name, config)
if err != nil {
return err
}
}
for name, config := range volumeConfigs {
err := p.AddVolumeConfig(name, config)
if err != nil {
return err
}
}
for name, config := range networkConfigs {
err := p.AddNetworkConfig(name, config)
if err != nil {
return err
}
}
return nil
}
func (p *Project) loadWrappers(wrappers map[string]*serviceWrapper, servicesToConstruct []string) error {
for _, name := range servicesToConstruct {
wrapper, err := newServiceWrapper(name, p)
if err != nil {
return err
}
wrappers[name] = wrapper
}
return nil
}
// Build builds the specified services (like docker build).
func (p *Project) Build(buildOptions options.Build, services ...string) error {
return p.perform(events.ProjectBuildStart, events.ProjectBuildDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceBuildStart, events.ServiceBuild, func(service Service) error {
return service.Build(buildOptions)
})
}), nil)
}
// Create creates the specified services (like docker create).
func (p *Project) Create(options options.Create, services ...string) error {
if options.NoRecreate && options.ForceRecreate {
return fmt.Errorf("no-recreate and force-recreate cannot be combined")
}
return p.perform(events.ProjectCreateStart, events.ProjectCreateDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceCreateStart, events.ServiceCreate, func(service Service) error {
return service.Create(options)
})
}), nil)
}
// Stop stops the specified services (like docker stop).
func (p *Project) Stop(timeout int, services ...string) error {
return p.perform(events.ProjectStopStart, events.ProjectStopDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServiceStopStart, events.ServiceStop, func(service Service) error {
return service.Stop(timeout)
})
}), nil)
}
// Down stops the specified services and clean related containers (like docker stop + docker rm).
func (p *Project) Down(opts options.Down, services ...string) error {
if !opts.RemoveImages.Valid() {
return fmt.Errorf("--rmi flag must be local, all or empty")
}
if err := p.Stop(10, services...); err != nil {
return err
}
if opts.RemoveOrphans {
if err := p.removeOrphanContainers(); err != nil {
return err
}
}
if err := p.Delete(options.Delete{
RemoveVolume: opts.RemoveVolume,
}, services...); err != nil {
return err
}
return p.forEach([]string{}, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.NoEvent, events.NoEvent, func(service Service) error {
return service.RemoveImage(opts.RemoveImages)
})
}), func(service Service) error {
return service.Create(options.Create{})
})
}
func (p *Project) removeOrphanContainers() error {
client := p.clientFactory.Create(nil)
filter := filters.NewArgs()
filter.Add("label", labels.PROJECT.EqString(p.Name))
containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{
Filter: filter,
})
if err != nil {
return err
}
currentServices := map[string]struct{}{}
for _, serviceName := range p.ServiceConfigs.Keys() {
currentServices[serviceName] = struct{}{}
}
for _, container := range containers {
serviceLabel := container.Labels[labels.SERVICE.Str()]
if _, ok := currentServices[serviceLabel]; !ok {
if err := client.ContainerKill(context.Background(), container.ID, "SIGKILL"); err != nil {
return err
}
if _, err := client.ContainerRemove(context.Background(), container.ID, types.ContainerRemoveOptions{
Force: true,
}); err != nil {
return err
}
}
}
return nil
}
// Restart restarts the specified services (like docker restart).
func (p *Project) Restart(timeout int, services ...string) error {
return p.perform(events.ProjectRestartStart, events.ProjectRestartDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceRestartStart, events.ServiceRestart, func(service Service) error {
return service.Restart(timeout)
})
}), nil)
}
// Port returns the public port for a port binding of the specified service.
func (p *Project) Port(index int, protocol, serviceName, privatePort string) (string, error) {
service, err := p.CreateService(serviceName)
if err != nil {
return "", err
}
containers, err := service.Containers()
if err != nil {
return "", err
}
if index < 1 || index > len(containers) {
return "", fmt.Errorf("Invalid index %d", index)
}
return containers[index-1].Port(fmt.Sprintf("%s/%s", privatePort, protocol))
}
// Ps list containers for the specified services.
func (p *Project) Ps(onlyID bool, services ...string) (InfoSet, error) {
allInfo := InfoSet{}
for _, name := range p.ServiceConfigs.Keys() {
service, err := p.CreateService(name)
if err != nil {
return nil, err
}
info, err := service.Info(onlyID)
if err != nil {
return nil, err
}
allInfo = append(allInfo, info...)
}
return allInfo, nil
}
// Start starts the specified services (like docker start).
func (p *Project) Start(services ...string) error {
return p.perform(events.ProjectStartStart, events.ProjectStartDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceStartStart, events.ServiceStart, func(service Service) error {
return service.Start()
})
}), nil)
}
// Run executes a one off command (like `docker run image command`).
func (p *Project) Run(ctx context.Context, serviceName string, commandParts []string) (int, error) {
if !p.ServiceConfigs.Has(serviceName) {
return 1, fmt.Errorf("%s is not defined in the template", serviceName)
}
var exitCode int
err := p.forEach([]string{}, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceRunStart, events.ServiceRun, func(service Service) error {
if service.Name() == serviceName {
code, err := service.Run(ctx, commandParts)
exitCode = code
return err
}
return nil
})
}), func(service Service) error {
return service.Create(options.Create{})
})
return exitCode, err
}
// Up creates and starts the specified services (kinda like docker run).
func (p *Project) Up(options options.Up, services ...string) error {
return p.perform(events.ProjectUpStart, events.ProjectUpDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceUpStart, events.ServiceUp, func(service Service) error {
return service.Up(options)
})
}), func(service Service) error {
return service.Create(options.Create)
})
}
// Log aggregates and prints out the logs for the specified services.
func (p *Project) Log(follow bool, services ...string) error {
return p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.NoEvent, events.NoEvent, func(service Service) error {
return service.Log(follow)
})
}), nil)
}
// Scale scales the specified services.
func (p *Project) Scale(timeout int, servicesScale map[string]int) error {
// This code is a bit verbose but I wanted to parse everything up front
order := make([]string, 0, 0)
services := make(map[string]Service)
for name := range servicesScale {
if !p.ServiceConfigs.Has(name) {
return fmt.Errorf("%s is not defined in the template", name)
}
service, err := p.CreateService(name)
if err != nil {
return fmt.Errorf("Failed to lookup service: %s: %v", service, err)
}
order = append(order, name)
services[name] = service
}
for _, name := range order {
scale := servicesScale[name]
log.Infof("Setting scale %s=%d...", name, scale)
err := services[name].Scale(scale, timeout)
if err != nil {
return fmt.Errorf("Failed to set the scale %s=%d: %v", name, scale, err)
}
}
return nil
}
// Pull pulls the specified services (like docker pull).
func (p *Project) Pull(services ...string) error {
return p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServicePullStart, events.ServicePull, func(service Service) error {
return service.Pull()
})
}), nil)
}
// listStoppedContainers lists the stopped containers for the specified services.
func (p *Project) listStoppedContainers(services ...string) ([]string, error) {
stoppedContainers := []string{}
err := p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.NoEvent, events.NoEvent, func(service Service) error {
containers, innerErr := service.Containers()
if innerErr != nil {
return innerErr
}
for _, container := range containers {
running, innerErr := container.IsRunning()
if innerErr != nil {
log.Error(innerErr)
}
if !running {
containerID, innerErr := container.ID()
if innerErr != nil {
log.Error(innerErr)
}
stoppedContainers = append(stoppedContainers, containerID)
}
}
return nil
})
}), nil)
if err != nil {
return nil, err
}
return stoppedContainers, nil
}
// Delete removes the specified services (like docker rm).
func (p *Project) Delete(options options.Delete, services ...string) error {
stoppedContainers, err := p.listStoppedContainers(services...)
if err != nil {
return err
}
if len(stoppedContainers) == 0 {
p.Notify(events.ProjectDeleteDone, "", nil)
fmt.Println("No stopped containers")
return nil
}
if options.BeforeDeleteCallback != nil && !options.BeforeDeleteCallback(stoppedContainers) {
return nil
}
return p.perform(events.ProjectDeleteStart, events.ProjectDeleteDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServiceDeleteStart, events.ServiceDelete, func(service Service) error {
return service.Delete(options)
})
}), nil)
}
// Kill kills the specified services (like docker kill).
func (p *Project) Kill(signal string, services ...string) error {
return p.perform(events.ProjectKillStart, events.ProjectKillDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServiceKillStart, events.ServiceKill, func(service Service) error {
return service.Kill(signal)
})
}), nil)
}
// Pause pauses the specified services containers (like docker pause).
func (p *Project) Pause(services ...string) error {
return p.perform(events.ProjectPauseStart, events.ProjectPauseDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServicePauseStart, events.ServicePause, func(service Service) error {
return service.Pause()
})
}), nil)
}
// Unpause pauses the specified services containers (like docker pause).
func (p *Project) Unpause(services ...string) error {
return p.perform(events.ProjectUnpauseStart, events.ProjectUnpauseDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServiceUnpauseStart, events.ServiceUnpause, func(service Service) error {
return service.Unpause()
})
}), nil)
}
func (p *Project) perform(start, done events.EventType, services []string, action wrapperAction, cycleAction serviceAction) error {
p.Notify(start, "", nil)
err := p.forEach(services, action, cycleAction)
p.Notify(done, "", nil)
return err
}
func isSelected(wrapper *serviceWrapper, selected map[string]bool) bool {
return len(selected) == 0 || selected[wrapper.name]
}
func (p *Project) forEach(services []string, action wrapperAction, cycleAction serviceAction) error {
selected := make(map[string]bool)
wrappers := make(map[string]*serviceWrapper)
for _, s := range services {
selected[s] = true
}
return p.traverse(true, selected, wrappers, action, cycleAction)
}
func (p *Project) startService(wrappers map[string]*serviceWrapper, history []string, selected, launched map[string]bool, wrapper *serviceWrapper, action wrapperAction, cycleAction serviceAction) error {
if launched[wrapper.name] {
return nil
}
launched[wrapper.name] = true
history = append(history, wrapper.name)
for _, dep := range wrapper.service.DependentServices() {
target := wrappers[dep.Target]
if target == nil {
log.Errorf("Failed to find %s", dep.Target)
continue
}
if utils.Contains(history, dep.Target) {
cycle := strings.Join(append(history, dep.Target), "->")
if dep.Optional {
log.Debugf("Ignoring cycle for %s", cycle)
wrapper.IgnoreDep(dep.Target)
if cycleAction != nil {
var err error
log.Debugf("Running cycle action for %s", cycle)
err = cycleAction(target.service)
if err != nil {
return err
}
}
} else {
return fmt.Errorf("Cycle detected in path %s", cycle)
}
continue
}
err := p.startService(wrappers, history, selected, launched, target, action, cycleAction)
if err != nil {
return err
}
}
if isSelected(wrapper, selected) {
log.Debugf("Launching action for %s", wrapper.name)
go action(wrapper, wrappers)
} else {
wrapper.Ignore()
}
return nil
}
func (p *Project) traverse(start bool, selected map[string]bool, wrappers map[string]*serviceWrapper, action wrapperAction, cycleAction serviceAction) error {
restart := false
wrapperList := []string{}
if start {
for _, name := range p.ServiceConfigs.Keys() {
wrapperList = append(wrapperList, name)
}
} else {
for _, wrapper := range wrappers {
if err := wrapper.Reset(); err != nil {
return err
}
}
wrapperList = p.reload
}
p.loadWrappers(wrappers, wrapperList)
p.reload = []string{}
// check service name
for s := range selected {
if wrappers[s] == nil {
return errors.New("No such service: " + s)
}
}
launched := map[string]bool{}
for _, wrapper := range wrappers {
if err := p.startService(wrappers, []string{}, selected, launched, wrapper, action, cycleAction); err != nil {
return err
}
}
var firstError error
for _, wrapper := range wrappers {
if !isSelected(wrapper, selected) {
continue
}
if err := wrapper.Wait(); err == ErrRestart {
restart = true
} else if err != nil {
log.Errorf("Failed to start: %s : %v", wrapper.name, err)
if firstError == nil {
firstError = err
}
}
}
if restart {
if p.ReloadCallback != nil {
if err := p.ReloadCallback(); err != nil {
log.Errorf("Failed calling callback: %v", err)
}
}
return p.traverse(false, selected, wrappers, action, cycleAction)
}
return firstError
}
// AddListener adds the specified listener to the project.
// This implements implicitly events.Emitter.
func (p *Project) AddListener(c chan<- events.Event) {
if !p.hasListeners {
for _, l := range p.listeners {
close(l)
}
p.hasListeners = true
p.listeners = []chan<- events.Event{c}
} else {
p.listeners = append(p.listeners, c)
}
}
// Notify notifies all project listener with the specified eventType, service name and datas.
// This implements implicitly events.Notifier interface.
func (p *Project) Notify(eventType events.EventType, serviceName string, data map[string]string) {
if eventType == events.NoEvent {
return
}
event := events.Event{
EventType: eventType,
ServiceName: serviceName,
Data: data,
}
for _, l := range p.listeners {
l <- event
}
}

View File

@@ -0,0 +1,222 @@
package project
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/hyperhq/libcompose/config"
"github.com/hyperhq/libcompose/project/options"
"github.com/hyperhq/libcompose/yaml"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
type TestServiceFactory struct {
Counts map[string]int
}
type TestService struct {
factory *TestServiceFactory
name string
config *config.ServiceConfig
EmptyService
Count int
}
func (t *TestService) Config() *config.ServiceConfig {
return t.config
}
func (t *TestService) Name() string {
return t.name
}
func (t *TestService) Run(ctx context.Context, commandParts []string) (int, error) {
return 0, nil
}
func (t *TestService) Create(options options.Create) error {
key := t.name + ".create"
t.factory.Counts[key] = t.factory.Counts[key] + 1
return nil
}
func (t *TestService) DependentServices() []ServiceRelationship {
return nil
}
func (t *TestServiceFactory) Create(project *Project, name string, serviceConfig *config.ServiceConfig) (Service, error) {
return &TestService{
factory: t,
config: serviceConfig,
name: name,
}, nil
}
func TestTwoCall(t *testing.T) {
factory := &TestServiceFactory{
Counts: map[string]int{},
}
p := NewProject(nil, &Context{
ServiceFactory: factory,
})
p.ServiceConfigs = config.NewServiceConfigs()
p.ServiceConfigs.Add("foo", &config.ServiceConfig{})
if err := p.Create(options.Create{}, "foo"); err != nil {
t.Fatal(err)
}
if err := p.Create(options.Create{}, "foo"); err != nil {
t.Fatal(err)
}
if factory.Counts["foo.create"] != 2 {
t.Fatal("Failed to create twice")
}
}
func TestParseWithBadContent(t *testing.T) {
p := NewProject(nil, &Context{
ComposeBytes: [][]byte{
[]byte("garbage"),
},
})
err := p.Parse()
if err == nil {
t.Fatal("Should have failed parse")
}
if !strings.HasPrefix(err.Error(), "Invalid timestamp: 'garbage'") {
t.Fatalf("Should have failed parse: %#v", err)
}
}
func TestParseWithGoodContent(t *testing.T) {
p := NewProject(nil, &Context{
ComposeBytes: [][]byte{
[]byte("not-garbage:\n image: foo"),
},
})
err := p.Parse()
if err != nil {
t.Fatal(err)
}
}
type TestEnvironmentLookup struct {
}
func (t *TestEnvironmentLookup) Lookup(key, serviceName string, config *config.ServiceConfig) []string {
return []string{fmt.Sprintf("%s=X", key)}
}
func TestEnvironmentResolve(t *testing.T) {
factory := &TestServiceFactory{
Counts: map[string]int{},
}
p := NewProject(nil, &Context{
ServiceFactory: factory,
EnvironmentLookup: &TestEnvironmentLookup{},
})
p.ServiceConfigs = config.NewServiceConfigs()
p.ServiceConfigs.Add("foo", &config.ServiceConfig{
Environment: yaml.MaporEqualSlice([]string{
"A",
"A=",
"A=B",
}),
})
service, err := p.CreateService("foo")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(service.Config().Environment, yaml.MaporEqualSlice{"A=X", "A=X", "A=B"}) {
t.Fatal("Invalid environment", service.Config().Environment)
}
}
func TestParseWithMultipleComposeFiles(t *testing.T) {
/*
configOne := []byte(`
multiple:
image: tianon/true
ports:
- 8000`)
configTwo := []byte(`
multiple:
image: busybox
container_name: multi
ports:
- 9000`)
configThree := []byte(`
multiple:
image: busybox
mem_limit: 40000000
ports:
- 10000`)
*/
configOne := []byte(`
multiple:
image: tianon/true`)
configTwo := []byte(`
multiple:
image: busybox
container_name: multi`)
configThree := []byte(`
multiple:
image: busybox
size: xxs`)
p := NewProject(nil, &Context{
ComposeBytes: [][]byte{configOne, configTwo},
})
err := p.Parse()
assert.Nil(t, err)
multipleConfig, _ := p.ServiceConfigs.Get("multiple")
assert.Equal(t, "busybox", multipleConfig.Image)
assert.Equal(t, "multi", multipleConfig.ContainerName)
//assert.Equal(t, []string{"8000", "9000"}, multipleConfig.Ports)
p = NewProject(nil, &Context{
ComposeBytes: [][]byte{configTwo, configOne},
})
err = p.Parse()
assert.Nil(t, err)
multipleConfig, _ = p.ServiceConfigs.Get("multiple")
assert.Equal(t, "tianon/true", multipleConfig.Image)
assert.Equal(t, "multi", multipleConfig.ContainerName)
//assert.Equal(t, []string{"9000", "8000"}, multipleConfig.Ports)
p = NewProject(nil, &Context{
ComposeBytes: [][]byte{configOne, configTwo, configThree},
})
err = p.Parse()
assert.Nil(t, err)
multipleConfig, _ = p.ServiceConfigs.Get("multiple")
assert.Equal(t, "busybox", multipleConfig.Image)
assert.Equal(t, "multi", multipleConfig.ContainerName)
//assert.Equal(t, []string{"8000", "9000", "10000"}, multipleConfig.Ports)
//assert.Equal(t, int64(40000000), multipleConfig.MemLimit)
}

View File

@@ -0,0 +1,115 @@
package project
import (
"sync"
log "github.com/Sirupsen/logrus"
"github.com/hyperhq/libcompose/project/events"
)
type serviceWrapper struct {
name string
service Service
done sync.WaitGroup
state ServiceState
err error
project *Project
noWait bool
ignored map[string]bool
}
func newServiceWrapper(name string, p *Project) (*serviceWrapper, error) {
wrapper := &serviceWrapper{
name: name,
state: StateUnknown,
project: p,
ignored: map[string]bool{},
}
return wrapper, wrapper.Reset()
}
func (s *serviceWrapper) IgnoreDep(name string) {
s.ignored[name] = true
}
func (s *serviceWrapper) Reset() error {
if s.state != StateExecuted {
service, err := s.project.CreateService(s.name)
if err != nil {
log.Errorf("Failed to create service for %s : %v", s.name, err)
return err
}
s.service = service
}
if s.err == ErrRestart {
s.err = nil
}
s.done.Add(1)
return nil
}
func (s *serviceWrapper) Ignore() {
defer s.done.Done()
s.state = StateExecuted
s.project.Notify(events.ServiceUpIgnored, s.service.Name(), nil)
}
func (s *serviceWrapper) waitForDeps(wrappers map[string]*serviceWrapper) bool {
if s.noWait {
return true
}
for _, dep := range s.service.DependentServices() {
if s.ignored[dep.Target] {
continue
}
if wrapper, ok := wrappers[dep.Target]; ok {
if wrapper.Wait() == ErrRestart {
s.project.Notify(events.ProjectReload, wrapper.service.Name(), nil)
s.err = ErrRestart
return false
}
} else {
log.Errorf("Failed to find %s", dep.Target)
}
}
return true
}
func (s *serviceWrapper) Do(wrappers map[string]*serviceWrapper, start, done events.EventType, action func(service Service) error) {
defer s.done.Done()
if s.state == StateExecuted {
return
}
if wrappers != nil && !s.waitForDeps(wrappers) {
return
}
s.state = StateExecuted
s.project.Notify(start, s.service.Name(), nil)
s.err = action(s.service)
if s.err == ErrRestart {
s.project.Notify(done, s.service.Name(), nil)
s.project.Notify(events.ProjectReloadTrigger, s.service.Name(), nil)
} else if s.err != nil {
log.Errorf("Failed %s %s : %v", start, s.name, s.err)
} else {
s.project.Notify(done, s.service.Name(), nil)
}
}
func (s *serviceWrapper) Wait() error {
s.done.Wait()
return s.err
}

View File

@@ -0,0 +1,89 @@
package project
import (
"errors"
"golang.org/x/net/context"
"github.com/hyperhq/libcompose/config"
"github.com/hyperhq/libcompose/project/options"
)
// Service defines what a libcompose service provides.
type Service interface {
Info(qFlag bool) (InfoSet, error)
Name() string
Build(buildOptions options.Build) error
Create(options options.Create) error
Up(options options.Up) error
Start() error
Stop(timeout int) error
Delete(options options.Delete) error
Restart(timeout int) error
Log(follow bool) error
Pull() error
Kill(signal string) error
Config() *config.ServiceConfig
DependentServices() []ServiceRelationship
Containers() ([]Container, error)
Scale(count int, timeout int) error
Pause() error
Unpause() error
Run(ctx context.Context, commandParts []string) (int, error)
RemoveImage(imageType options.ImageType) error
}
// ServiceState holds the state of a service.
type ServiceState string
// State definitions
var (
StateExecuted = ServiceState("executed")
StateUnknown = ServiceState("unknown")
)
// Error definitions
var (
ErrRestart = errors.New("Restart execution")
ErrUnsupported = errors.New("UnsupportedOperation")
)
// ServiceFactory is an interface factory to create Service object for the specified
// project, with the specified name and service configuration.
type ServiceFactory interface {
Create(project *Project, name string, serviceConfig *config.ServiceConfig) (Service, error)
}
// ServiceRelationshipType defines the type of service relationship.
type ServiceRelationshipType string
// RelTypeLink means the services are linked (docker links).
const RelTypeLink = ServiceRelationshipType("")
// RelTypeNetNamespace means the services share the same network namespace.
const RelTypeNetNamespace = ServiceRelationshipType("netns")
// RelTypeIpcNamespace means the service share the same ipc namespace.
const RelTypeIpcNamespace = ServiceRelationshipType("ipc")
// RelTypeVolumesFrom means the services share some volumes.
const RelTypeVolumesFrom = ServiceRelationshipType("volumesFrom")
// ServiceRelationship holds the relationship information between two services.
type ServiceRelationship struct {
Target, Alias string
Type ServiceRelationshipType
Optional bool
}
// NewServiceRelationship creates a new Relationship based on the specified alias
// and relationship type.
func NewServiceRelationship(nameAlias string, relType ServiceRelationshipType) ServiceRelationship {
name, alias := NameAlias(nameAlias)
return ServiceRelationship{
Target: name,
Alias: alias,
Type: relType,
}
}

70
vendor/github.com/hyperhq/libcompose/project/utils.go generated vendored Normal file
View File

@@ -0,0 +1,70 @@
package project
import (
"strings"
"github.com/docker/engine-api/types/container"
)
// DefaultDependentServices return the dependent services (as an array of ServiceRelationship)
// for the specified project and service. It looks for : links, volumesFrom, net and ipc configuration.
func DefaultDependentServices(p *Project, s Service) []ServiceRelationship {
config := s.Config()
if config == nil {
return []ServiceRelationship{}
}
result := []ServiceRelationship{}
for _, link := range config.Links {
result = append(result, NewServiceRelationship(link, RelTypeLink))
}
/*
for _, volumesFrom := range config.VolumesFrom {
result = append(result, NewServiceRelationship(volumesFrom, RelTypeVolumesFrom))
}
result = appendNs(p, result, s.Config().NetworkMode, RelTypeNetNamespace)
result = appendNs(p, result, s.Config().Ipc, RelTypeIpcNamespace)
*/
return result
}
func appendNs(p *Project, rels []ServiceRelationship, conf string, relType ServiceRelationshipType) []ServiceRelationship {
service := GetContainerFromIpcLikeConfig(p, conf)
if service != "" {
rels = append(rels, NewServiceRelationship(service, relType))
}
return rels
}
// NameAlias returns the name and alias based on the specified string.
// If the name contains a colon (like name:alias) it will split it, otherwise
// it will return the specified name as name and alias.
func NameAlias(name string) (string, string) {
parts := strings.SplitN(name, ":", 2)
if len(parts) == 2 {
return parts[0], parts[1]
}
return parts[0], parts[0]
}
// GetContainerFromIpcLikeConfig returns name of the service that shares the IPC
// namespace with the specified service.
func GetContainerFromIpcLikeConfig(p *Project, conf string) string {
ipc := container.IpcMode(conf)
if !ipc.IsContainer() {
return ""
}
name := ipc.Container()
if name == "" {
return ""
}
if p.ServiceConfigs.Has(name) {
return name
}
return ""
}

386
vendor/github.com/hyperhq/libcompose/test/nop.go generated vendored Normal file
View File

@@ -0,0 +1,386 @@
package test
import (
"errors"
"io"
"golang.org/x/net/context"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/container"
"github.com/docker/engine-api/types/filters"
"github.com/docker/engine-api/types/network"
"github.com/docker/engine-api/types/registry"
)
var (
errNoEngine = errors.New("Engine no longer exists")
)
// NopClient is a nop API Client based on engine-api
type NopClient struct {
}
// NewNopClient creates a new nop client
func NewNopClient() *NopClient {
return &NopClient{}
}
// ClientVersion returns the version string associated with this instance of the Client
func (client *NopClient) ClientVersion() string {
return ""
}
// ContainerAttach attaches a connection to a container in the server
func (client *NopClient) ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) {
return types.HijackedResponse{}, errNoEngine
}
// ContainerCommit applies changes into a container and creates a new tagged image
func (client *NopClient) ContainerCommit(ctx context.Context, container string, options types.ContainerCommitOptions) (types.ContainerCommitResponse, error) {
return types.ContainerCommitResponse{}, errNoEngine
}
// ContainerCreate creates a new container based in the given configuration
func (client *NopClient) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (types.ContainerCreateResponse, error) {
return types.ContainerCreateResponse{}, errNoEngine
}
// ContainerDiff shows differences in a container filesystem since it was started
func (client *NopClient) ContainerDiff(ctx context.Context, container string) ([]types.ContainerChange, error) {
return nil, errNoEngine
}
// ContainerExecAttach attaches a connection to an exec process in the server
func (client *NopClient) ContainerExecAttach(ctx context.Context, execID string, config types.ExecConfig) (types.HijackedResponse, error) {
return types.HijackedResponse{}, errNoEngine
}
// ContainerExecCreate creates a new exec configuration to run an exec process
func (client *NopClient) ContainerExecCreate(ctx context.Context, container string, config types.ExecConfig) (types.ContainerExecCreateResponse, error) {
return types.ContainerExecCreateResponse{}, errNoEngine
}
// ContainerExecInspect returns information about a specific exec process on the docker host
func (client *NopClient) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) {
return types.ContainerExecInspect{}, errNoEngine
}
// ContainerExecResize changes the size of the tty for an exec process running inside a container
func (client *NopClient) ContainerExecResize(ctx context.Context, execID string, options types.ResizeOptions) error {
return errNoEngine
}
// ContainerExecStart starts an exec process already create in the docker host
func (client *NopClient) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
return errNoEngine
}
// ContainerExport retrieves the raw contents of a container and returns them as an io.ReadCloser
func (client *NopClient) ContainerExport(ctx context.Context, container string) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ContainerInspect returns the container information
func (client *NopClient) ContainerInspect(ctx context.Context, container string) (types.ContainerJSON, error) {
return types.ContainerJSON{}, errNoEngine
}
// ContainerInspectWithRaw returns the container information and its raw representation
func (client *NopClient) ContainerInspectWithRaw(ctx context.Context, container string, getSize bool) (types.ContainerJSON, []byte, error) {
return types.ContainerJSON{}, nil, errNoEngine
}
// ContainerKill terminates the container process but does not remove the container from the docker host
func (client *NopClient) ContainerKill(ctx context.Context, container string, signal string) error {
return errNoEngine
}
// ContainerList returns the list of containers in the docker host
func (client *NopClient) ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) {
return nil, errNoEngine
}
// ContainerLogs returns the logs generated by a container in an io.ReadCloser
func (client *NopClient) ContainerLogs(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ContainerPause pauses the main process of a given container without terminating it
func (client *NopClient) ContainerPause(ctx context.Context, container string) error {
return errNoEngine
}
// ContainerRemove kills and removes a container from the docker host
func (client *NopClient) ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) ([]string, error) {
return []string{}, errNoEngine
}
// ContainerRename changes the name of a given container
func (client *NopClient) ContainerRename(ctx context.Context, container, newContainerName string) error {
return errNoEngine
}
// ContainerResize changes the size of the tty for a container
func (client *NopClient) ContainerResize(ctx context.Context, container string, options types.ResizeOptions) error {
return errNoEngine
}
// ContainerRestart stops and starts a container again
func (client *NopClient) ContainerRestart(ctx context.Context, container string, timeout int) error {
return errNoEngine
}
// ContainerStatPath returns Stat information about a path inside the container filesystem
func (client *NopClient) ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error) {
return types.ContainerPathStat{}, errNoEngine
}
// ContainerStats returns near realtime stats for a given container
func (client *NopClient) ContainerStats(ctx context.Context, container string, stream bool) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ContainerStart sends a request to the docker daemon to start a container
func (client *NopClient) ContainerStart(ctx context.Context, container, checkpointID string) error {
return errNoEngine
}
// ContainerStop stops a container without terminating the process
func (client *NopClient) ContainerStop(ctx context.Context, container string, timeout int) error {
return errNoEngine
}
// ContainerTop shows process information from within a container
func (client *NopClient) ContainerTop(ctx context.Context, container string, arguments []string) (types.ContainerProcessList, error) {
return types.ContainerProcessList{}, errNoEngine
}
// ContainerUnpause resumes the process execution within a container
func (client *NopClient) ContainerUnpause(ctx context.Context, container string) error {
return errNoEngine
}
// ContainerUpdate updates resources of a container
func (client *NopClient) ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) error {
return errNoEngine
}
// ContainerWait pauses execution until a container exits
func (client *NopClient) ContainerWait(ctx context.Context, container string) (int, error) {
return 0, errNoEngine
}
// CopyFromContainer gets the content from the container and returns it as a Reader to manipulate it in the host
func (client *NopClient) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
return nil, types.ContainerPathStat{}, errNoEngine
}
// CopyToContainer copies content into the container filesystem
func (client *NopClient) CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error {
return errNoEngine
}
// Events returns a stream of events in the daemon in a ReadCloser
func (client *NopClient) Events(ctx context.Context, options types.EventsOptions) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ImageBuild sends request to the daemon to build images
func (client *NopClient) ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
return types.ImageBuildResponse{}, errNoEngine
}
// ImageCreate creates a new image based in the parent options
func (client *NopClient) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ImageHistory returns the changes in an image in history format
func (client *NopClient) ImageHistory(ctx context.Context, image string) ([]types.ImageHistory, error) {
return nil, errNoEngine
}
// ImageImport creates a new image based in the source options
func (client *NopClient) ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ImageInspectWithRaw returns the image information and it's raw representation
func (client *NopClient) ImageInspectWithRaw(ctx context.Context, image string, getSize bool) (types.ImageInspect, []byte, error) {
return types.ImageInspect{}, nil, errNoEngine
}
// ImageList returns a list of images in the docker host
func (client *NopClient) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.Image, error) {
return nil, errNoEngine
}
// ImageLoad loads an image in the docker host from the client host
func (client *NopClient) ImageLoad(ctx context.Context, input interface{}) (types.ImageLoadResponse, error) {
return types.ImageLoadResponse{}, errNoEngine
}
// ImagePull requests the docker host to pull an image from a remote registry
func (client *NopClient) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ImagePush requests the docker host to push an image to a remote registry
func (client *NopClient) ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ImageRemove removes an image from the docker host
func (client *NopClient) ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDelete, error) {
return nil, errNoEngine
}
// ImageSearch makes the docker host to search by a term in a remote registry
func (client *NopClient) ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error) {
return nil, errNoEngine
}
// ImageSave retrieves one or more images from the docker host as an io.ReadCloser
func (client *NopClient) ImageSave(ctx context.Context, images []string) (io.ReadCloser, error) {
return nil, errNoEngine
}
// ImageTag tags an image in the docker host
func (client *NopClient) ImageTag(ctx context.Context, image, ref string, options types.ImageTagOptions) error {
return errNoEngine
}
// Info returns information about the docker server
func (client *NopClient) Info(ctx context.Context) (types.Info, error) {
return types.Info{}, errNoEngine
}
// NetworkConnect connects a container to an existent network in the docker host
func (client *NopClient) NetworkConnect(ctx context.Context, networkID, container string, config *network.EndpointSettings) error {
return errNoEngine
}
// NetworkCreate creates a new network in the docker host
func (client *NopClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
return types.NetworkCreateResponse{}, errNoEngine
}
// NetworkDisconnect disconnects a container from an existent network in the docker host
func (client *NopClient) NetworkDisconnect(ctx context.Context, networkID, container string, force bool) error {
return errNoEngine
}
// NetworkInspect returns the information for a specific network configured in the docker host
func (client *NopClient) NetworkInspect(ctx context.Context, networkID string) (types.NetworkResource, error) {
return types.NetworkResource{}, errNoEngine
}
// NetworkInspectWithRaw returns the information for a specific network configured in the docker host and it's raw representation.
func (cli *NopClient) NetworkInspectWithRaw(ctx context.Context, networkID string) (types.NetworkResource, []byte, error) {
return types.NetworkResource{}, []byte{}, errNoEngine
}
// NetworkList returns the list of networks configured in the docker host
func (client *NopClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
return nil, errNoEngine
}
// NetworkRemove removes an existent network from the docker host
func (client *NopClient) NetworkRemove(ctx context.Context, networkID string) error {
return errNoEngine
}
// RegistryLogin authenticates the docker server with a given docker registry
func (client *NopClient) RegistryLogin(ctx context.Context, auth types.AuthConfig) (types.AuthResponse, error) {
return types.AuthResponse{}, errNoEngine
}
// ServerVersion returns information of the docker client and server host
func (client *NopClient) ServerVersion(ctx context.Context) (types.Version, error) {
return types.Version{}, errNoEngine
}
// UpdateClientVersion updates the version string associated with this instance of the Client
func (client *NopClient) UpdateClientVersion(v string) {
}
// VolumeCreate creates a volume in the docker host
func (client *NopClient) VolumeCreate(ctx context.Context, options types.VolumeCreateRequest) (types.Volume, error) {
return types.Volume{}, errNoEngine
}
// VolumeInspect returns the information about a specific volume in the docker host
func (client *NopClient) VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error) {
return types.Volume{}, errNoEngine
}
// VolumeInspectWithRaw returns the information about a specific volume in the docker host
func (client *NopClient) VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error) {
return types.Volume{}, []byte{}, errNoEngine
}
// VolumeList returns the volumes configured in the docker host
func (client *NopClient) VolumeList(ctx context.Context, filter filters.Args) (types.VolumesListResponse, error) {
return types.VolumesListResponse{}, errNoEngine
}
// VolumeRemove removes a volume from the docker host
func (client *NopClient) VolumeRemove(ctx context.Context, volumeID string) error {
return errNoEngine
}
// CheckpointCreate creates a checkpoint from the given container with the given name
func (client *NopClient) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error {
return errNoEngine
}
// CheckpointDelete deletes the checkpoint with the given name from the given container
func (cli *NopClient) CheckpointDelete(ctx context.Context, containerID string, checkpointID string) error {
return errNoEngine
}
// CheckpointList returns the volumes configured in the docker host.
func (cli *NopClient) CheckpointList(ctx context.Context, container string) ([]types.Checkpoint, error) {
return []types.Checkpoint{}, errNoEngine
}
func (cli *NopClient) FipAllocate(ctx context.Context, count string) ([]string, error) {
return []string{}, errNoEngine
}
func (cli *NopClient) FipRelease(ctx context.Context, ip string) error {
return errNoEngine
}
func (cli *NopClient) FipAssociate(ctx context.Context, ip, container string) error {
return errNoEngine
}
func (cli *NopClient) FipDisassociate(ctx context.Context, container string) (string, error) {
return "", errNoEngine
}
func (cli *NopClient) FipList(ctx context.Context, options types.NetworkListOptions) ([]map[string]string, error) {
return []map[string]string{}, errNoEngine
}
// SnapshotList returns the snapshots configured in the docker host.
func (cli *NopClient) SnapshotList(ctx context.Context, filter filters.Args) (types.SnapshotsListResponse, error) {
return types.SnapshotsListResponse{}, errNoEngine
}
// SnapshotInspect returns the information about a specific snapshot in the docker host.
func (cli *NopClient) SnapshotInspect(ctx context.Context, snapshotID string) (types.Snapshot, error) {
return types.Snapshot{}, errNoEngine
}
// SnapshotCreate creates a snapshot in the docker host.
func (cli *NopClient) SnapshotCreate(ctx context.Context, options types.SnapshotCreateRequest) (types.Snapshot, error) {
return types.Snapshot{}, errNoEngine
}
// SnapshotRemove removes a snapshot from the docker host.
func (cli *NopClient) SnapshotRemove(ctx context.Context, snapshotID string) error {
return errNoEngine
}

136
vendor/github.com/hyperhq/libcompose/utils/util.go generated vendored Normal file
View File

@@ -0,0 +1,136 @@
package utils
import (
"encoding/json"
"sync"
"github.com/Sirupsen/logrus"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
)
// InParallel holds a pool and a waitgroup to execute tasks in parallel and to be able
// to wait for completion of all tasks.
type InParallel struct {
wg sync.WaitGroup
pool sync.Pool
}
// Add runs the specified task in parallel and adds it to the waitGroup.
func (i *InParallel) Add(task func() error) {
i.wg.Add(1)
go func() {
defer i.wg.Done()
err := task()
if err != nil {
i.pool.Put(err)
}
}()
}
// Wait waits for all tasks to complete and returns the latest error encountered if any.
func (i *InParallel) Wait() error {
i.wg.Wait()
obj := i.pool.Get()
if err, ok := obj.(error); ok {
return err
}
return nil
}
// ConvertByJSON converts a struct (src) to another one (target) using json marshalling/unmarshalling.
// If the structure are not compatible, this will throw an error as the unmarshalling will fail.
func ConvertByJSON(src, target interface{}) error {
newBytes, err := json.Marshal(src)
if err != nil {
return err
}
err = json.Unmarshal(newBytes, target)
if err != nil {
logrus.Errorf("Failed to unmarshall: %v\n%s", err, string(newBytes))
}
return err
}
// Convert converts a struct (src) to another one (target) using yaml marshalling/unmarshalling.
// If the structure are not compatible, this will throw an error as the unmarshalling will fail.
func Convert(src, target interface{}) error {
newBytes, err := yaml.Marshal(src)
if err != nil {
return err
}
err = yaml.Unmarshal(newBytes, target)
if err != nil {
logrus.Errorf("Failed to unmarshall: %v\n%s", err, string(newBytes))
}
return err
}
// CopySlice creates an exact copy of the provided string slice
func CopySlice(s []string) []string {
if s == nil {
return nil
}
r := make([]string, len(s))
copy(r, s)
return r
}
// CopyMap creates an exact copy of the provided string-to-string map
func CopyMap(m map[string]string) map[string]string {
if m == nil {
return nil
}
r := map[string]string{}
for k, v := range m {
r[k] = v
}
return r
}
// FilterStringSet accepts a string set `s` (in the form of `map[string]bool`) and a filtering function `f`
// and returns a string set containing only the strings `x` for which `f(x) == true`
func FilterStringSet(s map[string]bool, f func(x string) bool) map[string]bool {
result := map[string]bool{}
for k := range s {
if f(k) {
result[k] = true
}
}
return result
}
// FilterString returns a json representation of the specified map
// that is used as filter for docker.
func FilterString(data map[string][]string) string {
// I can't imagine this would ever fail
bytes, _ := json.Marshal(data)
return string(bytes)
}
// Contains checks if the specified string (key) is present in the specified collection.
func Contains(collection []string, key string) bool {
for _, value := range collection {
if value == key {
return true
}
}
return false
}
// Merge performs a union of two string slices: the result is an unordered slice
// that includes every item from either argument exactly once
func Merge(coll1, coll2 []string) []string {
m := map[string]struct{}{}
for _, v := range append(coll1, coll2...) {
m[v] = struct{}{}
}
r := make([]string, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}

View File

@@ -0,0 +1,85 @@
// +build !race
package utils
import (
"fmt"
"sync"
"testing"
)
type safeMap struct {
mu sync.RWMutex
m map[int]bool
}
func (s *safeMap) Add(index int, ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
s.m[index] = ok
}
func (s *safeMap) Read() map[int]bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.m
}
func TestInParallel(t *testing.T) {
size := 5
booleanMap := safeMap{
m: make(map[int]bool, size+1),
}
tasks := InParallel{}
for i := 0; i < size; i++ {
task := func(index int) func() error {
return func() error {
booleanMap.Add(index, true)
return nil
}
}(i)
tasks.Add(task)
}
err := tasks.Wait()
if err != nil {
t.Fatal(err)
}
// Make sure every value is true
for _, value := range booleanMap.Read() {
if !value {
t.Fatalf("booleanMap expected to contain only true values, got at least one false")
}
}
}
func TestInParallelError(t *testing.T) {
size := 5
booleanMap := safeMap{
m: make(map[int]bool, size+1),
}
tasks := InParallel{}
for i := 0; i < size; i++ {
task := func(index int) func() error {
return func() error {
booleanMap.Add(index, false)
t.Log("index", index)
if index%2 == 0 {
t.Log("return an error for", index)
return fmt.Errorf("Error with %v", index)
}
booleanMap.Add(index, true)
return nil
}
}(i)
tasks.Add(task)
}
err := tasks.Wait()
if err == nil {
t.Fatalf("Expected an error on Wait, got nothing.")
}
for key, value := range booleanMap.Read() {
if key%2 != 0 && !value {
t.Fatalf("booleanMap expected to contain true values on odd number, got %v", booleanMap)
}
}
}

213
vendor/github.com/hyperhq/libcompose/utils/util_test.go generated vendored Normal file
View File

@@ -0,0 +1,213 @@
package utils
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type jsonfrom struct {
Element1 string `json:"element2"`
Element2 int `json:"element1"`
}
type jsonto struct {
Elt1 int `json:"element1"`
Elt2 string `json:"element2"`
Elt3 int
}
func TestConvertByJSON(t *testing.T) {
valids := []struct {
src jsonfrom
expected jsonto
}{
{
jsonfrom{Element2: 1},
jsonto{1, "", 0},
},
{
jsonfrom{},
jsonto{0, "", 0},
},
{
jsonfrom{"element1", 2},
jsonto{2, "element1", 0},
},
}
for _, valid := range valids {
var target jsonto
err := ConvertByJSON(valid.src, &target)
if err != nil || target.Elt1 != valid.expected.Elt1 || target.Elt2 != valid.expected.Elt2 || target.Elt3 != 0 {
t.Fatalf("Expected %v from %v got %v, %v", valid.expected, valid.src, target, err)
}
}
}
func TestConvertByJSONInvalid(t *testing.T) {
invalids := []interface{}{
// Incompatible struct
struct {
Element1 int `json:"element2"`
Element2 string `json:"element1"`
}{1, "element1"},
// Not marshable struct
struct {
Element1 func(int) int
}{
func(i int) int { return 0 },
},
}
for _, invalid := range invalids {
var target jsonto
if err := ConvertByJSON(invalid, &target); err == nil {
t.Fatalf("Expected an error converting %v to %v, got nothing", invalid, target)
}
}
}
type yamlfrom struct {
Element1 string `yaml:"element2"`
Element2 int `yaml:"element1"`
}
type yamlto struct {
Elt1 int `yaml:"element1"`
Elt2 string `yaml:"element2"`
Elt3 int
}
func TestConvert(t *testing.T) {
valids := []struct {
src yamlfrom
expected yamlto
}{
{
yamlfrom{Element2: 1},
yamlto{1, "", 0},
},
{
yamlfrom{},
yamlto{0, "", 0},
},
{
yamlfrom{"element1", 2},
yamlto{2, "element1", 0},
},
}
for _, valid := range valids {
var target yamlto
err := Convert(valid.src, &target)
if err != nil || target.Elt1 != valid.expected.Elt1 || target.Elt2 != valid.expected.Elt2 || target.Elt3 != 0 {
t.Fatalf("Expected %v from %v got %v, %v", valid.expected, valid.src, target, err)
}
}
}
func TestConvertInvalid(t *testing.T) {
invalids := []interface{}{
// Incompatible struct
struct {
Element1 int `yaml:"element2"`
Element2 string `yaml:"element1"`
}{1, "element1"},
// Not marshable struct
// This one panics :-|
// struct {
// Element1 func(int) int
// }{
// func(i int) int { return 0 },
// },
}
for _, invalid := range invalids {
var target yamlto
if err := Convert(invalid, &target); err == nil {
t.Fatalf("Expected an error converting %v to %v, got nothing", invalid, target)
}
}
}
func TestFilterStringSet(t *testing.T) {
s := map[string]bool{"abcd": true, "b": true, "cde": true, "d": true, "ef": true}
expected := map[string]bool{"abcd": true, "cde": true}
result := FilterStringSet(s, func(x string) bool { return len(x) > 2 })
assert.Equal(t, expected, result)
}
func TestFilterString(t *testing.T) {
datas := []struct {
value map[string][]string
expected string
}{
{
map[string][]string{},
"{}",
},
{
map[string][]string{
"key": {},
},
`{"key":[]}`,
},
{
map[string][]string{
"key": {"value1", "value2"},
},
`{"key":["value1","value2"]}`,
},
{
map[string][]string{
"key1": {"value1", "value2"},
"key2": {"value3", "value4"},
},
`{"key1":["value1","value2"],"key2":["value3","value4"]}`,
},
}
for _, data := range datas {
actual := FilterString(data.value)
if actual != data.expected {
t.Fatalf("Expected '%v' for %v, got '%v'", data.expected, data.value, actual)
}
}
}
func TestContains(t *testing.T) {
cases := []struct {
collection []string
key string
contains bool
}{
{
[]string{}, "", false,
},
{
[]string{""}, "", true,
},
{
[]string{"value1", "value2"}, "value3", false,
},
{
[]string{"value1", "value2"}, "value1", true,
},
{
[]string{"value1", "value2"}, "value2", true,
},
}
for _, element := range cases {
actual := Contains(element.collection, element.key)
if actual != element.contains {
t.Fatalf("Expected contains to be %v for %v in %v, but was %v", element.contains, element.key, element.collection, actual)
}
}
}
func TestMerge(t *testing.T) {
a := []string{"a", "b", "c"}
b := []string{"b", "c", "d", "e"}
expected := []string{"a", "b", "c", "d", "e"}
r := Merge(a, b)
assert.Equal(t, len(expected), len(r))
for _, v := range expected {
assert.True(t, Contains(r, v))
}
assert.Equal(t, "a:b", fmt.Sprint("a", ":", "b"))
}

288
vendor/github.com/hyperhq/libcompose/yaml/types_yaml.go generated vendored Normal file
View File

@@ -0,0 +1,288 @@
package yaml
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"github.com/docker/engine-api/types/strslice"
"github.com/flynn/go-shlex"
)
// Stringorslice represents a string or an array of strings.
// Using engine-api Strslice and augment it with YAML marshalling stuff.
type Stringorslice strslice.StrSlice
// UnmarshalYAML implements the Unmarshaller interface.
func (s *Stringorslice) UnmarshalYAML(tag string, value interface{}) error {
switch value := value.(type) {
case []interface{}:
parts, err := toStrings(value)
if err != nil {
return err
}
*s = parts
case string:
*s = []string{value}
default:
return fmt.Errorf("Failed to unmarshal Stringorslice: %#v", value)
}
return nil
}
// Ulimits represents a list of Ulimit.
// It is, however, represented in yaml as keys (and thus map in Go)
type Ulimits struct {
Elements []Ulimit
}
// MarshalYAML implements the Marshaller interface.
func (u Ulimits) MarshalYAML() (tag string, value interface{}, err error) {
ulimitMap := make(map[string]Ulimit)
for _, ulimit := range u.Elements {
ulimitMap[ulimit.Name] = ulimit
}
return "", ulimitMap, nil
}
// UnmarshalYAML implements the Unmarshaller interface.
func (u *Ulimits) UnmarshalYAML(tag string, value interface{}) error {
ulimits := make(map[string]Ulimit)
yamlUlimits := reflect.ValueOf(value)
switch yamlUlimits.Kind() {
case reflect.Map:
for _, key := range yamlUlimits.MapKeys() {
var name string
var soft, hard int64
mapValue := yamlUlimits.MapIndex(key).Elem()
name = key.Elem().String()
switch mapValue.Kind() {
case reflect.Int64:
soft = mapValue.Int()
hard = mapValue.Int()
case reflect.Map:
if len(mapValue.MapKeys()) != 2 {
return fmt.Errorf("Failed to unmarshal Ulimit: %#v", mapValue)
}
for _, subKey := range mapValue.MapKeys() {
subValue := mapValue.MapIndex(subKey).Elem()
switch subKey.Elem().String() {
case "soft":
soft = subValue.Int()
case "hard":
hard = subValue.Int()
}
}
default:
return fmt.Errorf("Failed to unmarshal Ulimit: %#v, %v", mapValue, mapValue.Kind())
}
ulimits[name] = Ulimit{
Name: name,
ulimitValues: ulimitValues{
Soft: soft,
Hard: hard,
},
}
}
keys := make([]string, 0, len(ulimits))
for key := range ulimits {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
u.Elements = append(u.Elements, ulimits[key])
}
default:
return fmt.Errorf("Failed to unmarshal Ulimit: %#v", value)
}
return nil
}
// Ulimit represents ulimit information.
type Ulimit struct {
ulimitValues
Name string
}
type ulimitValues struct {
Soft int64 `yaml:"soft"`
Hard int64 `yaml:"hard"`
}
// MarshalYAML implements the Marshaller interface.
func (u Ulimit) MarshalYAML() (tag string, value interface{}, err error) {
if u.Soft == u.Hard {
return "", u.Soft, nil
}
return "", u.ulimitValues, err
}
// NewUlimit creates a Ulimit based on the specified parts.
func NewUlimit(name string, soft int64, hard int64) Ulimit {
return Ulimit{
Name: name,
ulimitValues: ulimitValues{
Soft: soft,
Hard: hard,
},
}
}
// Command represents a docker command, can be a string or an array of strings.
type Command strslice.StrSlice
// UnmarshalYAML implements the Unmarshaller interface.
func (s *Command) UnmarshalYAML(tag string, value interface{}) error {
switch value := value.(type) {
case []interface{}:
parts, err := toStrings(value)
if err != nil {
return err
}
*s = parts
case string:
parts, err := shlex.Split(value)
if err != nil {
return err
}
*s = parts
default:
return fmt.Errorf("Failed to unmarshal Command: %#v", value)
}
return nil
}
// SliceorMap represents a slice or a map of strings.
type SliceorMap map[string]string
// UnmarshalYAML implements the Unmarshaller interface.
func (s *SliceorMap) UnmarshalYAML(tag string, value interface{}) error {
switch value := value.(type) {
case map[interface{}]interface{}:
parts := map[string]string{}
for k, v := range value {
if sk, ok := k.(string); ok {
if sv, ok := v.(string); ok {
parts[sk] = sv
} else {
return fmt.Errorf("Cannot unmarshal '%v' of type %T into a string value", v, v)
}
} else {
return fmt.Errorf("Cannot unmarshal '%v' of type %T into a string value", k, k)
}
}
*s = parts
case []interface{}:
parts := map[string]string{}
for _, s := range value {
if str, ok := s.(string); ok {
str := strings.TrimSpace(str)
keyValueSlice := strings.SplitN(str, "=", 2)
key := keyValueSlice[0]
val := ""
if len(keyValueSlice) == 2 {
val = keyValueSlice[1]
}
parts[key] = val
} else {
return fmt.Errorf("Cannot unmarshal '%v' of type %T into a string value", s, s)
}
}
*s = parts
default:
return fmt.Errorf("Failed to unmarshal SliceorMap: %#v", value)
}
return nil
}
// MaporEqualSlice represents a slice of strings that gets unmarshal from a
// YAML map into 'key=value' string.
type MaporEqualSlice []string
// UnmarshalYAML implements the Unmarshaller interface.
func (s *MaporEqualSlice) UnmarshalYAML(tag string, value interface{}) error {
parts, err := unmarshalToStringOrSepMapParts(value, "=")
if err != nil {
return err
}
*s = parts
return nil
}
// MaporColonSlice represents a slice of strings that gets unmarshal from a
// YAML map into 'key:value' string.
type MaporColonSlice []string
// UnmarshalYAML implements the Unmarshaller interface.
func (s *MaporColonSlice) UnmarshalYAML(tag string, value interface{}) error {
parts, err := unmarshalToStringOrSepMapParts(value, ":")
if err != nil {
return err
}
*s = parts
return nil
}
// MaporSpaceSlice represents a slice of strings that gets unmarshal from a
// YAML map into 'key value' string.
type MaporSpaceSlice []string
// UnmarshalYAML implements the Unmarshaller interface.
func (s *MaporSpaceSlice) UnmarshalYAML(tag string, value interface{}) error {
parts, err := unmarshalToStringOrSepMapParts(value, " ")
if err != nil {
return err
}
*s = parts
return nil
}
func unmarshalToStringOrSepMapParts(value interface{}, key string) ([]string, error) {
switch value := value.(type) {
case []interface{}:
return toStrings(value)
case map[interface{}]interface{}:
return toSepMapParts(value, key)
default:
return nil, fmt.Errorf("Failed to unmarshal Map or Slice: %#v", value)
}
}
func toSepMapParts(value map[interface{}]interface{}, sep string) ([]string, error) {
if len(value) == 0 {
return nil, nil
}
parts := make([]string, 0, len(value))
for k, v := range value {
if sk, ok := k.(string); ok {
if sv, ok := v.(string); ok {
parts = append(parts, sk+sep+sv)
} else if sv, ok := v.(int64); ok {
parts = append(parts, sk+sep+strconv.FormatInt(sv, 10))
} else {
return nil, fmt.Errorf("Cannot unmarshal '%v' of type %T into a string value", v, v)
}
} else {
return nil, fmt.Errorf("Cannot unmarshal '%v' of type %T into a string value", k, k)
}
}
return parts, nil
}
func toStrings(s []interface{}) ([]string, error) {
if len(s) == 0 {
return nil, nil
}
r := make([]string, len(s))
for k, v := range s {
if sv, ok := v.(string); ok {
r[k] = sv
} else {
return nil, fmt.Errorf("Cannot unmarshal '%v' of type %T into a string value", v, v)
}
}
return r, nil
}

View File

@@ -0,0 +1,278 @@
package yaml
import (
"fmt"
"strings"
"testing"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/stretchr/testify/assert"
)
type StructStringorslice struct {
Foo Stringorslice
}
func TestStringorsliceYaml(t *testing.T) {
str := `{foo: [bar, baz]}`
s := StructStringorslice{}
yaml.Unmarshal([]byte(str), &s)
assert.Equal(t, Stringorslice{"bar", "baz"}, s.Foo)
d, err := yaml.Marshal(&s)
assert.Nil(t, err)
s2 := StructStringorslice{}
yaml.Unmarshal(d, &s2)
assert.Equal(t, Stringorslice{"bar", "baz"}, s2.Foo)
}
type StructSliceorMap struct {
Foos SliceorMap `yaml:"foos,omitempty"`
Bars []string `yaml:"bars"`
}
type StructCommand struct {
Entrypoint Command `yaml:"entrypoint,flow,omitempty"`
Command Command `yaml:"command,flow,omitempty"`
}
func TestSliceOrMapYaml(t *testing.T) {
str := `{foos: [bar=baz, far=faz]}`
s := StructSliceorMap{}
yaml.Unmarshal([]byte(str), &s)
assert.Equal(t, SliceorMap{"bar": "baz", "far": "faz"}, s.Foos)
d, err := yaml.Marshal(&s)
assert.Nil(t, err)
s2 := StructSliceorMap{}
yaml.Unmarshal(d, &s2)
assert.Equal(t, SliceorMap{"bar": "baz", "far": "faz"}, s2.Foos)
}
var sampleStructSliceorMap = `
foos:
io.rancher.os.bar: baz
io.rancher.os.far: true
bars: []
`
func TestUnmarshalSliceOrMap(t *testing.T) {
s := StructSliceorMap{}
err := yaml.Unmarshal([]byte(sampleStructSliceorMap), &s)
assert.Equal(t, fmt.Errorf("Cannot unmarshal 'true' of type bool into a string value"), err)
}
func TestStr2SliceOrMapPtrMap(t *testing.T) {
s := map[string]*StructSliceorMap{"udav": {
Foos: SliceorMap{"io.rancher.os.bar": "baz", "io.rancher.os.far": "true"},
Bars: []string{},
}}
d, err := yaml.Marshal(&s)
assert.Nil(t, err)
s2 := map[string]*StructSliceorMap{}
yaml.Unmarshal(d, &s2)
assert.Equal(t, s, s2)
}
type StructMaporslice struct {
Foo MaporEqualSlice
}
func contains(list []string, item string) bool {
for _, test := range list {
if test == item {
return true
}
}
return false
}
func TestMaporsliceYaml(t *testing.T) {
str := `{foo: {bar: baz, far: faz}}`
s := StructMaporslice{}
yaml.Unmarshal([]byte(str), &s)
assert.Equal(t, 2, len(s.Foo))
assert.True(t, contains(s.Foo, "bar=baz"))
assert.True(t, contains(s.Foo, "far=faz"))
d, err := yaml.Marshal(&s)
assert.Nil(t, err)
s2 := StructMaporslice{}
yaml.Unmarshal(d, &s2)
assert.Equal(t, 2, len(s2.Foo))
assert.True(t, contains(s2.Foo, "bar=baz"))
assert.True(t, contains(s2.Foo, "far=faz"))
}
var sampleStructCommand = `command: bash`
func TestUnmarshalCommand(t *testing.T) {
s := &StructCommand{}
err := yaml.Unmarshal([]byte(sampleStructCommand), s)
assert.Nil(t, err)
assert.Equal(t, Command{"bash"}, s.Command)
assert.Nil(t, s.Entrypoint)
bytes, err := yaml.Marshal(s)
assert.Nil(t, err)
s2 := &StructCommand{}
err = yaml.Unmarshal(bytes, s2)
assert.Nil(t, err)
assert.Equal(t, Command{"bash"}, s2.Command)
assert.Nil(t, s2.Entrypoint)
}
var sampleEmptyCommand = `{}`
func TestUnmarshalEmptyCommand(t *testing.T) {
s := &StructCommand{}
err := yaml.Unmarshal([]byte(sampleEmptyCommand), s)
assert.Nil(t, err)
assert.Nil(t, s.Command)
bytes, err := yaml.Marshal(s)
assert.Nil(t, err)
assert.Equal(t, "{}", strings.TrimSpace(string(bytes)))
s2 := &StructCommand{}
err = yaml.Unmarshal(bytes, s2)
assert.Nil(t, err)
assert.Nil(t, s2.Command)
}
func TestMarshalUlimit(t *testing.T) {
ulimits := []struct {
ulimits *Ulimits
expected string
}{
{
ulimits: &Ulimits{
Elements: []Ulimit{
{
ulimitValues: ulimitValues{
Soft: 65535,
Hard: 65535,
},
Name: "nproc",
},
},
},
expected: `nproc: 65535
`,
},
{
ulimits: &Ulimits{
Elements: []Ulimit{
{
Name: "nofile",
ulimitValues: ulimitValues{
Soft: 20000,
Hard: 40000,
},
},
},
},
expected: `nofile:
soft: 20000
hard: 40000
`,
},
}
for _, ulimit := range ulimits {
bytes, err := yaml.Marshal(ulimit.ulimits)
assert.Nil(t, err)
assert.Equal(t, ulimit.expected, string(bytes), "should be equal")
}
}
func TestUnmarshalUlimits(t *testing.T) {
ulimits := []struct {
yaml string
expected *Ulimits
}{
{
yaml: "nproc: 65535",
expected: &Ulimits{
Elements: []Ulimit{
{
Name: "nproc",
ulimitValues: ulimitValues{
Soft: 65535,
Hard: 65535,
},
},
},
},
},
{
yaml: `nofile:
soft: 20000
hard: 40000`,
expected: &Ulimits{
Elements: []Ulimit{
{
Name: "nofile",
ulimitValues: ulimitValues{
Soft: 20000,
Hard: 40000,
},
},
},
},
},
{
yaml: `nproc: 65535
nofile:
soft: 20000
hard: 40000`,
expected: &Ulimits{
Elements: []Ulimit{
{
Name: "nofile",
ulimitValues: ulimitValues{
Soft: 20000,
Hard: 40000,
},
},
{
Name: "nproc",
ulimitValues: ulimitValues{
Soft: 65535,
Hard: 65535,
},
},
},
},
},
}
for _, ulimit := range ulimits {
actual := &Ulimits{}
err := yaml.Unmarshal([]byte(ulimit.yaml), actual)
assert.Nil(t, err)
assert.Equal(t, ulimit.expected, actual, "should be equal")
}
}