فهرست منبع

Add v3.1 schema and support validating multiple version.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
(cherry picked from commit 65374488f92512cf34667cb71ea6d62985310f65)
Signed-off-by: Victor Vieux <vieux@docker.com>
Daniel Nephin 8 سال پیش
والد
کامیت
118e55df62

+ 1 - 6
cli/compose/loader/loader.go

@@ -62,16 +62,11 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
 		}
 		}
 	}
 	}
 
 
-	if err := schema.Validate(configDict); err != nil {
+	if err := schema.Validate(configDict, schema.Version(configDict)); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
 	cfg := types.Config{}
 	cfg := types.Config{}
-	version := configDict["version"].(string)
-	if version != "3" && version != "3.0" {
-		return nil, fmt.Errorf(`Unsupported Compose file version: %#v. The only version supported is "3" (or "3.0")`, version)
-	}
-
 	if services, ok := configDict["services"]; ok {
 	if services, ok := configDict["services"]; ok {
 		servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", os.LookupEnv)
 		servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", os.LookupEnv)
 		if err != nil {
 		if err != nil {

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
cli/compose/schema/bindata.go


+ 426 - 0
cli/compose/schema/data/config_schema_v3.1.json

@@ -0,0 +1,426 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "id": "config_schema_v3.1.json",
+  "type": "object",
+  "required": ["version"],
+
+  "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
+    },
+
+    "secrets": {
+      "id": "#/properties/secrets",
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z0-9._-]+$": {
+          "$ref": "#/definitions/secret"
+        }
+      },
+      "additionalProperties": false
+    }
+  },
+
+  "additionalProperties": false,
+
+  "definitions": {
+
+    "service": {
+      "id": "#/definitions/service",
+      "type": "object",
+
+      "properties": {
+        "deploy": {"$ref": "#/definitions/deployment"},
+        "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"},
+        "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
+        },
+
+        "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
+        "healthcheck": {"$ref": "#/definitions/healthcheck"},
+        "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",
+                  "patternProperties": {
+                    "^.+$": {"type": ["string", "number", "null"]}
+                  }
+                }
+            },
+            "additionalProperties": false
+        },
+
+        "mac_address": {"type": "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"]},
+        "secrets": {
+          "type": "array",
+          "items": {
+            "oneOf": [
+              {"type": "string"},
+              {
+                "type": "object",
+                "properties": {
+                  "source": {"type": "string"},
+                  "target": {"type": "string"},
+                  "uid": {"type": "string"},
+                  "gid": {"type": "string"},
+                  "mode": {"type": "number"}
+                }
+              }
+            ]
+          }
+        },
+        "sysctls": {"$ref": "#/definitions/list_or_dict"},
+        "stdin_open": {"type": "boolean"},
+        "stop_signal": {"type": "string"},
+        "stop_grace_period": {"type": "string", "format": "duration"},
+        "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"},
+        "userns_mode": {"type": "string"},
+        "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "working_dir": {"type": "string"}
+      },
+      "additionalProperties": false
+    },
+
+    "healthcheck": {
+      "id": "#/definitions/healthcheck",
+      "type": ["object", "null"],
+      "properties": {
+        "interval": {"type":"string"},
+        "timeout": {"type":"string"},
+        "retries": {"type": "number"},
+        "test": {
+          "oneOf": [
+            {"type": "string"},
+            {"type": "array", "items": {"type": "string"}}
+          ]
+        },
+        "disable": {"type": "boolean"}
+      },
+      "additionalProperties": false
+    },
+    "deployment": {
+      "id": "#/definitions/deployment",
+      "type": ["object", "null"],
+      "properties": {
+        "mode": {"type": "string"},
+        "replicas": {"type": "integer"},
+        "labels": {"$ref": "#/definitions/list_or_dict"},
+        "update_config": {
+          "type": "object",
+          "properties": {
+            "parallelism": {"type": "integer"},
+            "delay": {"type": "string", "format": "duration"},
+            "failure_action": {"type": "string"},
+            "monitor": {"type": "string", "format": "duration"},
+            "max_failure_ratio": {"type": "number"}
+          },
+          "additionalProperties": false
+        },
+        "resources": {
+          "type": "object",
+          "properties": {
+            "limits": {"$ref": "#/definitions/resource"},
+            "reservations": {"$ref": "#/definitions/resource"}
+          }
+        },
+        "restart_policy": {
+          "type": "object",
+          "properties": {
+            "condition": {"type": "string"},
+            "delay": {"type": "string", "format": "duration"},
+            "max_attempts": {"type": "integer"},
+            "window": {"type": "string", "format": "duration"}
+          },
+          "additionalProperties": false
+        },
+        "placement": {
+          "type": "object",
+          "properties": {
+            "constraints": {"type": "array", "items": {"type": "string"}}
+          },
+          "additionalProperties": false
+        }
+      },
+      "additionalProperties": false
+    },
+
+    "resource": {
+      "id": "#/definitions/resource",
+      "type": "object",
+      "properties": {
+        "cpus": {"type": "string"},
+        "memory": {"type": "string"}
+      },
+      "additionalProperties": false
+    },
+
+    "network": {
+      "id": "#/definitions/network",
+      "type": ["object", "null"],
+      "properties": {
+        "driver": {"type": "string"},
+        "driver_opts": {
+          "type": "object",
+          "patternProperties": {
+            "^.+$": {"type": ["string", "number"]}
+          }
+        },
+        "ipam": {
+          "type": "object",
+          "properties": {
+            "driver": {"type": "string"},
+            "config": {
+              "type": "array",
+              "items": {
+                "type": "object",
+                "properties": {
+                  "subnet": {"type": "string"}
+                },
+                "additionalProperties": false
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        "external": {
+          "type": ["boolean", "object"],
+          "properties": {
+            "name": {"type": "string"}
+          },
+          "additionalProperties": false
+        },
+        "labels": {"$ref": "#/definitions/list_or_dict"}
+      },
+      "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"}
+          }
+        }
+      },
+      "labels": {"$ref": "#/definitions/list_or_dict"},
+      "additionalProperties": false
+    },
+
+    "secret": {
+      "id": "#/definitions/secret",
+      "type": "object",
+      "properties": {
+        "file": {"type": "string"},
+        "external": {
+          "type": ["boolean", "object"],
+          "properties": {
+            "name": {"type": "string"}
+          }
+        }
+      },
+      "labels": {"$ref": "#/definitions/list_or_dict"},
+      "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"]
+          }
+        }
+      }
+    }
+  }
+}

