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

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)
}