Repository: compose-spec/compose-go
Pull request #45 (2020-05-14 13:21:10)

Introduce `Project` as named, merge result for a set of config files
This is code I had to write to implement a Compose compliant command line.

- introduce Project for a set of merged compose files (to replace use of Config)
- load Compose project from CLI options
- manage project Name and WorkingDir
- set Compose model defaults

Getting this hosted in compose-go make this code reusable in another context, so we don't have to duplicate code

ndeloof: Need to be updated to cover https://github.com/compose-spec/compose-spec/pull/78
ndeloof: @thaJeztah I'm not using an interface here with the expectation anyone would write a custom implementation without using WhiteList as a base. I wish I could create an `AbstractChecker`.
Is there a better golang-compliant approach to define default methods implementation but give a chance for implementors to override?

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2d4e9b3..5bb43ef 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,7 +16,7 @@ jobs:
   test:
     strategy:
       matrix:
-        go-version: [1.12.x, 1.13.x, 1.14.x]
+        go-version: [1.13.x, 1.14.x]
         platform: [ubuntu-latest, macos-latest, windows-latest]
     runs-on: ${{ matrix.platform }}
     timeout-minutes: 5
diff --git a/MAINTENANCE.md b/MAINTENANCE.md
new file mode 100644
index 0000000..40ba91f
--- /dev/null
+++ b/MAINTENANCE.md
@@ -0,0 +1,9 @@
+# Maintenance
+
+The compose-go library has to be kept up-to-date with approved changes in the [Compose specification](https://github.com/compose-spec/compose-spec).
+As we define new attributes to be added to the spec, this typically requires:
+
+1. Updating `schema` to latest version from compose-spec
+1. Creating the matching struct/field in `types`
+1. Creating the matching `CheckXX` method in `compatibility`
+1. If the new attribute replaces a legacy one we want to deprecate, creating the adequate logic in `normalize.go`
\ No newline at end of file
diff --git a/cli/options.go b/cli/options.go
new file mode 100644
index 0000000..69a9f35
--- /dev/null
+++ b/cli/options.go
@@ -0,0 +1,165 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package cli
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/compose-spec/compose-go/errdefs"
+	"github.com/compose-spec/compose-go/loader"
+	"github.com/compose-spec/compose-go/types"
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+)
+
+// ProjectOptions groups the command line options recommended for a Compose implementation
+type ProjectOptions struct {
+	ConfigPaths []string
+	Name        string
+}
+
+// DefaultFileNames defines the Compose file names for auto-discovery (in order of preference)
+var DefaultFileNames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"}
+
+const (
+	ComposeProjectName   = "COMPOSE_PROJECT_NAME"
+	ComposeFileSeparator = "COMPOSE_FILE_SEPARATOR"
+	ComposeFilePath      = "COMPOSE_FILE"
+)
+
+// ProjectFromOptions load a compose project based on command line options
+func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) {
+	configPaths, err := getConfigPathsFromOptions(options)
+	if err != nil {
+		return nil, err
+	}
+
+	configs, err := parseConfigs(configPaths)
+	if err != nil {
+		return nil, err
+	}
+
+	return loader.Load(types.ConfigDetails{
+		ConfigFiles: configs,
+		Environment: environment(),
+	}, func(opts *loader.Options) {
+		if options.Name != "" {
+			opts.Name = options.Name
+		} else if nameFromEnv, ok := os.LookupEnv(ComposeProjectName); ok {
+			opts.Name = nameFromEnv
+		}
+	})
+}
+
+// getConfigPathsFromOptions retrieves the config files for project based on project options
+func getConfigPathsFromOptions(options *ProjectOptions) ([]string, error) {
+	paths := []string{}
+	pwd, err := os.Getwd()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(options.ConfigPaths) != 0 {
+		for _, f := range options.ConfigPaths {
+			if f == "-" {
+				paths = append(paths, f)
+				continue
+			}
+			if !filepath.IsAbs(f) {
+				f = filepath.Join(pwd, f)
+			}
+			if _, err := os.Stat(f); err != nil {
+				return nil, err
+			}
+			paths = append(paths, f)
+		}
+		return paths, nil
+	}
+
+	sep := os.Getenv(ComposeFileSeparator)
+	if sep == "" {
+		sep = string(os.PathListSeparator)
+	}
+	f := os.Getenv(ComposeFilePath)
+	if f != "" {
+		return strings.Split(f, sep), nil
+	}
+
+	for {
+		candidates := []string{}
+		for _, n := range DefaultFileNames {
+			f := filepath.Join(pwd, n)
+			if _, err := os.Stat(f); err == nil {
+				candidates = append(candidates, f)
+			}
+		}
+		if len(candidates) > 0 {
+			winner := candidates[0]
+			if len(candidates) > 1 {
+				logrus.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", "))
+				logrus.Warnf("Using %s", winner)
+			}
+			return []string{winner}, nil
+		}
+		parent := filepath.Dir(pwd)
+		if parent == pwd {
+			return nil, errors.Wrap(errdefs.ErrNotFound, "can't find a suitable configuration file in this directory or any parent")
+		}
+		pwd = parent
+	}
+}
+
+func parseConfigs(configPaths []string) ([]types.ConfigFile, error) {
+	files := []types.ConfigFile{}
+	for _, f := range configPaths {
+		var (
+			b   []byte
+			err error
+		)
+		if f == "-" {
+			b, err = ioutil.ReadAll(os.Stdin)
+		} else {
+			b, err = ioutil.ReadFile(f)
+		}
+		if err != nil {
+			return nil, err
+		}
+		config, err := loader.ParseYAML(b)
+		if err != nil {
+			return nil, err
+		}
+		files = append(files, types.ConfigFile{Filename: f, Config: config})
+	}
+	return files, nil
+}
+
+func environment() map[string]string {
+	return getAsEqualsMap(os.Environ())
+}
+
+// getAsEqualsMap split key=value formatted strings into a key : value map
+func getAsEqualsMap(em []string) map[string]string {
+	m := make(map[string]string)
+	for _, v := range em {
+		kv := strings.SplitN(v, "=", 2)
+		m[kv[0]] = kv[1]
+	}
+	return m
+}
diff --git a/compatibility/allowlist.go b/compatibility/allowlist.go
new file mode 100644
index 0000000..c69a5eb
--- /dev/null
+++ b/compatibility/allowlist.go
@@ -0,0 +1,50 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compatibility
+
+import (
+	"fmt"
+
+	"github.com/compose-spec/compose-go/errdefs"
+	"github.com/pkg/errors"
+)
+
+// AllowList implements the Checker interface by rejecting all attributes that are not listed as "supported".
+type AllowList struct {
+	Supported []string
+	errors    []error
+}
+
+// Errors returns the list of errors encountered when checking against the allow list
+func (c *AllowList) Errors() []error {
+	return c.errors
+}
+
+func (c *AllowList) supported(attributes ...string) bool {
+	for _, a := range attributes {
+		for _, s := range c.Supported {
+			if s == a {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+func (c *AllowList) error(message string, args ...interface{}) {
+	c.errors = append(c.errors, errors.Wrap(errdefs.ErrUnsupported, fmt.Sprintf(message, args...)))
+}
diff --git a/compatibility/build.go b/compatibility/build.go
new file mode 100644
index 0000000..e51d3cb
--- /dev/null
+++ b/compatibility/build.go
@@ -0,0 +1,77 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compatibility
+
+import "github.com/compose-spec/compose-go/types"
+
+func (c *AllowList) CheckBuild(service *types.ServiceConfig) bool {
+	if !c.supported("services.build") && service.Build != nil {
+		service.Build = nil
+		c.error("services.build")
+		return false
+	}
+	return true
+}
+
+func (c *AllowList) CheckBuildArgs(build *types.BuildConfig) {
+	if !c.supported("services.build.args") && len(build.Args) != 0 {
+		build.Args = nil
+		c.error("services.build.args")
+	}
+}
+
+func (c *AllowList) CheckBuildLabels(build *types.BuildConfig) {
+	if !c.supported("services.build.labels") && len(build.Labels) != 0 {
+		build.Labels = nil
+		c.error("services.build.labels")
+	}
+}
+
+func (c *AllowList) CheckBuildCacheFrom(build *types.BuildConfig) {
+	if !c.supported("services.build.cache_from") && len(build.CacheFrom) != 0 {
+		build.CacheFrom = nil
+		c.error("services.build.cache_from")
+	}
+}
+
+func (c *AllowList) CheckBuildExtraHosts(build *types.BuildConfig) {
+	if !c.supported("services.build.extra_hosts") && len(build.ExtraHosts) != 0 {
+		build.ExtraHosts = nil
+		c.error("services.build.extra_hosts")
+	}
+}
+
+func (c *AllowList) CheckBuildIsolation(build *types.BuildConfig) {
+	if !c.supported("services.build.isolation") && build.Isolation != "" {
+		build.Isolation = ""
+		c.error("services.build.isolation")
+	}
+}
+
+func (c *AllowList) CheckBuildNetwork(build *types.BuildConfig) {
+	if !c.supported("services.build.network") && build.Network != "" {
+		build.Network = ""
+		c.error("services.build.network")
+	}
+}
+
+func (c *AllowList) CheckBuildTarget(build *types.BuildConfig) {
+	if !c.supported("services.build.target") && build.Target != "" {
+		build.Target = ""
+		c.error("services.build.target")
+	}
+}
diff --git a/compatibility/checker.go b/compatibility/checker.go
new file mode 100644
index 0000000..62ae63b
--- /dev/null
+++ b/compatibility/checker.go
@@ -0,0 +1,404 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compatibility
+
+import "github.com/compose-spec/compose-go/types"
+
+type Checker interface {
+	Errors() []error
+	CheckBlkioConfig(build *types.ServiceConfig)
+	CheckBuild(build *types.ServiceConfig) bool
+	CheckBuildArgs(build *types.BuildConfig)
+	CheckBuildLabels(build *types.BuildConfig)
+	CheckBuildCacheFrom(build *types.BuildConfig)
+	CheckBuildExtraHosts(build *types.BuildConfig)
+	CheckBuildIsolation(build *types.BuildConfig)
+	CheckBuildNetwork(build *types.BuildConfig)
+	CheckBuildTarget(build *types.BuildConfig)
+	CheckCapAdd(service *types.ServiceConfig)
+	CheckCapDrop(service *types.ServiceConfig)
+	CheckCgroupParent(service *types.ServiceConfig)
+	CheckCPUCount(service *types.ServiceConfig)
+	CheckCPUPercent(service *types.ServiceConfig)
+	CheckCPUPeriod(service *types.ServiceConfig)
+	CheckCPUQuota(service *types.ServiceConfig)
+	CheckCPURTRuntime(service *types.ServiceConfig)
+	CheckCPURTPeriod(service *types.ServiceConfig)
+	CheckCPUs(service *types.ServiceConfig)
+	CheckCPUSet(service *types.ServiceConfig)
+	CheckCPUShares(service *types.ServiceConfig)
+	CheckCommand(service *types.ServiceConfig)
+	CheckConfigs(service *types.ServiceConfig)
+	CheckContainerName(service *types.ServiceConfig)
+	CheckCredentialSpec(service *types.ServiceConfig)
+	CheckDependsOn(service *types.ServiceConfig)
+	CheckDevices(service *types.ServiceConfig)
+	CheckDNS(service *types.ServiceConfig)
+	CheckDNSOpts(service *types.ServiceConfig)
+	CheckDNSSearch(service *types.ServiceConfig)
+	CheckDomainName(service *types.ServiceConfig)
+	CheckEntrypoint(service *types.ServiceConfig)
+	CheckEnvironment(service *types.ServiceConfig)
+	CheckEnvFile(service *types.ServiceConfig)
+	CheckExpose(service *types.ServiceConfig)
+	CheckExtends(service *types.ServiceConfig)
+	CheckExternalLinks(service *types.ServiceConfig)
+	CheckExtraHosts(service *types.ServiceConfig)
+	CheckGroupAdd(service *types.ServiceConfig)
+	CheckHostname(service *types.ServiceConfig)
+	CheckHealthCheckTest(h *types.HealthCheckConfig)
+	CheckHealthCheckTimeout(h *types.HealthCheckConfig)
+	CheckHealthCheckInterval(h *types.HealthCheckConfig)
+	CheckHealthCheckRetries(h *types.HealthCheckConfig)
+	CheckHealthCheckStartPeriod(h *types.HealthCheckConfig)
+	CheckInit(service *types.ServiceConfig)
+	CheckIpc(service *types.ServiceConfig)
+	CheckIsolation(service *types.ServiceConfig)
+	CheckLabels(service *types.ServiceConfig)
+	CheckLinks(service *types.ServiceConfig)
+	CheckLoggingDriver(logging *types.LoggingConfig)
+	CheckLoggingOptions(logging *types.LoggingConfig)
+	CheckMemLimit(service *types.ServiceConfig)
+	CheckMemReservation(service *types.ServiceConfig)
+	CheckMemSwapLimit(service *types.ServiceConfig)
+	CheckMemSwappiness(service *types.ServiceConfig)
+	CheckMacAddress(service *types.ServiceConfig)
+	CheckNet(service *types.ServiceConfig)
+	CheckNetworkMode(service *types.ServiceConfig)
+	CheckNetworkAliases(n *types.ServiceNetworkConfig)
+	CheckNetworkIpv4Address(n *types.ServiceNetworkConfig)
+	CheckNetworkIpv6Address(n *types.ServiceNetworkConfig)
+	CheckOomKillDisable(service *types.ServiceConfig)
+	CheckOomScoreAdj(service *types.ServiceConfig)
+	CheckPid(service *types.ServiceConfig)
+	CheckPidLimit(service *types.ServiceConfig)
+	CheckPlatform(service *types.ServiceConfig)
+	CheckPortsMode(p *types.ServicePortConfig)
+	CheckPortsTarget(p *types.ServicePortConfig)
+	CheckPortsPublished(p *types.ServicePortConfig)
+	CheckPortsProtocol(p *types.ServicePortConfig)
+	CheckPrivileged(service *types.ServiceConfig)
+	CheckReadOnly(service *types.ServiceConfig)
+	CheckRestart(service *types.ServiceConfig)
+	CheckRuntime(service *types.ServiceConfig)
+	CheckScale(service *types.ServiceConfig)
+	CheckSecrets(service *types.ServiceConfig)
+	CheckFileReferenceSource(s string, config *types.FileReferenceConfig)
+	CheckFileReferenceTarget(s string, config *types.FileReferenceConfig)
+	CheckFileReferenceUID(s string, config *types.FileReferenceConfig)
+	CheckFileReferenceGID(s string, config *types.FileReferenceConfig)
+	CheckFileReferenceMode(s string, config *types.FileReferenceConfig)
+	CheckSecurityOpt(service *types.ServiceConfig)
+	CheckShmSize(service *types.ServiceConfig)
+	CheckStdinOpen(service *types.ServiceConfig)
+	CheckStopGracePeriod(service *types.ServiceConfig)
+	CheckStopSignal(service *types.ServiceConfig)
+	CheckSysctls(service *types.ServiceConfig)
+	CheckTmpfs(service *types.ServiceConfig)
+	CheckTty(service *types.ServiceConfig)
+	CheckUlimits(service *types.ServiceConfig)
+	CheckUser(service *types.ServiceConfig)
+	CheckUserNSMode(service *types.ServiceConfig)
+	CheckUts(service *types.ServiceConfig)
+	CheckVolumeDriver(service *types.ServiceConfig)
+	CheckVolumesSource(config *types.ServiceVolumeConfig)
+	CheckVolumesTarget(config *types.ServiceVolumeConfig)
+	CheckVolumesReadOnly(config *types.ServiceVolumeConfig)
+	CheckVolumesConsistency(config *types.ServiceVolumeConfig)
+	CheckVolumesBind(config *types.ServiceVolumeBind)
+	CheckVolumesVolume(config *types.ServiceVolumeVolume)
+	CheckVolumesTmpfs(config *types.ServiceVolumeTmpfs)
+	CheckVolumesFrom(service *types.ServiceConfig)
+	CheckWorkingDir(service *types.ServiceConfig)
+	CheckVolumeConfigDriver(config *types.VolumeConfig)
+	CheckVolumeConfigDriverOpts(config *types.VolumeConfig)
+	CheckVolumeConfigExternal(config *types.VolumeConfig)
+	CheckVolumeConfigLabels(config *types.VolumeConfig)
+	CheckFileObjectConfigFile(s string, config *types.FileObjectConfig)
+	CheckFileObjectConfigExternal(s string, config *types.FileObjectConfig)
+	CheckFileObjectConfigLabels(s string, config *types.FileObjectConfig)
+	CheckFileObjectConfigDriver(s string, config *types.FileObjectConfig)
+	CheckFileObjectConfigDriverOpts(s string, config *types.FileObjectConfig)
+	CheckFileObjectConfigTemplateDriver(s string, config *types.FileObjectConfig)
+	CheckDeploy(deploy *types.ServiceConfig) bool
+	CheckDeployEndpointMode(deploy *types.DeployConfig)
+	CheckDeployLabels(deploy *types.DeployConfig)
+	CheckDeployMode(deploy *types.DeployConfig)
+	CheckDeployReplicas(deploy *types.DeployConfig)
+	CheckDeployRestartPolicy(deploy *types.DeployConfig) bool
+	CheckDeployRollbackConfig(deploy *types.DeployConfig) bool
+	CheckDeployUpdateConfig(deploy *types.DeployConfig) bool
+	CheckPlacementConstraints(p *types.Placement)
+	CheckPlacementMaxReplicas(p *types.Placement)
+	CheckPlacementPreferences(p *types.Placement)
+	CheckRestartPolicyDelay(policy *types.RestartPolicy)
+	CheckRestartPolicyCondition(policy *types.RestartPolicy)
+	CheckRestartPolicyMaxAttempts(policy *types.RestartPolicy)
+	CheckRestartPolicyWindow(policy *types.RestartPolicy)
+	CheckUpdateConfigDelay(rollback string, config *types.UpdateConfig)
+	CheckUpdateConfigFailureAction(rollback string, config *types.UpdateConfig)
+	CheckUpdateConfigMaxFailureRatio(rollback string, config *types.UpdateConfig)
+	CheckUpdateConfigMonitor(rollback string, config *types.UpdateConfig)
+	CheckUpdateConfigOrder(rollback string, config *types.UpdateConfig)
+	CheckUpdateConfigParallelism(rollback string, config *types.UpdateConfig)
+	CheckDeployResourcesNanoCPUs(s string, resource *types.Resource)
+	CheckDeployResourcesMemoryBytes(s string, resource *types.Resource)
+	CheckDeployResourcesGenericResources(s string, resource *types.Resource)
+	CheckDeployResourcesLimits(deploy *types.DeployConfig) bool
+	CheckDeployResourcesReservations(deploy *types.DeployConfig) bool
+	CheckHealthCheck(service *types.ServiceConfig) bool
+	CheckLogging(service *types.ServiceConfig) bool
+	CheckNetworks(service *types.ServiceConfig) bool
+	CheckPorts(service *types.ServiceConfig) bool
+	CheckServiceVolumes(service *types.ServiceConfig) bool
+	CheckNetworkConfigIpam(network *types.NetworkConfig)
+	CheckNetworkConfigDriver(network *types.NetworkConfig)
+	CheckNetworkConfigDriverOpts(network *types.NetworkConfig)
+	CheckNetworkConfigExternal(network *types.NetworkConfig)
+	CheckNetworkConfigInternal(network *types.NetworkConfig)
+	CheckNetworkConfigAttachable(network *types.NetworkConfig)
+	CheckNetworkConfigLabels(network *types.NetworkConfig)
+}
+
+func Check(project *types.Project, c Checker) {
+	for i, service := range project.Services {
+		CheckServiceConfig(&service, c)
+		project.Services[i] = service
+	}
+
+	for i, network := range project.Networks {
+		CheckNetworkConfig(&network, c)
+		project.Networks[i] = network
+	}
+
+	for i, volume := range project.Volumes {
+		CheckVolumeConfig(&volume, c)
+		project.Volumes[i] = volume
+	}
+
+	for i, config := range project.Configs {
+		CheckConfigsConfig(&config, c)
+		project.Configs[i] = config
+	}
+
+	for i, secret := range project.Secrets {
+		CheckSecretsConfig(&secret, c)
+		project.Secrets[i] = secret
+	}
+}
+
+func CheckServiceConfig(service *types.ServiceConfig, c Checker) {
+	c.CheckBlkioConfig(service)
+	if service.Build != nil && c.CheckBuild(service) {
+		c.CheckBuildArgs(service.Build)
+		c.CheckBuildLabels(service.Build)
+		c.CheckBuildCacheFrom(service.Build)
+		c.CheckBuildNetwork(service.Build)
+		c.CheckBuildTarget(service.Build)
+	}
+	c.CheckCapAdd(service)
+	c.CheckCapDrop(service)
+	c.CheckCgroupParent(service)
+	c.CheckCPUCount(service)
+	c.CheckCPUPercent(service)
+	c.CheckCPUPeriod(service)
+	c.CheckCPUQuota(service)
+	c.CheckCPURTPeriod(service)
+	c.CheckCPURTRuntime(service)
+	c.CheckCPUs(service)
+	c.CheckCPUSet(service)
+	c.CheckCPUShares(service)
+	c.CheckCommand(service)
+	c.CheckConfigs(service)
+	c.CheckContainerName(service)
+	c.CheckCredentialSpec(service)
+	c.CheckDependsOn(service)
+	if service.Deploy != nil && c.CheckDeploy(service) {
+		c.CheckDeployEndpointMode(service.Deploy)
+		c.CheckDeployLabels(service.Deploy)
+		c.CheckDeployMode(service.Deploy)
+		c.CheckPlacementConstraints(&service.Deploy.Placement)
+		c.CheckPlacementMaxReplicas(&service.Deploy.Placement)
+		c.CheckPlacementPreferences(&service.Deploy.Placement)
+		c.CheckDeployReplicas(service.Deploy)
+		if service.Deploy.Resources.Limits != nil && c.CheckDeployResourcesLimits(service.Deploy) {
+			c.CheckDeployResourcesNanoCPUs(ResourceLimits, service.Deploy.Resources.Limits)
+			c.CheckDeployResourcesMemoryBytes(ResourceLimits, service.Deploy.Resources.Limits)
+			c.CheckDeployResourcesGenericResources(ResourceLimits, service.Deploy.Resources.Limits)
+		}
+		if service.Deploy.Resources.Reservations != nil && c.CheckDeployResourcesReservations(service.Deploy) {
+			c.CheckDeployResourcesNanoCPUs(ResourceReservations, service.Deploy.Resources.Limits)
+			c.CheckDeployResourcesMemoryBytes(ResourceReservations, service.Deploy.Resources.Limits)
+			c.CheckDeployResourcesGenericResources(ResourceReservations, service.Deploy.Resources.Limits)
+		}
+		if service.Deploy.RestartPolicy != nil && c.CheckDeployRestartPolicy(service.Deploy) {
+			c.CheckRestartPolicyCondition(service.Deploy.RestartPolicy)
+			c.CheckRestartPolicyDelay(service.Deploy.RestartPolicy)
+			c.CheckRestartPolicyMaxAttempts(service.Deploy.RestartPolicy)
+			c.CheckRestartPolicyWindow(service.Deploy.RestartPolicy)
+		}
+		if service.Deploy.UpdateConfig != nil && c.CheckDeployUpdateConfig(service.Deploy) {
+			c.CheckUpdateConfigDelay(UpdateConfigUpdate, service.Deploy.UpdateConfig)
+			c.CheckUpdateConfigFailureAction(UpdateConfigUpdate, service.Deploy.UpdateConfig)
+			c.CheckUpdateConfigMaxFailureRatio(UpdateConfigUpdate, service.Deploy.UpdateConfig)
+			c.CheckUpdateConfigMonitor(UpdateConfigUpdate, service.Deploy.UpdateConfig)
+			c.CheckUpdateConfigOrder(UpdateConfigUpdate, service.Deploy.UpdateConfig)
+			c.CheckUpdateConfigParallelism(UpdateConfigUpdate, service.Deploy.UpdateConfig)
+		}
+		if service.Deploy.RollbackConfig != nil && c.CheckDeployRollbackConfig(service.Deploy) {
+			c.CheckUpdateConfigDelay(UpdateConfigRollback, service.Deploy.RollbackConfig)
+			c.CheckUpdateConfigFailureAction(UpdateConfigRollback, service.Deploy.RollbackConfig)
+			c.CheckUpdateConfigMaxFailureRatio(UpdateConfigRollback, service.Deploy.RollbackConfig)
+			c.CheckUpdateConfigMonitor(UpdateConfigRollback, service.Deploy.RollbackConfig)
+			c.CheckUpdateConfigOrder(UpdateConfigRollback, service.Deploy.RollbackConfig)
+			c.CheckUpdateConfigParallelism(UpdateConfigRollback, service.Deploy.RollbackConfig)
+		}
+	}
+	c.CheckDevices(service)
+	c.CheckDNS(service)
+	c.CheckDNSOpts(service)
+	c.CheckDNSSearch(service)
+	c.CheckDomainName(service)
+	c.CheckEntrypoint(service)
+	c.CheckEnvironment(service)
+	c.CheckEnvFile(service)
+	c.CheckExpose(service)
+	c.CheckExtends(service)
+	c.CheckExternalLinks(service)
+	c.CheckExtraHosts(service)
+	c.CheckGroupAdd(service)
+	c.CheckHostname(service)
+	if service.HealthCheck != nil && c.CheckHealthCheck(service) {
+		c.CheckHealthCheckInterval(service.HealthCheck)
+		c.CheckHealthCheckRetries(service.HealthCheck)
+		c.CheckHealthCheckStartPeriod(service.HealthCheck)
+		c.CheckHealthCheckTest(service.HealthCheck)
+		c.CheckHealthCheckTimeout(service.HealthCheck)
+	}
+	c.CheckInit(service)
+	c.CheckIpc(service)
+	c.CheckIsolation(service)
+	c.CheckLabels(service)
+	c.CheckLinks(service)
+	if service.Logging != nil && c.CheckLogging(service) {
+		c.CheckLoggingDriver(service.Logging)
+		c.CheckLoggingOptions(service.Logging)
+	}
+	c.CheckMemLimit(service)
+	c.CheckMemReservation(service)
+	c.CheckMemSwapLimit(service)
+	c.CheckMemSwappiness(service)
+	c.CheckMacAddress(service)
+	c.CheckNet(service)
+	c.CheckNetworkMode(service)
+	if len(service.Networks) > 0 && c.CheckNetworks(service) {
+		for _, n := range service.Networks {
+			if n != nil {
+				c.CheckNetworkAliases(n)
+				c.CheckNetworkIpv4Address(n)
+				c.CheckNetworkIpv6Address(n)
+			}
+		}
+	}
+	c.CheckOomKillDisable(service)
+	c.CheckOomScoreAdj(service)
+	c.CheckPid(service)
+	c.CheckPidLimit(service)
+	c.CheckPlatform(service)
+	if len(service.Ports) > 0 && c.CheckPorts(service) {
+		for i, p := range service.Ports {
+			c.CheckPortsMode(&p)
+			c.CheckPortsTarget(&p)
+			c.CheckPortsProtocol(&p)
+			c.CheckPortsProtocol(&p)
+			service.Ports[i] = p
+		}
+	}
+	c.CheckPrivileged(service)
+	c.CheckReadOnly(service)
+	c.CheckRestart(service)
+	c.CheckRuntime(service)
+	c.CheckScale(service)
+	c.CheckSecrets(service)
+	c.CheckSecurityOpt(service)
+	c.CheckShmSize(service)
+	c.CheckStdinOpen(service)
+	c.CheckStopGracePeriod(service)
+	c.CheckStopSignal(service)
+	c.CheckSysctls(service)
+	c.CheckTmpfs(service)
+	c.CheckTty(service)
+	c.CheckUlimits(service)
+	c.CheckUser(service)
+	c.CheckUserNSMode(service)
+	c.CheckUts(service)
+	c.CheckVolumeDriver(service)
+	if len(service.Volumes) > 0 && c.CheckServiceVolumes(service) {
+		for i, v := range service.Volumes {
+			c.CheckVolumesSource(&v)
+			c.CheckVolumesTarget(&v)
+			c.CheckVolumesReadOnly(&v)
+			switch v.Type {
+			case types.VolumeTypeBind:
+				c.CheckVolumesBind(v.Bind)
+			case types.VolumeTypeVolume:
+				c.CheckVolumesVolume(v.Volume)
+			case types.VolumeTypeTmpfs:
+				c.CheckVolumesTmpfs(v.Tmpfs)
+			}
+			service.Volumes[i] = v
+		}
+	}
+	c.CheckVolumesFrom(service)
+	c.CheckWorkingDir(service)
+}
+
+func CheckNetworkConfig(network *types.NetworkConfig, c Checker) {
+	c.CheckNetworkConfigDriver(network)
+	c.CheckNetworkConfigDriverOpts(network)
+	c.CheckNetworkConfigIpam(network)
+	c.CheckNetworkConfigExternal(network)
+	c.CheckNetworkConfigInternal(network)
+	c.CheckNetworkConfigAttachable(network)
+	c.CheckNetworkConfigLabels(network)
+}
+
+func CheckVolumeConfig(config *types.VolumeConfig, c Checker) {
+	c.CheckVolumeConfigDriver(config)
+	c.CheckVolumeConfigDriverOpts(config)
+	c.CheckVolumeConfigExternal(config)
+	c.CheckVolumeConfigLabels(config)
+}
+
+func CheckConfigsConfig(config *types.ConfigObjConfig, c Checker) {
+	ref := types.FileObjectConfig(*config)
+	CheckFileObjectConfig("configs", &ref, c)
+}
+
+func CheckSecretsConfig(config *types.SecretConfig, c Checker) {
+	ref := types.FileObjectConfig(*config)
+	CheckFileObjectConfig("secrets", &ref, c)
+}
+
+func CheckFileObjectConfig(s string, config *types.FileObjectConfig, c Checker) {
+	c.CheckFileObjectConfigDriver(s, config)
+	c.CheckFileObjectConfigDriverOpts(s, config)
+	c.CheckFileObjectConfigExternal(s, config)
+	c.CheckFileObjectConfigFile(s, config)
+	c.CheckFileObjectConfigLabels(s, config)
+	c.CheckFileObjectConfigTemplateDriver(s, config)
+}
diff --git a/compatibility/configs.go b/compatibility/configs.go
new file mode 100644
index 0000000..054a5ba
--- /dev/null
+++ b/compatibility/configs.go
@@ -0,0 +1,71 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compatibility
+
+import (
+	"fmt"
+
+	"github.com/compose-spec/compose-go/types"
+)
+
+func (c *AllowList) CheckFileObjectConfigFile(s string, config *types.FileObjectConfig) {
+	k := fmt.Sprintf("%s.file", s)
+	if !c.supported(k) && config.File != "" {
+		config.File = ""
+		c.error(k)
+	}
+}
+
+func (c *AllowList) CheckFileObjectConfigExternal(s string, config *types.FileObjectConfig) {
+	k := fmt.Sprintf("%s.external", s)
+	if !c.supported(k) && config.External.External {
+		config.External.External = false
+		c.error(k)
+	}
+}
+
+func (c *AllowList) CheckFileObjectConfigLabels(s string, config *types.FileObjectConfig) {
+	k := fmt.Sprintf("%s.labels", s)
+	if !c.supported(k) && len(config.Labels) != 0 {
+		config.Labels = nil
+		c.error(k)
+	}
+}
+
+func (c *AllowList) CheckFileObjectConfigDriver(s string, config *types.FileObjectConfig) {
+	k := fmt.Sprintf("%s.driver", s)
+	if !c.supported(k) && config.Driver != "" {
+		config.Driver = ""
+		c.error(k)
+	}
+}
+
+func (c *AllowList) CheckFileObjectConfigDriverOpts(s string, config *types.FileObjectConfig) {
+	k := fmt.Sprintf("%s.driver_opts", s)
+	if !c.supported(k) && len(config.DriverOpts) != 0 {
+		config.DriverOpts = nil
+		c.error(k)
+	}
+}
+
+func (c *AllowList) CheckFileObjectConfigTemplateDriver(s string, config *types.FileObjectConfig) {
+	k := fmt.Sprintf("%s.template_driver", s)
+	if !c.supported(k) && config.TemplateDriver != "" {
+		config.TemplateDriver = ""
+		c.error(k)
+	}
+}
diff --git a/compatibility/deploy.go b/compatibility/deploy.go
new file mode 100644
index 0000000..e96c5f7
--- /dev/null
+++ b/compatibility/deploy.go
@@ -0,0 +1,224 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compatibility
+
+import (
+	"fmt"
+
+	"github.com/compose-spec/compose-go/types"
+)
+
+func (c *AllowList) CheckDeploy(service *types.ServiceConfig) bool {
+	if !c.supported("deploy") && service.Deploy != nil {
+		service.Deploy = nil
+		c.error("deploy")
+		return false
+	}
+	return true
+}
+
+func (c *AllowList) CheckDeployMode(config *types.DeployConfig) {
+	if !c.supported("services.deploy.mode") && config.Mode != "" {
+		config.Mode = ""
+		c.error("services.deploy.mode")
+	}
+}
+func (c *AllowList) CheckDeployReplicas(config *types.DeployConfig) {
+	if !c.supported("services.deploy.replicas") && config.Replicas != nil {
+		config.Replicas = nil
+		c.error("services.deploy.replicas")
+	}
+}
+func (c *AllowList) CheckDeployLabels(config *types.DeployConfig) {
+	if !c.supported("services.deploy.labels") && len(config.Labels) != 0 {
+		config.Labels = nil
+		c.error("services.deploy.labels")
+	}
+}
+
+const (
+	UpdateConfigUpdate   = "update_config"
+	UpdateConfigRollback = "rolback_config"
+)
+
+func (c *AllowList) CheckDeployUpdateConfig(config *types.DeployConfig) bool {
+	if !c.supported("services.deploy.update_config") {
+		config.UpdateConfig = nil
+		c.error("services.deploy.update_config")
+		return false
+	}
+	return true
+}
+
+func (c *AllowList) CheckDeployRollbackConfig(config *types.DeployConfig) bool {
+	if !c.supported("services.deploy.rollback_config") {
+		config.RollbackConfig = nil
+		c.error("services.deploy.rollback_config")
+		return false
+	}
+	return true
+}
+
+func (c *AllowList) CheckUpdateConfigParallelism(s string, config *types.UpdateConfig) {
+	k := fmt.Sprintf("services.deploy.%s.parallelism", s)
+	if !c.supported(k) && config.Parallelism != nil {
+		config.Parallelism = nil
+		c.error(k)
+	}
+}
+func (c *AllowList) CheckUpdateConfigDelay(s string, config *types.UpdateConfig) {
+	k := fmt.Sprintf("services.deploy.%s.delay", s)
+	if !c.supported(k) && config.Delay != 0 {
+		config.Delay = 0
+		c.error(k)
+	}
+}
+func (c *AllowList) CheckUpdateConfigFailureAction(s string, config *types.UpdateConfig) {
+	k := fmt.Sprintf("services.deploy.%s.failure_action", s)
+	if !c.supported(k) && config.FailureAction != "" {
+		config.FailureAction = ""
+		c.error(k)
+	}
+}
+func (c *AllowList) CheckUpdateConfigMonitor(s string, config *types.UpdateConfig) {
+	k := fmt.Sprintf("services.deploy.%s.monitor", s)
+	if !c.supported(k) && config.Monitor != 0 {
+		config.Monitor = 0
+		c.error(k)
+	}
+}
+func (c *AllowList) CheckUpdateConfigMaxFailureRatio(s string, config *types.UpdateConfig) {
+	k := fmt.Sprintf("services.deploy.%s.max_failure_ratio", s)
+	if !c.supported(k) && config.MaxFailureRatio != 0 {
+		config.MaxFailureRatio = 0
+		c.error(k)
+	}
+}
+func (c *AllowList) CheckUpdateConfigOrder(s string, config *types.UpdateConfig) {
+	k := fmt.Sprintf("services.deploy.%s.order", s)
+	if !c.supported(k) && config.Order != "" {
+		config.Order = ""
+		c.error(k)
+	}
+}
+
+const (
+	ResourceLimits       = "limits"
+	ResourceReservations = "reservations"
+)
+
+func (c *AllowList) CheckDeployResourcesLimits(config *types.DeployConfig) bool {
+	if !c.supported("services.deploy.resources.limits") {
+		config.Resources.Limits = nil
+		c.error("services.deploy.resources.limits")
+		return false
+	}
+	return true
+}
+
+func (c *AllowList) CheckDeployResourcesReservations(config *types.DeployConfig) bool {
+	if !c.supported("services.deploy.resources.reservations") {
+		config.Resources.Reservations = nil
+		c.error("services.deploy.resources.reservations")
+		return false
+	}
+	return true
+}
+
+func (c *AllowList) CheckDeployResourcesNanoCPUs(s string, r *types.Resource) {
+	k := fmt.Sprintf("services.deploy.resources.%s.cpus", s)
+	if !c.supported(k) && r.NanoCPUs != "" {
+		r.NanoCPUs = ""
+		c.error(k)
+	}
+}
+func (c *AllowList) CheckDeployResourcesMemoryBytes(s string, r *types.Resource) {
+	k := fmt.Sprintf("services.deploy.resources.%s.memory", s)
+	if !c.supported(k) && r.MemoryBytes != 0 {
+		r.MemoryBytes = 0
+		c.error(k)
+	}
+}
+func (c *AllowList) CheckDeployResourcesGenericResources(s string, r *types.Resource) {
+	k := fmt.Sprintf("services.deploy.resources.%s.generic_resources", s)
+	if !c.supported(k) && len(r.GenericResources) != 0 {
+		r.GenericResources = nil
+		c.error(k)
+	}
+}
+
+func (c *AllowList) CheckDeployRestartPolicy(config *types.DeployConfig) bool {
+	if !c.supported("services.deploy.restart_policy") {
+		config.RestartPolicy = nil
+		c.error("services.deploy.restart_policy")
+		return false
+	}
+	return true
+}
+
+func (c *AllowList) CheckRestartPolicyCondition(p *types.RestartPolicy) {
+	if !c.supported("services.deploy.restart_policy.condition") && p.Condition != "" {
+		p.Condition = ""
+		c.error("services.deploy.restart_policy.condition")
+	}
+}
+func (c *AllowList) CheckRestartPolicyDelay(p *types.RestartPolicy) {
+	if !c.supported("services.deploy.restart_policy.delay") && p.Delay != nil {
+		p.Delay = nil
+		c.error("services.deploy.restart_policy.delay")
+	}
+}
+func (c *AllowList) CheckRestartPolicyMaxAttempts(p *types.RestartPolicy) {
+	if !c.supported("services.deploy.restart_policy.max_attempts") && p.MaxAttempts != nil {
+		p.MaxAttempts = nil
+		c.error("services.deploy.restart_policy.max_attempts")
+	}
+}
+func (c *AllowList) CheckRestartPolicyWindow(p *types.RestartPolicy) {
+	if !c.supported("services.deploy.restart_policy.window") && p.Window != nil {
+		p.Window = nil
+		c.error("services.deploy.restart_policy.window")
+	}
+}
+
+func (c *AllowList) CheckPlacementConstraints(p *types.Placement) {
+	if !c.supported("services.deploy.placement", "services.deploy.placement.constraints") && len(p.Constraints) != 0 {
+		p.Constraints = nil
+		c.error("services.deploy.restart_policy.constraints")
+	}
+}
+
+func (c *AllowList) CheckPlacementPreferences(p *types.Placement) {
+	if !c.supported("services.deploy.placement", "services.deploy.placement.preferences") && p.Preferences != nil {
+		p.Preferences = nil
+		c.error("services.deploy.restart_policy.preferences")
+	}
+}
+
+func (c *AllowList) CheckPlacementMaxReplicas(p *types.Placement) {
+	if !c.supported("services.deploy.placement", "services.deploy.placement.max_replicas_per_node") && p.MaxReplicas != 0 {
+		p.MaxReplicas = 0
+		c.error("services.deploy.restart_policy.max_replicas_per_node")
+	}
+}
+
+func (c *AllowList) CheckDeployEndpointMode(config *types.DeployConfig) {
+	if !c.supported("services.deploy.endpoint_mode") && config.EndpointMode != "" {
+		config.EndpointMode = ""
+		c.error("services.deploy.endpoint_mode")
+	}
+}
diff --git a/compatibility/networks.go b/compatibility/networks.go
new file mode 100644
index 0000000..eeab88c
--- /dev/null
+++ b/compatibility/networks.go
@@ -0,0 +1,99 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compatibility
+
+import "github.com/compose-spec/compose-go/types"
+
+func (c *AllowList) CheckNetworkConfig(network *types.NetworkConfig) {
+	c.CheckNetworkConfigDriver(network)
+	c.CheckNetworkConfigDriverOpts(network)
+	c.CheckNetworkConfigIpam(network)
+	c.CheckNetworkConfigExternal(network)
+	c.CheckNetworkConfigInternal(network)
+	c.CheckNetworkConfigAttachable(network)
+	c.CheckNetworkConfigLabels(network)
+}
+
+func (c *AllowList) CheckNetworkConfigDriver(config *types.NetworkConfig) {
+	if !c.supported("networks.driver") && config.Driver != "" {
+		config.Driver = ""
+		c.error("networks.driver")
+	}
+}
+
+func (c *AllowList) CheckNetworkConfigDriverOpts(config *types.NetworkConfig) {
+	if !c.supported("networks.driver_opts") && len(config.DriverOpts) != 0 {
+		config.DriverOpts = nil
+		c.error("networks.driver_opts")
+	}
+}
+
+func (c *AllowList) CheckNetworkConfigIpam(config *types.NetworkConfig) {
+	c.CheckNetworkConfigIpamDriver(&config.Ipam)
+	if len(config.Ipam.Config) != 0 {
+		if !c.supported("networks.ipam.config") {
+			c.error("networks.ipam.config")
+			return
+		}
+		for _, p := range config.Ipam.Config {
+			c.CheckNetworkConfigIpamSubnet(p)
+		}
+	}
+}
+
+func (c *AllowList) CheckNetworkConfigIpamDriver(config *types.IPAMConfig) {
+	if !c.supported("networks.ipam.driver") && config.Driver != "" {
+		config.Driver = ""
+		c.error("networks.ipam.driver")
+	}
+}
+
+func (c *AllowList) CheckNetworkConfigIpamSubnet(config *types.IPAMPool) {
+	if !c.supported("networks.ipam.config.subnet") && config.Subnet != "" {
+		config.Subnet = ""
+		c.error("networks.ipam.config.subnet")
+	}
+
+}
+
+func (c *AllowList) CheckNetworkConfigExternal(config *types.NetworkConfig) {
+	if !c.supported("networks.external") && config.External.External {
+		config.External.External = false
+		c.error("networks.external")
+	}
+}
+
+func (c *AllowList) CheckNetworkConfigInternal(config *types.NetworkConfig) {
+	if !c.supported("networks.internal") && config.Internal {
+		config.Internal = false
+		c.error("networks.internal")
+	}
+}
+
+func (c *AllowList) CheckNetworkConfigAttachable(config *types.NetworkConfig) {
+	if !c.supported("networks.attachable") && config.Attachable {
+		config.Attachable = false
+		c.error("networks.attachable")
+	}
+}
+
+func (c *AllowList) CheckNetworkConfigLabels(config *types.NetworkConfig) {
+	if !c.supported("networks.labels") && len(config.Labels) != 0 {
+		config.Labels = nil
+		c.error("networks.labels")
+	}
+}
diff --git a/compatibility/services.go b/compatibility/services.go
new file mode 100644
index 0000000..5203d4f
--- /dev/null
+++ b/compatibility/services.go
@@ -0,0 +1,777 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compatibility
+
+import (
+	"fmt"
+
+	"github.com/compose-spec/compose-go/types"
+)
+
+func (c *AllowList) CheckBlkioConfig(service *types.ServiceConfig) {
+	if !c.supported("services.blkio_config") && service.BlkioConfig != "" {
+		service.BlkioConfig = ""
+		c.error("services.blkio_config")
+	}
+}
+
+func (c *AllowList) CheckCapAdd(service *types.ServiceConfig) {
+	if !c.supported("services.cap_add") && len(service.CapAdd) != 0 {
+		service.CapAdd = nil
+		c.error("services.cap_add")
+	}
+}
+
+func (c *AllowList) CheckCapDrop(service *types.ServiceConfig) {
+	if !c.supported("services.cap_drop") && len(service.CapDrop) != 0 {
+		service.CapDrop = nil
+		c.error("services.cap_drop")
+	}
+}
+
+func (c *AllowList) CheckCgroupParent(service *types.ServiceConfig) {
+	if !c.supported("services.cgroup_parent") && service.CgroupParent != "" {
+		service.CgroupParent = ""
+		c.error("services.cgroup_parent")
+	}
+}
+
+func (c *AllowList) CheckCPUQuota(service *types.ServiceConfig) {
+	if !c.supported("services.cpu_quota") && service.CPUQuota != 0 {
+		service.CPUQuota = 0
+		c.error("services.cpu_quota")
+	}
+}
+
+func (c *AllowList) CheckCPUCount(service *types.ServiceConfig) {
+	if !c.supported("services.cpu_count") && service.CPUCount != 0 {
+		service.CPUCount = 0
+		c.error("services.cpu_count")
+	}
+}
+
+func (c *AllowList) CheckCPUPercent(service *types.ServiceConfig) {
+	if !c.supported("services.cpu_percent") && service.CPUPercent != 0 {
+		service.CPUPercent = 0
+		c.error("services.cpu_percent")
+	}
+}
+
+func (c *AllowList) CheckCPUPeriod(service *types.ServiceConfig) {
+	if !c.supported("services.cpu_period") && service.CPUPeriod != 0 {
+		service.CPUPeriod = 0
+		c.error("services.cpu_period")
+	}
+}
+
+func (c *AllowList) CheckCPURTRuntime(service *types.ServiceConfig) {
+	if !c.supported("services.cpu_rt_runtime") && service.CPURTRuntime != 0 {
+		service.CPURTRuntime = 0
+		c.error("services.cpu_rt_period")
+	}
+}
+
+func (c *AllowList) CheckCPURTPeriod(service *types.ServiceConfig) {
+	if !c.supported("services.cpu_rt_period") && service.CPURTPeriod != 0 {
+		service.CPURTPeriod = 0
+		c.error("services.cpu_rt_period")
+	}
+}
+
+func (c *AllowList) CheckCPUs(service *types.ServiceConfig) {
+	if !c.supported("services.cpus") && service.CPUS != 0 {
+		service.CPUS = 0
+		c.error("services.cpus")
+	}
+}
+
+func (c *AllowList) CheckCPUSet(service *types.ServiceConfig) {
+	if !c.supported("services.cpuset") && service.CPUSet != "" {
+		service.CPUSet = ""
+		c.error("services.cpuset")
+	}
+}
+
+func (c *AllowList) CheckCPUShares(service *types.ServiceConfig) {
+	if !c.supported("services.cpu_shares") && service.CPUShares != 0 {
+		service.CPUShares = 0
+		c.error("services.cpu_shares")
+	}
+}
+
+func (c *AllowList) CheckCommand(service *types.ServiceConfig) {
+	if !c.supported("services.command") && len(service.Command) != 0 {
+		service.Command = nil
+		c.error("services.command")
+	}
+}
+
+func (c *AllowList) CheckConfigs(service *types.ServiceConfig) {
+	if len(service.Configs) != 0 {
+		if !c.supported("services.configs") {
+			service.Configs = nil
+			c.error("services.configs")
+			return
+		}
+		for i, s := range service.Secrets {
+			ref := types.FileReferenceConfig(s)
+			c.CheckFileReference("configs", &ref)
+			service.Secrets[i] = s
+		}
+	}
+}
+
+func (c *AllowList) CheckContainerName(service *types.ServiceConfig) {
+	if !c.supported("services.container_name") && service.ContainerName != "" {
+		service.ContainerName = ""
+		c.error("services.container_name")
+	}
+}
+
+func (c *AllowList) CheckCredentialSpec(service *types.ServiceConfig) {
+	if !c.supported("services.credential_spec") && service.CredentialSpec != nil {
+		service.CredentialSpec = nil
+		c.error("services.credential_spec")
+	}
+}
+
+func (c *AllowList) CheckDependsOn(service *types.ServiceConfig) {
+	if !c.supported("services.depends_on") && len(service.DependsOn) != 0 {
+		service.DependsOn = nil
+		c.error("services.depends_on")
+	}
+}
+
+func (c *AllowList) CheckDevices(service *types.ServiceConfig) {
+	if !c.supported("services.devices") && len(service.Devices) != 0 {
+		service.Devices = nil
+		c.error("services.devices")
+	}
+}
+
+func (c *AllowList) CheckDNS(service *types.ServiceConfig) {
+	if !c.supported("services.dns") && service.DNS != nil {
+		service.DNS = nil
+		c.error("services.dns")
+	}
+}
+
+func (c *AllowList) CheckDNSOpts(service *types.ServiceConfig) {
+	if !c.supported("services.dns_opt") && len(service.DNSOpts) != 0 {
+		service.DNSOpts = nil
+		c.error("services.dns_opt")
+	}
+}
+
+func (c *AllowList) CheckDNSSearch(service *types.ServiceConfig) {
+	if !c.supported("services.dns_search") && len(service.DNSSearch) != 0 {
+		service.DNSSearch = nil
+		c.error("services.dns_search")
+	}
+}
+
+func (c *AllowList) CheckDomainName(service *types.ServiceConfig) {
+	if !c.supported("services.domainname") && service.DomainName != "" {
+		service.DomainName = ""
+		c.error("services.domainname")
+	}
+}
+
+func (c *AllowList) CheckEntrypoint(service *types.ServiceConfig) {
+	if !c.supported("services.entrypoint") && len(service.Entrypoint) != 0 {
+		service.Entrypoint = nil
+		c.error("services.entrypoint")
+	}
+}
+
+func (c *AllowList) CheckEnvironment(service *types.ServiceConfig) {
+	if !c.supported("services.environment") && len(service.Environment) != 0 {
+		service.Environment = nil
+		c.error("services.environment")
+	}
+}
+
+func (c *AllowList) CheckEnvFile(service *types.ServiceConfig) {
+	if !c.supported("services.env_file") && len(service.EnvFile) != 0 {
+		service.EnvFile = nil
+		c.error("services.env_file")
+	}
+}
+
+func (c *AllowList) CheckExpose(service *types.ServiceConfig) {
+	if !c.supported("services.expose") && len(service.Expose) != 0 {
+		service.Expose = nil
+		c.error("services.expose")
+	}
+}
+
+func (c *AllowList) CheckExtends(service *types.ServiceConfig) {
+	if !c.supported("services.extends") && len(service.Extends) != 0 {
+		service.Extends = nil
+		c.error("services.extends")
+	}
+}
+
+func (c *AllowList) CheckExternalLinks(service *types.ServiceConfig) {
+	if !c.supported("services.external_links") && len(service.ExternalLinks) != 0 {
+		service.ExternalLinks = nil
+		c.error("services.external_links")
+	}
+}
+
+func (c *AllowList) CheckExtraHosts(service *types.ServiceConfig) {
+	if !c.supported("services.extra_hosts") && len(service.ExtraHosts) != 0 {
+		service.ExtraHosts = nil
+		c.error("services.extra_hosts")
+	}
+}
+
+func (c *AllowList) CheckGroupAdd(service *types.ServiceConfig) {
+	if !c.supported("services.group_app") && len(service.GroupAdd) != 0 {
+		service.GroupAdd = nil
+		c.error("services.group_app")
+	}
+}
+
+func (c *AllowList) CheckHostname(service *types.ServiceConfig) {
+	if !c.supported("services.hostname") && service.Hostname != "" {
+		service.Hostname = ""
+		c.error("services.hostname")
+	}
+}
+
+func (c *AllowList) CheckHealthCheck(service *types.ServiceConfig) bool {
+	if !c.supported("services.healthcheck") {
+		service.HealthCheck = nil
+		c.error("services.healthcheck")
+		return false
+	}
+	return true
+}
+
+func (c *AllowList) CheckHealthCheckTest(h *types.HealthCheckConfig) {
+	if !c.supported("services.healthcheck.test") && len(h.Test) != 0 {
+		h.Test = nil
+		c.error("services.healthcheck.test")
+	}
+}
+
+func (c *AllowList) CheckHealthCheckTimeout(h *types.HealthCheckConfig) {
+	if !c.supported("services.healthcheck.timeout") && h.Timeout != nil {
+		h.Timeout = nil
+		c.error("services.healthcheck.timeout")
+	}
+}
+
+func (c *AllowList) CheckHealthCheckInterval(h *types.HealthCheckConfig) {
+	if !c.supported("services.healthcheck.interval") && h.Interval != nil {
+		h.Interval = nil
+		c.error("services.healthcheck.interval")
+	}
+}
+
+func (c *AllowList) CheckHealthCheckRetries(h *types.HealthCheckConfig) {
+	if !c.supported("services.healthcheck.retries") && h.Retries != nil {
+		h.Retries = nil
+		c.error("services.healthcheck.retries")
+	}
+}
+
+func (c *AllowList) CheckHealthCheckStartPeriod(h *types.HealthCheckConfig) {
+	if !c.supported("services.healthcheck.start_period") && h.StartPeriod != nil {
+		h.StartPeriod = nil
+		c.error("services.healthcheck.start_period")
+	}
+}
+
+func (c *AllowList) CheckInit(service *types.ServiceConfig) {
+	if !c.supported("services.init") && service.Init != nil {
+		service.Init = nil
+		c.error("services.init")
+	}
+}
+
+func (c *AllowList) CheckIpc(service *types.ServiceConfig) {
+	if !c.supported("services.ipc") && service.Ipc != "" {
+		service.Ipc = ""
+		c.error("services.ipc")
+	}
+}
+
+func (c *AllowList) CheckIsolation(service *types.ServiceConfig) {
+	if !c.supported("services.isolation") && service.Isolation != "" {
+		service.Isolation = ""
+		c.error("services.isolation")
+	}
+}
+
+func (c *AllowList) CheckLabels(service *types.ServiceConfig) {
+	if !c.supported("services.labels") && len(service.Labels) != 0 {
+		service.Labels = nil
+		c.error("services.labels")
+	}
+}
+
+func (c *AllowList) CheckLinks(service *types.ServiceConfig) {
+	if !c.supported("services.links") && len(service.Links) != 0 {
+		service.Links = nil
+		c.error("services.links")
+	}
+}
+
+func (c *AllowList) CheckLogging(service *types.ServiceConfig) bool {
+	if !c.supported("services.logging") {
+		service.Logging = nil
+		c.error("services.logging")
+		return false
+	}
+	return true
+}
+
+func (c *AllowList) CheckLoggingDriver(logging *types.LoggingConfig) {
+	if !c.supported("services.logging.driver") && logging.Driver != "" {
+		logging.Driver = ""
+		c.error("services.logging.driver")
+	}
+}
+
+func (c *AllowList) CheckLoggingOptions(logging *types.LoggingConfig) {
+	if !c.supported("services.logging.options") && len(logging.Options) != 0 {
+		logging.Options = nil
+		c.error("services.logging.options")
+	}
+}
+
+func (c *AllowList) CheckMemLimit(service *types.ServiceConfig) {
+	if !c.supported("services.mem_limit") && service.MemLimit != 0 {
+		service.MemLimit = 0
+		c.error("services.mem_limit")
+	}
+}
+
+func (c *AllowList) CheckMemReservation(service *types.ServiceConfig) {
+	if !c.supported("services.mem_reservation") && service.MemReservation != 0 {
+		service.MemReservation = 0
+		c.error("services.mem_reservation")
+	}
+}
+
+func (c *AllowList) CheckMemSwapLimit(service *types.ServiceConfig) {
+	if !c.supported("services.memswap_limit") && service.MemSwapLimit != 0 {
+		service.MemSwapLimit = 0
+		c.error("services.memswap_limit")
+	}
+}
+
+func (c *AllowList) CheckMemSwappiness(service *types.ServiceConfig) {
+	if !c.supported("services.mem_swappiness") && service.MemSwappiness != 0 {
+		service.MemSwappiness = 0
+		c.error("services.mem_swappiness")
+	}
+}
+
+func (c *AllowList) CheckMacAddress(service *types.ServiceConfig) {
+	if !c.supported("services.mac_address") && service.MacAddress != "" {
+		service.MacAddress = ""
+		c.error("services.mac_address")
+	}
+}
+
+func (c *AllowList) CheckNet(service *types.ServiceConfig) {
+	if !c.supported("services.net") && service.Net != "" {
+		service.Net = ""
+		c.error("services.net")
+	}
+}
+
+func (c *AllowList) CheckNetworkMode(service *types.ServiceConfig) {
+	if !c.supported("services.network_mode") && service.NetworkMode != "" {
+		service.NetworkMode = ""
+		c.error("services.network_mode")
+	}
+}
+
+func (c *AllowList) CheckNetworks(service *types.ServiceConfig) bool {
+	if !c.supported("services.networks") {
+		service.Networks = nil
+		c.error("services.networks")
+		return false
+	}
+	return true
+}
+
+func (c *AllowList) CheckNetworkAliases(n *types.ServiceNetworkConfig) {
+	if !c.supported("services.networks.aliases") && len(n.Aliases) != 0 {
+		n.Aliases = nil
+		c.error("services.networks.aliases")
+	}
+}
+
+func (c *AllowList) CheckNetworkIpv4Address(n *types.ServiceNetworkConfig) {
+	if !c.supported("services.networks.ipv4_address") && n.Ipv4Address != "" {
+		n.Ipv4Address = ""
+		c.error("services.networks.ipv4_address")
+	}
+}
+
+func (c *AllowList) CheckNetworkIpv6Address(n *types.ServiceNetworkConfig) {
+	if !c.supported("services.networks.ipv6_address") && n.Ipv6Address != "" {
+		n.Ipv6Address = ""
+		c.error("services.networks.ipv6_address")
+	}
+}
+
+func (c *AllowList) CheckOomKillDisable(service *types.ServiceConfig) {
+	if !c.supported("services.oom_kill_disable") && service.OomKillDisable {
+		service.OomKillDisable = false
+		c.error("services.oom_kill_disable")
+	}
+}
+
+func (c *AllowList) CheckOomScoreAdj(service *types.ServiceConfig) {
+	if !c.supported("services.oom_score_adj") && service.OomScoreAdj != 0 {
+		service.OomScoreAdj = 0
+		c.error("services.oom_score_adj")
+	}
+}
+
+func (c *AllowList) CheckPid(service *types.ServiceConfig) {
+	if !c.supported("services.pid") && service.Pid != "" {
+		service.Pid = ""
+		c.error("services.pid")
+	}
+}
+
+func (c *AllowList) CheckPidLimit(service *types.ServiceConfig) {
+	if !c.supported("services.pid_limit") && service.PidLimit != 0 {
+		service.PidLimit = 0
+		c.error("services.pid_limit")
+	}
+}
+
+func (c *AllowList) CheckPlatform(service *types.ServiceConfig) {
+	if !c.supported("services.platform") && service.Platform != "" {
+		service.Platform = ""
+		c.error("services.platform")
+	}
+}
+
+func (c *AllowList) CheckPorts(service *types.ServiceConfig) bool {
+	if !c.supported("services.ports") {
+		service.Ports = nil
+		c.error("services.ports")
+		return false
+	}
+	return true
+}
+
+func (c *AllowList) CheckPortsMode(p *types.ServicePortConfig) {
+	if !c.supported("services.ports.mode") && p.Mode != "" {
+		p.Mode = ""
+		c.error("services.ports.mode")
+	}
+}
+
+func (c *AllowList) CheckPortsTarget(p *types.ServicePortConfig) {
+	if !c.supported("services.ports.target") && p.Target != 0 {
+		p.Target = 0
+		c.error("services.ports.target")
+	}
+}
+
+func (c *AllowList) CheckPortsPublished(p *types.ServicePortConfig) {
+	if !c.supported("services.ports.published") && p.Published != 0 {
+		p.Published = 0
+		c.error("services.ports.published")
+	}
+}
+
+func (c *AllowList) CheckPortsProtocol(p *types.ServicePortConfig) {
+	if !c.supported("services.ports.protocol") && p.Protocol != "" {
+		p.Protocol = ""
+		c.error("services.ports.protocol")
+	}
+}
+
+func (c *AllowList) CheckPrivileged(service *types.ServiceConfig) {
+	if !c.supported("services.privileged") && service.Privileged {
+		service.Privileged = false
+		c.error("services.privileged")
+	}
+}
+
+func (c *AllowList) CheckReadOnly(service *types.ServiceConfig) {
+	if !c.supported("services.read_only") && service.ReadOnly {
+		service.ReadOnly = false
+		c.error("services.read_only")
+	}
+}
+
+func (c *AllowList) CheckRestart(service *types.ServiceConfig) {
+	if !c.supported("services.restart") && service.Restart != "" {
+		service.Restart = ""
+		c.error("services.restart")
+	}
+}
+
+func (c *AllowList) CheckRuntime(service *types.ServiceConfig) {
+	if !c.supported("services.runtime") && service.Runtime != "" {
+		service.Runtime = ""
+		c.error("services.runtime")
+	}
+}
+
+func (c *AllowList) CheckScale(service *types.ServiceConfig) {
+	if !c.supported("services.scale") && service.Scale != 0 {
+		service.Scale = 0
+		c.error("services.scale")
+	}
+}
+
+func (c *AllowList) CheckSecrets(service *types.ServiceConfig) {
+	if len(service.Secrets) != 0 {
+		if !c.supported("services.secrets") {
+			service.Secrets = nil
+			c.error("services.secrets")
+		}
+		for i, s := range service.Secrets {
+			ref := types.FileReferenceConfig(s)
+			c.CheckFileReference("services.secrets", &ref)
+			service.Secrets[i] = s
+		}
+	}
+}
+
+func (c *AllowList) CheckFileReference(s string, config *types.FileReferenceConfig) {
+	c.CheckFileReferenceSource(s, config)
+	c.CheckFileReferenceTarget(s, config)
+	c.CheckFileReferenceGID(s, config)
+	c.CheckFileReferenceUID(s, config)
+	c.CheckFileReferenceMode(s, config)
+}
+
+func (c *AllowList) CheckFileReferenceSource(s string, config *types.FileReferenceConfig) {
+	k := fmt.Sprintf("%s.source", s)
+	if !c.supported(k) && config.Source != "" {
+		config.Source = ""
+		c.error(k)
+	}
+}
+
+func (c *AllowList) CheckFileReferenceTarget(s string, config *types.FileReferenceConfig) {
+	k := fmt.Sprintf("%s.target", s)
+	if !c.supported(k) && config.Target == "" {
+		config.Target = ""
+		c.error(k)
+	}
+}
+
+func (c *AllowList) CheckFileReferenceUID(s string, config *types.FileReferenceConfig) {
+	k := fmt.Sprintf("%s.uid", s)
+	if !c.supported(k) && config.UID != "" {
+		config.UID = ""
+		c.error(k)
+	}
+}
+
+func (c *AllowList) CheckFileReferenceGID(s string, config *types.FileReferenceConfig) {
+	k := fmt.Sprintf("%s.gid", s)
+	if !c.supported(k) && config.GID != "" {
+		config.GID = ""
+		c.error(k)
+	}
+}
+
+func (c *AllowList) CheckFileReferenceMode(s string, config *types.FileReferenceConfig) {
+	k := fmt.Sprintf("%s.mode", s)
+	if !c.supported(k) && config.Mode != nil {
+		config.Mode = nil
+		c.error(k)
+	}
+}
+
+func (c *AllowList) CheckSecurityOpt(service *types.ServiceConfig) {
+	if !c.supported("services.security_opt") && len(service.SecurityOpt) != 0 {
+		service.SecurityOpt = nil
+		c.error("services.security_opt")
+	}
+}
+
+func (c *AllowList) CheckShmSize(service *types.ServiceConfig) {
+	if !c.supported("services.shm_size") && service.ShmSize != "" {
+		service.ShmSize = ""
+		c.error("services.shm_size")
+	}
+}
+
+func (c *AllowList) CheckStdinOpen(service *types.ServiceConfig) {
+	if !c.supported("services.stdin_open") && service.StdinOpen {
+		service.StdinOpen = true
+		c.error("services.stdin_open")
+	}
+}
+
+func (c *AllowList) CheckStopGracePeriod(service *types.ServiceConfig) {
+	if !c.supported("services.stop_grace_period") && service.StopGracePeriod != nil {
+		service.StopGracePeriod = nil
+		c.error("services.stop_grace_period")
+	}
+}
+
+func (c *AllowList) CheckStopSignal(service *types.ServiceConfig) {
+	if !c.supported("services.stop_signal") && service.StopSignal != "" {
+		service.StopSignal = ""
+		c.error("services.stop_signal")
+	}
+}
+
+func (c *AllowList) CheckSysctls(service *types.ServiceConfig) {
+	if !c.supported("services.sysctls") && len(service.Sysctls) != 0 {
+		service.Sysctls = nil
+		c.error("services.sysctls")
+	}
+}
+
+func (c *AllowList) CheckTmpfs(service *types.ServiceConfig) {
+	if !c.supported("services.tmpfs") && len(service.Tmpfs) != 0 {
+		service.Tmpfs = nil
+		c.error("services.tmpfs")
+	}
+}
+
+func (c *AllowList) CheckTty(service *types.ServiceConfig) {
+	if !c.supported("services.tty") && service.Tty {
+		service.Tty = false
+		c.error("services.tty")
+	}
+}
+
+func (c *AllowList) CheckUlimits(service *types.ServiceConfig) {
+	if !c.supported("services.ulimits") && len(service.Ulimits) != 0 {
+		service.Ulimits = nil
+		c.error("services.ulimits")
+	}
+}
+
+func (c *AllowList) CheckUser(service *types.ServiceConfig) {
+	if !c.supported("services.user") && service.User != "" {
+		service.User = ""
+		c.error("services.user")
+	}
+}
+
+func (c *AllowList) CheckUserNSMode(service *types.ServiceConfig) {
+	if !c.supported("services.userns_mode") && service.UserNSMode != "" {
+		service.UserNSMode = ""
+		c.error("services.userns_mode")
+	}
+}
+
+func (c *AllowList) CheckUts(service *types.ServiceConfig) {
+	if !c.supported("services.build") && service.Uts != "" {
+		service.Uts = ""
+		c.error("services.uts")
+	}
+}
+
+func (c *AllowList) CheckVolumeDriver(service *types.ServiceConfig) {
+	if !c.supported("services.volume_driver") && service.VolumeDriver != "" {
+		service.VolumeDriver = ""
+		c.error("services.volume_driver")
+	}
+}
+
+func (c *AllowList) CheckServiceVolumes(service *types.ServiceConfig) bool {
+	if !c.supported("services.volumes") {
+		service.Volumes = nil
+		c.error("services.volumes")
+		return false
+	}
+	return true
+}
+
+func (c *AllowList) CheckVolumesSource(config *types.ServiceVolumeConfig) {
+	if !c.supported("services.volumes.source") && config.Source != "" {
+		config.Source = ""
+		c.error("services.volumes.source")
+	}
+}
+
+func (c *AllowList) CheckVolumesTarget(config *types.ServiceVolumeConfig) {
+	if !c.supported("services.volumes.target") && config.Target != "" {
+		config.Target = ""
+		c.error("services.volumes.target")
+	}
+}
+
+func (c *AllowList) CheckVolumesReadOnly(config *types.ServiceVolumeConfig) {
+	if !c.supported("services.volumes.read_only") && config.ReadOnly {
+		config.ReadOnly = false
+		c.error("services.volumes.read_only")
+	}
+}
+
+func (c *AllowList) CheckVolumesConsistency(config *types.ServiceVolumeConfig) {
+	if !c.supported("services.volumes.consistency") && config.Consistency != "" {
+		config.Consistency = ""
+		c.error("services.volumes.consistency")
+	}
+}
+
+func (c *AllowList) CheckVolumesBind(config *types.ServiceVolumeBind) {
+	if config == nil {
+		return
+	}
+	if !c.supported("services.volumes.bind.propagation") && config.Propagation != "" {
+		config.Propagation = ""
+		c.error("services.volumes.bind.propagation")
+	}
+}
+
+func (c *AllowList) CheckVolumesVolume(config *types.ServiceVolumeVolume) {
+	if config == nil {
+		return
+	}
+	if !c.supported("services.volumes.nocopy") && config.NoCopy {
+		config.NoCopy = false
+		c.error("services.volumes.nocopy")
+	}
+}
+
+func (c *AllowList) CheckVolumesTmpfs(config *types.ServiceVolumeTmpfs) {
+	if config == nil {
+		return
+	}
+	if !c.supported("services.volumes.tmpfs.size") && config.Size != 0 {
+		config.Size = 0
+		c.error("services.volumes.tmpfs.size")
+	}
+}
+
+func (c *AllowList) CheckVolumesFrom(service *types.ServiceConfig) {
+	if !c.supported("services.volumes_from") && len(service.VolumesFrom) != 0 {
+		service.VolumesFrom = nil
+		c.error("services.volumes_from")
+	}
+}
+
+func (c *AllowList) CheckWorkingDir(service *types.ServiceConfig) {
+	if !c.supported("services.working_dir") && service.WorkingDir != "" {
+		service.WorkingDir = ""
+		c.error("services.working_dir")
+	}
+}
diff --git a/compatibility/volumes.go b/compatibility/volumes.go
new file mode 100644
index 0000000..b29e9b4
--- /dev/null
+++ b/compatibility/volumes.go
@@ -0,0 +1,47 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compatibility
+
+import "github.com/compose-spec/compose-go/types"
+
+func (c *AllowList) CheckVolumeConfigDriver(config *types.VolumeConfig) {
+	if !c.supported("volumes.driver") && config.Driver != "" {
+		config.Driver = ""
+		c.error("volumes.driver")
+	}
+}
+
+func (c *AllowList) CheckVolumeConfigDriverOpts(config *types.VolumeConfig) {
+	if !c.supported("volumes.driver_opts") && len(config.DriverOpts) != 0 {
+		config.DriverOpts = nil
+		c.error("volumes.driver_opts")
+	}
+}
+
+func (c *AllowList) CheckVolumeConfigExternal(config *types.VolumeConfig) {
+	if !c.supported("volumes.external") && config.External.External {
+		config.External.External = false
+		c.error("volumes.external")
+	}
+}
+
+func (c *AllowList) CheckVolumeConfigLabels(config *types.VolumeConfig) {
+	if !c.supported("volumes.labels") && len(config.Labels) != 0 {
+		config.Labels = nil
+		c.error("volumes.labels")
+	}
+}
diff --git a/errdefs/errors.go b/errdefs/errors.go
new file mode 100644
index 0000000..04e9ea2
--- /dev/null
+++ b/errdefs/errors.go
@@ -0,0 +1,45 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package errdefs
+
+import "errors"
+
+var (
+	// ErrNotFound is returned when an object is not found
+	ErrNotFound = errors.New("not found")
+
+	// ErrInvalid is returned when a compose project is invalid
+	ErrInvalid = errors.New("invalid compose project")
+
+	// ErrUnsupported is returned when a compose project uses an unsupported attribute
+	ErrUnsupported = errors.New("unsupported attribute")
+)
+
+// IsNotFoundError returns true if the unwrapped error is ErrNotFound
+func IsNotFoundError(err error) bool {
+	return errors.Is(err, ErrNotFound)
+}
+
+// IsInvalidError returns true if the unwrapped error is ErrInvalid
+func IsInvalidError(err error) bool {
+	return errors.Is(err, ErrInvalid)
+}
+
+// IsUnsupportedError returns true if the unwrapped error is ErrUnsupported
+func IsUnsupportedError(err error) bool {
+	return errors.Is(err, ErrUnsupported)
+}
diff --git a/go.mod b/go.mod
index c24107e..9a72645 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/compose-spec/compose-go
 
-go 1.12
+go 1.13
 
 require (
 	github.com/docker/go-connections v0.4.0
diff --git a/go.sum b/go.sum
index 12ab5c4..bbb5a7a 100644
--- a/go.sum
+++ b/go.sum
@@ -12,16 +12,10 @@ github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
 github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
 github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
 github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
-github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4=
-github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/mapstructure v1.3.0 h1:iDwIio/3gk2QtLLEsqU5lInaMzos0hDTz8a6lazSFVw=
 github.com/mitchellh/mapstructure v1.3.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA=
@@ -33,15 +27,10 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
-github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
 github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
diff --git a/loader/loader.go b/loader/loader.go
index a50c2cb..298fd7d 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -22,6 +22,7 @@ import (
 	"path"
 	"path/filepath"
 	"reflect"
+	"regexp"
 	"sort"
 	"strings"
 	"time"
@@ -45,10 +46,16 @@ type Options struct {
 	SkipValidation bool
 	// Skip interpolation
 	SkipInterpolation bool
+	// Skip normalization
+	SkipNormalization bool
+	// Skip consistency check
+	SkipConsistencyCheck bool
 	// Interpolation options
 	Interpolate *interp.Options
 	// Discard 'env_file' entries after resolving to 'environment' section
 	discardEnvFiles bool
+	// Set project name
+	Name string
 }
 
 // WithDiscardEnvFiles sets the Options to discard the `env_file` section after resolving to
@@ -76,17 +83,29 @@ func ParseYAML(source []byte) (map[string]interface{}, error) {
 }
 
 // Load reads a ConfigDetails and returns a fully loaded configuration
-func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.Config, error) {
+func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) {
 	if len(configDetails.ConfigFiles) < 1 {
 		return nil, errors.Errorf("No files specified")
 	}
 
+	workingDir := configDetails.WorkingDir
+	if workingDir == "" {
+		for _, path := range configDetails.ConfigFiles {
+			if path.Filename != "-" {
+				workingDir = filepath.Dir(path.Filename)
+				break
+			}
+		}
+	}
+
 	opts := &Options{
 		Interpolate: &interp.Options{
 			Substitute:      template.Substitute,
 			LookupValue:     configDetails.LookupEnv,
 			TypeCastMapping: interpolateTypeCastMapping,
 		},
+		Name: regexp.MustCompile(`[^a-z0-9\\-_]+`).
+			ReplaceAllString(strings.ToLower(filepath.Base(workingDir)), ""),
 	}
 
 	for _, op := range options {
@@ -135,7 +154,37 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
 		configs = append(configs, cfg)
 	}
 
-	return merge(configs)
+	model, err := merge(configs)
+	if err != nil {
+		return nil, err
+	}
+
+	project := &types.Project{
+		Name:       opts.Name,
+		WorkingDir: workingDir,
+		Services:   model.Services,
+		Networks:   model.Networks,
+		Volumes:    model.Volumes,
+		Secrets:    model.Secrets,
+		Configs:    model.Configs,
+		Extensions: model.Extensions,
+	}
+
+	if !opts.SkipNormalization {
+		err = normalize(project)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if !opts.SkipConsistencyCheck {
+		err = checkConsistency(project)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return project, nil
 }
 
 func groupXFieldsIntoExtensions(dict map[string]interface{}) map[string]interface{} {
diff --git a/loader/normalize.go b/loader/normalize.go
new file mode 100644
index 0000000..9052e0e
--- /dev/null
+++ b/loader/normalize.go
@@ -0,0 +1,152 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package loader
+
+import (
+	"github.com/compose-spec/compose-go/errdefs"
+	"github.com/compose-spec/compose-go/types"
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+)
+
+// normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults
+func normalize(project *types.Project) error {
+	// If none defined, Compose model involves an implicit "default" network
+	if len(project.Networks) == 0 {
+		project.Networks["default"] = types.NetworkConfig{}
+	}
+
+	err := relocateExternalName(project)
+	if err != nil {
+		return err
+	}
+
+	for i, s := range project.Services {
+		if len(s.Networks) == 0 {
+			// Service without explicit network attachment are implicitly exposed on default network
+			s.Networks = map[string]*types.ServiceNetworkConfig{"default": nil}
+		}
+
+		err := relocateLogDriver(s)
+		if err != nil {
+			return err
+		}
+
+		err = relocateLogOpt(s)
+		if err != nil {
+			return err
+		}
+
+		err = relocateDockerfile(s)
+		if err != nil {
+			return err
+		}
+
+		project.Services[i] = s
+	}
+	return nil
+}
+
+func relocateExternalName(project *types.Project) error {
+	for i, n := range project.Networks {
+		if n.External.Name != "" {
+			if n.Name != "" {
+				return errors.Wrap(errdefs.ErrInvalid, "can't use both 'networks.external.name' (deprecated) and 'networks.name'")
+			}
+			n.Name = n.External.Name
+		}
+		project.Networks[i] = n
+	}
+
+	for i, v := range project.Volumes {
+		if v.External.Name != "" {
+			if v.Name != "" {
+				return errors.Wrap(errdefs.ErrInvalid, "can't use both 'volumes.external.name' (deprecated) and 'volumes.name'")
+			}
+			v.Name = v.External.Name
+		}
+		project.Volumes[i] = v
+	}
+
+	for i, s := range project.Secrets {
+		if s.External.Name != "" {
+			if s.Name != "" {
+				return errors.Wrap(errdefs.ErrInvalid, "can't use both 'secrets.external.name' (deprecated) and 'secrets.name'")
+			}
+			s.Name = s.External.Name
+		}
+		project.Secrets[i] = s
+	}
+
+	for i, c := range project.Configs {
+		if c.External.Name != "" {
+			if c.Name != "" {
+				return errors.Wrap(errdefs.ErrInvalid, "can't use both 'configs.external.name' (deprecated) and 'configs.name'")
+			}
+			c.Name = c.External.Name
+		}
+		project.Configs[i] = c
+	}
+	return nil
+}
+
+func relocateLogOpt(s types.ServiceConfig) error {
+	if len(s.LogOpt) != 0 {
+		logrus.Warn("`log_opts` is deprecated. Use the `logging` element")
+		if s.Logging == nil {
+			s.Logging = &types.LoggingConfig{}
+		}
+		for k, v := range s.LogOpt {
+			if _, ok := s.Logging.Options[k]; !ok {
+				s.Logging.Options[k] = v
+			} else {
+				return errors.Wrap(errdefs.ErrInvalid, "can't use both 'log_opt' (deprecated) and 'logging.options'")
+			}
+		}
+	}
+	return nil
+}
+
+func relocateLogDriver(s types.ServiceConfig) error {
+	if s.LogDriver != "" {
+		logrus.Warn("`log_driver` is deprecated. Use the `logging` element")
+		if s.Logging == nil {
+			s.Logging = &types.LoggingConfig{}
+		}
+		if s.Logging.Driver == "" {
+			s.Logging.Driver = s.LogDriver
+		} else {
+			return errors.Wrap(errdefs.ErrInvalid, "can't use both 'log_driver' (deprecated) and 'logging.driver'")
+		}
+	}
+	return nil
+}
+
+func relocateDockerfile(s types.ServiceConfig) error {
+	if s.Dockerfile != "" {
+		logrus.Warn("`dockerfile` is deprecated. Use the `build` element")
+		if s.Build == nil {
+			s.Build = &types.BuildConfig{}
+		}
+		if s.Dockerfile == "" {
+			s.Build.Dockerfile = s.Dockerfile
+		} else {
+			return errors.Wrap(errdefs.ErrInvalid, "can't use both 'dockerfile' (deprecated) and 'build.dockerfile'")
+		}
+	}
+	return nil
+}
diff --git a/loader/validate.go b/loader/validate.go
new file mode 100644
index 0000000..244b445
--- /dev/null
+++ b/loader/validate.go
@@ -0,0 +1,52 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package loader
+
+import (
+	"fmt"
+
+	"github.com/compose-spec/compose-go/errdefs"
+	"github.com/compose-spec/compose-go/types"
+	"github.com/pkg/errors"
+)
+
+// checkConsistency validate a compose model is consistent
+func checkConsistency(project *types.Project) error {
+	for _, s := range project.Services {
+		for network := range s.Networks {
+			if _, ok := project.Networks[network]; !ok {
+				return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined network %s", s.Name, network))
+			}
+		}
+		for _, volume := range s.Volumes {
+			if _, ok := project.Volumes[volume.Source]; !ok {
+				return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined volume %s", s.Name, volume.Source))
+			}
+		}
+		for _, secret := range s.Secrets {
+			if _, ok := project.Secrets[secret.Source]; !ok {
+				return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined secret %s", s.Name, secret.Source))
+			}
+		}
+		for _, config := range s.Configs {
+			if _, ok := project.Configs[config.Source]; !ok {
+				return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined config %s", s.Name, config.Source))
+			}
+		}
+	}
+	return nil
+}
diff --git a/types/config.go b/types/config.go
index a92af9e..4b1394f 100644
--- a/types/config.go
+++ b/types/config.go
@@ -18,8 +18,6 @@ package types
 
 import (
 	"encoding/json"
-	"fmt"
-	"sort"
 )
 
 // ConfigDetails are the details about a group of ConfigFiles
@@ -42,124 +40,29 @@ type ConfigFile struct {
 	Config   map[string]interface{}
 }
 
-// Config is a full compose file configuration
+// Config is a full compose file configuration and model
 type Config struct {
-	Filename   string                     `yaml:"-" json:"-"`
-	Version    string                     `json:"version"`
-	Services   Services                   `json:"services"`
-	Networks   map[string]NetworkConfig   `yaml:",omitempty" json:"networks,omitempty"`
-	Volumes    map[string]VolumeConfig    `yaml:",omitempty" json:"volumes,omitempty"`
-	Secrets    map[string]SecretConfig    `yaml:",omitempty" json:"secrets,omitempty"`
-	Configs    map[string]ConfigObjConfig `yaml:",omitempty" json:"configs,omitempty"`
-	Extensions map[string]interface{}     `yaml:",inline" json:"-"`
+	Filename   string                 `yaml:"-" json:"-"`
+	Version    string                 `json:"version"`
+	Services   Services               `json:"services"`
+	Networks   Networks               `yaml:",omitempty" json:"networks,omitempty"`
+	Volumes    Volumes                `yaml:",omitempty" json:"volumes,omitempty"`
+	Secrets    Secrets                `yaml:",omitempty" json:"secrets,omitempty"`
+	Configs    Configs                `yaml:",omitempty" json:"configs,omitempty"`
+	Extensions map[string]interface{} `yaml:",inline" json:"-"`
 }
 
-// ServiceNames return names for all services in this Compose config
-func (c Config) ServiceNames() []string {
-	names := []string{}
-	for _, s := range c.Services {
-		names = append(names, s.Name)
-	}
-	sort.Strings(names)
-	return names
-}
+// Volumes is a map of VolumeConfig
+type Volumes map[string]VolumeConfig
 
-// VolumeNames return names for all volumes in this Compose config
-func (c Config) VolumeNames() []string {
-	names := []string{}
-	for k := range c.Volumes {
-		names = append(names, k)
-	}
-	sort.Strings(names)
-	return names
-}
+// Networks is a map of NetworkConfig
+type Networks map[string]NetworkConfig
 
-// NetworkNames return names for all volumes in this Compose config
-func (c Config) NetworkNames() []string {
-	names := []string{}
-	for k := range c.Networks {
-		names = append(names, k)
-	}
-	sort.Strings(names)
-	return names
-}
+// Secrets is a map of SecretConfig
+type Secrets map[string]SecretConfig
 
-// SecretNames return names for all secrets in this Compose config
-func (c Config) SecretNames() []string {
-	names := []string{}
-	for k := range c.Secrets {
-		names = append(names, k)
-	}
-	sort.Strings(names)
-	return names
-}
-
-// ConfigNames return names for all configs in this Compose config
-func (c Config) ConfigNames() []string {
-	names := []string{}
-	for k := range c.Configs {
-		names = append(names, k)
-	}
-	sort.Strings(names)
-	return names
-}
-
-// GetServices retrieve services by names, or return all services if no name specified
-func (c Config) GetServices(names []string) (Services, error) {
-	if len(names) == 0 {
-		return c.Services, nil
-	}
-	services := Services{}
-	for _, name := range names {
-		service, err := c.GetService(name)
-		if err != nil {
-			return nil, err
-		}
-		services = append(services, service)
-	}
-	return services, nil
-}
-
-// GetService retrieve a specific service by name
-func (c Config) GetService(name string) (ServiceConfig, error) {
-	for _, s := range c.Services {
-		if s.Name == name {
-			return s, nil
-		}
-	}
-	return ServiceConfig{}, fmt.Errorf("no such service: %s", name)
-}
-
-type ServiceFunc func(service ServiceConfig) error
-
-// WithServices run ServiceFunc on each service and dependencies in dependency order
-func (c Config) WithServices(names []string, fn ServiceFunc) error {
-	return c.withServices(names, fn, map[string]bool{})
-}
-
-func (c Config) withServices(names []string, fn ServiceFunc, done map[string]bool) error {
-	services, err := c.GetServices(names)
-	if err != nil {
-		return err
-	}
-	for _, service := range services {
-		if done[service.Name] {
-			continue
-		}
-		dependencies := service.GetDependencies()
-		if len(dependencies) > 0 {
-			err := c.withServices(dependencies, fn, done)
-			if err != nil {
-				return err
-			}
-		}
-		if err := fn(service); err != nil {
-			return err
-		}
-		done[service.Name] = true
-	}
-	return nil
-}
+// Configs is a map of ConfigObjConfig
+type Configs map[string]ConfigObjConfig
 
 // MarshalJSON makes Config implement json.Marshaler
 func (c Config) MarshalJSON() ([]byte, error) {
diff --git a/types/project.go b/types/project.go
new file mode 100644
index 0000000..740cd08
--- /dev/null
+++ b/types/project.go
@@ -0,0 +1,155 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package types
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"sort"
+)
+
+// Project is the result of loading a set of compose files
+type Project struct {
+	Name       string
+	WorkingDir string
+	Services   Services               `json:"services"`
+	Networks   Networks               `yaml:",omitempty" json:"networks,omitempty"`
+	Volumes    Volumes                `yaml:",omitempty" json:"volumes,omitempty"`
+	Secrets    Secrets                `yaml:",omitempty" json:"secrets,omitempty"`
+	Configs    Configs                `yaml:",omitempty" json:"configs,omitempty"`
+	Extensions map[string]interface{} `yaml:",inline" json:"-"`
+}
+
+// ServiceNames return names for all services in this Compose config
+func (p Project) ServiceNames() []string {
+	names := []string{}
+	for _, s := range p.Services {
+		names = append(names, s.Name)
+	}
+	sort.Strings(names)
+	return names
+}
+
+// VolumeNames return names for all volumes in this Compose config
+func (p Project) VolumeNames() []string {
+	names := []string{}
+	for k := range p.Volumes {
+		names = append(names, k)
+	}
+	sort.Strings(names)
+	return names
+}
+
+// NetworkNames return names for all volumes in this Compose config
+func (p Project) NetworkNames() []string {
+	names := []string{}
+	for k := range p.Networks {
+		names = append(names, k)
+	}
+	sort.Strings(names)
+	return names
+}
+
+// SecretNames return names for all secrets in this Compose config
+func (p Project) SecretNames() []string {
+	names := []string{}
+	for k := range p.Secrets {
+		names = append(names, k)
+	}
+	sort.Strings(names)
+	return names
+}
+
+// ConfigNames return names for all configs in this Compose config
+func (p Project) ConfigNames() []string {
+	names := []string{}
+	for k := range p.Configs {
+		names = append(names, k)
+	}
+	sort.Strings(names)
+	return names
+}
+
+// GetServices retrieve services by names, or return all services if no name specified
+func (p Project) GetServices(names []string) (Services, error) {
+	if len(names) == 0 {
+		return p.Services, nil
+	}
+	services := Services{}
+	for _, name := range names {
+		service, err := p.GetService(name)
+		if err != nil {
+			return nil, err
+		}
+		services = append(services, service)
+	}
+	return services, nil
+}
+
+// GetService retrieve a specific service by name
+func (p Project) GetService(name string) (ServiceConfig, error) {
+	for _, s := range p.Services {
+		if s.Name == name {
+			return s, nil
+		}
+	}
+	return ServiceConfig{}, fmt.Errorf("no such service: %s", name)
+}
+
+type ServiceFunc func(service ServiceConfig) error
+
+// WithServices run ServiceFunc on each service and dependencies in dependency order
+func (p Project) WithServices(names []string, fn ServiceFunc) error {
+	return p.withServices(names, fn, map[string]bool{})
+}
+
+func (p Project) withServices(names []string, fn ServiceFunc, done map[string]bool) error {
+	services, err := p.GetServices(names)
+	if err != nil {
+		return err
+	}
+	for _, service := range services {
+		if done[service.Name] {
+			continue
+		}
+		dependencies := service.GetDependencies()
+		if len(dependencies) > 0 {
+			err := p.withServices(dependencies, fn, done)
+			if err != nil {
+				return err
+			}
+		}
+		if err := fn(service); err != nil {
+			return err
+		}
+		done[service.Name] = true
+	}
+	return nil
+}
+
+// RelativePath resolve a relative path based project's working directory
+func (p *Project) RelativePath(path string) string {
+	if path[0] == '~' {
+		home, _ := os.UserHomeDir()
+		path = filepath.Join(home, path[1:])
+	}
+	if filepath.IsAbs(path) {
+		return path
+	}
+	return filepath.Join(p.WorkingDir, path)
+}
diff --git a/types/types.go b/types/types.go
index 2895a9e..f2bc09d 100644
--- a/types/types.go
+++ b/types/types.go
@@ -78,10 +78,17 @@ type ServiceConfig struct {
 	Name string `yaml:"-" json:"-"`
 
 	Build           *BuildConfig                     `yaml:",omitempty" json:"build,omitempty"`
+	BlkioConfig     string                           `yaml:",omitempty" json:"blkio_config,omitempty"`
 	CapAdd          []string                         `mapstructure:"cap_add" yaml:"cap_add,omitempty" json:"cap_add,omitempty"`
 	CapDrop         []string                         `mapstructure:"cap_drop" yaml:"cap_drop,omitempty" json:"cap_drop,omitempty"`
 	CgroupParent    string                           `mapstructure:"cgroup_parent" yaml:"cgroup_parent,omitempty" json:"cgroup_parent,omitempty"`
+	CPUCount        int64                            `mapstructure:"cpu_count" yaml:"cpu_count,omitempty" json:"cpu_count,omitempty"`
+	CPUPercent      float32                          `mapstructure:"cpu_percent" yaml:"cpu_percent,omitempty" json:"cpu_percent,omitempty"`
+	CPUPeriod       int64                            `mapstructure:"cpu_period" yaml:"cpu_period,omitempty" json:"cpu_period,omitempty"`
 	CPUQuota        int64                            `mapstructure:"cpu_quota" yaml:"cpu_quota,omitempty" json:"cpu_quota,omitempty"`
+	CPURTPeriod     int64                            `mapstructure:"cpu_rt_period" yaml:"cpu_rt_period,omitempty" json:"cpu_rt_period,omitempty"`
+	CPURTRuntime    int64                            `mapstructure:"cpu_rt_runtime" yaml:"cpu_rt_runtime,omitempty" json:"cpu_rt_runtime,omitempty"`
+	CPUS            float32                          `mapstructure:"cpus" yaml:"cpus,omitempty" json:"cpus,omitempty"`
 	CPUSet          string                           `mapstructure:"cpuset" yaml:"cpuset,omitempty" json:"cpuset,omitempty"`
 	CPUShares       int64                            `mapstructure:"cpu_shares" yaml:"cpu_shares,omitempty" json:"cpu_shares,omitempty"`
 	Command         ShellCommand                     `yaml:",omitempty" json:"command,omitempty"`
@@ -126,10 +133,14 @@ type ServiceConfig struct {
 	OomKillDisable  bool                             `mapstructure:"oom_kill_disable" yaml:"oom_kill_disable,omitempty" json:"oom_kill_disable,omitempty"`
 	OomScoreAdj     int64                            `mapstructure:"oom_score_adj" yaml:"oom_score_adj,omitempty" json:"oom_score_adj,omitempty"`
 	Pid             string                           `yaml:",omitempty" json:"pid,omitempty"`
+	PidLimit        int64                            `mapstructure:"pid_limit" yaml:"pid_limit,omitempty" json:"pid_limit,omitempty"`
+	Platform        string                           `yaml:",omitempty" json:"platform,omitempty"`
 	Ports           []ServicePortConfig              `yaml:",omitempty" json:"ports,omitempty"`
 	Privileged      bool                             `yaml:",omitempty" json:"privileged,omitempty"`
 	ReadOnly        bool                             `mapstructure:"read_only" yaml:"read_only,omitempty" json:"read_only,omitempty"`
 	Restart         string                           `yaml:",omitempty" json:"restart,omitempty"`
+	Runtime         string                           `yaml:",omitempty" json:"runtime,omitempty"`
+	Scale           int                              `yaml:",omitempty" json:"scale,omitempty"`
 	Secrets         []ServiceSecretConfig            `yaml:",omitempty" json:"secrets,omitempty"`
 	SecurityOpt     []string                         `mapstructure:"security_opt" yaml:"security_opt,omitempty" json:"security_opt,omitempty"`
 	ShmSize         string                           `mapstructure:"shm_size" yaml:"shm_size,omitempty" json:"shm_size,omitempty"`
@@ -186,6 +197,8 @@ type BuildConfig struct {
 	Args       MappingWithEquals `yaml:",omitempty" json:"args,omitempty"`
 	Labels     Labels            `yaml:",omitempty" json:"labels,omitempty"`
 	CacheFrom  StringList        `mapstructure:"cache_from" yaml:"cache_from,omitempty" json:"cache_from,omitempty"`
+	ExtraHosts HostsList         `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"`
+	Isolation  string            `yaml:",omitempty" json:"isolation,omitempty"`
 	Network    string            `yaml:",omitempty" json:"network,omitempty"`
 	Target     string            `yaml:",omitempty" json:"target,omitempty"`
 


Test patch:
diff --git a/cli/options_test.go b/cli/options_test.go
new file mode 100644
index 0000000..4f86740
--- /dev/null
+++ b/cli/options_test.go
@@ -0,0 +1,61 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package cli
+
+import (
+	"os"
+	"testing"
+
+	"gotest.tools/v3/assert"
+)
+
+func TestProjectName(t *testing.T) {
+	p, err := ProjectFromOptions(&ProjectOptions{
+		Name:        "my_project",
+		ConfigPaths: []string{"testdata/simple/compose.yaml"},
+	})
+	assert.NilError(t, err)
+	assert.Equal(t, p.Name, "my_project")
+
+	p, err = ProjectFromOptions(&ProjectOptions{
+		ConfigPaths: []string{"testdata/simple/compose.yaml"},
+	})
+	assert.NilError(t, err)
+	assert.Equal(t, p.Name, "simple")
+
+	os.Setenv("COMPOSE_PROJECT_NAME", "my_project_from_env")
+	defer os.Unsetenv("COMPOSE_PROJECT_NAME")
+	p, err = ProjectFromOptions(&ProjectOptions{
+		ConfigPaths: []string{"testdata/simple/compose.yaml"},
+	})
+	assert.NilError(t, err)
+	assert.Equal(t, p.Name, "my_project_from_env")
+}
+
+func TestProjectFromSetOfFiles(t *testing.T) {
+	p, err := ProjectFromOptions(&ProjectOptions{
+		Name: "my_project",
+		ConfigPaths: []string{
+			"testdata/simple/compose.yaml",
+			"testdata/simple/compose-with-overrides.yaml",
+		},
+	})
+	assert.NilError(t, err)
+	service, err := p.GetService("simple")
+	assert.NilError(t, err)
+	assert.Equal(t, service.Image, "haproxy")
+}
diff --git a/cli/testdata/simple/compose-with-overrides.yaml b/cli/testdata/simple/compose-with-overrides.yaml
new file mode 100644
index 0000000..3dc8a0b
--- /dev/null
+++ b/cli/testdata/simple/compose-with-overrides.yaml
@@ -0,0 +1,4 @@
+version: "3"
+services:
+  simple:
+    image: haproxy
diff --git a/cli/testdata/simple/compose.yaml b/cli/testdata/simple/compose.yaml
new file mode 100644
index 0000000..431d516
--- /dev/null
+++ b/cli/testdata/simple/compose.yaml
@@ -0,0 +1,4 @@
+version: "3"
+services:
+  simple:
+    image: nginx
diff --git a/compatibility/allowlist_test.go b/compatibility/allowlist_test.go
new file mode 100644
index 0000000..fc783a2
--- /dev/null
+++ b/compatibility/allowlist_test.go
@@ -0,0 +1,78 @@
+/*
+   Copyright 2020 The Compose Specification Authors.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compatibility
+
+import (
+	"testing"
+
+	"github.com/compose-spec/compose-go/errdefs"
+	"github.com/compose-spec/compose-go/loader"
+	"github.com/compose-spec/compose-go/types"
+	"gotest.tools/v3/assert"
+)
+
+func TestAllowList(t *testing.T) {
+	var checker Checker = customChecker{
+		&AllowList{
+			Supported: []string{
+				"services.network_mode",
+				"services.privileged",
+				"services.networks",
+			},
+		},
+	}
+	dict, err := loader.ParseYAML([]byte(`
+version: "3"
+services:
+  foo:
+    image: busybox
+    network_mode: host
+    privileged: true
+    mac_address: "a:b:c:d"
+`))
+	assert.NilError(t, err)
+
+	project, err := loader.Load(types.ConfigDetails{
+		ConfigFiles: []types.ConfigFile{
+			{Filename: "filename.yml", Config: dict},
+		},
+	})
+	assert.NilError(t, err)
+
+	Check(project, checker)
+	errors := checker.Errors()
+	assert.Check(t, len(errors) == 2)
+	assert.Check(t, errdefs.IsUnsupportedError(errors[0]))
+	assert.Equal(t, errors[0].Error(), "services.mac_address: unsupported attribute")
+
+	assert.Check(t, errdefs.IsUnsupportedError(errors[1]))
+	assert.Equal(t, errors[1].Error(), "services.network_mode=host: unsupported attribute")
+
+	service, err := project.GetService("foo")
+	assert.NilError(t, err)
+	assert.Check(t, service.MacAddress == "")
+}
+
+type customChecker struct {
+	*AllowList
+}
+
+func (c customChecker) CheckNetworkMode(service *types.ServiceConfig) {
+	if service.NetworkMode == "host" {
+		c.error("services.network_mode=host")
+	}
+}
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 512e725..b52f1c0 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -47,17 +47,20 @@ func buildConfigDetails(source map[string]interface{}, env map[string]string) ty
 	}
 }
 
-func loadYAML(yaml string) (*types.Config, error) {
+func loadYAML(yaml string) (*types.Project, error) {
 	return loadYAMLWithEnv(yaml, nil)
 }
 
-func loadYAMLWithEnv(yaml string, env map[string]string) (*types.Config, error) {
+func loadYAMLWithEnv(yaml string, env map[string]string) (*types.Project, error) {
 	dict, err := ParseYAML([]byte(yaml))
 	if err != nil {
 		return nil, err
 	}
 
-	return Load(buildConfigDetails(dict, env))
+	return Load(buildConfigDetails(dict, env), func(options *Options) {
+		options.SkipConsistencyCheck = true
+		options.SkipNormalization = true
+	})
 }
 
 var sampleYAML = `
@@ -251,9 +254,11 @@ func TestParseYAML(t *testing.T) {
 }
 
 func TestLoad(t *testing.T) {
-	actual, err := Load(buildConfigDetails(sampleDict, nil))
+	actual, err := Load(buildConfigDetails(sampleDict, nil), func(options *Options) {
+		options.SkipNormalization = true
+		options.SkipConsistencyCheck = true
+	})
 	assert.NilError(t, err)
-	assert.Check(t, is.Equal(sampleConfig.Version, actual.Version))
 	assert.Check(t, is.DeepEqual(serviceSort(sampleConfig.Services), serviceSort(actual.Services)))
 	assert.Check(t, is.DeepEqual(sampleConfig.Networks, actual.Networks))
 	assert.Check(t, is.DeepEqual(sampleConfig.Volumes, actual.Volumes))
@@ -631,11 +636,17 @@ networks:
 		"thebool":  "true",
 	}
 
-	config, err := Load(buildConfigDetails(dict, env))
+	config, err := Load(buildConfigDetails(dict, env), func(options *Options) {
+		options.SkipNormalization = true
+		options.SkipConsistencyCheck = true
+	})
+	assert.NilError(t, err)
+
+	workingDir, err := os.Getwd()
 	assert.NilError(t, err)
-	expected := &types.Config{
-		Filename: "filename.yml",
-		Version:  "3.9",
+	expected := &types.Project{
+		Name:       "loader",
+		WorkingDir: workingDir,
 		Services: []types.ServiceConfig{
 			{
 				Name: "web",
@@ -1153,7 +1164,7 @@ networks:
 `)
 	assert.NilError(t, err)
 
-	expected := map[string]types.NetworkConfig{
+	expected := types.Networks{
 		"mynet1": {
 			Driver:     "overlay",
 			Attachable: true,
@@ -1451,9 +1462,12 @@ networks:
   network3:
 `)
 	assert.NilError(t, err)
-	expected := &types.Config{
-		Filename: "filename.yml",
-		Version:  "3.9",
+
+	workingDir, err := os.Getwd()
+	assert.NilError(t, err)
+	expected := &types.Project{
+		Name:       "loader",
+		WorkingDir: workingDir,
 		Services: types.Services{
 			{
 				Name:  "hello-world",
@@ -1610,9 +1624,13 @@ secrets:
     template_driver: secret-driver
 `)
 	assert.NilError(t, err)
-	expected := &types.Config{
-		Filename: "filename.yml",
-		Version:  "3.9",
+
+	workingDir, err := os.Getwd()
+	assert.NilError(t, err)
+
+	expected := &types.Project{
+		Name:       "loader",
+		WorkingDir: workingDir,
 		Services: types.Services{
 			{
 				Name:  "hello-world",
@@ -1672,9 +1690,13 @@ secrets:
       OptionB: value for driver option B
 `)
 	assert.NilError(t, err)
-	expected := &types.Config{
-		Filename: "filename.yml",
-		Version:  "3.9",
+
+	workingDir, err := os.Getwd()
+	assert.NilError(t, err)
+
+	expected := &types.Project{
+		Name:       "loader",
+		WorkingDir: workingDir,
 		Services: types.Services{
 			{
 				Name:  "hello-world",
diff --git a/loader/merge_test.go b/loader/merge_test.go
index bfd474f..1e33cda 100644
--- a/loader/merge_test.go
+++ b/loader/merge_test.go
@@ -224,11 +224,11 @@ func TestLoadLogging(t *testing.T) {
 					},
 				},
 			}
-			config, err := Load(configDetails)
+			config, err := loadTestProject(configDetails)
 			assert.NilError(t, err)
-			assert.DeepEqual(t, &types.Config{
-				Filename: "base.yml",
-				Version:  "3.9",
+			assert.DeepEqual(t, &types.Project{
+				Name:       "",
+				WorkingDir: ".",
 				Services: []types.ServiceConfig{
 					{
 						Name:        "foo",
@@ -236,15 +236,22 @@ func TestLoadLogging(t *testing.T) {
 						Environment: types.MappingWithEquals{},
 					},
 				},
-				Networks: map[string]types.NetworkConfig{},
-				Volumes:  map[string]types.VolumeConfig{},
-				Secrets:  map[string]types.SecretConfig{},
-				Configs:  map[string]types.ConfigObjConfig{},
+				Networks: types.Networks{},
+				Volumes:  types.Volumes{},
+				Secrets:  types.Secrets{},
+				Configs:  types.Configs{},
 			}, config)
 		})
 	}
 }
 
+func loadTestProject(configDetails types.ConfigDetails) (*types.Project, error) {
+	return Load(configDetails, func(options *Options) {
+		options.SkipNormalization = true
+		options.SkipConsistencyCheck = true
+	})
+}
+
 func TestLoadMultipleServicePorts(t *testing.T) {
 	portsCases := []struct {
 		name         string
@@ -343,11 +350,11 @@ func TestLoadMultipleServicePorts(t *testing.T) {
 					},
 				},
 			}
-			config, err := Load(configDetails)
+			config, err := loadTestProject(configDetails)
 			assert.NilError(t, err)
-			assert.DeepEqual(t, &types.Config{
-				Filename: "base.yml",
-				Version:  "3.9",
+			assert.DeepEqual(t, &types.Project{
+				Name:       "",
+				WorkingDir: ".",
 				Services: []types.ServiceConfig{
 					{
 						Name:        "foo",
@@ -355,10 +362,10 @@ func TestLoadMultipleServicePorts(t *testing.T) {
 						Environment: types.MappingWithEquals{},
 					},
 				},
-				Networks: map[string]types.NetworkConfig{},
-				Volumes:  map[string]types.VolumeConfig{},
-				Secrets:  map[string]types.SecretConfig{},
-				Configs:  map[string]types.ConfigObjConfig{},
+				Networks: types.Networks{},
+				Volumes:  types.Volumes{},
+				Secrets:  types.Secrets{},
+				Configs:  types.Configs{},
 			}, config)
 		})
 	}
@@ -469,11 +476,11 @@ func TestLoadMultipleSecretsConfig(t *testing.T) {
 					},
 				},
 			}
-			config, err := Load(configDetails)
+			config, err := loadTestProject(configDetails)
 			assert.NilError(t, err)
-			assert.DeepEqual(t, &types.Config{
-				Filename: "base.yml",
-				Version:  "3.9",
+			assert.DeepEqual(t, &types.Project{
+				Name:       "",
+				WorkingDir: ".",
 				Services: []types.ServiceConfig{
 					{
 						Name:        "foo",
@@ -481,10 +488,10 @@ func TestLoadMultipleSecretsConfig(t *testing.T) {
 						Environment: types.MappingWithEquals{},
 					},
 				},
-				Networks: map[string]types.NetworkConfig{},
-				Volumes:  map[string]types.VolumeConfig{},
-				Secrets:  map[string]types.SecretConfig{},
-				Configs:  map[string]types.ConfigObjConfig{},
+				Networks: types.Networks{},
+				Volumes:  types.Volumes{},
+				Secrets:  types.Secrets{},
+				Configs:  types.Configs{},
 			}, config)
 		})
 	}
@@ -595,11 +602,11 @@ func TestLoadMultipleConfigobjsConfig(t *testing.T) {
 					},
 				},
 			}
-			config, err := Load(configDetails)
+			config, err := loadTestProject(configDetails)
 			assert.NilError(t, err)
-			assert.DeepEqual(t, &types.Config{
-				Filename: "base.yml",
-				Version:  "3.9",
+			assert.DeepEqual(t, &types.Project{
+				Name:       "",
+				WorkingDir: ".",
 				Services: []types.ServiceConfig{
 					{
 						Name:        "foo",
@@ -607,10 +614,10 @@ func TestLoadMultipleConfigobjsConfig(t *testing.T) {
 						Environment: types.MappingWithEquals{},
 					},
 				},
-				Networks: map[string]types.NetworkConfig{},
-				Volumes:  map[string]types.VolumeConfig{},
-				Secrets:  map[string]types.SecretConfig{},
-				Configs:  map[string]types.ConfigObjConfig{},
+				Networks: types.Networks{},
+				Volumes:  types.Volumes{},
+				Secrets:  types.Secrets{},
+				Configs:  types.Configs{},
 			}, config)
 		})
 	}
@@ -711,11 +718,11 @@ func TestLoadMultipleUlimits(t *testing.T) {
 					},
 				},
 			}
-			config, err := Load(configDetails)
+			config, err := loadTestProject(configDetails)
 			assert.NilError(t, err)
-			assert.DeepEqual(t, &types.Config{
-				Filename: "base.yml",
-				Version:  "3.9",
+			assert.DeepEqual(t, &types.Project{
+				Name:       "",
+				WorkingDir: ".",
 				Services: []types.ServiceConfig{
 					{
 						Name:        "foo",
@@ -723,10 +730,10 @@ func TestLoadMultipleUlimits(t *testing.T) {
 						Environment: types.MappingWithEquals{},
 					},
 				},
-				Networks: map[string]types.NetworkConfig{},
-				Volumes:  map[string]types.VolumeConfig{},
-				Secrets:  map[string]types.SecretConfig{},
-				Configs:  map[string]types.ConfigObjConfig{},
+				Networks: types.Networks{},
+				Volumes:  types.Volumes{},
+				Secrets:  types.Secrets{},
+				Configs:  types.Configs{},
 			}, config)
 		})
 	}
@@ -830,11 +837,11 @@ func TestLoadMultipleServiceNetworks(t *testing.T) {
 					},
 				},
 			}
-			config, err := Load(configDetails)
+			config, err := loadTestProject(configDetails)
 			assert.NilError(t, err)
-			assert.DeepEqual(t, &types.Config{
-				Filename: "base.yml",
-				Version:  "3.9",
+			assert.DeepEqual(t, &types.Project{
+				Name:       "",
+				WorkingDir: ".",
 				Services: []types.ServiceConfig{
 					{
 						Name:        "foo",
@@ -842,10 +849,10 @@ func TestLoadMultipleServiceNetworks(t *testing.T) {
 						Environment: types.MappingWithEquals{},
 					},
 				},
-				Networks: map[string]types.NetworkConfig{},
-				Volumes:  map[string]types.VolumeConfig{},
-				Secrets:  map[string]types.SecretConfig{},
-				Configs:  map[string]types.ConfigObjConfig{},
+				Networks: types.Networks{},
+				Volumes:  types.Volumes{},
+				Secrets:  types.Secrets{},
+				Configs:  types.Configs{},
 			}, config)
 		})
 	}
@@ -918,11 +925,11 @@ func TestLoadMultipleConfigs(t *testing.T) {
 			{Filename: "override.yml", Config: override},
 		},
 	}
-	config, err := Load(configDetails)
+	config, err := loadTestProject(configDetails)
 	assert.NilError(t, err)
-	assert.DeepEqual(t, &types.Config{
-		Filename: "base.yml",
-		Version:  "3.9",
+	assert.DeepEqual(t, &types.Project{
+		Name:       "",
+		WorkingDir: ".",
 		Services: []types.ServiceConfig{
 			{
 				Name:        "bar",
@@ -958,10 +965,10 @@ func TestLoadMultipleConfigs(t *testing.T) {
 				CapAdd:      []string{"NET_ADMIN", "SYS_ADMIN"},
 				Environment: types.MappingWithEquals{},
 			}},
-		Networks: map[string]types.NetworkConfig{},
-		Volumes:  map[string]types.VolumeConfig{},
-		Secrets:  map[string]types.SecretConfig{},
-		Configs:  map[string]types.ConfigObjConfig{},
+		Networks: types.Networks{},
+		Volumes:  types.Volumes{},
+		Secrets:  types.Secrets{},
+		Configs:  types.Configs{},
 	}, config)
 }
 
@@ -1011,11 +1018,11 @@ func TestLoadMultipleNetworks(t *testing.T) {
 			{Filename: "override.yml", Config: override},
 		},
 	}
-	config, err := Load(configDetails)
+	config, err := loadTestProject(configDetails)
 	assert.NilError(t, err)
-	assert.DeepEqual(t, &types.Config{
-		Filename: "base.yml",
-		Version:  "3.9",
+	assert.DeepEqual(t, &types.Project{
+		Name:       "",
+		WorkingDir: ".",
 		Services: []types.ServiceConfig{
 			{
 				Name:        "foo",
@@ -1030,9 +1037,9 @@ func TestLoadMultipleNetworks(t *testing.T) {
 				},
 			},
 		},
-		Volumes: map[string]types.VolumeConfig{},
-		Secrets: map[string]types.SecretConfig{},
-		Configs: map[string]types.ConfigObjConfig{},
+		Volumes: types.Volumes{},
+		Secrets: types.Secrets{},
+		Configs: types.Configs{},
 	}, config)
 }
 
diff --git a/loader/types_test.go b/loader/types_test.go
index adb6393..61b2f15 100644
--- a/loader/types_test.go
+++ b/loader/types_test.go
@@ -41,7 +41,10 @@ func TestMarshallConfig(t *testing.T) {
 	// Make sure the expected still
 	dict, err := ParseYAML([]byte("version: '3.7'\n" + expected))
 	assert.NilError(t, err)
-	_, err = Load(buildConfigDetails(dict, map[string]string{}))
+	_, err = Load(buildConfigDetails(dict, map[string]string{}), func(options *Options) {
+		options.SkipNormalization = true
+		options.SkipConsistencyCheck = true
+	})
 	assert.NilError(t, err)
 }
 
@@ -60,6 +63,9 @@ func TestJSONMarshallConfig(t *testing.T) {
 
 	dict, err := ParseYAML([]byte(expected))
 	assert.NilError(t, err)
-	_, err = Load(buildConfigDetails(dict, map[string]string{}))
+	_, err = Load(buildConfigDetails(dict, map[string]string{}), func(options *Options) {
+		options.SkipNormalization = true
+		options.SkipConsistencyCheck = true
+	})
 	assert.NilError(t, err)
 }
diff --git a/types/config_test.go b/types/config_test.go
index 58d0c24..80b1efb 100644
--- a/types/config_test.go
+++ b/types/config_test.go
@@ -23,7 +23,7 @@ import (
 )
 
 func Test_WithServices(t *testing.T) {
-	c := Config{
+	p := Project{
 		Services: append(Services{},
 			ServiceConfig{
 				Name:      "service_1",
@@ -41,7 +41,7 @@ func Test_WithServices(t *testing.T) {
 		return nil
 	}
 
-	err := c.WithServices(nil, fn)
+	err := p.WithServices(nil, fn)
 	assert.NilError(t, err)
 	assert.DeepEqual(t, order, []string{"service_2", "service_3", "service_1"})
 }