Vendor integration dependencies.

This commit is contained in:
Timo Reimann 2017-02-07 22:33:23 +01:00
parent dd5e3fba01
commit 55b57c736b
2451 changed files with 731611 additions and 0 deletions

View file

@ -0,0 +1,44 @@
package config
import (
"github.com/docker/libcompose/utils"
"github.com/docker/libcompose/yaml"
)
// ConvertServices converts a set of v1 service configs to v2 service configs
func ConvertServices(v1Services map[string]*ServiceConfigV1) (map[string]*ServiceConfig, error) {
v2Services := make(map[string]*ServiceConfig)
replacementFields := make(map[string]*ServiceConfig)
for name, service := range v1Services {
replacementFields[name] = &ServiceConfig{
Build: yaml.Build{
Context: service.Build,
Dockerfile: service.Dockerfile,
},
Logging: Log{
Driver: service.LogDriver,
Options: service.LogOpt,
},
NetworkMode: service.Net,
}
v1Services[name].Build = ""
v1Services[name].Dockerfile = ""
v1Services[name].LogDriver = ""
v1Services[name].LogOpt = nil
v1Services[name].Net = ""
}
if err := utils.Convert(v1Services, &v2Services); err != nil {
return nil, err
}
for name := range v2Services {
v2Services[name].Build = replacementFields[name].Build
v2Services[name].Logging = replacementFields[name].Logging
v2Services[name].NetworkMode = replacementFields[name].NetworkMode
}
return v2Services, nil
}

View file

@ -0,0 +1,95 @@
package config
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"reflect"
"sort"
"github.com/docker/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,166 @@
package config
import (
"bufio"
"bytes"
"fmt"
"strings"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/docker/docker/pkg/urlutil"
)
var (
noMerge = []string{
"links",
"volumes_from",
}
defaultParseOptions = ParseOptions{
Interpolate: true,
Validate: true,
}
)
// Merge merges a compose file into an existing set of service configs
func Merge(existingServices *ServiceConfigs, environmentLookup EnvironmentLookup, resourceLookup ResourceLookup, file string, bytes []byte, options *ParseOptions) (string, map[string]*ServiceConfig, map[string]*VolumeConfig, map[string]*NetworkConfig, error) {
if options == nil {
options = &defaultParseOptions
}
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, options)
if err != nil {
return "", nil, nil, nil, err
}
volumeConfigs, err = ParseVolumes(bytes)
if err != nil {
return "", nil, nil, nil, err
}
networkConfigs, err = ParseNetworks(bytes)
if err != nil {
return "", nil, nil, nil, err
}
} else {
serviceConfigsV1, err := MergeServicesV1(existingServices, environmentLookup, resourceLookup, file, bytes, options)
if err != nil {
return "", nil, nil, nil, err
}
serviceConfigs, err = ConvertServices(serviceConfigsV1)
if err != nil {
return "", nil, nil, nil, err
}
}
adjustValues(serviceConfigs)
if options.Postprocess != nil {
var err error
serviceConfigs, err = options.Postprocess(serviceConfigs)
if err != nil {
return "", nil, nil, nil, err
}
}
return config.Version, 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,199 @@
package config
import (
"fmt"
"path"
"github.com/Sirupsen/logrus"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/docker/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, options *ParseOptions) (map[string]*ServiceConfigV1, error) {
datas := make(RawServiceMap)
if err := yaml.Unmarshal(bytes, &datas); err != nil {
return nil, err
}
if options.Interpolate {
if err := Interpolate(environmentLookup, &datas); err != nil {
return nil, err
}
}
if options.Preprocess != nil {
var err error
datas, err = options.Preprocess(datas)
if err != nil {
return nil, err
}
}
if options.Validate {
if err := validate(datas); err != nil {
return nil, err
}
}
for name, data := range datas {
data, err := parseV1(resourceLookup, environmentLookup, file, data, datas, options)
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
}
if options.Validate {
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, options *ParseOptions) (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, options)
} 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
}
if options.Interpolate {
err = Interpolate(environmentLookup, &baseRawServices)
if err != nil {
return nil, err
}
}
if options.Preprocess != nil {
var err error
baseRawServices, err = options.Preprocess(baseRawServices)
if err != nil {
return nil, err
}
}
if options.Validate {
if err := validate(baseRawServices); 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, options)
}
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
}