+ 27 - 3
cli/compose/schema/schema.go

@@ -7,9 +7,15 @@ import (
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
+	"github.com/pkg/errors"
 	"github.com/xeipuuv/gojsonschema"
 	"github.com/xeipuuv/gojsonschema"
 )
 )
 
 
+const (
+	defaultVersion = "1.0"
+	versionField   = "version"
+)
+
 type portsFormatChecker struct{}
 type portsFormatChecker struct{}
 
 
 func (checker portsFormatChecker) IsFormat(input string) bool {
 func (checker portsFormatChecker) IsFormat(input string) bool {
@@ -30,11 +36,29 @@ func init() {
 	gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{})
 	gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{})
 }
 }
 
 
+// Version returns the version of the config, defaulting to version 1.0
+func Version(config map[string]interface{}) string {
+	version, ok := config[versionField]
+	if !ok {
+		return defaultVersion
+	}
+	return normalizeVersion(fmt.Sprintf("%v", version))
+}
+
+func normalizeVersion(version string) string {
+	switch version {
+	case "3":
+		return "3.0"
+	default:
+		return version
+	}
+}
+
 // Validate uses the jsonschema to validate the configuration
 // Validate uses the jsonschema to validate the configuration
-func Validate(config map[string]interface{}) error {
-	schemaData, err := Asset("data/config_schema_v3.0.json")
+func Validate(config map[string]interface{}, version string) error {
+	schemaData, err := Asset(fmt.Sprintf("data/config_schema_v%s.json", version))
 	if err != nil {
 	if err != nil {
-		return err
+		return errors.Errorf("unsupported Compose file version: %s", version)
 	}
 	}
 
 
 	schemaLoader := gojsonschema.NewStringLoader(string(schemaData))
 	schemaLoader := gojsonschema.NewStringLoader(string(schemaData))

+ 23 - 6
cli/compose/schema/schema_test.go

@@ -8,9 +8,9 @@ import (
 
 
 type dict map[string]interface{}
 type dict map[string]interface{}
 
 
-func TestValid(t *testing.T) {
+func TestValidate(t *testing.T) {
 	config := dict{
 	config := dict{
-		"version": "2.1",
+		"version": "3.0",
 		"services": dict{
 		"services": dict{
 			"foo": dict{
 			"foo": dict{
 				"image": "busybox",
 				"image": "busybox",
@@ -18,12 +18,12 @@ func TestValid(t *testing.T) {
 		},
 		},
 	}
 	}
 
 
-	assert.NoError(t, Validate(config))
+	assert.NoError(t, Validate(config, "3.0"))
 }
 }
 
 
-func TestUndefinedTopLevelOption(t *testing.T) {
+func TestValidateUndefinedTopLevelOption(t *testing.T) {
 	config := dict{
 	config := dict{
-		"version": "2.1",
+		"version": "3.0",
 		"helicopters": dict{
 		"helicopters": dict{
 			"foo": dict{
 			"foo": dict{
 				"image": "busybox",
 				"image": "busybox",
@@ -31,5 +31,22 @@ func TestUndefinedTopLevelOption(t *testing.T) {
 		},
 		},
 	}
 	}
 
 
-	assert.Error(t, Validate(config))
+	err := Validate(config, "3.0")
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "Additional property helicopters is not allowed")
+}
+
+func TestValidateInvalidVersion(t *testing.T) {
+	config := dict{
+		"version": "2.1",
+		"services": dict{
+			"foo": dict{
+				"image": "busybox",
+			},
+		},
+	}
+
+	err := Validate(config, "2.1")
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "unsupported Compose file version: 2.1")
 }
 }

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است