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