View file

@ -0,0 +1,217 @@
package config
import (
"fmt"
"path"
"github.com/Sirupsen/logrus"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/docker/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, options *ParseOptions) (map[string]*ServiceConfig, error) {
var config Config
if err := yaml.Unmarshal(bytes, &config); err != nil {
return nil, err
}
datas := config.Services
if options.Interpolate {
if err := Interpolate(environmentLookup, &datas); err != nil {
return nil, err
}
}
if options.Preprocess != nil {
var err error
datas, err = options.Preprocess(datas)
if err != nil {
return nil, err
}
}
for name, data := range datas {
data, err := parseV2(resourceLookup, environmentLookup, file, data, datas, options)
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(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(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, options *ParseOptions) (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, options)
} 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
if options.Interpolate {
err = Interpolate(environmentLookup, &baseRawServices)
if 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, options)
}
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
}
var build map[interface{}]interface{}
if buildAsString, ok := serviceData["build"].(string); ok {
build = map[interface{}]interface{}{
"context": buildAsString,
}
} else {
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
}

View file

@ -0,0 +1,510 @@
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": {
"build": {"type": "string"},
"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"},
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"dns": {"$ref": "#/definitions/string_or_list"},
"dns_search": {"$ref": "#/definitions/string_or_list"},
"dockerfile": {"type": "string"},
"domainname": {"type": "string"},
"entrypoint": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"env_file": {"$ref": "#/definitions/string_or_list"},
"environment": {"$ref": "#/definitions/list_or_dict"},
"expose": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "expose"
},
"uniqueItems": true
},
"extends": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"service": {"type": "string"},
"file": {"type": "string"}
},
"required": ["service"],
"additionalProperties": false
}
]
},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"log_driver": {"type": "string"},
"log_opt": {"type": "object"},
"mac_address": {"type": "string"},
"mem_limit": {"type": ["number", "string"]},
"memswap_limit": {"type": ["number", "string"]},
"net": {"type": "string"},
"pid": {"type": ["string", "null"]},
"ports": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "ports"
},
"uniqueItems": true
},
"privileged": {"type": "boolean"},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"shm_size": {"type": ["number", "string"]},
"stdin_open": {"type": "boolean"},
"stop_signal": {"type": "string"},
"tty": {"type": "boolean"},
"ulimits": {
"type": "object",
"patternProperties": {
"^[a-z]+$": {
"oneOf": [
{"type": "integer"},
{
"type":"object",
"properties": {
"hard": {"type": "integer"},
"soft": {"type": "integer"}
},
"required": ["soft", "hard"],
"additionalProperties": false
}
]
}
}
},
"user": {"type": "string"},
"volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"volume_driver": {"type": "string"},
"volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"working_dir": {"type": "string"}
},
"dependencies": {
"memswap_limit": ["mem_limit"]
},
"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": ["build"],
"not": {"required": ["image"]}
},
{
"required": ["image"],
"not": {"anyOf": [
{"required": ["build"]},
{"required": ["dockerfile"]}
]}
}
]
}
}
}
}
`
var schemaV2 = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v2.0.json",
"type": "object",
"properties": {
"version": {
"type": "string"
},
"services": {
"id": "#/properties/services",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/service"
}
},
"additionalProperties": false
},
"networks": {
"id": "#/properties/networks",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/network"
}
}
},
"volumes": {
"id": "#/properties/volumes",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/volume"
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"definitions": {
"service": {
"id": "#/definitions/service",
"type": "object",
"properties": {
"build": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
}
]
},
"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"},
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"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": {"$ref": "#/definitions/string_or_list"},
"environment": {"$ref": "#/definitions/list_or_dict"},
"expose": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "expose"
},
"uniqueItems": true
},
"extends": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"service": {"type": "string"},
"file": {"type": "string"}
},
"required": ["service"],
"additionalProperties": false
}
]
},
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"options": {"type": "object"}
},
"additionalProperties": false
},
"mac_address": {"type": "string"},
"mem_limit": {"type": ["number", "string"]},
"memswap_limit": {"type": ["number", "string"]},
"network_mode": {"type": "string"},
"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
}
]
},
"pid": {"type": ["string", "null"]},
"ports": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "ports"
},
"uniqueItems": true
},
"privileged": {"type": "boolean"},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"shm_size": {"type": ["number", "string"]},
"stdin_open": {"type": "boolean"},
"stop_signal": {"type": "string"},
"tmpfs": {"$ref": "#/definitions/string_or_list"},
"tty": {"type": "boolean"},
"ulimits": {
"type": "object",
"patternProperties": {
"^[a-z]+$": {
"oneOf": [
{"type": "integer"},
{
"type":"object",
"properties": {
"hard": {"type": "integer"},
"soft": {"type": "integer"}
},
"required": ["soft", "hard"],
"additionalProperties": false
}
]
}
}
},
"user": {"type": "string"},
"volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"volume_driver": {"type": "string"},
"volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"working_dir": {"type": "string"}
},
"dependencies": {
"memswap_limit": ["mem_limit"]
},
"additionalProperties": false
},
"network": {
"id": "#/definitions/network",
"type": "object",
"properties": {
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"ipam": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"config": {
"type": "array"
}
},
"additionalProperties": false
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"volume": {
"id": "#/definitions/volume",
"type": ["object", "null"],
"properties": {
"driver": {"type": "string"},
"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": ["build"]},
{"required": ["image"]}
],
"properties": {
"build": {
"required": ["context"]
}
}
}
}
}
}
`

