2017-02-07 21:33:23 +00:00
package config
import (
"fmt"
"strconv"
"strings"
2017-07-06 14:28:13 +00:00
"github.com/docker/libcompose/utils"
2017-02-07 21:33:23 +00:00
"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 ""
}
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 {
2017-07-06 14:28:13 +00:00
newService [ k ] = utils . ConvertKeysToStrings ( v )
2017-02-07 21:33:23 +00:00
}
return newService
}
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 {
2017-07-06 14:28:13 +00:00
serviceMap = convertServiceMapKeysToStrings ( serviceMap )
dataLoader := gojsonschema . NewGoLoader ( serviceMap )
result , err := gojsonschema . Validate ( schemaLoaderV1 , dataLoader )
if err != nil {
2017-02-07 21:33:23 +00:00
return err
}
2017-07-06 14:28:13 +00:00
return generateErrorMessages ( serviceMap , schemaV1 , result )
}
2017-02-07 21:33:23 +00:00
2017-07-06 14:28:13 +00:00
func validateV2 ( serviceMap RawServiceMap ) error {
serviceMap = convertServiceMapKeysToStrings ( serviceMap )
2017-02-07 21:33:23 +00:00
dataLoader := gojsonschema . NewGoLoader ( serviceMap )
2017-07-06 14:28:13 +00:00
result , err := gojsonschema . Validate ( schemaLoaderV2 , dataLoader )
2017-02-07 21:33:23 +00:00
if err != nil {
return err
}
2017-07-06 14:28:13 +00:00
return generateErrorMessages ( serviceMap , schemaV2 , result )
}
func generateErrorMessages ( serviceMap RawServiceMap , schema map [ string ] interface { } , result * gojsonschema . Result ) error {
var validationErrors [ ] string
2017-02-07 21:33:23 +00:00
// 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 {
service = convertServiceKeysToStrings ( service )
var validationErrors [ ] string
dataLoader := gojsonschema . NewGoLoader ( service )
2017-07-06 14:28:13 +00:00
result , err := gojsonschema . Validate ( constraintSchemaLoaderV1 , dataLoader )
2017-02-07 21:33:23 +00:00
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
}
2017-07-06 14:28:13 +00:00
func validateServiceConstraintsv2 ( service RawService , serviceName string ) error {
service = convertServiceKeysToStrings ( service )
var validationErrors [ ] string
dataLoader := gojsonschema . NewGoLoader ( service )
result , err := gojsonschema . Validate ( constraintSchemaLoaderV2 , dataLoader )
if err != nil {
return err
}
if ! result . Valid ( ) {
for _ , err := range result . Errors ( ) {
if err . Type ( ) == "required" {
_ , containsImage := service [ "image" ]
_ , containsBuild := service [ "build" ]
if containsBuild || ! containsImage && ! containsBuild {
validationErrors = append ( validationErrors , fmt . Sprintf ( "Service '%s' has neither an image nor a build context specified. At least one must be provided." , serviceName ) )
}
}
}
return fmt . Errorf ( strings . Join ( validationErrors , "\n" ) )
}
return nil
}