Initial commit
This commit is contained in:
43
vendor/github.com/hyperhq/libcompose/config/convert.go
generated
vendored
Normal file
43
vendor/github.com/hyperhq/libcompose/config/convert.go
generated
vendored
Normal 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
95
vendor/github.com/hyperhq/libcompose/config/hash.go
generated
vendored
Normal 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))
|
||||
}
|
||||
169
vendor/github.com/hyperhq/libcompose/config/interpolation.go
generated
vendored
Normal file
169
vendor/github.com/hyperhq/libcompose/config/interpolation.go
generated
vendored
Normal 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
|
||||
}
|
||||
226
vendor/github.com/hyperhq/libcompose/config/interpolation_test.go
generated
vendored
Normal file
226
vendor/github.com/hyperhq/libcompose/config/interpolation_test.go
generated
vendored
Normal 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}"`)
|
||||
}
|
||||
90
vendor/github.com/hyperhq/libcompose/config/marshal_config_test.go
generated
vendored
Normal file
90
vendor/github.com/hyperhq/libcompose/config/marshal_config_test.go
generated
vendored
Normal 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
150
vendor/github.com/hyperhq/libcompose/config/merge.go
generated
vendored
Normal 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)
|
||||
}
|
||||
292
vendor/github.com/hyperhq/libcompose/config/merge_test.go
generated
vendored
Normal file
292
vendor/github.com/hyperhq/libcompose/config/merge_test.go
generated
vendored
Normal 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
173
vendor/github.com/hyperhq/libcompose/config/merge_v1.go
generated
vendored
Normal 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
211
vendor/github.com/hyperhq/libcompose/config/merge_v2.go
generated
vendored
Normal 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
339
vendor/github.com/hyperhq/libcompose/config/schema.go
generated
vendored
Normal 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"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
96
vendor/github.com/hyperhq/libcompose/config/schema_helpers.go
generated
vendored
Normal file
96
vendor/github.com/hyperhq/libcompose/config/schema_helpers.go
generated
vendored
Normal 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
238
vendor/github.com/hyperhq/libcompose/config/types.go
generated
vendored
Normal 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
42
vendor/github.com/hyperhq/libcompose/config/utils.go
generated
vendored
Normal 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 ""
|
||||
}
|
||||
309
vendor/github.com/hyperhq/libcompose/config/validation.go
generated
vendored
Normal file
309
vendor/github.com/hyperhq/libcompose/config/validation.go
generated
vendored
Normal 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
|
||||
}
|
||||
360
vendor/github.com/hyperhq/libcompose/config/validation_test.go
generated
vendored
Normal file
360
vendor/github.com/hyperhq/libcompose/config/validation_test.go
generated
vendored
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user