View file

@ -0,0 +1,92 @@
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() error {
if schema != nil {
return nil
}
var schemaRaw interface{}
err := json.Unmarshal([]byte(schemaV1), &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
}

View file

@ -0,0 +1,234 @@
package config
import (
"sync"
"github.com/docker/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"`
Command yaml.Command `yaml:"command,flow,omitempty"`
ContainerName string `yaml:"container_name,omitempty"`
Devices []string `yaml:"devices,omitempty"`
DNS yaml.Stringorslice `yaml:"dns,omitempty"`
DNSSearch yaml.Stringorslice `yaml:"dns_search,omitempty"`
Dockerfile string `yaml:"dockerfile,omitempty"`
DomainName string `yaml:"domainname,omitempty"`
Entrypoint yaml.Command `yaml:"entrypoint,flow,omitempty"`
EnvFile yaml.Stringorslice `yaml:"env_file,omitempty"`
Environment yaml.MaporEqualSlice `yaml:"environment,omitempty"`
Hostname string `yaml:"hostname,omitempty"`
Image string `yaml:"image,omitempty"`
Labels yaml.SliceorMap `yaml:"labels,omitempty"`
Links yaml.MaporColonSlice `yaml:"links,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"`
Restart string `yaml:"restart,omitempty"`
ReadOnly bool `yaml:"read_only,omitempty"`
ShmSize int64 `yaml:"shm_size,omitempty"`
StdinOpen bool `yaml:"stdin_open,omitempty"`
SecurityOpt []string `yaml:"security_opt,omitempty"`
Tty bool `yaml:"tty,omitempty"`
User string `yaml:"user,omitempty"`
VolumeDriver string `yaml:"volume_driver,omitempty"`
Volumes []string `yaml:"volumes,omitempty"`
VolumesFrom []string `yaml:"volumes_from,omitempty"`
WorkingDir string `yaml:"working_dir,omitempty"`
Expose []string `yaml:"expose,omitempty"`
ExternalLinks []string `yaml:"external_links,omitempty"`
LogOpt map[string]string `yaml:"log_opt,omitempty"`
ExtraHosts []string `yaml:"extra_hosts,omitempty"`
Ulimits yaml.Ulimits `yaml:"ulimits,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 yaml.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"`
Command yaml.Command `yaml:"command,flow,omitempty"`
CgroupParent string `yaml:"cgroup_parent,omitempty"`
ContainerName string `yaml:"container_name,omitempty"`
Devices []string `yaml:"devices,omitempty"`
DependsOn []string `yaml:"depends_on,omitempty"`
DNS yaml.Stringorslice `yaml:"dns,omitempty"`
DNSSearch yaml.Stringorslice `yaml:"dns_search,omitempty"`
DomainName string `yaml:"domain_name,omitempty"`
Entrypoint yaml.Command `yaml:"entrypoint,flow,omitempty"`
EnvFile yaml.Stringorslice `yaml:"env_file,omitempty"`
Environment yaml.MaporEqualSlice `yaml:"environment,omitempty"`
Expose []string `yaml:"expose,omitempty"`
Extends yaml.MaporEqualSlice `yaml:"extends,omitempty"`
ExternalLinks []string `yaml:"external_links,omitempty"`
ExtraHosts []string `yaml:"extra_hosts,omitempty"`
Image string `yaml:"image,omitempty"`
Hostname string `yaml:"hostname,omitempty"`
Ipc string `yaml:"ipc,omitempty"`
Labels yaml.SliceorMap `yaml:"labels,omitempty"`
Links yaml.MaporColonSlice `yaml:"links,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 *yaml.Networks `yaml:"networks,omitempty"`
Pid string `yaml:"pid,omitempty"`
Ports []string `yaml:"ports,omitempty"`
Privileged bool `yaml:"privileged,omitempty"`
SecurityOpt []string `yaml:"security_opt,omitempty"`
ShmSize int64 `yaml:"shm_size,omitempty"`
StopSignal string `yaml:"stop_signal,omitempty"`
VolumeDriver string `yaml:"volume_driver,omitempty"`
Volumes []string `yaml:"volumes,omitempty"`
VolumesFrom []string `yaml:"volumes_from,omitempty"`
Uts string `yaml:"uts,omitempty"`
Restart string `yaml:"restart,omitempty"`
ReadOnly bool `yaml:"read_only,omitempty"`
StdinOpen bool `yaml:"stdin_open,omitempty"`
Tty bool `yaml:"tty,omitempty"`
User string `yaml:"user,omitempty"`
WorkingDir string `yaml:"working_dir,omitempty"`
Ulimits yaml.Ulimits `yaml:"ulimits,omitempty"`
}
// VolumeConfig holds v2 volume configuration
type VolumeConfig struct {
Driver string `yaml:"driver,omitempty"`
DriverOpts map[string]string `yaml:"driver_opts,omitempty"`
External yaml.External `yaml:"external,omitempty"`
}
// Ipam holds v2 network IPAM information
type Ipam struct {
Driver string `yaml:"driver,omitempty"`
Config []IpamConfig `yaml:"config,omitempty"`
}
// IpamConfig holds v2 network IPAM configuration information
type IpamConfig struct {
Subnet string `yaml:"subnet,omitempty"`
IPRange string `yaml:"ip_range,omitempty"`
Gateway string `yaml:"gateway,omitempty"`
AuxAddress map[string]string `yaml:"aux_addresses,omitempty"`
}
// NetworkConfig holds v2 network configuration
type NetworkConfig struct {
Driver string `yaml:"driver,omitempty"`
DriverOpts map[string]string `yaml:"driver_opts,omitempty"`
External yaml.External `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
// ParseOptions are a set of options to customize the parsing process
type ParseOptions struct {
Interpolate bool
Validate bool
Preprocess func(RawServiceMap) (RawServiceMap, error)
Postprocess func(map[string]*ServiceConfig) (map[string]*ServiceConfig, error)
}

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,300 @@
package config
import (
"fmt"
"strconv"
"strings"
"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) error {
if err := setupSchemaLoaders(); 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":
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()))
}
}
}
return fmt.Errorf(strings.Join(validationErrors, "\n"))
}
return nil
}
func validateServiceConstraints(service RawService, serviceName string) error {
if err := setupSchemaLoaders(); 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
}