diff --git a/.gitattributes b/.gitattributes index 21576f30d..644ded05d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ # required for golangci-lint on Windows *.go text eol=lf +# golden files are compared byte-for-byte against marshaled output +loader/testdata/golden/* text eol=lf diff --git a/graph/graph_test.go b/graph/graph_test.go index ca1d784bb..fefe1dd18 100644 --- a/graph/graph_test.go +++ b/graph/graph_test.go @@ -31,8 +31,8 @@ import ( func TestTraversalWithMultipleParents(t *testing.T) { dependent := types.ServiceConfig{ - Name: "dependent", - DependsOn: make(types.DependsOnConfig), + Name: "dependent", + WorkloadSpec: types.WorkloadSpec{DependsOn: make(types.DependsOnConfig)}, } project := types.Project{ @@ -119,8 +119,8 @@ func TestBuildGraph(t *testing.T) { desc: "builds graph with single service", services: types.Services{ "test": { - Name: "test", - DependsOn: types.DependsOnConfig{}, + Name: "test", + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{}}, }, }, expectedVertices: map[string]*vertex[types.ServiceConfig]{ @@ -136,12 +136,12 @@ func TestBuildGraph(t *testing.T) { desc: "builds graph with two separate services", services: types.Services{ "test": { - Name: "test", - DependsOn: types.DependsOnConfig{}, + Name: "test", + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{}}, }, "another": { - Name: "another", - DependsOn: types.DependsOnConfig{}, + Name: "another", + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{}}, }, }, expectedVertices: map[string]*vertex[types.ServiceConfig]{ @@ -164,13 +164,13 @@ func TestBuildGraph(t *testing.T) { services: types.Services{ "test": { Name: "test", - DependsOn: types.DependsOnConfig{ + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "another": types.ServiceDependency{}, - }, + }}, }, "another": { - Name: "another", - DependsOn: types.DependsOnConfig{}, + Name: "another", + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{}}, }, }, expectedVertices: map[string]*vertex[types.ServiceConfig]{ @@ -197,11 +197,11 @@ func TestBuildGraph(t *testing.T) { services: types.Services{ "test": { Name: "test", - DependsOn: types.DependsOnConfig{ + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "another": types.ServiceDependency{ Required: false, }, - }, + }}, }, }, expectedVertices: map[string]*vertex[types.ServiceConfig]{ @@ -218,11 +218,11 @@ func TestBuildGraph(t *testing.T) { services: types.Services{ "test": { Name: "test", - DependsOn: types.DependsOnConfig{ + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "another": types.ServiceDependency{ Required: true, }, - }, + }}, }, }, expectedError: `service "test" depends on unknown service "another"`, @@ -232,18 +232,18 @@ func TestBuildGraph(t *testing.T) { services: types.Services{ "test": { Name: "test", - DependsOn: types.DependsOnConfig{ + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "another": types.ServiceDependency{ Required: true, }, - }, + }}, }, }, disabled: types.Services{ "another": { - Name: "another", - Profiles: []string{"test"}, - DependsOn: types.DependsOnConfig{}, + Name: "another", + Profiles: []string{"test"}, + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{}}, }, }, expectedError: `service "another" is required by "test" but is disabled. Can be enabled by profiles [test]`, @@ -253,19 +253,19 @@ func TestBuildGraph(t *testing.T) { services: types.Services{ "test": { Name: "test", - DependsOn: types.DependsOnConfig{ + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "another": types.ServiceDependency{}, - }, + }}, }, "another": { Name: "another", - DependsOn: types.DependsOnConfig{ + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "another_dep": types.ServiceDependency{}, - }, + }}, }, "another_dep": { - Name: "another_dep", - DependsOn: types.DependsOnConfig{}, + Name: "another_dep", + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{}}, }, }, expectedVertices: map[string]*vertex[types.ServiceConfig]{ @@ -435,15 +435,15 @@ func exampleProject() *types.Project { Services: types.Services{ "test1": { Name: "test1", - DependsOn: map[string]types.ServiceDependency{ + WorkloadSpec: types.WorkloadSpec{DependsOn: map[string]types.ServiceDependency{ "test2": {}, - }, + }}, }, "test2": { Name: "test2", - DependsOn: map[string]types.ServiceDependency{ + WorkloadSpec: types.WorkloadSpec{DependsOn: map[string]types.ServiceDependency{ "test3": {}, - }, + }}, }, "test3": { Name: "test3", diff --git a/loader/environment.go b/loader/environment.go index 3f7277b8a..6360c6107 100644 --- a/loader/environment.go +++ b/loader/environment.go @@ -25,27 +25,32 @@ import ( // ResolveEnvironment update the environment variables for the format {- VAR} (without interpolation) func ResolveEnvironment(dict map[string]any, environment types.Mapping) { resolveServicesEnvironment(dict, environment) + resolveContainerEnvironment(dict, "jobs", environment) resolveSecretsEnvironment(dict, environment) resolveConfigsEnvironment(dict, environment) } func resolveServicesEnvironment(dict map[string]any, environment types.Mapping) { - services, ok := dict["services"].(map[string]any) + resolveContainerEnvironment(dict, "services", environment) +} + +func resolveContainerEnvironment(dict map[string]any, key string, environment types.Mapping) { + containers, ok := dict[key].(map[string]any) if !ok { return } - for service, cfg := range services { - serviceConfig, ok := cfg.(map[string]any) + for name, cfg := range containers { + config, ok := cfg.(map[string]any) if !ok { continue } - serviceEnv, ok := serviceConfig["environment"].([]any) + envList, ok := config["environment"].([]any) if !ok { continue } envs := []any{} - for _, env := range serviceEnv { + for _, env := range envList { varEnv, ok := env.(string) if !ok { continue @@ -57,10 +62,10 @@ func resolveServicesEnvironment(dict map[string]any, environment types.Mapping) envs = append(envs, varEnv) } } - serviceConfig["environment"] = envs - services[service] = serviceConfig + config["environment"] = envs + containers[name] = config } - dict["services"] = services + dict[key] = containers } func resolveSecretsEnvironment(dict map[string]any, environment types.Mapping) { diff --git a/loader/extends.go b/loader/extends.go index 9c6783ac3..18174c040 100644 --- a/loader/extends.go +++ b/loader/extends.go @@ -28,22 +28,24 @@ import ( ) func ApplyExtends(ctx context.Context, dict map[string]any, opts *Options, tracker *cycleTracker, post PostProcessor) error { - a, ok := dict["services"] - if !ok { - return nil - } - services, ok := a.(map[string]any) - if !ok { - return fmt.Errorf("services must be a mapping") - } - for name := range services { - merged, err := applyServiceExtends(ctx, name, services, opts, tracker, post) - if err != nil { - return err + for _, key := range []string{"services", "jobs"} { + a, ok := dict[key] + if !ok { + continue + } + entries, ok := a.(map[string]any) + if !ok { + return fmt.Errorf("%s must be a mapping", key) + } + for name := range entries { + merged, err := applyServiceExtends(ctx, name, entries, opts, tracker, post) + if err != nil { + return err + } + entries[name] = merged } - services[name] = merged + dict[key] = entries } - dict["services"] = services return nil } diff --git a/loader/golden_test.go b/loader/golden_test.go new file mode 100644 index 000000000..1a546a374 --- /dev/null +++ b/loader/golden_test.go @@ -0,0 +1,98 @@ +/* + 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 ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/compose-spec/compose-go/v2/types" + "gotest.tools/v3/assert" +) + +// TestGoldenFiles guards the canonical model produced for `services:` against +// regressions: each testdata/golden/*.yaml corpus file is loaded and the +// resulting project, marshalled to YAML and JSON, must be byte-identical to the +// committed .golden.yaml / .golden.json files. +// +// The committed golden files were verified semantically identical (sorted-key +// JSON comparison) to the model produced by the pre-ContainerSpec-extraction +// parser, proving the refactor behavior-preserving for services. Only the +// marshalling key order changed (service-level fields now serialize before the +// inlined container spec block) — a cosmetic, release-noted difference. +// +// Regenerate with: UPDATE_GOLDEN=1 go test ./loader/ -run TestGoldenFiles +func TestGoldenFiles(t *testing.T) { + inputs, err := filepath.Glob(filepath.Join("testdata", "golden", "*.yaml")) + assert.NilError(t, err) + + update := os.Getenv("UPDATE_GOLDEN") != "" + seen := 0 + for _, input := range inputs { + if strings.HasSuffix(input, ".golden.yaml") { + continue + } + seen++ + t.Run(filepath.Base(input), func(t *testing.T) { + content, err := os.ReadFile(input) + assert.NilError(t, err) + + p, err := LoadWithContext(context.TODO(), types.ConfigDetails{ + WorkingDir: filepath.Join("testdata", "golden"), + ConfigFiles: []types.ConfigFile{{Filename: "compose.yaml", Content: content}}, + Environment: map[string]string{ + "GOLDEN_TAG": "1.2.3", + "GOLDEN_PORT": "8080", + }, + }, func(options *Options) { + options.SetProjectName("golden", true) + options.Profiles = []string{"*"} + options.SkipConsistencyCheck = true + // keep paths as written so golden files are host- and OS-independent + options.ResolvePaths = false + }) + assert.NilError(t, err) + + yamlBytes, err := p.MarshalYAML() + assert.NilError(t, err) + jsonBytes, err := p.MarshalJSON() + assert.NilError(t, err) + + base := strings.TrimSuffix(input, ".yaml") + goldenYAML := base + ".golden.yaml" + goldenJSON := base + ".golden.json" + + if update { + assert.NilError(t, os.WriteFile(goldenYAML, yamlBytes, 0o644)) + assert.NilError(t, os.WriteFile(goldenJSON, jsonBytes, 0o644)) + return + } + + expectedYAML, err := os.ReadFile(goldenYAML) + assert.NilError(t, err, "missing golden file, run with UPDATE_GOLDEN=1 to create it") + assert.Equal(t, string(expectedYAML), string(yamlBytes)) + + expectedJSON, err := os.ReadFile(goldenJSON) + assert.NilError(t, err, "missing golden file, run with UPDATE_GOLDEN=1 to create it") + assert.Equal(t, string(expectedJSON), string(jsonBytes)) + }) + } + assert.Assert(t, seen > 0, "no golden corpus files found") +} diff --git a/loader/include.go b/loader/include.go index e7e6ebb66..a7e8c2ac3 100644 --- a/loader/include.go +++ b/loader/include.go @@ -227,23 +227,10 @@ func ApplyInclude(ctx context.Context, workingDir string, environment types.Mapp // importResources import into model all resources defined by imported, and report error on conflict func importResources(source map[string]any, target map[string]any, processor PostProcessor) error { - if err := importResource(source, target, "services", processor); err != nil { - return err - } - if err := importResource(source, target, "volumes", processor); err != nil { - return err - } - if err := importResource(source, target, "networks", processor); err != nil { - return err - } - if err := importResource(source, target, "secrets", processor); err != nil { - return err - } - if err := importResource(source, target, "configs", processor); err != nil { - return err - } - if err := importResource(source, target, "models", processor); err != nil { - return err + for _, key := range []string{"services", "jobs", "volumes", "networks", "secrets", "configs", "models"} { + if err := importResource(source, target, key, processor); err != nil { + return err + } } return nil } diff --git a/loader/include_test.go b/loader/include_test.go index 2f0207b11..8b6302b80 100644 --- a/loader/include_test.go +++ b/loader/include_test.go @@ -116,10 +116,12 @@ services: }) assert.NilError(t, err) assert.DeepEqual(t, p.Services["bar"], types.ServiceConfig{ - Name: "bar", - Image: "busybox", - Environment: types.MappingWithEquals{ - "ZOT": strPtr("QIX"), + Name: "bar", + ContainerSpec: types.ContainerSpec{ + Image: "busybox", + Environment: types.MappingWithEquals{ + "ZOT": strPtr("QIX"), + }, }, }) } diff --git a/loader/interpolate.go b/loader/interpolate.go index dc8dc7356..ac263fe53 100644 --- a/loader/interpolate.go +++ b/loader/interpolate.go @@ -26,59 +26,89 @@ import ( "github.com/sirupsen/logrus" ) -var interpolateTypeCastMapping = map[tree.Path]interp.Cast{ - servicePath("cpu_count"): toInt64, - servicePath("cpu_percent"): toFloat, - servicePath("cpu_period"): toInt64, - servicePath("cpu_quota"): toInt64, - servicePath("cpu_rt_period"): toInt64, - servicePath("cpu_rt_runtime"): toInt64, - servicePath("cpus"): toFloat32, - servicePath("cpu_shares"): toInt64, - servicePath("init"): toBoolean, - servicePath("depends_on", tree.PathMatchAll, "required"): toBoolean, - servicePath("depends_on", tree.PathMatchAll, "restart"): toBoolean, - servicePath("deploy", "replicas"): toInt, - servicePath("deploy", "update_config", "parallelism"): toInt, - servicePath("deploy", "update_config", "max_failure_ratio"): toFloat, - servicePath("deploy", "rollback_config", "parallelism"): toInt, - servicePath("deploy", "rollback_config", "max_failure_ratio"): toFloat, - servicePath("deploy", "restart_policy", "max_attempts"): toInt, - servicePath("deploy", "placement", "max_replicas_per_node"): toInt, - servicePath("healthcheck", "retries"): toInt, - servicePath("healthcheck", "disable"): toBoolean, - servicePath("oom_kill_disable"): toBoolean, - servicePath("oom_score_adj"): toInt64, - servicePath("pids_limit"): toInt64, - servicePath("ports", tree.PathMatchList, "target"): toInt, - servicePath("privileged"): toBoolean, - servicePath("read_only"): toBoolean, - servicePath("scale"): toInt, - servicePath("stdin_open"): toBoolean, - servicePath("tty"): toBoolean, - servicePath("ulimits", tree.PathMatchAll): toInt, - servicePath("ulimits", tree.PathMatchAll, "hard"): toInt, - servicePath("ulimits", tree.PathMatchAll, "soft"): toInt, - servicePath("volumes", tree.PathMatchList, "read_only"): toBoolean, - servicePath("volumes", tree.PathMatchList, "volume", "nocopy"): toBoolean, - iPath("networks", tree.PathMatchAll, "external"): toBoolean, - iPath("networks", tree.PathMatchAll, "internal"): toBoolean, - iPath("networks", tree.PathMatchAll, "attachable"): toBoolean, - iPath("networks", tree.PathMatchAll, "enable_ipv4"): toBoolean, - iPath("networks", tree.PathMatchAll, "enable_ipv6"): toBoolean, - iPath("volumes", tree.PathMatchAll, "external"): toBoolean, - iPath("secrets", tree.PathMatchAll, "external"): toBoolean, - iPath("configs", tree.PathMatchAll, "external"): toBoolean, +var interpolateTypeCastMapping = buildInterpolateTypeCastMapping() + +// buildInterpolateTypeCastMapping registers numeric/boolean casts per layer of +// the specification: container_spec attributes apply wherever a container is +// declared (services, jobs, pre_start init containers), workload_spec +// attributes to services and jobs, service-only attributes to services. +func buildInterpolateTypeCastMapping() map[tree.Path]interp.Cast { + casts := map[tree.Path]interp.Cast{ + iPath("networks", tree.PathMatchAll, "external"): toBoolean, + iPath("networks", tree.PathMatchAll, "internal"): toBoolean, + iPath("networks", tree.PathMatchAll, "attachable"): toBoolean, + iPath("networks", tree.PathMatchAll, "enable_ipv4"): toBoolean, + iPath("networks", tree.PathMatchAll, "enable_ipv6"): toBoolean, + iPath("volumes", tree.PathMatchAll, "external"): toBoolean, + iPath("secrets", tree.PathMatchAll, "external"): toBoolean, + iPath("configs", tree.PathMatchAll, "external"): toBoolean, + } + containerSpec := []tree.Path{ + iPath("services", tree.PathMatchAll), + iPath("jobs", tree.PathMatchAll), + iPath("services", tree.PathMatchAll, "pre_start", tree.PathMatchAll), + } + workloadSpec := []tree.Path{ + iPath("services", tree.PathMatchAll), + iPath("jobs", tree.PathMatchAll), + } + serviceOnly := []tree.Path{ + iPath("services", tree.PathMatchAll), + } + add := func(prefixes []tree.Path, cast interp.Cast, parts ...string) { + for _, prefix := range prefixes { + p := prefix + for _, part := range parts { + p = p.Next(part) + } + casts[p] = cast + } + } + + add(containerSpec, toInt64, "cpu_count") + add(containerSpec, toFloat, "cpu_percent") + add(containerSpec, toInt64, "cpu_period") + add(containerSpec, toInt64, "cpu_quota") + add(containerSpec, toInt64, "cpu_rt_period") + add(containerSpec, toInt64, "cpu_rt_runtime") + add(containerSpec, toFloat32, "cpus") + add(containerSpec, toInt64, "cpu_shares") + add(containerSpec, toBoolean, "init") + add(containerSpec, toBoolean, "oom_kill_disable") + add(containerSpec, toInt64, "oom_score_adj") + add(containerSpec, toInt64, "pids_limit") + add(containerSpec, toBoolean, "privileged") + add(containerSpec, toBoolean, "read_only") + add(containerSpec, toInt, "ulimits", tree.PathMatchAll) + add(containerSpec, toInt, "ulimits", tree.PathMatchAll, "hard") + add(containerSpec, toInt, "ulimits", tree.PathMatchAll, "soft") + add(containerSpec, toBoolean, "volumes", tree.PathMatchList, "read_only") + add(containerSpec, toBoolean, "volumes", tree.PathMatchList, "volume", "nocopy") + + add(workloadSpec, toBoolean, "depends_on", tree.PathMatchAll, "required") + add(workloadSpec, toBoolean, "depends_on", tree.PathMatchAll, "restart") + add(workloadSpec, toInt, "healthcheck", "retries") + add(workloadSpec, toBoolean, "healthcheck", "disable") + add(workloadSpec, toInt, "ports", tree.PathMatchList, "target") + add(workloadSpec, toBoolean, "stdin_open") + add(workloadSpec, toBoolean, "tty") + + add(serviceOnly, toInt, "deploy", "replicas") + add(serviceOnly, toInt, "deploy", "update_config", "parallelism") + add(serviceOnly, toFloat, "deploy", "update_config", "max_failure_ratio") + add(serviceOnly, toInt, "deploy", "rollback_config", "parallelism") + add(serviceOnly, toFloat, "deploy", "rollback_config", "max_failure_ratio") + add(serviceOnly, toInt, "deploy", "restart_policy", "max_attempts") + add(serviceOnly, toInt, "deploy", "placement", "max_replicas_per_node") + add(serviceOnly, toInt, "scale") + + return casts } func iPath(parts ...string) tree.Path { return tree.NewPath(parts...) } -func servicePath(parts ...string) tree.Path { - return iPath(append([]string{"services", tree.PathMatchAll}, parts...)...) -} - func toInt(value string) (interface{}, error) { return strconv.Atoi(value) } diff --git a/loader/loader.go b/loader/loader.go index 13843c42d..fddb546be 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -835,6 +835,7 @@ func Transform(source interface{}, target interface{}) error { ), Result: target, TagName: "yaml", + Squash: true, Metadata: &data, } decoder, err := mapstructure.NewDecoder(config) @@ -844,9 +845,9 @@ func Transform(source interface{}, target interface{}) error { return decoder.Decode(source) } -// nameServices create implicit `name` key for convenience accessing service +// nameServices create implicit `name` key for convenience accessing service or job func nameServices(from reflect.Value, to reflect.Value) (interface{}, error) { - if to.Type() == reflect.TypeOf(types.Services{}) { + if to.Type() == reflect.TypeOf(types.Services{}) || to.Type() == reflect.TypeOf(types.Jobs{}) { nameK := reflect.ValueOf("name") iter := from.MapRange() for iter.Next() { diff --git a/loader/loader_test.go b/loader/loader_test.go index 09a9719ce..97338a245 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -179,19 +179,23 @@ func strPtr(val string) *string { var sampleConfig = types.Config{ Services: types.Services{ "foo": { - Name: "foo", - Image: "busybox", - Environment: map[string]*string{}, - Networks: map[string]*types.ServiceNetworkConfig{ - "with_me": nil, + Name: "foo", + ContainerSpec: types.ContainerSpec{ + Image: "busybox", + Environment: map[string]*string{}, + Networks: map[string]*types.ServiceNetworkConfig{ + "with_me": nil, + }, }, }, "bar": { - Name: "bar", - Image: "busybox", - Environment: map[string]*string{"FOO": strPtr("1")}, - Networks: map[string]*types.ServiceNetworkConfig{ - "with_ipam": nil, + Name: "bar", + ContainerSpec: types.ContainerSpec{ + Image: "busybox", + Environment: map[string]*string{"FOO": strPtr("1")}, + Networks: map[string]*types.ServiceNetworkConfig{ + "with_ipam": nil, + }, }, }, }, @@ -791,23 +795,6 @@ networks: Services: types.Services{ "web": { Name: "web", - Configs: []types.ServiceConfigObjConfig{ - { - Source: "appconfig", - Mode: ptr(types.FileMode(0o555)), - }, - }, - Secrets: []types.ServiceSecretConfig{ - { - Source: "super", - Target: "/run/secrets/super", - Mode: ptr(types.FileMode(0o555)), - }, - }, - HealthCheck: &types.HealthCheckConfig{ - Retries: ptr(uint64(555)), - Disable: true, - }, Deploy: &types.DeployConfig{ Replicas: ptr(555), UpdateConfig: &types.UpdateConfig{ @@ -825,22 +812,21 @@ networks: MaxReplicas: 555, }, }, - Ports: []types.ServicePortConfig{ - {Target: 555, Mode: "ingress", Protocol: "tcp"}, - {Target: 34567, Mode: "ingress", Protocol: "tcp"}, - {Target: 555, Mode: "ingress", Protocol: "tcp", Published: "555", Extensions: map[string]interface{}{"x-foo-bar": true}}, - }, - Ulimits: map[string]*types.UlimitsConfig{ + ContainerSpec: types.ContainerSpec{Configs: []types.ServiceConfigObjConfig{ + { + Source: "appconfig", + Mode: ptr(types.FileMode(0o555)), + }, + }, Secrets: []types.ServiceSecretConfig{ + { + Source: "super", + Target: "/run/secrets/super", + Mode: ptr(types.FileMode(0o555)), + }, + }, Ulimits: map[string]*types.UlimitsConfig{ "nproc": {Single: 555}, "nofile": {Hard: 555, Soft: 555}, - }, - Privileged: true, - ReadOnly: true, - ShmSize: types.UnitBytes(2 * 1024 * 1024 * 1024), - StopGracePeriod: &typesDuration, - StdinOpen: true, - Tty: true, - Volumes: []types.ServiceVolumeConfig{ + }, Privileged: true, ReadOnly: true, ShmSize: types.UnitBytes(2 * 1024 * 1024 * 1024), StopGracePeriod: &typesDuration, Volumes: []types.ServiceVolumeConfig{ { Source: "data", Type: "volume", @@ -848,8 +834,15 @@ networks: ReadOnly: true, Volume: &types.ServiceVolumeVolume{NoCopy: true}, }, - }, - Environment: types.MappingWithEquals{}, + }, Environment: types.MappingWithEquals{}}, + WorkloadSpec: types.WorkloadSpec{HealthCheck: &types.HealthCheckConfig{ + Retries: ptr(uint64(555)), + Disable: true, + }, Ports: []types.ServicePortConfig{ + {Target: 555, Mode: "ingress", Protocol: "tcp"}, + {Target: 34567, Mode: "ingress", Protocol: "tcp"}, + {Target: 555, Mode: "ingress", Protocol: "tcp", Published: "555", Extensions: map[string]interface{}{"x-foo-bar": true}}, + }, StdinOpen: true, Tty: true}, }, }, Configs: map[string]types.ConfigObjConfig{ @@ -894,28 +887,32 @@ services: WorkingDir: workingDir, Services: types.Services{ "service_1": { - Name: "service_1", - Environment: types.MappingWithEquals{}, - Labels: types.Labels{ - "BAR": "bar_from_label_file", - "BAZ": "baz_from_label_file", - "FOO": "foo_from_label_file", - "LABEL.WITH.DOT": "ok", - "LABEL_WITH_UNDERSCORE": "ok", - }, - LabelFiles: []string{ - filepath.Join(workingDir, "example1.label"), + Name: "service_1", + ContainerSpec: types.ContainerSpec{ + Environment: types.MappingWithEquals{}, + Labels: types.Labels{ + "BAR": "bar_from_label_file", + "BAZ": "baz_from_label_file", + "FOO": "foo_from_label_file", + "LABEL.WITH.DOT": "ok", + "LABEL_WITH_UNDERSCORE": "ok", + }, + LabelFiles: []string{ + filepath.Join(workingDir, "example1.label"), + }, }, }, "service_2": { - Name: "service_2", - Environment: types.MappingWithEquals{}, - Labels: types.Labels{ - "BAR": "bar_from_label_file_2", - "QUX": "quz_from_label_file_2", - }, - LabelFiles: []string{ - filepath.Join(workingDir, "example2.label"), + Name: "service_2", + ContainerSpec: types.ContainerSpec{ + Environment: types.MappingWithEquals{}, + Labels: types.Labels{ + "BAR": "bar_from_label_file_2", + "QUX": "quz_from_label_file_2", + }, + LabelFiles: []string{ + filepath.Join(workingDir, "example2.label"), + }, }, }, }, @@ -1598,11 +1595,13 @@ networks: WorkingDir: workingDir, Services: types.Services{ "hello-world": { - Name: "hello-world", - Image: "redis:alpine", - Networks: map[string]*types.ServiceNetworkConfig{ - "network1": nil, - "network3": nil, + Name: "hello-world", + ContainerSpec: types.ContainerSpec{ + Image: "redis:alpine", + Networks: map[string]*types.ServiceNetworkConfig{ + "network1": nil, + "network3": nil, + }, }, }, }, @@ -1660,24 +1659,20 @@ func TestLoadWithExtends(t *testing.T) { expServices := types.Services{ "importer": { Name: "importer", - Image: "nginx", ContainerName: "imported", - Environment: types.MappingWithEquals{ + ContainerSpec: types.ContainerSpec{Image: "nginx", Environment: types.MappingWithEquals{ "SOURCE": strPtr("extends"), - }, - EnvFiles: []types.EnvFile{ + }, EnvFiles: []types.EnvFile{ { Path: expectedEnvFilePath, Required: true, }, - }, - Networks: map[string]*types.ServiceNetworkConfig{"default": nil}, - Volumes: []types.ServiceVolumeConfig{{ + }, Networks: map[string]*types.ServiceNetworkConfig{"default": nil}, Volumes: []types.ServiceVolumeConfig{{ Type: "bind", Source: "/opt/data", Target: "/var/lib/mysql", Bind: &types.ServiceVolumeBind{CreateHostPath: true}, - }}, + }}}, }, } assert.Check(t, is.DeepEqual(expServices, actual.Services)) @@ -1700,13 +1695,12 @@ func TestLoadWithExtendsWithContextUrl(t *testing.T) { expServices := types.Services{ "importer-with-https-url": { - Name: "importer-with-https-url", - Build: &types.BuildConfig{ + Name: "importer-with-https-url", + ContainerSpec: types.ContainerSpec{Environment: types.MappingWithEquals{}, Networks: map[string]*types.ServiceNetworkConfig{"default": nil}}, + WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{ Context: "https://github.com/docker/compose.git", Dockerfile: "Dockerfile", - }, - Environment: types.MappingWithEquals{}, - Networks: map[string]*types.ServiceNetworkConfig{"default": nil}, + }}, }, } assert.Check(t, is.DeepEqual(expServices, actual.Services)) @@ -1884,8 +1878,10 @@ func TestLoadServiceWithEnvFile(t *testing.T) { Services: types.Services{ "test": { Name: "test", - EnvFiles: []types.EnvFile{ - {Path: file.Name(), Required: true}, + ContainerSpec: types.ContainerSpec{ + EnvFiles: []types.EnvFile{ + {Path: file.Name(), Required: true}, + }, }, }, }, @@ -1909,8 +1905,10 @@ func TestLoadServiceWithLabelFile(t *testing.T) { Services: types.Services{ "test": { Name: "test", - LabelFiles: []string{ - file.Name(), + ContainerSpec: types.ContainerSpec{ + LabelFiles: []string{ + file.Name(), + }, }, }, }, @@ -1927,8 +1925,10 @@ func TestLoadServiceWithLabelFile_NotExists(t *testing.T) { Services: types.Services{ "test": { Name: "test", - LabelFiles: []string{ - "test", + ContainerSpec: types.ContainerSpec{ + LabelFiles: []string{ + "test", + }, }, }, }, @@ -2173,15 +2173,17 @@ volumes: assert.NilError(t, err) assert.DeepEqual(t, p.Services, types.Services{ "foo": { - Name: "foo", - Image: "busybox", - Environment: types.MappingWithEquals{}, - Volumes: []types.ServiceVolumeConfig{ - { - Type: types.VolumeTypeVolume, - Source: "0", - Target: "/foo", - Volume: &types.ServiceVolumeVolume{}, + Name: "foo", + ContainerSpec: types.ContainerSpec{ + Image: "busybox", + Environment: types.MappingWithEquals{}, + Volumes: []types.ServiceVolumeConfig{ + { + Type: types.VolumeTypeVolume, + Source: "0", + Target: "/foo", + Volume: &types.ServiceVolumeVolume{}, + }, }, }, }, @@ -2243,30 +2245,27 @@ services: assert.NilError(t, err) assert.DeepEqual(t, p.Services, types.Services{ "foo": { - Name: "foo", - Image: "busybox", - Environment: types.MappingWithEquals{}, - DependsOn: types.DependsOnConfig{"imported": {Condition: "service_started", Required: true}}, + Name: "foo", + ContainerSpec: types.ContainerSpec{Image: "busybox", Environment: types.MappingWithEquals{}}, + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{"imported": {Condition: "service_started", Required: true}}}, }, "imported": { Name: "imported", - ContainerName: "extends", // as defined by ./testdata/subdir/extra.env - Environment: types.MappingWithEquals{"SOURCE": strPtr("extends")}, - EnvFiles: []types.EnvFile{ - { - Path: filepath.Join(workingDir, "testdata", "subdir", "extra.env"), - Required: true, - }, - }, - Image: "nginx", - Volumes: []types.ServiceVolumeConfig{ - { - Type: "bind", - Source: "/opt/data", - Target: "/var/lib/mysql", - Bind: &types.ServiceVolumeBind{CreateHostPath: true}, - }, - }, + ContainerName: "extends", + ContainerSpec: types.ContainerSpec{ // as defined by ./testdata/subdir/extra.env + Environment: types.MappingWithEquals{"SOURCE": strPtr("extends")}, EnvFiles: []types.EnvFile{ + { + Path: filepath.Join(workingDir, "testdata", "subdir", "extra.env"), + Required: true, + }, + }, Image: "nginx", Volumes: []types.ServiceVolumeConfig{ + { + Type: "bind", + Source: "/opt/data", + Target: "/var/lib/mysql", + Bind: &types.ServiceVolumeBind{CreateHostPath: true}, + }, + }}, }, }) /* TODO(ndeloof) restore support for include tracking @@ -2384,14 +2383,13 @@ services: assert.NilError(t, err) assert.DeepEqual(t, p.Services, types.Services{ "foo": { - Name: "foo", - Image: "nginx", - Environment: types.MappingWithEquals{}, - DependsOn: types.DependsOnConfig{ + Name: "foo", + ContainerSpec: types.ContainerSpec{Image: "nginx", Environment: types.MappingWithEquals{}}, + WorkloadSpec: types.WorkloadSpec{DependsOn: types.DependsOnConfig{ "bar": {Condition: types.ServiceConditionStarted, Required: true}, "baz": {Condition: types.ServiceConditionHealthy, Required: false}, "qux": {Condition: types.ServiceConditionCompletedSuccessfully, Required: true}, - }, + }}, }, }) } @@ -2442,21 +2440,23 @@ services: assert.NilError(t, err) assert.DeepEqual(t, p.Services, types.Services{ "foo": { - Name: "foo", - Image: "foo", - Environment: types.MappingWithEquals{"FOO": strPtr("BAR")}, - EnvFiles: []types.EnvFile{ - { - Path: filepath.Join(config.WorkingDir, "testdata", "remote", "env"), - Required: true, + Name: "foo", + ContainerSpec: types.ContainerSpec{ + Image: "foo", + Environment: types.MappingWithEquals{"FOO": strPtr("BAR")}, + EnvFiles: []types.EnvFile{ + { + Path: filepath.Join(config.WorkingDir, "testdata", "remote", "env"), + Required: true, + }, }, - }, - Volumes: []types.ServiceVolumeConfig{ - { - Type: types.VolumeTypeBind, - Source: filepath.Join(config.WorkingDir, "testdata", "remote"), - Target: "/foo", - Bind: &types.ServiceVolumeBind{CreateHostPath: true}, + Volumes: []types.ServiceVolumeConfig{ + { + Type: types.VolumeTypeBind, + Source: filepath.Join(config.WorkingDir, "testdata", "remote"), + Target: "/foo", + Bind: &types.ServiceVolumeBind{CreateHostPath: true}, + }, }, }, }, diff --git a/loader/mapstructure_test.go b/loader/mapstructure_test.go index 4638ae082..1ad0831e2 100644 --- a/loader/mapstructure_test.go +++ b/loader/mapstructure_test.go @@ -30,6 +30,7 @@ func TestDecodeMapStructure(t *testing.T) { config := &mapstructure.DecoderConfig{ Result: &target, TagName: "yaml", + Squash: true, Metadata: &data, DecodeHook: mapstructure.ComposeDecodeHookFunc(decoderHook), } diff --git a/loader/merge_reset_test.go b/loader/merge_reset_test.go index f721b5f8b..9a93c3d78 100644 --- a/loader/merge_reset_test.go +++ b/loader/merge_reset_test.go @@ -56,9 +56,11 @@ func Test_LoadWithReset(t *testing.T) { }) assert.NilError(t, err) assert.DeepEqual(t, p.Services["foo"], types.ServiceConfig{ - Name: "foo", - Image: "foo", - Environment: types.MappingWithEquals{}, + Name: "foo", + ContainerSpec: types.ContainerSpec{ + Image: "foo", + Environment: types.MappingWithEquals{}, + }, }) } diff --git a/loader/normalize.go b/loader/normalize.go index 165ce4e56..734d56c05 100644 --- a/loader/normalize.go +++ b/loader/normalize.go @@ -29,13 +29,17 @@ import ( func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) { normalizeNetworks(dict) - if d, ok := dict["services"]; ok { - services := d.(map[string]any) - for name, s := range services { - service := s.(map[string]any) + for _, key := range []string{"services", "jobs"} { + d, ok := dict[key] + if !ok { + continue + } + containers := d.(map[string]any) + for name, s := range containers { + container := s.(map[string]any) - if service["pull_policy"] == types.PullPolicyIfNotPresent { - service["pull_policy"] = types.PullPolicyMissing + if container["pull_policy"] == types.PullPolicyIfNotPresent { + container["pull_policy"] = types.PullPolicyMissing } fn := func(s string) (string, bool) { @@ -43,7 +47,7 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) { return v, ok } - if b, ok := service["build"]; ok { + if b, ok := container["build"]; ok { build := b.(map[string]any) if build["context"] == nil { build["context"] = "." @@ -56,20 +60,20 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) { build["args"], _ = resolve(a, fn, false) } - service["build"] = build + container["build"] = build } - if e, ok := service["environment"]; ok { - service["environment"], _ = resolve(e, fn, true) + if e, ok := container["environment"]; ok { + container["environment"], _ = resolve(e, fn, true) } var dependsOn map[string]any - if d, ok := service["depends_on"]; ok { + if d, ok := container["depends_on"]; ok { dependsOn = d.(map[string]any) } else { dependsOn = map[string]any{} } - if l, ok := service["links"]; ok { + if l, ok := container["links"]; ok { links := l.([]any) for _, e := range links { link := e.(string) @@ -88,7 +92,7 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) { } for _, namespace := range []string{"network_mode", "ipc", "pid", "uts", "cgroup"} { - if n, ok := service[namespace]; ok { + if n, ok := container[namespace]; ok { ref := n.(string) if strings.HasPrefix(ref, types.ServicePrefix) { shared := ref[len(types.ServicePrefix):] @@ -103,7 +107,7 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) { } } - if v, ok := service["volumes"]; ok { + if v, ok := container["volumes"]; ok { volumes := v.([]any) for i, volume := range volumes { vol := volume.(map[string]any) @@ -111,10 +115,10 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) { vol["target"] = path.Clean(target) volumes[i] = vol } - service["volumes"] = volumes + container["volumes"] = volumes } - if n, ok := service["volumes_from"]; ok { + if n, ok := container["volumes_from"]; ok { volumesFrom := n.([]any) for _, v := range volumesFrom { vol := v.(string) @@ -131,15 +135,15 @@ func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) { } } if len(dependsOn) > 0 { - service["depends_on"] = dependsOn + container["depends_on"] = dependsOn } - inheritPreStartImage(service) + inheritPreStartImage(container) - services[name] = service + containers[name] = container } - dict["services"] = services + dict[key] = containers } setNameFromKey(dict) @@ -176,33 +180,37 @@ func normalizeNetworks(dict map[string]any) { // implicit `default` network must be introduced only if actually used by some service usesDefaultNetwork := false - if s, ok := dict["services"]; ok { - services := s.(map[string]any) - for name, se := range services { - service := se.(map[string]any) - if _, ok := service["provider"]; ok { + for _, key := range []string{"services", "jobs"} { + s, ok := dict[key] + if !ok { + continue + } + containers := s.(map[string]any) + for name, se := range containers { + container := se.(map[string]any) + if _, ok := container["provider"]; ok { continue } - if _, ok := service["network_mode"]; ok { + if _, ok := container["network_mode"]; ok { continue } - if n, ok := service["networks"]; !ok { - // If none explicitly declared, service is connected to default network - service["networks"] = map[string]any{"default": nil} + if n, ok := container["networks"]; !ok { + // If none explicitly declared, container is connected to default network + container["networks"] = map[string]any{"default": nil} usesDefaultNetwork = true } else { net := n.(map[string]any) if len(net) == 0 { // networks section declared but empty (corner case) - service["networks"] = map[string]any{"default": nil} + container["networks"] = map[string]any{"default": nil} usesDefaultNetwork = true } else if _, ok := net["default"]; ok { usesDefaultNetwork = true } } - services[name] = service + containers[name] = container } - dict["services"] = services + dict[key] = containers } if _, ok := networks["default"]; !ok && usesDefaultNetwork { diff --git a/loader/omitEmpty.go b/loader/omitEmpty.go index fd2d8e865..8f004664d 100644 --- a/loader/omitEmpty.go +++ b/loader/omitEmpty.go @@ -20,6 +20,7 @@ import "github.com/compose-spec/compose-go/v2/tree" var omitempty = []tree.Path{ "services.*.dns", + "jobs.*.dns", } // OmitEmpty removes empty attributes which are irrelevant when unset diff --git a/loader/testdata/golden/merge-fragments.golden.json b/loader/testdata/golden/merge-fragments.golden.json new file mode 100644 index 000000000..bedb195e4 --- /dev/null +++ b/loader/testdata/golden/merge-fragments.golden.json @@ -0,0 +1,92 @@ +{ + "name": "golden", + "networks": { + "default": { + "name": "golden_default", + "ipam": {} + } + }, + "services": { + "api": { + "command": [ + "api" + ], + "entrypoint": null, + "environment": { + "EXTRA": "fallback", + "LOG_LEVEL": "debug", + "REGION": "eu-west-1" + }, + "image": "example/base:latest", + "labels": { + "com.example.team": "platform" + }, + "networks": { + "default": null + } + }, + "base": { + "profiles": [ + "never" + ], + "command": null, + "entrypoint": null, + "environment": { + "LOG_LEVEL": "info", + "REGION": "eu-west-1" + }, + "image": "example/base:latest", + "labels": { + "com.example.team": "platform" + }, + "networks": { + "default": null + } + }, + "minimal": { + "command": null, + "entrypoint": null, + "image": "busybox", + "networks": { + "default": null + } + }, + "worker": { + "deploy": { + "replicas": 3, + "resources": {}, + "placement": {} + }, + "command": [ + "worker" + ], + "entrypoint": null, + "environment": { + "EXTRA": "fallback", + "LOG_LEVEL": "debug", + "REGION": "eu-west-1" + }, + "image": "example/base:latest", + "labels": { + "com.example.team": "platform" + }, + "networks": { + "default": null + } + } + }, + "x-common-env": { + "LOG_LEVEL": "info", + "REGION": "eu-west-1" + }, + "x-defaults": { + "environment": { + "LOG_LEVEL": "info", + "REGION": "eu-west-1" + }, + "image": "example/base:latest", + "labels": { + "com.example.team": "platform" + } + } +} \ No newline at end of file diff --git a/loader/testdata/golden/merge-fragments.golden.yaml b/loader/testdata/golden/merge-fragments.golden.yaml new file mode 100644 index 000000000..af8996d6c --- /dev/null +++ b/loader/testdata/golden/merge-fragments.golden.yaml @@ -0,0 +1,59 @@ +name: golden +services: + api: + command: + - api + environment: + EXTRA: fallback + LOG_LEVEL: debug + REGION: eu-west-1 + image: example/base:latest + labels: + com.example.team: platform + networks: + default: null + base: + profiles: + - never + environment: + LOG_LEVEL: info + REGION: eu-west-1 + image: example/base:latest + labels: + com.example.team: platform + networks: + default: null + minimal: + image: busybox + networks: + default: null + x-custom-metadata: + owner: team-a + tier: 2 + worker: + deploy: + replicas: 3 + command: + - worker + environment: + EXTRA: fallback + LOG_LEVEL: debug + REGION: eu-west-1 + image: example/base:latest + labels: + com.example.team: platform + networks: + default: null +networks: + default: + name: golden_default +x-common-env: + LOG_LEVEL: info + REGION: eu-west-1 +x-defaults: + environment: + LOG_LEVEL: info + REGION: eu-west-1 + image: example/base:latest + labels: + com.example.team: platform diff --git a/loader/testdata/golden/merge-fragments.yaml b/loader/testdata/golden/merge-fragments.yaml new file mode 100644 index 000000000..e4aef0620 --- /dev/null +++ b/loader/testdata/golden/merge-fragments.yaml @@ -0,0 +1,38 @@ +name: golden + +x-common-env: &common-env + LOG_LEVEL: info + REGION: eu-west-1 + +x-defaults: &defaults + image: example/base:latest + environment: *common-env + labels: + com.example.team: platform + +services: + base: + <<: *defaults + profiles: + - never + + api: + <<: *defaults + command: ["api"] + environment: + LOG_LEVEL: debug + REGION: eu-west-1 + EXTRA: "${UNDEFINED:-fallback}" + + worker: + extends: + service: api + command: ["worker"] + deploy: + replicas: 3 + + minimal: + image: busybox + x-custom-metadata: + owner: team-a + tier: 2 diff --git a/loader/testdata/golden/runtime-flags.golden.json b/loader/testdata/golden/runtime-flags.golden.json new file mode 100644 index 000000000..071838b9d --- /dev/null +++ b/loader/testdata/golden/runtime-flags.golden.json @@ -0,0 +1,148 @@ +{ + "name": "golden", + "networks": { + "default": { + "name": "golden_default", + "ipam": {} + } + }, + "services": { + "kitchen-sink": { + "attach": false, + "container_name": "sink", + "annotations": { + "com.example.annotation": "value" + }, + "blkio_config": { + "weight": 300, + "weight_device": [ + { + "Path": "/dev/sda", + "Weight": 400 + } + ] + }, + "cap_add": [ + "NET_ADMIN" + ], + "cap_drop": [ + "ALL" + ], + "cgroup_parent": "my-parent", + "cgroup": "private", + "cpu_count": 2, + "cpu_percent": 50, + "cpu_period": 100000, + "cpu_quota": 50000, + "cpu_rt_period": 1000000, + "cpu_rt_runtime": 950000, + "cpus": 1.5, + "cpuset": "0,1", + "cpu_shares": 512, + "command": null, + "device_cgroup_rules": [ + "c 1:3 mr" + ], + "devices": [ + { + "source": "/dev/ttyUSB0", + "target": "/dev/ttyUSB0", + "permissions": "rwm" + } + ], + "dns": [ + "8.8.8.8" + ], + "dns_opt": [ + "use-vc" + ], + "dns_search": [ + "example.com" + ], + "domainname": "example.com", + "entrypoint": null, + "extra_hosts": [ + "otherhost=50.31.209.229" + ], + "group_add": [ + "mail" + ], + "hostname": "sink-host", + "image": "example/sink:latest", + "init": true, + "ipc": "shareable", + "isolation": "default", + "labels": { + "com.example.flag": "1" + }, + "mem_limit": "536870912", + "mem_reservation": "268435456", + "memswap_limit": "1073741824", + "mem_swappiness": "60", + "mac_address": "02:42:ac:11:00:02", + "networks": { + "default": null + }, + "oom_kill_disable": true, + "oom_score_adj": 500, + "pid": "host", + "pids_limit": 100, + "platform": "linux/amd64", + "privileged": true, + "pull_policy": "always", + "read_only": true, + "runtime": "runc", + "security_opt": [ + "label=disable" + ], + "shm_size": "67108864", + "stop_grace_period": "1m30s", + "stop_signal": "SIGUSR1", + "storage_opt": { + "size": "20G" + }, + "sysctls": { + "net.core.somaxconn": "1024" + }, + "tmpfs": [ + "/run", + "/tmp" + ], + "ulimits": { + "nofile": { + "soft": 20000, + "hard": 40000 + }, + "nproc": 65535 + }, + "user": "1000:1000", + "userns_mode": "host", + "uts": "host", + "working_dir": "/app", + "stdin_open": true, + "tty": true + }, + "linked": { + "external_links": [ + "legacy:legacy-alias" + ], + "links": [ + "kitchen-sink:sink" + ], + "command": null, + "entrypoint": null, + "image": "example/linked", + "network_mode": "service:kitchen-sink", + "volumes_from": [ + "kitchen-sink" + ], + "depends_on": { + "kitchen-sink": { + "condition": "service_started", + "restart": true, + "required": true + } + } + } + } +} \ No newline at end of file diff --git a/loader/testdata/golden/runtime-flags.golden.yaml b/loader/testdata/golden/runtime-flags.golden.yaml new file mode 100644 index 000000000..aa043eb2e --- /dev/null +++ b/loader/testdata/golden/runtime-flags.golden.yaml @@ -0,0 +1,107 @@ +name: golden +services: + kitchen-sink: + attach: false + container_name: sink + annotations: + com.example.annotation: value + blkio_config: + weight: 300 + weight_device: + - path: /dev/sda + weight: 400 + cap_add: + - NET_ADMIN + cap_drop: + - ALL + cgroup_parent: my-parent + cgroup: private + cpu_count: 2 + cpu_percent: 50 + cpu_period: 100000 + cpu_quota: 50000 + cpu_rt_period: 1000000 + cpu_rt_runtime: 950000 + cpus: 1.5 + cpuset: 0,1 + cpu_shares: 512 + device_cgroup_rules: + - c 1:3 mr + devices: + - source: /dev/ttyUSB0 + target: /dev/ttyUSB0 + permissions: rwm + dns: + - 8.8.8.8 + dns_opt: + - use-vc + dns_search: + - example.com + domainname: example.com + extra_hosts: + - otherhost=50.31.209.229 + group_add: + - mail + hostname: sink-host + image: example/sink:latest + init: true + ipc: shareable + isolation: default + labels: + com.example.flag: "1" + mem_limit: "536870912" + mem_reservation: "268435456" + memswap_limit: "1073741824" + mem_swappiness: "60" + mac_address: 02:42:ac:11:00:02 + networks: + default: null + oom_kill_disable: true + oom_score_adj: 500 + pid: host + pids_limit: 100 + platform: linux/amd64 + privileged: true + pull_policy: always + read_only: true + runtime: runc + security_opt: + - label=disable + shm_size: "67108864" + stop_grace_period: 1m30s + stop_signal: SIGUSR1 + storage_opt: + size: 20G + sysctls: + net.core.somaxconn: "1024" + tmpfs: + - /run + - /tmp + ulimits: + nofile: + soft: 20000 + hard: 40000 + nproc: 65535 + user: 1000:1000 + userns_mode: host + uts: host + working_dir: /app + stdin_open: true + tty: true + linked: + external_links: + - legacy:legacy-alias + links: + - kitchen-sink:sink + image: example/linked + network_mode: service:kitchen-sink + volumes_from: + - kitchen-sink + depends_on: + kitchen-sink: + condition: service_started + restart: true + required: true +networks: + default: + name: golden_default diff --git a/loader/testdata/golden/runtime-flags.yaml b/loader/testdata/golden/runtime-flags.yaml new file mode 100644 index 000000000..8c33f71eb --- /dev/null +++ b/loader/testdata/golden/runtime-flags.yaml @@ -0,0 +1,96 @@ +name: golden +services: + kitchen-sink: + image: example/sink:latest + annotations: + com.example.annotation: value + attach: false + blkio_config: + weight: 300 + weight_device: + - path: /dev/sda + weight: 400 + cap_add: + - NET_ADMIN + cap_drop: + - ALL + cgroup: private + cgroup_parent: my-parent + cpu_count: 2 + cpu_percent: 50 + cpu_period: 100000 + cpu_quota: 50000 + cpu_rt_period: 1000000 + cpu_rt_runtime: 950000 + cpu_shares: 512 + cpus: 1.5 + cpuset: "0,1" + container_name: sink + device_cgroup_rules: + - "c 1:3 mr" + devices: + - /dev/ttyUSB0:/dev/ttyUSB0 + dns: + - 8.8.8.8 + dns_opt: + - use-vc + dns_search: + - example.com + domainname: example.com + extra_hosts: + - "otherhost:50.31.209.229" + group_add: + - mail + hostname: sink-host + init: true + ipc: shareable + isolation: default + labels: + - com.example.flag=1 + mac_address: 02:42:ac:11:00:02 + mem_limit: 512m + mem_reservation: 256m + mem_swappiness: 60 + memswap_limit: 1g + oom_kill_disable: true + oom_score_adj: 500 + pid: host + pids_limit: 100 + platform: linux/amd64 + privileged: true + pull_policy: always + read_only: true + runtime: runc + security_opt: + - label=disable + shm_size: 64m + stdin_open: true + stop_grace_period: 1m30s + stop_signal: SIGUSR1 + storage_opt: + size: 20G + sysctls: + net.core.somaxconn: 1024 + tmpfs: + - /run + - /tmp + tty: true + ulimits: + nproc: 65535 + nofile: + soft: 20000 + hard: 40000 + user: "1000:1000" + userns_mode: host + uts: host + working_dir: /app + + linked: + image: example/linked + links: + - kitchen-sink:sink + external_links: + - legacy:legacy-alias + volumes_from: + - kitchen-sink + network_mode: "service:kitchen-sink" diff --git a/loader/testdata/golden/web-stack.golden.json b/loader/testdata/golden/web-stack.golden.json new file mode 100644 index 000000000..e3a459a75 --- /dev/null +++ b/loader/testdata/golden/web-stack.golden.json @@ -0,0 +1,244 @@ +{ + "configs": { + "redis_conf": { + "name": "golden_redis_conf", + "file": "./redis.conf" + } + }, + "name": "golden", + "networks": { + "backend": { + "name": "golden_backend", + "driver": "bridge", + "ipam": { + "config": [ + { + "subnet": "172.28.0.0/16" + } + ] + } + }, + "default": { + "name": "golden_default", + "ipam": {} + }, + "frontend": { + "name": "golden_frontend", + "ipam": {} + } + }, + "secrets": { + "db_password": { + "name": "golden_db_password", + "file": "./db_password.txt" + } + }, + "services": { + "cache": { + "command": null, + "configs": [ + { + "source": "redis_conf", + "target": "/etc/redis/redis.conf" + } + ], + "entrypoint": null, + "image": "redis:7", + "networks": { + "default": null + } + }, + "db": { + "command": null, + "entrypoint": null, + "environment": { + "POSTGRES_DB": "app", + "POSTGRES_PASSWORD_FILE": "/run/secrets/db_password" + }, + "image": "postgres:16", + "networks": { + "default": null + }, + "secrets": [ + { + "source": "db_password", + "target": "/run/secrets/db_password", + "mode": "0400" + } + ], + "volumes": [ + { + "type": "volume", + "source": "db-data", + "target": "/var/lib/postgresql/data", + "volume": {} + } + ], + "healthcheck": { + "test": [ + "CMD-SHELL", + "pg_isready -U postgres" + ], + "interval": "10s" + } + }, + "web": { + "profiles": [ + "frontend" + ], + "deploy": { + "replicas": 2, + "resources": { + "limits": { + "cpus": 0.5, + "memory": "268435456" + }, + "reservations": { + "memory": "134217728" + } + }, + "restart_policy": { + "condition": "on-failure", + "max_attempts": 3 + }, + "placement": {} + }, + "restart": "unless-stopped", + "scale": 2, + "post_start": [ + { + "command": [ + "./warm-cache.sh" + ], + "user": "root" + } + ], + "pre_stop": [ + { + "command": [ + "./drain.sh" + ] + } + ], + "command": [ + "serve", + "--port", + "8080" + ], + "entrypoint": [ + "/entrypoint.sh" + ], + "environment": { + "DEBUG": null, + "MODE": "production" + }, + "image": "example/web:1.2.3", + "labels": { + "com.example.app": "web" + }, + "logging": { + "driver": "json-file", + "options": { + "max-size": "10m" + } + }, + "networks": { + "backend": null, + "frontend": { + "aliases": [ + "www" + ], + "priority": 10 + } + }, + "volumes": [ + { + "type": "bind", + "source": "./static", + "target": "/usr/share/nginx/html", + "read_only": true, + "bind": {} + }, + { + "type": "volume", + "source": "data", + "target": "/data", + "volume": { + "nocopy": true + } + }, + { + "type": "tmpfs", + "target": "/scratch", + "tmpfs": { + "size": "10485760" + } + } + ], + "build": { + "context": "./web", + "dockerfile": "Dockerfile.prod", + "args": { + "TAG": "1.2.3" + }, + "labels": { + "com.example.stage": "build" + }, + "additional_contexts": { + "assets": "./assets" + }, + "target": "runtime" + }, + "depends_on": { + "cache": { + "condition": "service_started", + "required": true + }, + "db": { + "condition": "service_healthy", + "required": true + } + }, + "expose": [ + "9090" + ], + "healthcheck": { + "test": [ + "CMD", + "curl", + "-f", + "http://localhost/health" + ], + "timeout": "5s", + "interval": "30s", + "retries": 3, + "start_period": "10s" + }, + "ports": [ + { + "mode": "ingress", + "target": 80, + "published": "8080", + "protocol": "tcp" + }, + { + "mode": "host", + "target": 443, + "published": "8443", + "protocol": "tcp" + } + ] + } + }, + "volumes": { + "data": { + "name": "golden_data" + }, + "db-data": { + "name": "golden_db-data", + "labels": { + "com.example.retain": "true" + } + } + } +} \ No newline at end of file diff --git a/loader/testdata/golden/web-stack.golden.yaml b/loader/testdata/golden/web-stack.golden.yaml new file mode 100644 index 000000000..d1758c91a --- /dev/null +++ b/loader/testdata/golden/web-stack.golden.yaml @@ -0,0 +1,154 @@ +name: golden +services: + cache: + configs: + - source: redis_conf + target: /etc/redis/redis.conf + image: redis:7 + networks: + default: null + db: + environment: + POSTGRES_DB: app + POSTGRES_PASSWORD_FILE: /run/secrets/db_password + image: postgres:16 + networks: + default: null + secrets: + - source: db_password + target: /run/secrets/db_password + mode: "0400" + volumes: + - type: volume + source: db-data + target: /var/lib/postgresql/data + volume: {} + healthcheck: + test: + - CMD-SHELL + - pg_isready -U postgres + interval: 10s + web: + profiles: + - frontend + deploy: + replicas: 2 + resources: + limits: + cpus: 0.5 + memory: "268435456" + reservations: + memory: "134217728" + restart_policy: + condition: on-failure + max_attempts: 3 + restart: unless-stopped + scale: 2 + post_start: + - command: + - ./warm-cache.sh + user: root + pre_stop: + - command: + - ./drain.sh + command: + - serve + - --port + - "8080" + entrypoint: + - /entrypoint.sh + environment: + DEBUG: null + MODE: production + image: example/web:1.2.3 + labels: + com.example.app: web + logging: + driver: json-file + options: + max-size: 10m + networks: + backend: null + frontend: + aliases: + - www + priority: 10 + volumes: + - type: bind + source: ./static + target: /usr/share/nginx/html + read_only: true + bind: {} + - type: volume + source: data + target: /data + volume: + nocopy: true + - type: tmpfs + target: /scratch + tmpfs: + size: "10485760" + build: + context: ./web + dockerfile: Dockerfile.prod + args: + TAG: 1.2.3 + labels: + com.example.stage: build + additional_contexts: + assets: ./assets + target: runtime + depends_on: + cache: + condition: service_started + required: true + db: + condition: service_healthy + required: true + expose: + - "9090" + healthcheck: + test: + - CMD + - curl + - -f + - http://localhost/health + timeout: 5s + interval: 30s + retries: 3 + start_period: 10s + ports: + - mode: ingress + target: 80 + published: "8080" + protocol: tcp + - mode: host + target: 443 + published: "8443" + protocol: tcp +networks: + backend: + name: golden_backend + driver: bridge + ipam: + config: + - subnet: 172.28.0.0/16 + default: + name: golden_default + frontend: + name: golden_frontend +volumes: + data: + name: golden_data + db-data: + name: golden_db-data + labels: + com.example.retain: "true" +secrets: + db_password: + name: golden_db_password + file: ./db_password.txt +configs: + redis_conf: + name: golden_redis_conf + file: ./redis.conf diff --git a/loader/testdata/golden/web-stack.yaml b/loader/testdata/golden/web-stack.yaml new file mode 100644 index 000000000..017e4ef84 --- /dev/null +++ b/loader/testdata/golden/web-stack.yaml @@ -0,0 +1,126 @@ +name: golden +services: + web: + build: + context: ./web + dockerfile: Dockerfile.prod + args: + TAG: ${GOLDEN_TAG} + UNSET_ARG: null + labels: + com.example.stage: build + target: runtime + additional_contexts: + assets: ./assets + image: example/web:${GOLDEN_TAG} + command: ["serve", "--port", "${GOLDEN_PORT}"] + entrypoint: /entrypoint.sh + environment: + - MODE=production + - DEBUG + ports: + - "${GOLDEN_PORT}:80" + - target: 443 + published: "8443" + protocol: tcp + mode: host + expose: + - "9090" + depends_on: + db: + condition: service_healthy + required: true + cache: + condition: service_started + networks: + frontend: + aliases: + - www + priority: 10 + backend: null + volumes: + - ./static:/usr/share/nginx/html:ro + - type: volume + source: data + target: /data + volume: + nocopy: true + - type: tmpfs + target: /scratch + tmpfs: + size: 10485760 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + deploy: + replicas: 2 + resources: + limits: + cpus: "0.50" + memory: 256M + reservations: + memory: 128M + restart_policy: + condition: on-failure + max_attempts: 3 + labels: + com.example.app: web + logging: + driver: json-file + options: + max-size: 10m + restart: unless-stopped + scale: 2 + profiles: + - frontend + post_start: + - command: ./warm-cache.sh + user: root + pre_stop: + - command: ./drain.sh + + db: + image: postgres:16 + environment: + POSTGRES_DB: app + POSTGRES_PASSWORD_FILE: /run/secrets/db_password + secrets: + - source: db_password + target: /run/secrets/db_password + mode: 0400 + volumes: + - db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + + cache: + image: redis:7 + configs: + - source: redis_conf + target: /etc/redis/redis.conf + +networks: + frontend: {} + backend: + driver: bridge + ipam: + config: + - subnet: 172.28.0.0/16 + +volumes: + data: {} + db-data: + labels: + com.example.retain: "true" + +secrets: + db_password: + file: ./db_password.txt + +configs: + redis_conf: + file: ./redis.conf diff --git a/loader/tests/annotations_test.go b/loader/tests/annotations_test.go index acf4e534b..bf8a58419 100644 --- a/loader/tests/annotations_test.go +++ b/loader/tests/annotations_test.go @@ -35,11 +35,26 @@ services: image: alpine annotations: com.example.foo: bar +jobs: + list: + triggers: + manual: true + image: alpine + annotations: + - com.example.foo=bar + map: + triggers: + manual: true + image: alpine + annotations: + com.example.foo: bar `) expect := func(p *types.Project) { expected := types.Mapping{"com.example.foo": "bar"} assert.DeepEqual(t, p.Services["list"].Annotations, expected) assert.DeepEqual(t, p.Services["map"].Annotations, expected) + assert.DeepEqual(t, p.Jobs["list"].Annotations, expected) + assert.DeepEqual(t, p.Jobs["map"].Annotations, expected) } expect(p) diff --git a/loader/tests/attach_test.go b/loader/tests/attach_test.go index 5e42ce6f4..34b2d77bd 100644 --- a/loader/tests/attach_test.go +++ b/loader/tests/attach_test.go @@ -35,6 +35,11 @@ services: attach: false default: image: alpine +jobs: + default: + triggers: + manual: true + image: alpine `) expect := func(p *types.Project) { @@ -48,3 +53,24 @@ services: expect(yamlP) expect(jsonP) } + +func TestJobRejectsWorkloadOnlyAttributes(t *testing.T) { + // run-to-completion jobs don't accept service-lifecycle attributes + for attr, yaml := range map[string]string{ + "attach": " attach: true", + "container_name": " container_name: x", + "links": " links: [db]", + "external_links": " external_links: [db]", + "post_start": " post_start:\n - command: echo done", + } { + err := loadErr(t, ` +name: test +jobs: + job: + triggers: + manual: true + image: alpine +`+yaml) + assert.ErrorContains(t, err, attr) + } +} diff --git a/loader/tests/blkio_config_test.go b/loader/tests/blkio_config_test.go index e6f427938..46fdbbdba 100644 --- a/loader/tests/blkio_config_test.go +++ b/loader/tests/blkio_config_test.go @@ -46,6 +46,28 @@ services: device_write_iops: - path: /dev/sda rate: 200 +jobs: + foo: + triggers: + manual: true + image: busybox + blkio_config: + weight: 300 + weight_device: + - path: /dev/sda + weight: 400 + device_read_bps: + - path: /dev/sda + rate: 1024k + device_write_bps: + - path: /dev/sda + rate: 1024 + device_read_iops: + - path: /dev/sda + rate: 100 + device_write_iops: + - path: /dev/sda + rate: 200 `) expect := func(p *types.Project) { bc := p.Services["foo"].BlkioConfig @@ -57,6 +79,15 @@ services: assert.Equal(t, bc.DeviceWriteBps[0].Rate, types.UnitBytes(1024)) assert.Equal(t, bc.DeviceReadIOps[0].Rate, types.UnitBytes(100)) assert.Equal(t, bc.DeviceWriteIOps[0].Rate, types.UnitBytes(200)) + jbc := p.Jobs["foo"].BlkioConfig + assert.Equal(t, jbc.Weight, uint16(300)) + assert.Equal(t, jbc.WeightDevice[0].Path, "/dev/sda") + assert.Equal(t, jbc.WeightDevice[0].Weight, uint16(400)) + assert.Equal(t, jbc.DeviceReadBps[0].Path, "/dev/sda") + assert.Equal(t, jbc.DeviceReadBps[0].Rate, types.UnitBytes(1024*1024)) + assert.Equal(t, jbc.DeviceWriteBps[0].Rate, types.UnitBytes(1024)) + assert.Equal(t, jbc.DeviceReadIOps[0].Rate, types.UnitBytes(100)) + assert.Equal(t, jbc.DeviceWriteIOps[0].Rate, types.UnitBytes(200)) } expect(p) diff --git a/loader/tests/build_extra_test.go b/loader/tests/build_extra_test.go index a39682418..184a86251 100644 --- a/loader/tests/build_extra_test.go +++ b/loader/tests/build_extra_test.go @@ -33,10 +33,20 @@ services: cache_to: - user/app:cache - type=local,dest=path/to/cache +jobs: + foo: + triggers: + manual: true + build: + context: . + cache_to: + - user/app:cache + - type=local,dest=path/to/cache `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["foo"].Build.CacheTo, types.StringList{"user/app:cache", "type=local,dest=path/to/cache"}) + assert.DeepEqual(t, p.Jobs["foo"].Build.CacheTo, types.StringList{"user/app:cache", "type=local,dest=path/to/cache"}) } expect(p) @@ -53,10 +63,18 @@ services: build: context: . no_cache: true +jobs: + foo: + triggers: + manual: true + build: + context: . + no_cache: true `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Build.NoCache, true) + assert.Equal(t, p.Jobs["foo"].Build.NoCache, true) } expect(p) @@ -73,10 +91,18 @@ services: build: context: . pull: true +jobs: + foo: + triggers: + manual: true + build: + context: . + pull: true `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Build.Pull, true) + assert.Equal(t, p.Jobs["foo"].Build.Pull, true) } expect(p) @@ -93,10 +119,18 @@ services: build: context: . shm_size: 128m +jobs: + foo: + triggers: + manual: true + build: + context: . + shm_size: 128m `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Build.ShmSize, types.UnitBytes(128*1024*1024)) + assert.Equal(t, p.Jobs["foo"].Build.ShmSize, types.UnitBytes(128*1024*1024)) } expect(p) @@ -113,10 +147,18 @@ services: build: context: . isolation: process +jobs: + foo: + triggers: + manual: true + build: + context: . + isolation: process `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Build.Isolation, "process") + assert.Equal(t, p.Jobs["foo"].Build.Isolation, "process") } expect(p) @@ -133,10 +175,18 @@ services: build: context: . privileged: true +jobs: + foo: + triggers: + manual: true + build: + context: . + privileged: true `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Build.Privileged, true) + assert.Equal(t, p.Jobs["foo"].Build.Privileged, true) } expect(p) @@ -155,8 +205,18 @@ services: entitlements: - network.host - security.insecure +jobs: + foo: + triggers: + manual: true + build: + context: . + entitlements: + - network.host + - security.insecure `) assert.DeepEqual(t, p.Services["foo"].Build.Entitlements, []string{"network.host", "security.insecure"}) + assert.DeepEqual(t, p.Jobs["foo"].Build.Entitlements, []string{"network.host", "security.insecure"}) } func TestBuildAttestations(t *testing.T) { @@ -168,9 +228,19 @@ services: context: . provenance: mode=max sbom: true +jobs: + foo: + triggers: + manual: true + build: + context: . + provenance: mode=max + sbom: true `) assert.Equal(t, p.Services["foo"].Build.Provenance, "mode=max") assert.Equal(t, p.Services["foo"].Build.SBOM, "true") + assert.Equal(t, p.Jobs["foo"].Build.Provenance, "mode=max") + assert.Equal(t, p.Jobs["foo"].Build.SBOM, "true") } func TestBuildNoCacheFilter(t *testing.T) { @@ -185,7 +255,22 @@ services: build: context: . no_cache_filter: [foo, bar] +jobs: + string: + triggers: + manual: true + build: + context: . + no_cache_filter: foo + list: + triggers: + manual: true + build: + context: . + no_cache_filter: [foo, bar] `) assert.DeepEqual(t, p.Services["string"].Build.NoCacheFilter, types.StringList{"foo"}) assert.DeepEqual(t, p.Services["list"].Build.NoCacheFilter, types.StringList{"foo", "bar"}) + assert.DeepEqual(t, p.Jobs["string"].Build.NoCacheFilter, types.StringList{"foo"}) + assert.DeepEqual(t, p.Jobs["list"].Build.NoCacheFilter, types.StringList{"foo", "bar"}) } diff --git a/loader/tests/build_test.go b/loader/tests/build_test.go index 34e072cde..0bbd7616f 100644 --- a/loader/tests/build_test.go +++ b/loader/tests/build_test.go @@ -47,6 +47,29 @@ services: platforms: - linux/amd64 - linux/arm64 +jobs: + foo: + triggers: + manual: true + build: + context: ./dir + dockerfile: Dockerfile + args: + foo: bar + target: foo + network: foo + cache_from: + - foo + - bar + labels: [FOO=BAR] + additional_contexts: + foo: ./bar + tags: + - foo:v1.0.0 + - docker.io/username/foo:my-other-tag + platforms: + - linux/amd64 + - linux/arm64 `) expect := func(p *types.Project) { @@ -59,6 +82,16 @@ services: assert.DeepEqual(t, b.Labels, types.Labels{"FOO": "BAR"}) assert.DeepEqual(t, b.Tags, types.StringList{"foo:v1.0.0", "docker.io/username/foo:my-other-tag"}) assert.DeepEqual(t, b.Platforms, types.StringList{"linux/amd64", "linux/arm64"}) + + jb := p.Jobs["foo"].Build + assert.Equal(t, jb.Dockerfile, "Dockerfile") + assert.DeepEqual(t, jb.Args, types.MappingWithEquals{"foo": ptr("bar")}) + assert.Equal(t, jb.Target, "foo") + assert.Equal(t, jb.Network, "foo") + assert.DeepEqual(t, jb.CacheFrom, types.StringList{"foo", "bar"}) + assert.DeepEqual(t, jb.Labels, types.Labels{"FOO": "BAR"}) + assert.DeepEqual(t, jb.Tags, types.StringList{"foo:v1.0.0", "docker.io/username/foo:my-other-tag"}) + assert.DeepEqual(t, jb.Platforms, types.StringList{"linux/amd64", "linux/arm64"}) } expect(p) @@ -76,10 +109,19 @@ services: dockerfile_inline: | FROM alpine RUN echo "hello" > /world.txt +jobs: + bar: + triggers: + manual: true + build: + dockerfile_inline: | + FROM alpine + RUN echo "hello" > /world.txt `) expect := func(p *types.Project) { assert.Equal(t, p.Services["bar"].Build.DockerfileInline, "FROM alpine\nRUN echo \"hello\" > /world.txt\n") + assert.Equal(t, p.Jobs["bar"].Build.DockerfileInline, "FROM alpine\nRUN echo \"hello\" > /world.txt\n") } expect(p) @@ -97,10 +139,19 @@ services: context: . ssh: - default +jobs: + foo: + triggers: + manual: true + build: + context: . + ssh: + - default `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["foo"].Build.SSH, types.SSHConfig{{ID: "default", Path: ""}}) + assert.DeepEqual(t, p.Jobs["foo"].Build.SSH, types.SSHConfig{{ID: "default", Path: ""}}) } expect(p) @@ -124,6 +175,20 @@ services: uid: '103' gid: '103' mode: 0440 +jobs: + foo: + triggers: + manual: true + build: + context: . + secrets: + - source: secret1 + target: /run/secrets/secret1 + - source: secret2 + target: my_secret + uid: '103' + gid: '103' + mode: 0440 secrets: secret1: file: ./secret_data @@ -135,4 +200,10 @@ secrets: assert.Equal(t, secrets[0].Source, "secret1") assert.Equal(t, secrets[1].UID, "103") assert.Equal(t, *secrets[1].Mode, types.FileMode(0o440)) + + jobSecrets := p.Jobs["foo"].Build.Secrets + assert.Equal(t, len(jobSecrets), 2) + assert.Equal(t, jobSecrets[0].Source, "secret1") + assert.Equal(t, jobSecrets[1].UID, "103") + assert.Equal(t, *jobSecrets[1].Mode, types.FileMode(0o440)) } diff --git a/loader/tests/cap_add_drop_test.go b/loader/tests/cap_add_drop_test.go index b841f3cc8..9ac1145e7 100644 --- a/loader/tests/cap_add_drop_test.go +++ b/loader/tests/cap_add_drop_test.go @@ -34,11 +34,23 @@ services: cap_drop: - NET_ADMIN - SYS_ADMIN +jobs: + foo: + triggers: + manual: true + image: alpine + cap_add: + - ALL + cap_drop: + - NET_ADMIN + - SYS_ADMIN `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["foo"].CapAdd, []string{"ALL"}) assert.DeepEqual(t, p.Services["foo"].CapDrop, []string{"NET_ADMIN", "SYS_ADMIN"}) + assert.DeepEqual(t, p.Jobs["foo"].CapAdd, []string{"ALL"}) + assert.DeepEqual(t, p.Jobs["foo"].CapDrop, []string{"NET_ADMIN", "SYS_ADMIN"}) } expect(p) diff --git a/loader/tests/cgroup_parent_test.go b/loader/tests/cgroup_parent_test.go index f6ec36b0f..92e5ab1df 100644 --- a/loader/tests/cgroup_parent_test.go +++ b/loader/tests/cgroup_parent_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine cgroup_parent: m-executor-abcd +jobs: + foo: + triggers: + manual: true + image: alpine + cgroup_parent: m-executor-abcd `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].CgroupParent, "m-executor-abcd") + assert.Equal(t, p.Jobs["foo"].CgroupParent, "m-executor-abcd") } expect(p) diff --git a/loader/tests/cgroup_test.go b/loader/tests/cgroup_test.go index 563d2537c..393fbab80 100644 --- a/loader/tests/cgroup_test.go +++ b/loader/tests/cgroup_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine cgroup: private +jobs: + foo: + triggers: + manual: true + image: alpine + cgroup: private `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Cgroup, "private") + assert.Equal(t, p.Jobs["foo"].Cgroup, "private") } expect(p) diff --git a/loader/tests/command_test.go b/loader/tests/command_test.go index faaef0d89..8871b541f 100644 --- a/loader/tests/command_test.go +++ b/loader/tests/command_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine command: bundle exec thin -p 3000 +jobs: + foo: + triggers: + manual: true + image: alpine + command: bundle exec thin -p 3000 `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["foo"].Command, types.ShellCommand{"bundle", "exec", "thin", "-p", "3000"}) + assert.DeepEqual(t, p.Jobs["foo"].Command, types.ShellCommand{"bundle", "exec", "thin", "-p", "3000"}) } expect(p) @@ -49,6 +56,13 @@ services: foo: image: alpine command: ["bundle", "exec", "thin", "-p", "3000"] +jobs: + foo: + triggers: + manual: true + image: alpine + command: ["bundle", "exec", "thin", "-p", "3000"] `) assert.DeepEqual(t, p.Services["foo"].Command, types.ShellCommand{"bundle", "exec", "thin", "-p", "3000"}) + assert.DeepEqual(t, p.Jobs["foo"].Command, types.ShellCommand{"bundle", "exec", "thin", "-p", "3000"}) } diff --git a/loader/tests/configs_test.go b/loader/tests/configs_test.go index c4aa120ca..411d1031a 100644 --- a/loader/tests/configs_test.go +++ b/loader/tests/configs_test.go @@ -36,6 +36,18 @@ services: uid: '103' gid: '103' mode: 0440 +jobs: + foo: + triggers: + manual: true + image: alpine + configs: + - config1 + - source: config2 + target: /my_config + uid: '103' + gid: '103' + mode: 0440 configs: config1: file: ./config_data @@ -50,6 +62,15 @@ configs: assert.Equal(t, configs[1].UID, "103") assert.Equal(t, configs[1].GID, "103") assert.Equal(t, *configs[1].Mode, types.FileMode(0o440)) + + jobConfigs := p.Jobs["foo"].Configs + assert.Equal(t, len(jobConfigs), 2) + assert.Equal(t, jobConfigs[0].Source, "config1") + assert.Equal(t, jobConfigs[1].Source, "config2") + assert.Equal(t, jobConfigs[1].Target, "/my_config") + assert.Equal(t, jobConfigs[1].UID, "103") + assert.Equal(t, jobConfigs[1].GID, "103") + assert.Equal(t, *jobConfigs[1].Mode, types.FileMode(0o440)) } func TestTopLevelConfigs(t *testing.T) { @@ -58,6 +79,11 @@ name: test services: foo: image: alpine +jobs: + foo: + triggers: + manual: true + image: alpine configs: config1: file: ./config_data diff --git a/loader/tests/cpu_test.go b/loader/tests/cpu_test.go index 00a20e7a4..70ec69628 100644 --- a/loader/tests/cpu_test.go +++ b/loader/tests/cpu_test.go @@ -38,6 +38,20 @@ services: cpu_rt_runtime: 950000 cpus: 1.5 cpuset: "0,1" +jobs: + foo: + triggers: + manual: true + image: alpine + cpu_count: 4 + cpu_percent: 50 + cpu_shares: 1024 + cpu_quota: 50000 + cpu_period: 100000 + cpu_rt_period: 1000000 + cpu_rt_runtime: 950000 + cpus: 1.5 + cpuset: "0,1" `) expect := func(p *types.Project) { s := p.Services["foo"] @@ -50,6 +64,16 @@ services: assert.Equal(t, s.CPURTRuntime, int64(950000)) assert.Equal(t, s.CPUS, float32(1.5)) assert.Equal(t, s.CPUSet, "0,1") + j := p.Jobs["foo"] + assert.Equal(t, j.CPUCount, int64(4)) + assert.Equal(t, j.CPUPercent, float32(50)) + assert.Equal(t, j.CPUShares, int64(1024)) + assert.Equal(t, j.CPUQuota, int64(50000)) + assert.Equal(t, j.CPUPeriod, int64(100000)) + assert.Equal(t, j.CPURTPeriod, int64(1000000)) + assert.Equal(t, j.CPURTRuntime, int64(950000)) + assert.Equal(t, j.CPUS, float32(1.5)) + assert.Equal(t, j.CPUSet, "0,1") } expect(p) @@ -69,6 +93,16 @@ services: mem_swappiness: 60 memswap_limit: 1g shm_size: 64m +jobs: + foo: + triggers: + manual: true + image: alpine + mem_limit: 512m + mem_reservation: 256m + mem_swappiness: 60 + memswap_limit: 1g + shm_size: 64m `) expect := func(p *types.Project) { s := p.Services["foo"] @@ -77,6 +111,12 @@ services: assert.Equal(t, s.MemSwappiness, types.UnitBytes(60)) assert.Equal(t, s.MemSwapLimit, types.UnitBytes(1024*1024*1024)) assert.Equal(t, s.ShmSize, types.UnitBytes(64*1024*1024)) + j := p.Jobs["foo"] + assert.Equal(t, j.MemLimit, types.UnitBytes(512*1024*1024)) + assert.Equal(t, j.MemReservation, types.UnitBytes(256*1024*1024)) + assert.Equal(t, j.MemSwappiness, types.UnitBytes(60)) + assert.Equal(t, j.MemSwapLimit, types.UnitBytes(1024*1024*1024)) + assert.Equal(t, j.ShmSize, types.UnitBytes(64*1024*1024)) } expect(p) diff --git a/loader/tests/credential_spec_test.go b/loader/tests/credential_spec_test.go index 866db186e..72a4fa06a 100644 --- a/loader/tests/credential_spec_test.go +++ b/loader/tests/credential_spec_test.go @@ -31,9 +31,17 @@ services: image: alpine credential_spec: config: "0bt9dmxjvjiqermk6xrop3ekq" +jobs: + foo: + triggers: + manual: true + image: alpine + credential_spec: + config: "0bt9dmxjvjiqermk6xrop3ekq" `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].CredentialSpec.Config, "0bt9dmxjvjiqermk6xrop3ekq") + assert.Equal(t, p.Jobs["foo"].CredentialSpec.Config, "0bt9dmxjvjiqermk6xrop3ekq") } expect(p) diff --git a/loader/tests/depends_on_test.go b/loader/tests/depends_on_test.go index fd272e6d9..ae0196c90 100644 --- a/loader/tests/depends_on_test.go +++ b/loader/tests/depends_on_test.go @@ -36,6 +36,22 @@ services: image: postgres redis: image: redis +jobs: + web: + triggers: + manual: true + image: alpine + depends_on: + - db + - redis + db: + triggers: + manual: true + image: postgres + redis: + triggers: + manual: true + image: redis `) expect := func(p *types.Project) { deps := p.Services["web"].DependsOn @@ -43,6 +59,12 @@ services: assert.Equal(t, deps["db"].Condition, types.ServiceConditionStarted) assert.Equal(t, deps["db"].Required, true) assert.Equal(t, deps["redis"].Condition, types.ServiceConditionStarted) + + jdeps := p.Jobs["web"].DependsOn + assert.Equal(t, len(jdeps), 2) + assert.Equal(t, jdeps["db"].Condition, types.ServiceConditionStarted) + assert.Equal(t, jdeps["db"].Required, true) + assert.Equal(t, jdeps["redis"].Condition, types.ServiceConditionStarted) } expect(p) @@ -67,12 +89,36 @@ services: image: postgres redis: image: redis +jobs: + web: + triggers: + manual: true + image: alpine + depends_on: + db: + condition: service_healthy + restart: true + redis: + condition: service_started + db: + triggers: + manual: true + image: postgres + redis: + triggers: + manual: true + image: redis `) expect := func(p *types.Project) { deps := p.Services["web"].DependsOn assert.Equal(t, deps["db"].Condition, "service_healthy") assert.Equal(t, deps["db"].Restart, true) assert.Equal(t, deps["redis"].Condition, types.ServiceConditionStarted) + + jdeps := p.Jobs["web"].DependsOn + assert.Equal(t, jdeps["db"].Condition, "service_healthy") + assert.Equal(t, jdeps["db"].Restart, true) + assert.Equal(t, jdeps["redis"].Condition, types.ServiceConditionStarted) } expect(p) @@ -91,6 +137,17 @@ services: - x-foo x-foo: image: foo +jobs: + test: + triggers: + manual: true + image: test + depends_on: + - foo + foo: + triggers: + manual: true + image: foo `) assert.DeepEqual(t, p.Services["test"].DependsOn, types.DependsOnConfig{ "x-foo": types.ServiceDependency{ @@ -98,4 +155,10 @@ services: Required: true, }, }) + assert.DeepEqual(t, p.Jobs["test"].DependsOn, types.DependsOnConfig{ + "foo": types.ServiceDependency{ + Condition: types.ServiceConditionStarted, + Required: true, + }, + }) } diff --git a/loader/tests/device_cgroup_rules_test.go b/loader/tests/device_cgroup_rules_test.go index 3f4376171..ad86a1154 100644 --- a/loader/tests/device_cgroup_rules_test.go +++ b/loader/tests/device_cgroup_rules_test.go @@ -32,10 +32,19 @@ services: device_cgroup_rules: - "c 1:3 mr" - "a 7:* rmw" +jobs: + foo: + triggers: + manual: true + image: alpine + device_cgroup_rules: + - "c 1:3 mr" + - "a 7:* rmw" `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["foo"].DeviceCgroupRules, []string{"c 1:3 mr", "a 7:* rmw"}) + assert.DeepEqual(t, p.Jobs["foo"].DeviceCgroupRules, []string{"c 1:3 mr", "a 7:* rmw"}) } expect(p) diff --git a/loader/tests/devices_test.go b/loader/tests/devices_test.go index db87ac165..a9fe42b3a 100644 --- a/loader/tests/devices_test.go +++ b/loader/tests/devices_test.go @@ -32,6 +32,14 @@ services: devices: - /dev/source:/dev/target:permissions - /dev/single +jobs: + test: + triggers: + manual: true + image: alpine + devices: + - /dev/source:/dev/target:permissions + - /dev/single `) expect := func(p *types.Project) { @@ -39,6 +47,10 @@ services: {Source: "/dev/source", Target: "/dev/target", Permissions: "permissions"}, {Source: "/dev/single", Target: "/dev/single", Permissions: "rwm"}, }) + assert.DeepEqual(t, p.Jobs["test"].Devices, []types.DeviceMapping{ + {Source: "/dev/source", Target: "/dev/target", Permissions: "permissions"}, + {Source: "/dev/single", Target: "/dev/single", Permissions: "rwm"}, + }) } expect(p) @@ -57,10 +69,22 @@ services: - source: /dev/source target: /dev/target permissions: permissions +jobs: + test: + triggers: + manual: true + image: alpine + devices: + - source: /dev/source + target: /dev/target + permissions: permissions `) assert.DeepEqual(t, p.Services["test"].Devices, []types.DeviceMapping{ {Source: "/dev/source", Target: "/dev/target", Permissions: "permissions"}, }) + assert.DeepEqual(t, p.Jobs["test"].Devices, []types.DeviceMapping{ + {Source: "/dev/source", Target: "/dev/target", Permissions: "permissions"}, + }) } func TestDeviceReservation(t *testing.T) { @@ -85,4 +109,5 @@ services: assert.DeepEqual(t, devs[0].Capabilities, []string{"gpu"}) assert.Equal(t, devs[0].Count, types.DeviceCount(-1)) assert.Equal(t, devs[0].Options["q_bits"], "42") + // Note: deploy is not part of ContainerSpec/JobConfig, so no jobs assertion here } diff --git a/loader/tests/dns_opt_test.go b/loader/tests/dns_opt_test.go index 78b5f8e07..09c9e7469 100644 --- a/loader/tests/dns_opt_test.go +++ b/loader/tests/dns_opt_test.go @@ -32,10 +32,19 @@ services: dns_opt: - use-vc - no-tld-query +jobs: + foo: + triggers: + manual: true + image: alpine + dns_opt: + - use-vc + - no-tld-query `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["foo"].DNSOpts, []string{"use-vc", "no-tld-query"}) + assert.DeepEqual(t, p.Jobs["foo"].DNSOpts, []string{"use-vc", "no-tld-query"}) } expect(p) diff --git a/loader/tests/dns_test.go b/loader/tests/dns_test.go index bfc726f71..66352b2ce 100644 --- a/loader/tests/dns_test.go +++ b/loader/tests/dns_test.go @@ -35,11 +35,26 @@ services: string: image: alpine dns: 8.8.8.8 +jobs: + list: + triggers: + manual: true + image: alpine + dns: + - 8.8.8.8 + - 9.9.9.9 + string: + triggers: + manual: true + image: alpine + dns: 8.8.8.8 `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["list"].DNS, types.StringList{"8.8.8.8", "9.9.9.9"}) assert.DeepEqual(t, p.Services["string"].DNS, types.StringList{"8.8.8.8"}) + assert.DeepEqual(t, p.Jobs["list"].DNS, types.StringList{"8.8.8.8", "9.9.9.9"}) + assert.DeepEqual(t, p.Jobs["string"].DNS, types.StringList{"8.8.8.8"}) } expect(p) @@ -60,11 +75,26 @@ services: string: image: alpine dns_search: example.com +jobs: + list: + triggers: + manual: true + image: alpine + dns_search: + - dc1.example.com + - dc2.example.com + string: + triggers: + manual: true + image: alpine + dns_search: example.com `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["list"].DNSSearch, types.StringList{"dc1.example.com", "dc2.example.com"}) assert.DeepEqual(t, p.Services["string"].DNSSearch, types.StringList{"example.com"}) + assert.DeepEqual(t, p.Jobs["list"].DNSSearch, types.StringList{"dc1.example.com", "dc2.example.com"}) + assert.DeepEqual(t, p.Jobs["string"].DNSSearch, types.StringList{"example.com"}) } expect(p) @@ -80,6 +110,13 @@ services: foo: image: alpine dns: ${UNSET_VAR} +jobs: + foo: + triggers: + manual: true + image: alpine + dns: ${UNSET_VAR} `) assert.Equal(t, len(p.Services["foo"].DNS), 0) + assert.Equal(t, len(p.Jobs["foo"].DNS), 0) } diff --git a/loader/tests/domainname_test.go b/loader/tests/domainname_test.go index 59f89b072..e46b06e5f 100644 --- a/loader/tests/domainname_test.go +++ b/loader/tests/domainname_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine domainname: foo.com +jobs: + foo: + triggers: + manual: true + image: alpine + domainname: foo.com `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].DomainName, "foo.com") + assert.Equal(t, p.Jobs["foo"].DomainName, "foo.com") } expect(p) diff --git a/loader/tests/entrypoint_test.go b/loader/tests/entrypoint_test.go index c21c6d98b..ac87c246e 100644 --- a/loader/tests/entrypoint_test.go +++ b/loader/tests/entrypoint_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine entrypoint: ["/code/entrypoint.sh", "-p", "3000"] +jobs: + foo: + triggers: + manual: true + image: alpine + entrypoint: ["/code/entrypoint.sh", "-p", "3000"] `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["foo"].Entrypoint, types.ShellCommand{"/code/entrypoint.sh", "-p", "3000"}) + assert.DeepEqual(t, p.Jobs["foo"].Entrypoint, types.ShellCommand{"/code/entrypoint.sh", "-p", "3000"}) } expect(p) @@ -49,6 +56,13 @@ services: foo: image: alpine entrypoint: /code/entrypoint.sh -p 3000 +jobs: + foo: + triggers: + manual: true + image: alpine + entrypoint: /code/entrypoint.sh -p 3000 `) assert.DeepEqual(t, p.Services["foo"].Entrypoint, types.ShellCommand{"/code/entrypoint.sh", "-p", "3000"}) + assert.DeepEqual(t, p.Jobs["foo"].Entrypoint, types.ShellCommand{"/code/entrypoint.sh", "-p", "3000"}) } diff --git a/loader/tests/env_file_test.go b/loader/tests/env_file_test.go index 42eee2cff..52269d0bf 100644 --- a/loader/tests/env_file_test.go +++ b/loader/tests/env_file_test.go @@ -32,9 +32,18 @@ services: env_file: - path: .env required: false +jobs: + foo: + triggers: + manual: true + image: alpine + env_file: + - path: .env + required: false `) expect := func(p *types.Project) { assert.Equal(t, len(p.Services["foo"].EnvFiles), 1) + assert.Equal(t, len(p.Jobs["foo"].EnvFiles), 1) } expect(p) @@ -54,9 +63,20 @@ services: required: false - path: .env.local required: false +jobs: + foo: + triggers: + manual: true + image: alpine + env_file: + - path: .env + required: false + - path: .env.local + required: false `) expect := func(p *types.Project) { assert.Equal(t, len(p.Services["foo"].EnvFiles), 2) + assert.Equal(t, len(p.Jobs["foo"].EnvFiles), 2) } expect(p) @@ -75,11 +95,24 @@ services: - path: .env required: false format: raw +jobs: + foo: + triggers: + manual: true + image: alpine + env_file: + - path: .env + required: false + format: raw `) expect := func(p *types.Project) { assert.Equal(t, len(p.Services["foo"].EnvFiles), 1) assert.Equal(t, p.Services["foo"].EnvFiles[0].Format, "raw") assert.Equal(t, bool(p.Services["foo"].EnvFiles[0].Required), false) + + assert.Equal(t, len(p.Jobs["foo"].EnvFiles), 1) + assert.Equal(t, p.Jobs["foo"].EnvFiles[0].Format, "raw") + assert.Equal(t, bool(p.Jobs["foo"].EnvFiles[0].Required), false) } expect(p) diff --git a/loader/tests/environment_test.go b/loader/tests/environment_test.go index 3169430b4..116788530 100644 --- a/loader/tests/environment_test.go +++ b/loader/tests/environment_test.go @@ -39,6 +39,16 @@ services: BU: "" ZO: MEU: +jobs: + foo: + triggers: + manual: true + image: alpine + environment: + FOO: "1" + BAR: 2 + GA: 2.5 + BU: "" `, map[string]string{"MEU": "Shadoks"}) expect := func(p *types.Project) { @@ -49,6 +59,11 @@ services: assert.Equal(t, *env["BU"], "") assert.Equal(t, *env["MEU"], "Shadoks") assert.Assert(t, env["ZO"] == nil) + jenv := p.Jobs["foo"].Environment + assert.Equal(t, *jenv["FOO"], "1") + assert.Equal(t, *jenv["BAR"], "2") + assert.Equal(t, *jenv["GA"], "2.5") + assert.Equal(t, *jenv["BU"], "") } expect(p) } @@ -65,6 +80,15 @@ services: - BU= - ZO - MEU +jobs: + foo: + triggers: + manual: true + image: alpine + environment: + - FOO=1 + - BAR=2 + - BU= `, map[string]string{"MEU": "Shadoks"}) expect := func(p *types.Project) { @@ -74,6 +98,10 @@ services: assert.Equal(t, *env["BU"], "") assert.Equal(t, *env["MEU"], "Shadoks") assert.Assert(t, env["ZO"] == nil) + jenv := p.Jobs["foo"].Environment + assert.Equal(t, *jenv["FOO"], "1") + assert.Equal(t, *jenv["BAR"], "2") + assert.Equal(t, *jenv["BU"], "") } expect(p) } @@ -87,9 +115,19 @@ services: environment: FOO: true BAR: false +jobs: + foo: + triggers: + manual: true + image: alpine + environment: + FOO: true + BAR: false `) assert.Equal(t, *p.Services["foo"].Environment["FOO"], "true") assert.Equal(t, *p.Services["foo"].Environment["BAR"], "false") + assert.Equal(t, *p.Jobs["foo"].Environment["FOO"], "true") + assert.Equal(t, *p.Jobs["foo"].Environment["BAR"], "false") } func TestEnvironmentInvalidValue(t *testing.T) { diff --git a/loader/tests/expose_test.go b/loader/tests/expose_test.go index 9e5e2089a..be911f51c 100644 --- a/loader/tests/expose_test.go +++ b/loader/tests/expose_test.go @@ -32,10 +32,19 @@ services: expose: - "3000" - 8000 +jobs: + foo: + triggers: + manual: true + image: alpine + expose: + - "3000" + - 8000 `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["foo"].Expose, types.StringOrNumberList{"3000", "8000"}) + assert.DeepEqual(t, p.Jobs["foo"].Expose, types.StringOrNumberList{"3000", "8000"}) } expect(p) diff --git a/loader/tests/extensions_test.go b/loader/tests/extensions_test.go index e2090876d..25094ed11 100644 --- a/loader/tests/extensions_test.go +++ b/loader/tests/extensions_test.go @@ -57,10 +57,19 @@ services: image: alpine x-bar: baz x-foo: bar +jobs: + foo: + triggers: + manual: true + image: alpine + x-bar: baz + x-foo: bar `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Extensions["x-bar"], "baz") assert.Equal(t, p.Services["foo"].Extensions["x-foo"], "bar") + assert.Equal(t, p.Jobs["foo"].Extensions["x-bar"], "baz") + assert.Equal(t, p.Jobs["foo"].Extensions["x-foo"], "bar") } expect(p) diff --git a/loader/tests/extra_hosts_test.go b/loader/tests/extra_hosts_test.go index 1e114c147..6d1960b9e 100644 --- a/loader/tests/extra_hosts_test.go +++ b/loader/tests/extra_hosts_test.go @@ -32,12 +32,24 @@ services: extra_hosts: alpha: "50.31.209.229" zulu: "162.242.195.82" +jobs: + foo: + triggers: + manual: true + image: alpine + extra_hosts: + alpha: "50.31.209.229" + zulu: "162.242.195.82" `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["foo"].ExtraHosts, types.HostsList{ "alpha": []string{"50.31.209.229"}, "zulu": []string{"162.242.195.82"}, }) + assert.DeepEqual(t, p.Jobs["foo"].ExtraHosts, types.HostsList{ + "alpha": []string{"50.31.209.229"}, + "zulu": []string{"162.242.195.82"}, + }) } expect(p) @@ -56,11 +68,24 @@ services: - "alpha:50.31.209.229" - "zulu:127.0.0.2" - "zulu:ff02::1" +jobs: + foo: + triggers: + manual: true + image: alpine + extra_hosts: + - "alpha:50.31.209.229" + - "zulu:127.0.0.2" + - "zulu:ff02::1" `) assert.DeepEqual(t, p.Services["foo"].ExtraHosts, types.HostsList{ "alpha": []string{"50.31.209.229"}, "zulu": []string{"127.0.0.2", "ff02::1"}, }) + assert.DeepEqual(t, p.Jobs["foo"].ExtraHosts, types.HostsList{ + "alpha": []string{"50.31.209.229"}, + "zulu": []string{"127.0.0.2", "ff02::1"}, + }) } func TestExtraHostsRepeated(t *testing.T) { @@ -71,10 +96,20 @@ services: image: alpine extra_hosts: - "myhost=0.0.0.1,0.0.0.2" +jobs: + foo: + triggers: + manual: true + image: alpine + extra_hosts: + - "myhost=0.0.0.1,0.0.0.2" `) assert.DeepEqual(t, p.Services["foo"].ExtraHosts, types.HostsList{ "myhost": []string{"0.0.0.1", "0.0.0.2"}, }) + assert.DeepEqual(t, p.Jobs["foo"].ExtraHosts, types.HostsList{ + "myhost": []string{"0.0.0.1", "0.0.0.2"}, + }) } func TestExtraHostsLongSyntax(t *testing.T) { @@ -87,8 +122,20 @@ services: myhost: - "0.0.0.1" - "0.0.0.2" +jobs: + foo: + triggers: + manual: true + image: alpine + extra_hosts: + myhost: + - "0.0.0.1" + - "0.0.0.2" `) assert.DeepEqual(t, p.Services["foo"].ExtraHosts, types.HostsList{ "myhost": []string{"0.0.0.1", "0.0.0.2"}, }) + assert.DeepEqual(t, p.Jobs["foo"].ExtraHosts, types.HostsList{ + "myhost": []string{"0.0.0.1", "0.0.0.2"}, + }) } diff --git a/loader/tests/gpus_test.go b/loader/tests/gpus_test.go index 5c9d9e4e1..a7e4d7714 100644 --- a/loader/tests/gpus_test.go +++ b/loader/tests/gpus_test.go @@ -34,11 +34,25 @@ services: - driver: 3dfx device_ids: ["voodoo2"] capabilities: ["directX"] +jobs: + test: + triggers: + manual: true + image: alpine + gpus: + - driver: nvidia + - driver: 3dfx + device_ids: ["voodoo2"] + capabilities: ["directX"] `) assert.DeepEqual(t, p.Services["test"].Gpus, []types.DeviceRequest{ {Driver: "nvidia", Count: -1}, {Capabilities: []string{"directX"}, Driver: "3dfx", IDs: []string{"voodoo2"}}, }) + assert.DeepEqual(t, p.Jobs["test"].Gpus, []types.DeviceRequest{ + {Driver: "nvidia", Count: -1}, + {Capabilities: []string{"directX"}, Driver: "3dfx", IDs: []string{"voodoo2"}}, + }) } func TestGpusAll(t *testing.T) { @@ -48,7 +62,15 @@ services: test: image: alpine gpus: all +jobs: + test: + triggers: + manual: true + image: alpine + gpus: all `) assert.Equal(t, len(p.Services["test"].Gpus), 1) assert.Equal(t, p.Services["test"].Gpus[0].Count, types.DeviceCount(-1)) + assert.Equal(t, len(p.Jobs["test"].Gpus), 1) + assert.Equal(t, p.Jobs["test"].Gpus[0].Count, types.DeviceCount(-1)) } diff --git a/loader/tests/group_add_test.go b/loader/tests/group_add_test.go index 326ec0a16..8410fc6fc 100644 --- a/loader/tests/group_add_test.go +++ b/loader/tests/group_add_test.go @@ -32,10 +32,19 @@ services: group_add: - mail - "0" +jobs: + foo: + triggers: + manual: true + image: alpine + group_add: + - mail + - "0" `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["foo"].GroupAdd, []string{"mail", "0"}) + assert.DeepEqual(t, p.Jobs["foo"].GroupAdd, []string{"mail", "0"}) } expect(p) diff --git a/loader/tests/healthcheck_test.go b/loader/tests/healthcheck_test.go index 5a3f806e6..219c555cf 100644 --- a/loader/tests/healthcheck_test.go +++ b/loader/tests/healthcheck_test.go @@ -37,6 +37,18 @@ services: retries: 5 start_period: 15s start_interval: 5s +jobs: + foo: + triggers: + manual: true + image: alpine + healthcheck: + test: echo "hello world" + interval: 10s + timeout: 1s + retries: 5 + start_period: 15s + start_interval: 5s `) expect := func(p *types.Project) { hc := p.Services["foo"].HealthCheck @@ -46,6 +58,13 @@ services: assert.Equal(t, *hc.Retries, uint64(5)) assert.Equal(t, *hc.StartPeriod, types.Duration(15*time.Second)) assert.Equal(t, *hc.StartInterval, types.Duration(5*time.Second)) + jhc := p.Jobs["foo"].HealthCheck + assert.DeepEqual(t, jhc.Test, types.HealthCheckTest{"CMD-SHELL", `echo "hello world"`}) + assert.Equal(t, *jhc.Interval, types.Duration(10*time.Second)) + assert.Equal(t, *jhc.Timeout, types.Duration(1*time.Second)) + assert.Equal(t, *jhc.Retries, uint64(5)) + assert.Equal(t, *jhc.StartPeriod, types.Duration(15*time.Second)) + assert.Equal(t, *jhc.StartInterval, types.Duration(5*time.Second)) } expect(p) diff --git a/loader/tests/helpers_test.go b/loader/tests/helpers_test.go index f7ff05d3a..c22600acd 100644 --- a/loader/tests/helpers_test.go +++ b/loader/tests/helpers_test.go @@ -85,3 +85,17 @@ func roundTrip(t *testing.T, p *types.Project) (fromYAML, fromJSON *types.Projec func ptr[T any](t T) *T { return &t } + +// loadErr loads a project expected to be rejected and returns the error. +func loadErr(t *testing.T, yaml string) error { + t.Helper() + _, err := loader.LoadWithContext(context.TODO(), types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{{Filename: "compose.yml", Content: []byte(yaml)}}, + Environment: map[string]string{}, + }, func(options *loader.Options) { + options.SkipConsistencyCheck = true + options.SkipNormalization = true + }) + assert.Assert(t, err != nil, "expected loading to fail") + return err +} diff --git a/loader/tests/hostname_test.go b/loader/tests/hostname_test.go index c25af05db..7002e6021 100644 --- a/loader/tests/hostname_test.go +++ b/loader/tests/hostname_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine hostname: foo +jobs: + foo: + triggers: + manual: true + image: alpine + hostname: foo `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Hostname, "foo") + assert.Equal(t, p.Jobs["foo"].Hostname, "foo") } expect(p) diff --git a/loader/tests/image_test.go b/loader/tests/image_test.go index 25bfcb7f6..6cc4a05f1 100644 --- a/loader/tests/image_test.go +++ b/loader/tests/image_test.go @@ -29,10 +29,16 @@ name: test services: foo: image: redis +jobs: + foo: + triggers: + manual: true + image: redis `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Image, "redis") + assert.Equal(t, p.Jobs["foo"].Image, "redis") } expect(p) diff --git a/loader/tests/init_test.go b/loader/tests/init_test.go index 8c25de427..e963139a7 100644 --- a/loader/tests/init_test.go +++ b/loader/tests/init_test.go @@ -35,12 +35,30 @@ services: init: false default: image: alpine +jobs: + with-init: + triggers: + manual: true + image: alpine + init: true + without-init: + triggers: + manual: true + image: alpine + init: false + default: + triggers: + manual: true + image: alpine `) expect := func(p *types.Project) { assert.Equal(t, *p.Services["with-init"].Init, true) assert.Equal(t, *p.Services["without-init"].Init, false) assert.Assert(t, p.Services["default"].Init == nil) + assert.Equal(t, *p.Jobs["with-init"].Init, true) + assert.Equal(t, *p.Jobs["without-init"].Init, false) + assert.Assert(t, p.Jobs["default"].Init == nil) } expect(p) diff --git a/loader/tests/ipc_uts_pid_test.go b/loader/tests/ipc_uts_pid_test.go index 58de3b366..1389ec07c 100644 --- a/loader/tests/ipc_uts_pid_test.go +++ b/loader/tests/ipc_uts_pid_test.go @@ -32,12 +32,23 @@ services: ipc: host uts: host pid: host +jobs: + foo: + triggers: + manual: true + image: alpine + ipc: host + uts: host + pid: host `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Ipc, "host") assert.Equal(t, p.Services["foo"].Uts, "host") assert.Equal(t, p.Services["foo"].Pid, "host") + assert.Equal(t, p.Jobs["foo"].Ipc, "host") + assert.Equal(t, p.Jobs["foo"].Uts, "host") + assert.Equal(t, p.Jobs["foo"].Pid, "host") } expect(p) diff --git a/loader/tests/isolation_test.go b/loader/tests/isolation_test.go index 486078edc..f6a360461 100644 --- a/loader/tests/isolation_test.go +++ b/loader/tests/isolation_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine isolation: process +jobs: + foo: + triggers: + manual: true + image: alpine + isolation: process `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Isolation, "process") + assert.Equal(t, p.Jobs["foo"].Isolation, "process") } expect(p) diff --git a/loader/tests/labels_test.go b/loader/tests/labels_test.go index 9dff8b964..d661b3c8a 100644 --- a/loader/tests/labels_test.go +++ b/loader/tests/labels_test.go @@ -33,6 +33,15 @@ services: com.example.description: "Accounting webapp" com.example.number: 42 com.example.empty-label: +jobs: + foo: + triggers: + manual: true + image: alpine + labels: + com.example.description: "Accounting webapp" + com.example.number: 42 + com.example.empty-label: `) expect := func(p *types.Project) { expected := types.Labels{ @@ -41,6 +50,7 @@ services: "com.example.empty-label": "", } assert.DeepEqual(t, p.Services["foo"].Labels, expected) + assert.DeepEqual(t, p.Jobs["foo"].Labels, expected) } expect(p) @@ -59,6 +69,15 @@ services: - "com.example.description=Accounting webapp" - "com.example.number=42" - "com.example.empty-label" +jobs: + foo: + triggers: + manual: true + image: alpine + labels: + - "com.example.description=Accounting webapp" + - "com.example.number=42" + - "com.example.empty-label" `) expected := types.Labels{ "com.example.description": "Accounting webapp", @@ -66,4 +85,5 @@ services: "com.example.empty-label": "", } assert.DeepEqual(t, p.Services["foo"].Labels, expected) + assert.DeepEqual(t, p.Jobs["foo"].Labels, expected) } diff --git a/loader/tests/logging_test.go b/loader/tests/logging_test.go index dd2d516f7..b0191a6ee 100644 --- a/loader/tests/logging_test.go +++ b/loader/tests/logging_test.go @@ -33,6 +33,15 @@ services: driver: syslog options: syslog-address: "tcp://192.168.0.42:123" +jobs: + foo: + triggers: + manual: true + image: alpine + logging: + driver: syslog + options: + syslog-address: "tcp://192.168.0.42:123" `) expect := func(p *types.Project) { expected := &types.LoggingConfig{ @@ -40,6 +49,7 @@ services: Options: map[string]string{"syslog-address": "tcp://192.168.0.42:123"}, } assert.DeepEqual(t, p.Services["foo"].Logging, expected) + assert.DeepEqual(t, p.Jobs["foo"].Logging, expected) } expect(p) diff --git a/loader/tests/mac_address_test.go b/loader/tests/mac_address_test.go index 8c582fd9b..44d107b89 100644 --- a/loader/tests/mac_address_test.go +++ b/loader/tests/mac_address_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine mac_address: "02:42:ac:11:65:43" +jobs: + foo: + triggers: + manual: true + image: alpine + mac_address: "02:42:ac:11:65:43" `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].MacAddress, "02:42:ac:11:65:43") + assert.Equal(t, p.Jobs["foo"].MacAddress, "02:42:ac:11:65:43") } expect(p) diff --git a/loader/tests/models_test.go b/loader/tests/models_test.go index 173a17732..9a624e821 100644 --- a/loader/tests/models_test.go +++ b/loader/tests/models_test.go @@ -35,6 +35,19 @@ services: foo: endpoint_var: MODEL_URL model_var: MODEL +jobs: + test_array: + triggers: + manual: true + models: + - foo + test_mapping: + triggers: + manual: true + models: + foo: + endpoint_var: MODEL_URL + model_var: MODEL models: foo: model: ai/model @@ -50,4 +63,8 @@ models: assert.Assert(t, p.Services["test_array"].Models["foo"] == nil) assert.Equal(t, p.Services["test_mapping"].Models["foo"].EndpointVariable, "MODEL_URL") assert.Equal(t, p.Services["test_mapping"].Models["foo"].ModelVariable, "MODEL") + + assert.Assert(t, p.Jobs["test_array"].Models["foo"] == nil) + assert.Equal(t, p.Jobs["test_mapping"].Models["foo"].EndpointVariable, "MODEL_URL") + assert.Equal(t, p.Jobs["test_mapping"].Models["foo"].ModelVariable, "MODEL") } diff --git a/loader/tests/network_mode_test.go b/loader/tests/network_mode_test.go index 3e056ce0d..100791108 100644 --- a/loader/tests/network_mode_test.go +++ b/loader/tests/network_mode_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine network_mode: "container:0cfeab0f748b" +jobs: + foo: + triggers: + manual: true + image: alpine + network_mode: "container:0cfeab0f748b" `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].NetworkMode, "container:0cfeab0f748b") + assert.Equal(t, p.Jobs["foo"].NetworkMode, "container:0cfeab0f748b") } expect(p) diff --git a/loader/tests/networks_test.go b/loader/tests/networks_test.go index 8342e4b74..7d15ef4c7 100644 --- a/loader/tests/networks_test.go +++ b/loader/tests/networks_test.go @@ -39,6 +39,21 @@ services: ipv6_address: "2001:3984:3989::10" mac_address: "02:42:72:98:65:08" other-other-network: +jobs: + foo: + triggers: + manual: true + image: alpine + networks: + some-network: + aliases: + - alias1 + - alias3 + other-network: + ipv4_address: 172.16.238.10 + ipv6_address: "2001:3984:3989::10" + mac_address: "02:42:72:98:65:08" + other-other-network: networks: some-network: other-network: @@ -50,6 +65,13 @@ networks: assert.Equal(t, nets["other-network"].Ipv6Address, "2001:3984:3989::10") assert.Equal(t, nets["other-network"].MacAddress, "02:42:72:98:65:08") assert.Assert(t, nets["other-other-network"] == nil) + + jobNets := p.Jobs["foo"].Networks + assert.DeepEqual(t, jobNets["some-network"].Aliases, []string{"alias1", "alias3"}) + assert.Equal(t, jobNets["other-network"].Ipv4Address, "172.16.238.10") + assert.Equal(t, jobNets["other-network"].Ipv6Address, "2001:3984:3989::10") + assert.Equal(t, jobNets["other-network"].MacAddress, "02:42:72:98:65:08") + assert.Assert(t, jobNets["other-other-network"] == nil) } func TestTopLevelNetworks(t *testing.T) { diff --git a/loader/tests/oom_test.go b/loader/tests/oom_test.go index 9b91b2d7c..74a2dc7f0 100644 --- a/loader/tests/oom_test.go +++ b/loader/tests/oom_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine oom_kill_disable: true +jobs: + foo: + triggers: + manual: true + image: alpine + oom_kill_disable: true `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].OomKillDisable, true) + assert.Equal(t, p.Jobs["foo"].OomKillDisable, true) } expect(p) @@ -49,10 +56,17 @@ services: foo: image: alpine oom_score_adj: 500 +jobs: + foo: + triggers: + manual: true + image: alpine + oom_score_adj: 500 `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].OomScoreAdj, int64(500)) + assert.Equal(t, p.Jobs["foo"].OomScoreAdj, int64(500)) } expect(p) diff --git a/loader/tests/pids_limit_test.go b/loader/tests/pids_limit_test.go index 310240115..720f1b0f0 100644 --- a/loader/tests/pids_limit_test.go +++ b/loader/tests/pids_limit_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine pids_limit: 100 +jobs: + foo: + triggers: + manual: true + image: alpine + pids_limit: 100 `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].PidsLimit, int64(100)) + assert.Equal(t, p.Jobs["foo"].PidsLimit, int64(100)) } expect(p) diff --git a/loader/tests/platform_test.go b/loader/tests/platform_test.go index d04a05ede..449c0dbcc 100644 --- a/loader/tests/platform_test.go +++ b/loader/tests/platform_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine platform: linux/amd64 +jobs: + foo: + triggers: + manual: true + image: alpine + platform: linux/amd64 `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Platform, "linux/amd64") + assert.Equal(t, p.Jobs["foo"].Platform, "linux/amd64") } expect(p) diff --git a/loader/tests/ports_test.go b/loader/tests/ports_test.go index ec045af5a..e1907d1cd 100644 --- a/loader/tests/ports_test.go +++ b/loader/tests/ports_test.go @@ -33,6 +33,15 @@ services: - "80:8080" - "90:8090/udp" - 8600 +jobs: + foo: + triggers: + manual: true + image: alpine + ports: + - "80:8080" + - "90:8090/udp" + - 8600 `) expect := func(p *types.Project) { ports := p.Services["foo"].Ports @@ -42,6 +51,14 @@ services: assert.Equal(t, ports[1].Target, uint32(8090)) assert.Equal(t, ports[1].Protocol, "udp") assert.Equal(t, ports[2].Target, uint32(8600)) + + jobPorts := p.Jobs["foo"].Ports + assert.Equal(t, len(jobPorts), 3) + assert.Equal(t, jobPorts[0].Target, uint32(8080)) + assert.Equal(t, jobPorts[0].Published, "80") + assert.Equal(t, jobPorts[1].Target, uint32(8090)) + assert.Equal(t, jobPorts[1].Protocol, "udp") + assert.Equal(t, jobPorts[2].Target, uint32(8600)) } expect(p) @@ -61,6 +78,16 @@ services: published: 8080 protocol: tcp mode: host +jobs: + foo: + triggers: + manual: true + image: alpine + ports: + - target: 80 + published: 8080 + protocol: tcp + mode: host `) expect := func(p *types.Project) { ports := p.Services["foo"].Ports @@ -69,6 +96,13 @@ services: assert.Equal(t, ports[0].Published, "8080") assert.Equal(t, ports[0].Protocol, "tcp") assert.Equal(t, ports[0].Mode, "host") + + jobPorts := p.Jobs["foo"].Ports + assert.Equal(t, len(jobPorts), 1) + assert.Equal(t, jobPorts[0].Target, uint32(80)) + assert.Equal(t, jobPorts[0].Published, "8080") + assert.Equal(t, jobPorts[0].Protocol, "tcp") + assert.Equal(t, jobPorts[0].Mode, "host") } expect(p) @@ -85,6 +119,13 @@ services: image: alpine ports: - "80-82:8080-8082" +jobs: + foo: + triggers: + manual: true + image: alpine + ports: + - "80-82:8080-8082" `) ports := p.Services["foo"].Ports assert.Equal(t, len(ports), 3) @@ -94,6 +135,15 @@ services: assert.Equal(t, ports[1].Published, "81") assert.Equal(t, ports[2].Target, uint32(8082)) assert.Equal(t, ports[2].Published, "82") + + jobPorts := p.Jobs["foo"].Ports + assert.Equal(t, len(jobPorts), 3) + assert.Equal(t, jobPorts[0].Target, uint32(8080)) + assert.Equal(t, jobPorts[0].Published, "80") + assert.Equal(t, jobPorts[1].Target, uint32(8081)) + assert.Equal(t, jobPorts[1].Published, "81") + assert.Equal(t, jobPorts[2].Target, uint32(8082)) + assert.Equal(t, jobPorts[2].Published, "82") } func TestNamedPort(t *testing.T) { @@ -106,8 +156,18 @@ services: - name: http published: 8080 target: 80 +jobs: + foo: + triggers: + manual: true + image: alpine + ports: + - name: http + published: 8080 + target: 80 `) assert.Equal(t, p.Services["foo"].Ports[0].Name, "http") + assert.Equal(t, p.Jobs["foo"].Ports[0].Name, "http") } func TestAppProtocol(t *testing.T) { @@ -121,6 +181,17 @@ services: target: 80 protocol: tcp app_protocol: http +jobs: + foo: + triggers: + manual: true + image: alpine + ports: + - published: 8080 + target: 80 + protocol: tcp + app_protocol: http `) assert.Equal(t, p.Services["foo"].Ports[0].AppProtocol, "http") + assert.Equal(t, p.Jobs["foo"].Ports[0].AppProtocol, "http") } diff --git a/loader/tests/privileged_test.go b/loader/tests/privileged_test.go index 61917d7c2..1981df30e 100644 --- a/loader/tests/privileged_test.go +++ b/loader/tests/privileged_test.go @@ -31,11 +31,20 @@ services: image: alpine privileged: true read_only: true +jobs: + foo: + triggers: + manual: true + image: alpine + privileged: true + read_only: true `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Privileged, true) assert.Equal(t, p.Services["foo"].ReadOnly, true) + assert.Equal(t, p.Jobs["foo"].Privileged, true) + assert.Equal(t, p.Jobs["foo"].ReadOnly, true) } expect(p) diff --git a/loader/tests/pull_policy_test.go b/loader/tests/pull_policy_test.go index f5a117288..6eda8a7e1 100644 --- a/loader/tests/pull_policy_test.go +++ b/loader/tests/pull_policy_test.go @@ -31,9 +31,16 @@ services: foo: image: alpine pull_policy: always +jobs: + foo: + triggers: + manual: true + image: alpine + pull_policy: always `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].PullPolicy, "always") + assert.Equal(t, p.Jobs["foo"].PullPolicy, "always") } expect(p) @@ -49,9 +56,16 @@ services: test: image: alpine pull_policy: every_2d +jobs: + test: + triggers: + manual: true + image: alpine + pull_policy: every_2d `) policy, duration, err := p.Services["test"].GetPullPolicy() assert.NilError(t, err) assert.Equal(t, policy, types.PullPolicyRefresh) assert.Equal(t, duration, 2*24*time.Hour) + assert.Equal(t, p.Jobs["test"].PullPolicy, "every_2d") } diff --git a/loader/tests/restart_test.go b/loader/tests/restart_test.go index ba4541d2c..7ba93195c 100644 --- a/loader/tests/restart_test.go +++ b/loader/tests/restart_test.go @@ -17,8 +17,10 @@ package tests import ( + "context" "testing" + "github.com/compose-spec/compose-go/v2/loader" "github.com/compose-spec/compose-go/v2/types" "gotest.tools/v3/assert" ) @@ -41,3 +43,20 @@ services: expect(yamlP) expect(jsonP) } + +func TestRestartNotAllowedOnJob(t *testing.T) { + // jobs run to completion: a restart policy is a service-only attribute + _, err := loader.LoadWithContext(context.TODO(), types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{{Filename: "compose.yml", Content: []byte(` +name: test +jobs: + foo: + image: alpine + restart: always + triggers: + manual: true +`)}}, + Environment: map[string]string{}, + }) + assert.ErrorContains(t, err, "restart") +} diff --git a/loader/tests/runtime_test.go b/loader/tests/runtime_test.go index 815e4676e..7e9c802d2 100644 --- a/loader/tests/runtime_test.go +++ b/loader/tests/runtime_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine runtime: nvidia +jobs: + foo: + triggers: + manual: true + image: alpine + runtime: nvidia `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Runtime, "nvidia") + assert.Equal(t, p.Jobs["foo"].Runtime, "nvidia") } expect(p) diff --git a/loader/tests/secrets_test.go b/loader/tests/secrets_test.go index 20a40acb8..bf7981176 100644 --- a/loader/tests/secrets_test.go +++ b/loader/tests/secrets_test.go @@ -37,6 +37,19 @@ services: uid: '103' gid: '103' mode: 0440 +jobs: + foo: + triggers: + manual: true + image: alpine + secrets: + - source: secret1 + target: /run/secrets/secret1 + - source: secret2 + target: my_secret + uid: '103' + gid: '103' + mode: 0440 secrets: secret1: file: ./secret_data @@ -52,6 +65,16 @@ secrets: assert.Equal(t, secrets[1].UID, "103") assert.Equal(t, secrets[1].GID, "103") assert.Equal(t, *secrets[1].Mode, types.FileMode(0o440)) + + jobSecrets := p.Jobs["foo"].Secrets + assert.Equal(t, len(jobSecrets), 2) + assert.Equal(t, jobSecrets[0].Source, "secret1") + assert.Equal(t, jobSecrets[0].Target, "/run/secrets/secret1") + assert.Equal(t, jobSecrets[1].Source, "secret2") + assert.Equal(t, jobSecrets[1].Target, "my_secret") + assert.Equal(t, jobSecrets[1].UID, "103") + assert.Equal(t, jobSecrets[1].GID, "103") + assert.Equal(t, *jobSecrets[1].Mode, types.FileMode(0o440)) } func TestTopLevelSecrets(t *testing.T) { @@ -123,9 +146,20 @@ services: - source: server-certificate target: server.cert mode: 0o440 +jobs: + foo: + triggers: + manual: true + image: alpine + secrets: + - source: server-certificate + target: server.cert + mode: 0o440 `) assert.Equal(t, len(p.Services["foo"].Secrets), 1) assert.Equal(t, *p.Services["foo"].Secrets[0].Mode, types.FileMode(0o440)) + assert.Equal(t, len(p.Jobs["foo"].Secrets), 1) + assert.Equal(t, *p.Jobs["foo"].Secrets[0].Mode, types.FileMode(0o440)) } func TestSecretFileModeString(t *testing.T) { @@ -138,7 +172,18 @@ services: - source: server-certificate target: server.cert mode: "0440" +jobs: + foo: + triggers: + manual: true + image: alpine + secrets: + - source: server-certificate + target: server.cert + mode: "0440" `) assert.Equal(t, len(p.Services["foo"].Secrets), 1) assert.Equal(t, *p.Services["foo"].Secrets[0].Mode, types.FileMode(0o440)) + assert.Equal(t, len(p.Jobs["foo"].Secrets), 1) + assert.Equal(t, *p.Jobs["foo"].Secrets[0].Mode, types.FileMode(0o440)) } diff --git a/loader/tests/security_opt_test.go b/loader/tests/security_opt_test.go index 6373608eb..7d6536f8d 100644 --- a/loader/tests/security_opt_test.go +++ b/loader/tests/security_opt_test.go @@ -32,11 +32,20 @@ services: security_opt: - label=level:s0:c100,c200 - label=type:svirt_apache_t +jobs: + foo: + triggers: + manual: true + image: alpine + security_opt: + - label=level:s0:c100,c200 + - label=type:svirt_apache_t `) expect := func(p *types.Project) { expected := []string{"label=level:s0:c100,c200", "label=type:svirt_apache_t"} assert.DeepEqual(t, p.Services["foo"].SecurityOpt, expected) + assert.DeepEqual(t, p.Jobs["foo"].SecurityOpt, expected) } expect(p) diff --git a/loader/tests/service_hooks_test.go b/loader/tests/service_hooks_test.go index e726bce07..cf3088424 100644 --- a/loader/tests/service_hooks_test.go +++ b/loader/tests/service_hooks_test.go @@ -56,23 +56,29 @@ services: environment: FOO: BAR `) - assert.DeepEqual(t, p.Services["test"].PreStart, []types.ServiceHook{ + assert.DeepEqual(t, p.Services["test"].PreStart, []types.PreStartHook{ { - Command: types.ShellCommand{"./manage.py", "migrate"}, - User: "root", - WorkingDir: "/app", - Environment: types.MappingWithEquals{ - "FOO": ptr("BAR"), + ContainerSpec: types.ContainerSpec{ + Command: types.ShellCommand{"./manage.py", "migrate"}, + User: "root", + WorkingDir: "/app", + Environment: types.MappingWithEquals{ + "FOO": ptr("BAR"), + }, }, }, { - Image: "busybox", - Command: types.ShellCommand{"sh", "-c", "chown -R 1000:1000 /data"}, - Privileged: true, + ContainerSpec: types.ContainerSpec{ + Image: "busybox", + Command: types.ShellCommand{"sh", "-c", "chown -R 1000:1000 /data"}, + Privileged: true, + }, PerReplica: true, }, { - Image: "migrator:latest", + ContainerSpec: types.ContainerSpec{ + Image: "migrator:latest", + }, }, }) assert.DeepEqual(t, p.Services["test"].PostStart, []types.ServiceHook{ @@ -117,3 +123,83 @@ services: assert.Equal(t, p.Services["test"].PreStart[0].Image, "alpine") assert.Equal(t, p.Services["test"].PreStart[1].Image, "busybox") } + +// TestPreStartAcceptsContainerSpec locks the compose-spec#656 contract: a +// pre_start hook is a full container specification, so runtime attributes +// (volumes, init, networks, …) load into the hook instead of being dropped. +func TestPreStartAcceptsContainerSpec(t *testing.T) { + p := load(t, ` +name: test +services: + test: + image: myapp + volumes: + - data:/data:ro + pre_start: + - image: busybox + command: chown -R 1000:1000 /data + user: root + init: true + volumes: + - data:/data:rw +volumes: + data: {} +`) + hook := p.Services["test"].PreStart[0] + assert.Equal(t, hook.Image, "busybox") + assert.Equal(t, hook.User, "root") + assert.Assert(t, hook.Init != nil && *hook.Init) + assert.Equal(t, len(hook.Volumes), 1) + assert.Equal(t, hook.Volumes[0].Source, "data") + assert.Equal(t, hook.Volumes[0].Target, "/data") + assert.Assert(t, !hook.Volumes[0].ReadOnly, "hook redeclares the volume read-write") + // the service's own mount is untouched + assert.Assert(t, p.Services["test"].Volumes[0].ReadOnly) +} + +// Exec hooks (post_start/pre_stop) run inside the service container: they +// take a command, not a container specification. +func TestPostStartRejectsContainerSpec(t *testing.T) { + _, err := loader.LoadWithContext(context.TODO(), types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{{Filename: "compose.yml", Content: []byte(` +name: test +services: + test: + image: alpine + post_start: + - image: busybox + command: echo hi +`)}}, + Environment: map[string]string{}, + }) + assert.ErrorContains(t, err, "additional properties 'image' not allowed") +} + +// Interpolation casts are registered per specification layer: container_spec +// attributes cast wherever a container is declared — including jobs and +// pre_start hooks, not only services. +func TestLayeredInterpolationCasts(t *testing.T) { + p, err := loader.LoadWithContext(context.TODO(), types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{{Filename: "compose.yml", Content: []byte(` +name: test +services: + test: + image: alpine + pre_start: + - command: setup + init: ${INIT} +jobs: + migrate: + image: alpine + command: migrate + cpus: ${CPUS} + triggers: + manual: true +`)}}, + Environment: map[string]string{"INIT": "true", "CPUS": "1.5"}, + }) + assert.NilError(t, err) + hook := p.Services["test"].PreStart[0] + assert.Assert(t, hook.Init != nil && *hook.Init, "hook init must cast to boolean") + assert.Equal(t, p.Jobs["migrate"].CPUS, float32(1.5), "job cpus must cast to float") +} diff --git a/loader/tests/stdin_tty_test.go b/loader/tests/stdin_tty_test.go index 9135da6ff..ae5f8a1bb 100644 --- a/loader/tests/stdin_tty_test.go +++ b/loader/tests/stdin_tty_test.go @@ -31,11 +31,20 @@ services: image: alpine stdin_open: true tty: true +jobs: + foo: + triggers: + manual: true + image: alpine + stdin_open: true + tty: true `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].StdinOpen, true) assert.Equal(t, p.Services["foo"].Tty, true) + assert.Equal(t, p.Jobs["foo"].StdinOpen, true) + assert.Equal(t, p.Jobs["foo"].Tty, true) } expect(p) diff --git a/loader/tests/stop_test.go b/loader/tests/stop_test.go index e0c442541..d4ab56434 100644 --- a/loader/tests/stop_test.go +++ b/loader/tests/stop_test.go @@ -31,9 +31,16 @@ services: foo: image: alpine stop_grace_period: 20s +jobs: + foo: + triggers: + manual: true + image: alpine + stop_grace_period: 20s `) expect := func(p *types.Project) { assert.Equal(t, *p.Services["foo"].StopGracePeriod, types.Duration(20*time.Second)) + assert.Equal(t, *p.Jobs["foo"].StopGracePeriod, types.Duration(20*time.Second)) } expect(p) @@ -49,9 +56,16 @@ services: foo: image: alpine stop_signal: SIGUSR1 +jobs: + foo: + triggers: + manual: true + image: alpine + stop_signal: SIGUSR1 `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].StopSignal, "SIGUSR1") + assert.Equal(t, p.Jobs["foo"].StopSignal, "SIGUSR1") } expect(p) diff --git a/loader/tests/storage_opt_test.go b/loader/tests/storage_opt_test.go index 0cc31f4e1..e47ecc42d 100644 --- a/loader/tests/storage_opt_test.go +++ b/loader/tests/storage_opt_test.go @@ -31,10 +31,18 @@ services: image: alpine storage_opt: size: "20G" +jobs: + foo: + triggers: + manual: true + image: alpine + storage_opt: + size: "20G" `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["foo"].StorageOpt, map[string]string{"size": "20G"}) + assert.DeepEqual(t, p.Jobs["foo"].StorageOpt, map[string]string{"size": "20G"}) } expect(p) diff --git a/loader/tests/sysctls_test.go b/loader/tests/sysctls_test.go index b3f44bd6a..f5a3b614a 100644 --- a/loader/tests/sysctls_test.go +++ b/loader/tests/sysctls_test.go @@ -41,6 +41,25 @@ services: net.ipv4.tcp_syncookies: 0 testing.one.one: "" testing.one.two: +jobs: + list: + triggers: + manual: true + image: busybox + sysctls: + - net.core.somaxconn=1024 + - net.ipv4.tcp_syncookies=0 + - testing.one.one= + - testing.one.two + map: + triggers: + manual: true + image: busybox + sysctls: + net.core.somaxconn: 1024 + net.ipv4.tcp_syncookies: 0 + testing.one.one: "" + testing.one.two: `) expect := func(p *types.Project) { @@ -52,6 +71,8 @@ services: } assert.DeepEqual(t, p.Services["list"].Sysctls, expected) assert.DeepEqual(t, p.Services["map"].Sysctls, expected) + assert.DeepEqual(t, p.Jobs["list"].Sysctls, expected) + assert.DeepEqual(t, p.Jobs["map"].Sysctls, expected) } expect(p) diff --git a/loader/tests/tmpfs_test.go b/loader/tests/tmpfs_test.go index 18eba974e..7073b93f3 100644 --- a/loader/tests/tmpfs_test.go +++ b/loader/tests/tmpfs_test.go @@ -35,11 +35,26 @@ services: string: image: alpine tmpfs: /run +jobs: + list: + triggers: + manual: true + image: alpine + tmpfs: + - /run + - /tmp + string: + triggers: + manual: true + image: alpine + tmpfs: /run `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["list"].Tmpfs, types.StringList{"/run", "/tmp"}) assert.DeepEqual(t, p.Services["string"].Tmpfs, types.StringList{"/run"}) + assert.DeepEqual(t, p.Jobs["list"].Tmpfs, types.StringList{"/run", "/tmp"}) + assert.DeepEqual(t, p.Jobs["string"].Tmpfs, types.StringList{"/run"}) } expect(p) diff --git a/loader/tests/triggers_test.go b/loader/tests/triggers_test.go new file mode 100644 index 000000000..7666a169b --- /dev/null +++ b/loader/tests/triggers_test.go @@ -0,0 +1,193 @@ +/* + 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 tests + +import ( + "context" + "testing" + + "github.com/compose-spec/compose-go/v2/loader" + "github.com/compose-spec/compose-go/v2/types" + "gotest.tools/v3/assert" +) + +func TestTriggersScheduleShortSyntax(t *testing.T) { + p := load(t, ` +name: test +jobs: + backup: + image: backup-tool + triggers: + schedule: + - "0 3 * * *" +`) + + expect := func(p *types.Project) { + schedule := p.Jobs["backup"].Triggers.Schedule + assert.Equal(t, len(schedule), 1) + // a plain crontab entry is canonicalized into a schedule object + assert.Equal(t, schedule[0].Cron, "0 3 * * *") + assert.Equal(t, schedule[0].Timezone, "") + } + expect(p) + + yamlP, jsonP := roundTrip(t, p) + expect(yamlP) + expect(jsonP) +} + +func TestTriggersScheduleList(t *testing.T) { + p := load(t, ` +name: test +jobs: + backup: + image: backup-tool + triggers: + schedule: + - cron: "0 3 * * *" + timezone: Europe/Paris + concurrency: queue + missed_fires: skip + - "0 1 * * 0" +`) + + expect := func(p *types.Project) { + schedule := p.Jobs["backup"].Triggers.Schedule + assert.Equal(t, len(schedule), 2) + assert.Equal(t, schedule[0].Cron, "0 3 * * *") + assert.Equal(t, schedule[0].Timezone, "Europe/Paris") + assert.Equal(t, schedule[0].Concurrency, "queue") + assert.Equal(t, schedule[0].MissedFires, "skip") + // plain crontab list entry is canonicalized too + assert.Equal(t, schedule[1].Cron, "0 1 * * 0") + } + expect(p) + + yamlP, jsonP := roundTrip(t, p) + expect(yamlP) + expect(jsonP) +} + +// manual is tri-state: an explicit false must survive loading and round-trip +// distinctly from unset, so consumers can refuse manual execution. +func TestTriggersManualExplicitFalse(t *testing.T) { + p := load(t, ` +name: test +jobs: + nightly: + image: batch + triggers: + manual: false + schedule: + - "0 3 * * *" +`) + + expect := func(p *types.Project) { + manual := p.Jobs["nightly"].Triggers.Manual + assert.Assert(t, manual != nil) + assert.Equal(t, *manual, false) + } + expect(p) + + yamlP, jsonP := roundTrip(t, p) + expect(yamlP) + expect(jsonP) +} + +func TestJobProfiles(t *testing.T) { + yaml := ` +name: test +jobs: + seed: + image: myapp + profiles: + - debug + triggers: + manual: true + always: + image: myapp + triggers: + manual: true +` + // a job with a profile is inactive by default + p := load(t, yaml) + assert.Equal(t, len(p.Jobs), 1) + _, enabled := p.Jobs["always"] + assert.Check(t, enabled) + _, disabled := p.DisabledJobs["seed"] + assert.Check(t, disabled) + assert.DeepEqual(t, p.DisabledJobs["seed"].Profiles, []string{"debug"}) + + // activating the profile enables the job + p, err := loader.LoadWithContext(context.TODO(), types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{{Filename: "compose.yml", Content: []byte(yaml)}}, + Environment: map[string]string{}, + }, func(options *loader.Options) { + options.SkipConsistencyCheck = true + options.SkipNormalization = true + options.Profiles = []string{"debug"} + }) + assert.NilError(t, err) + assert.Equal(t, len(p.Jobs), 2) + assert.Equal(t, len(p.DisabledJobs), 0) +} + +func TestTriggersScheduleInvalid(t *testing.T) { + // cron is required on the schedule object form + loadErr := func(yaml string) error { + _, err := loader.LoadWithContext(context.TODO(), types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{{Filename: "compose.yml", Content: []byte(yaml)}}, + Environment: map[string]string{}, + }) + return err + } + // schedule is always a list: a bare crontab expression is rejected + err := loadErr(` +name: test +jobs: + backup: + image: backup-tool + triggers: + schedule: "0 3 * * *" +`) + assert.ErrorContains(t, err, "jobs.backup.triggers.schedule") + + // cron is required on the schedule object form + err = loadErr(` +name: test +jobs: + backup: + image: backup-tool + triggers: + schedule: + - timezone: Europe/Paris +`) + assert.ErrorContains(t, err, "jobs.backup.triggers.schedule") + + // concurrency values are restricted + err = loadErr(` +name: test +jobs: + backup: + image: backup-tool + triggers: + schedule: + - cron: "0 3 * * *" + concurrency: replace +`) + assert.ErrorContains(t, err, "jobs.backup.triggers.schedule") +} diff --git a/loader/tests/ulimits_test.go b/loader/tests/ulimits_test.go index a785f9992..3bf6d5982 100644 --- a/loader/tests/ulimits_test.go +++ b/loader/tests/ulimits_test.go @@ -34,6 +34,16 @@ services: nofile: soft: 20000 hard: 40000 +jobs: + foo: + triggers: + manual: true + image: alpine + ulimits: + nproc: 65535 + nofile: + soft: 20000 + hard: 40000 `) expect := func(p *types.Project) { expected := map[string]*types.UlimitsConfig{ @@ -44,6 +54,7 @@ services: }, } assert.DeepEqual(t, p.Services["foo"].Ulimits, expected) + assert.DeepEqual(t, p.Jobs["foo"].Ulimits, expected) } expect(p) diff --git a/loader/tests/use_api_socket_test.go b/loader/tests/use_api_socket_test.go index 5f34222df..1c2d82005 100644 --- a/loader/tests/use_api_socket_test.go +++ b/loader/tests/use_api_socket_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine use_api_socket: true +jobs: + foo: + triggers: + manual: true + image: alpine + use_api_socket: true `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].UseAPISocket, true) + assert.Equal(t, p.Jobs["foo"].UseAPISocket, true) } expect(p) diff --git a/loader/tests/user_test.go b/loader/tests/user_test.go index b0a6be793..107330fde 100644 --- a/loader/tests/user_test.go +++ b/loader/tests/user_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine user: someone +jobs: + foo: + triggers: + manual: true + image: alpine + user: someone `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].User, "someone") + assert.Equal(t, p.Jobs["foo"].User, "someone") } expect(p) diff --git a/loader/tests/userns_mode_test.go b/loader/tests/userns_mode_test.go index 9a5d3c6dc..88dead4f8 100644 --- a/loader/tests/userns_mode_test.go +++ b/loader/tests/userns_mode_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine userns_mode: host +jobs: + foo: + triggers: + manual: true + image: alpine + userns_mode: host `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].UserNSMode, "host") + assert.Equal(t, p.Jobs["foo"].UserNSMode, "host") } expect(p) diff --git a/loader/tests/volume_bind_test.go b/loader/tests/volume_bind_test.go index d6acc3a44..25370a996 100644 --- a/loader/tests/volume_bind_test.go +++ b/loader/tests/volume_bind_test.go @@ -35,10 +35,22 @@ services: target: /container bind: propagation: rslave +jobs: + foo: + triggers: + manual: true + image: alpine + volumes: + - type: bind + source: /host + target: /container + bind: + propagation: rslave `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Volumes[0].Bind.Propagation, "rslave") + assert.Equal(t, p.Jobs["foo"].Volumes[0].Bind.Propagation, "rslave") } expect(p) @@ -59,10 +71,22 @@ services: target: /container bind: selinux: z +jobs: + foo: + triggers: + manual: true + image: alpine + volumes: + - type: bind + source: /host + target: /container + bind: + selinux: z `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Volumes[0].Bind.SELinux, "z") + assert.Equal(t, p.Jobs["foo"].Volumes[0].Bind.SELinux, "z") } expect(p) @@ -83,12 +107,24 @@ services: target: /data volume: nocopy: true +jobs: + foo: + triggers: + manual: true + image: alpine + volumes: + - type: volume + source: mydata + target: /data + volume: + nocopy: true volumes: mydata: `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].Volumes[0].Volume.NoCopy, true) + assert.Equal(t, p.Jobs["foo"].Volumes[0].Volume.NoCopy, true) } expect(p) @@ -112,7 +148,25 @@ services: source: /host target: /container bind: {} +jobs: + short: + triggers: + manual: true + image: alpine + volumes: + - /host:/container + long: + triggers: + manual: true + image: alpine + volumes: + - type: bind + source: /host + target: /container + bind: {} `) assert.Check(t, p.Services["short"].Volumes[0].Bind.CreateHostPath == true) assert.Check(t, p.Services["long"].Volumes[0].Bind.CreateHostPath == true) + assert.Check(t, p.Jobs["short"].Volumes[0].Bind.CreateHostPath == true) + assert.Check(t, p.Jobs["long"].Volumes[0].Bind.CreateHostPath == true) } diff --git a/loader/tests/volumes_from_test.go b/loader/tests/volumes_from_test.go index 7ec859f4b..c29a90f9a 100644 --- a/loader/tests/volumes_from_test.go +++ b/loader/tests/volumes_from_test.go @@ -34,10 +34,23 @@ services: - bar:ro bar: image: alpine +jobs: + foo: + triggers: + manual: true + image: alpine + volumes_from: + - bar + - bar:ro + bar: + triggers: + manual: true + image: alpine `) expect := func(p *types.Project) { assert.DeepEqual(t, p.Services["foo"].VolumesFrom, []string{"bar", "bar:ro"}) + assert.DeepEqual(t, p.Jobs["foo"].VolumesFrom, []string{"bar", "bar:ro"}) } expect(p) diff --git a/loader/tests/volumes_test.go b/loader/tests/volumes_test.go index 9a11ce582..30d5019ec 100644 --- a/loader/tests/volumes_test.go +++ b/loader/tests/volumes_test.go @@ -100,12 +100,29 @@ services: target: /mnt/image image: subpath: /foo +jobs: + foo: + triggers: + manual: true + image: alpine + volumes: + - type: image + source: app/image + target: /mnt/image + image: + subpath: /foo `) vol := p.Services["foo"].Volumes[0] assert.Equal(t, vol.Type, "image") assert.Equal(t, vol.Source, "app/image") assert.Equal(t, vol.Target, "/mnt/image") assert.Equal(t, vol.Image.SubPath, "/foo") + + jvol := p.Jobs["foo"].Volumes[0] + assert.Equal(t, jvol.Type, "image") + assert.Equal(t, jvol.Source, "app/image") + assert.Equal(t, jvol.Target, "/mnt/image") + assert.Equal(t, jvol.Image.SubPath, "/foo") } func TestNpipeVolume(t *testing.T) { @@ -118,9 +135,23 @@ services: - type: npipe source: \\.\pipe\docker_engine target: \\.\pipe\docker_engine +jobs: + foo: + triggers: + manual: true + image: alpine + volumes: + - type: npipe + source: \\.\pipe\docker_engine + target: \\.\pipe\docker_engine `) vol := p.Services["foo"].Volumes[0] assert.Equal(t, vol.Type, "npipe") assert.Equal(t, vol.Source, "\\\\.\\pipe\\docker_engine") assert.Equal(t, vol.Target, "\\\\.\\pipe\\docker_engine") + + jvol := p.Jobs["foo"].Volumes[0] + assert.Equal(t, jvol.Type, "npipe") + assert.Equal(t, jvol.Source, "\\\\.\\pipe\\docker_engine") + assert.Equal(t, jvol.Target, "\\\\.\\pipe\\docker_engine") } diff --git a/loader/tests/working_dir_test.go b/loader/tests/working_dir_test.go index 74b7419fd..2ff6fa996 100644 --- a/loader/tests/working_dir_test.go +++ b/loader/tests/working_dir_test.go @@ -30,10 +30,17 @@ services: foo: image: alpine working_dir: /code +jobs: + foo: + triggers: + manual: true + image: alpine + working_dir: /code `) expect := func(p *types.Project) { assert.Equal(t, p.Services["foo"].WorkingDir, "/code") + assert.Equal(t, p.Jobs["foo"].WorkingDir, "/code") } expect(p) diff --git a/loader/validate.go b/loader/validate.go index 851319479..4ac4c2dd5 100644 --- a/loader/validate.go +++ b/loader/validate.go @@ -86,6 +86,9 @@ func checkConsistency(project *types.Project) error { //nolint:gocyclo if errors.Is(err, errdefs.ErrDisabled) && !cfg.Required { continue } + if _, isJob := project.Jobs[dependedService]; isJob { + return fmt.Errorf("service %q cannot depend on job %q: services can only depend on other services: %w", s.Name, dependedService, errdefs.ErrInvalid) + } return fmt.Errorf("service %q depends on undefined service %q: %w", s.Name, dependedService, errdefs.ErrInvalid) } } @@ -205,6 +208,33 @@ func checkConsistency(project *types.Project) error { //nolint:gocyclo } + // names must be unique across services and jobs so a name always + // resolves to exactly one of them (e.g. `docker compose run `), + // including profile-disabled ones which may be enabled later + for name := range project.AllJobs() { + if _, ok := project.Services[name]; ok { + return fmt.Errorf("%q is declared both as a service and a job: service and job names must be unique: %w", name, errdefs.ErrInvalid) + } + if _, ok := project.DisabledServices[name]; ok { + return fmt.Errorf("%q is declared both as a service and a job: service and job names must be unique: %w", name, errdefs.ErrInvalid) + } + } + + for name, j := range project.Jobs { + // a job can depend on services and on other jobs + for depended, cfg := range j.DependsOn { + if _, isJob := project.Jobs[depended]; isJob { + continue + } + if _, err := project.GetService(depended); err != nil { + if errors.Is(err, errdefs.ErrDisabled) && !cfg.Required { + continue + } + return fmt.Errorf("job %q depends on undefined service or job %q: %w", name, depended, errdefs.ErrInvalid) + } + } + } + for name, secret := range project.Secrets { if secret.External { continue diff --git a/loader/validate_test.go b/loader/validate_test.go index 2dda05eaf..50e9df6a2 100644 --- a/loader/validate_test.go +++ b/loader/validate_test.go @@ -29,12 +29,14 @@ func TestValidateAnonymousVolume(t *testing.T) { project := &types.Project{ Services: types.Services{ "myservice": { - Name: "myservice", - Image: "my/service", - Volumes: []types.ServiceVolumeConfig{ - { - Type: types.VolumeTypeVolume, - Target: "/use/local", + Name: "myservice", + ContainerSpec: types.ContainerSpec{ + Image: "my/service", + Volumes: []types.ServiceVolumeConfig{ + { + Type: types.VolumeTypeVolume, + Target: "/use/local", + }, }, }, }, @@ -48,13 +50,15 @@ func TestValidateNamedVolume(t *testing.T) { project := &types.Project{ Services: types.Services{ "myservice": { - Name: "myservice", - Image: "my/service", - Volumes: []types.ServiceVolumeConfig{ - { - Type: types.VolumeTypeVolume, - Source: "myVolume", - Target: "/use/local", + Name: "myservice", + ContainerSpec: types.ContainerSpec{ + Image: "my/service", + Volumes: []types.ServiceVolumeConfig{ + { + Type: types.VolumeTypeVolume, + Source: "myVolume", + Target: "/use/local", + }, }, }, }, @@ -89,13 +93,15 @@ func TestValidateNetworkMode(t *testing.T) { project := &types.Project{ Services: types.Services{ "myservice1": { - Name: "myservice1", - Image: "scratch", + Name: "myservice1", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, }, "myservice2": { - Name: "myservice2", - Image: "scratch", - NetworkMode: "service:myservice1", + Name: "myservice2", + ContainerSpec: types.ContainerSpec{ + Image: "scratch", + NetworkMode: "service:myservice1", + }, }, }, } @@ -107,13 +113,15 @@ func TestValidateNetworkMode(t *testing.T) { project := &types.Project{ Services: types.Services{ "myservice1": { - Name: "myservice1", - Image: "scratch", + Name: "myservice1", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, }, "myservice2": { - Name: "myservice2", - Image: "scratch", - NetworkMode: "service:nonexistentservice", + Name: "myservice2", + ContainerSpec: types.ContainerSpec{ + Image: "scratch", + NetworkMode: "service:nonexistentservice", + }, }, }, } @@ -127,12 +135,14 @@ func TestValidateNetworkMode(t *testing.T) { "myservice1": { Name: "myservice1", ContainerName: "mycontainer_name", - Image: "scratch", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, }, "myservice2": { - Name: "myservice2", - Image: "scratch", - NetworkMode: "container:mycontainer_name", + Name: "myservice2", + ContainerSpec: types.ContainerSpec{ + Image: "scratch", + NetworkMode: "container:mycontainer_name", + }, }, }, } @@ -145,11 +155,13 @@ func TestValidateNetworkMode(t *testing.T) { Networks: types.Networks{"mynetwork": types.NetworkConfig{}}, Services: types.Services{ "myservice1": { - Name: "myservice1", - Image: "scratch", - NetworkMode: "host", - Networks: map[string]*types.ServiceNetworkConfig{ - "mynetwork": {}, + Name: "myservice1", + ContainerSpec: types.ContainerSpec{ + Image: "scratch", + NetworkMode: "host", + Networks: map[string]*types.ServiceNetworkConfig{ + "mynetwork": {}, + }, }, }, }, @@ -212,11 +224,13 @@ func TestValidateSecret(t *testing.T) { }, Services: types.Services{ "myservice": { - Name: "myservice", - Image: "scratch", - Secrets: []types.ServiceSecretConfig{ - { - Source: "foo", + Name: "myservice", + ContainerSpec: types.ContainerSpec{ + Image: "scratch", + Secrets: []types.ServiceSecretConfig{ + { + Source: "foo", + }, }, }, }, @@ -230,11 +244,13 @@ func TestValidateSecret(t *testing.T) { project := &types.Project{ Services: types.Services{ "myservice": { - Name: "myservice", - Image: "scratch", - Secrets: []types.ServiceSecretConfig{ - { - Source: "foo", + Name: "myservice", + ContainerSpec: types.ContainerSpec{ + Image: "scratch", + Secrets: []types.ServiceSecretConfig{ + { + Source: "foo", + }, }, }, }, @@ -249,11 +265,11 @@ func TestValidateDependsOn(t *testing.T) { project := types.Project{ Services: types.Services{ "myservice": { - Name: "myservice", - Image: "scratch", - DependsOn: map[string]types.ServiceDependency{ + Name: "myservice", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, + WorkloadSpec: types.WorkloadSpec{DependsOn: map[string]types.ServiceDependency{ "missingservice": {}, - }, + }}, }, }, } @@ -261,18 +277,106 @@ func TestValidateDependsOn(t *testing.T) { assert.Error(t, err, `service "myservice" depends on undefined service "missingservice": invalid compose project`) } +func TestValidateServiceDependsOnJob(t *testing.T) { + project := types.Project{ + Services: types.Services{ + "myservice": { + Name: "myservice", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, + WorkloadSpec: types.WorkloadSpec{DependsOn: map[string]types.ServiceDependency{ + "myjob": {}, + }}, + }, + }, + Jobs: types.Jobs{ + "myjob": { + Name: "myjob", + ContainerSpec: types.ContainerSpec{ + Image: "scratch", + }, + }, + }, + } + err := checkConsistency(&project) + assert.Error(t, err, `service "myservice" cannot depend on job "myjob": services can only depend on other services: invalid compose project`) +} + +func TestValidateJobDependsOn(t *testing.T) { + project := types.Project{ + Services: types.Services{ + "myservice": { + Name: "myservice", + ContainerSpec: types.ContainerSpec{ + Image: "scratch", + }, + }, + }, + Jobs: types.Jobs{ + // a job can depend on a service and on another job + "myjob": { + Name: "myjob", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, + WorkloadSpec: types.WorkloadSpec{DependsOn: map[string]types.ServiceDependency{ + "myservice": {}, + "otherjob": {}, + }}, + }, + "otherjob": { + Name: "otherjob", + ContainerSpec: types.ContainerSpec{ + Image: "scratch", + }, + }, + }, + } + assert.NilError(t, checkConsistency(&project)) + + project.Jobs["myjob"].DependsOn["missing"] = types.ServiceDependency{} + err := checkConsistency(&project) + assert.Error(t, err, `job "myjob" depends on undefined service or job "missing": invalid compose project`) +} + +func TestValidateServiceJobNameUniqueness(t *testing.T) { + project := types.Project{ + Services: types.Services{ + "foo": { + Name: "foo", + ContainerSpec: types.ContainerSpec{ + Image: "scratch", + }, + }, + }, + Jobs: types.Jobs{ + "foo": { + Name: "foo", + ContainerSpec: types.ContainerSpec{ + Image: "scratch", + }, + }, + }, + } + err := checkConsistency(&project) + assert.Error(t, err, `"foo" is declared both as a service and a job: service and job names must be unique: invalid compose project`) + + // a name owned by a profile-disabled service is not available for a job either + project.DisabledServices = types.Services{"foo": project.Services["foo"]} + delete(project.Services, "foo") + err = checkConsistency(&project) + assert.Error(t, err, `"foo" is declared both as a service and a job: service and job names must be unique: invalid compose project`) +} + func TestValidateContainerName(t *testing.T) { project := &types.Project{ Services: types.Services{ "myservice": { Name: "myservice", - Image: "scratch", ContainerName: "mycontainer", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, }, "myservice2": { Name: "myservice2", - Image: "scratch", ContainerName: "mycontainer", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, }, }, } @@ -285,8 +389,8 @@ func TestValidateWatch(t *testing.T) { project := types.Project{ Services: types.Services{ "myservice": { - Name: "myservice", - Image: "scratch", + Name: "myservice", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, Develop: &types.DevelopConfig{ Watch: []types.Trigger{ { @@ -307,8 +411,8 @@ func TestValidateWatch(t *testing.T) { project := types.Project{ Services: types.Services{ "myservice": { - Name: "myservice", - Image: "scratch", + Name: "myservice", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, Develop: &types.DevelopConfig{ Watch: []types.Trigger{ { @@ -328,8 +432,8 @@ func TestValidateWatch(t *testing.T) { project := types.Project{ Services: types.Services{ "myservice": { - Name: "myservice", - Image: "scratch", + Name: "myservice", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, Develop: &types.DevelopConfig{ Watch: []types.Trigger{ { @@ -349,8 +453,8 @@ func TestValidateWatch(t *testing.T) { project := types.Project{ Services: types.Services{ "myservice": { - Name: "myservice", - Image: "scratch", + Name: "myservice", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, Develop: &types.DevelopConfig{ Watch: []types.Trigger{ { @@ -370,18 +474,18 @@ func TestValidateWatch(t *testing.T) { project := types.Project{ Services: types.Services{ "myservice": { - Name: "myservice", - Image: "scratch", - DependsOn: map[string]types.ServiceDependency{ + Name: "myservice", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, + WorkloadSpec: types.WorkloadSpec{DependsOn: map[string]types.ServiceDependency{ "other": { Required: false, }, - }, + }}, }, }, DisabledServices: types.Services{ "other": { - Image: "scratch", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, }, }, } @@ -393,13 +497,13 @@ func TestValidateWatch(t *testing.T) { project := types.Project{ Services: types.Services{ "myservice": { - Name: "myservice", - Image: "scratch", - DependsOn: map[string]types.ServiceDependency{ + Name: "myservice", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, + WorkloadSpec: types.WorkloadSpec{DependsOn: map[string]types.ServiceDependency{ "other": { Required: false, }, - }, + }}, }, }, } @@ -412,22 +516,24 @@ func TestValidateMountConflict(t *testing.T) { project := &types.Project{ Services: types.Services{ "myservice": { - Name: "myservice", - Image: "scratch", - Tmpfs: []string{ - "/foo", - "/conflict:size=64m", - }, - Volumes: []types.ServiceVolumeConfig{ - { - Type: "bind", - Target: "/bar", - Source: ".", + Name: "myservice", + ContainerSpec: types.ContainerSpec{ + Image: "scratch", + Tmpfs: []string{ + "/foo", + "/conflict:size=64m", }, - { - Type: "bind", - Target: "/conflict", - Source: ".", + Volumes: []types.ServiceVolumeConfig{ + { + Type: "bind", + Target: "/bar", + Source: ".", + }, + { + Type: "bind", + Target: "/conflict", + Source: ".", + }, }, }, }, @@ -441,9 +547,9 @@ func TestValidateNegativeScale(t *testing.T) { project := &types.Project{ Services: types.Services{ "myservice": { - Name: "myservice", - Image: "scratch", - Scale: ptr(-1), + Name: "myservice", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, + Scale: ptr(-1), }, }, } @@ -453,8 +559,8 @@ func TestValidateNegativeScale(t *testing.T) { project = &types.Project{ Services: types.Services{ "myservice": { - Name: "myservice", - Image: "scratch", + Name: "myservice", + ContainerSpec: types.ContainerSpec{Image: "scratch"}, Deploy: &types.DeployConfig{ Replicas: ptr(-1), }, diff --git a/loader/with-version-struct_test.go b/loader/with-version-struct_test.go index f4deb17c3..d637649b3 100644 --- a/loader/with-version-struct_test.go +++ b/loader/with-version-struct_test.go @@ -35,25 +35,23 @@ func withVersionServices() types.Services { return types.Services{ "web": { Name: "web", - - Build: &types.BuildConfig{ - Context: buildCtx, - }, - Environment: types.MappingWithEquals{}, - Networks: map[string]*types.ServiceNetworkConfig{ + ContainerSpec: types.ContainerSpec{Environment: types.MappingWithEquals{}, Networks: map[string]*types.ServiceNetworkConfig{ "front": nil, "default": nil, - }, - VolumesFrom: []string{"other"}, + }, VolumesFrom: []string{"other"}}, + WorkloadSpec: types.WorkloadSpec{Build: &types.BuildConfig{ + Context: buildCtx, + }}, }, "other": { Name: "other", - - Image: "busybox:1.31.0-uclibc", - Command: []string{"top"}, - Environment: types.MappingWithEquals{}, - Volumes: []types.ServiceVolumeConfig{ - {Target: "/data", Type: "volume", Volume: &types.ServiceVolumeVolume{}}, + ContainerSpec: types.ContainerSpec{ + Image: "busybox:1.31.0-uclibc", + Command: []string{"top"}, + Environment: types.MappingWithEquals{}, + Volumes: []types.ServiceVolumeConfig{ + {Target: "/data", Type: "volume", Volume: &types.ServiceVolumeVolume{}}, + }, }, }, } diff --git a/override/merge.go b/override/merge.go index 9aed9b6b9..0a9f0d96f 100644 --- a/override/merge.go +++ b/override/merge.go @@ -42,32 +42,34 @@ func init() { mergeSpecials["networks.*.ipam.config"] = mergeIPAMConfig mergeSpecials["networks.*.labels"] = mergeToSequence mergeSpecials["volumes.*.labels"] = mergeToSequence - mergeSpecials["services.*.annotations"] = mergeToSequence - mergeSpecials["services.*.build"] = mergeBuild - mergeSpecials["services.*.build.args"] = mergeToSequence - mergeSpecials["services.*.build.additional_contexts"] = mergeToSequence - mergeSpecials["services.*.build.extra_hosts"] = mergeExtraHosts - mergeSpecials["services.*.build.labels"] = mergeToSequence - mergeSpecials["services.*.command"] = override - mergeSpecials["services.*.depends_on"] = mergeDependsOn + for _, prefix := range []tree.Path{"services", "jobs"} { + mergeSpecials[prefix+".*.annotations"] = mergeToSequence + mergeSpecials[prefix+".*.build"] = mergeBuild + mergeSpecials[prefix+".*.build.args"] = mergeToSequence + mergeSpecials[prefix+".*.build.additional_contexts"] = mergeToSequence + mergeSpecials[prefix+".*.build.extra_hosts"] = mergeExtraHosts + mergeSpecials[prefix+".*.build.labels"] = mergeToSequence + mergeSpecials[prefix+".*.command"] = override + mergeSpecials[prefix+".*.depends_on"] = mergeDependsOn + mergeSpecials[prefix+".*.dns"] = mergeToSequence + mergeSpecials[prefix+".*.dns_opt"] = mergeToSequence + mergeSpecials[prefix+".*.dns_search"] = mergeToSequence + mergeSpecials[prefix+".*.entrypoint"] = override + mergeSpecials[prefix+".*.env_file"] = mergeToSequence + mergeSpecials[prefix+".*.label_file"] = mergeToSequence + mergeSpecials[prefix+".*.environment"] = mergeToSequence + mergeSpecials[prefix+".*.extra_hosts"] = mergeExtraHosts + mergeSpecials[prefix+".*.healthcheck.test"] = override + mergeSpecials[prefix+".*.labels"] = mergeToSequence + mergeSpecials[prefix+".*.volumes.*.volume.labels"] = mergeToSequence + mergeSpecials[prefix+".*.logging"] = mergeLogging + mergeSpecials[prefix+".*.models"] = mergeModels + mergeSpecials[prefix+".*.networks"] = mergeNetworks + mergeSpecials[prefix+".*.sysctls"] = mergeToSequence + mergeSpecials[prefix+".*.tmpfs"] = mergeToSequence + mergeSpecials[prefix+".*.ulimits.*"] = mergeUlimit + } mergeSpecials["services.*.deploy.labels"] = mergeToSequence - mergeSpecials["services.*.dns"] = mergeToSequence - mergeSpecials["services.*.dns_opt"] = mergeToSequence - mergeSpecials["services.*.dns_search"] = mergeToSequence - mergeSpecials["services.*.entrypoint"] = override - mergeSpecials["services.*.env_file"] = mergeToSequence - mergeSpecials["services.*.label_file"] = mergeToSequence - mergeSpecials["services.*.environment"] = mergeToSequence - mergeSpecials["services.*.extra_hosts"] = mergeExtraHosts - mergeSpecials["services.*.healthcheck.test"] = override - mergeSpecials["services.*.labels"] = mergeToSequence - mergeSpecials["services.*.volumes.*.volume.labels"] = mergeToSequence - mergeSpecials["services.*.logging"] = mergeLogging - mergeSpecials["services.*.models"] = mergeModels - mergeSpecials["services.*.networks"] = mergeNetworks - mergeSpecials["services.*.sysctls"] = mergeToSequence - mergeSpecials["services.*.tmpfs"] = mergeToSequence - mergeSpecials["services.*.ulimits.*"] = mergeUlimit } // MergeYaml merges map[string]any yaml trees handling special rules diff --git a/paths/resolve.go b/paths/resolve.go index c58cb4106..34a198001 100644 --- a/paths/resolve.go +++ b/paths/resolve.go @@ -42,6 +42,17 @@ func ResolveRelativePaths(project map[string]any, base string, remotes []RemoteR "services.*.extends.file": r.absExtendsPath, "services.*.develop.watch.*.path": r.absSymbolicLink, "services.*.volumes.*": r.absVolumeMount, + "jobs.*.build.context": r.absContextPath, + "jobs.*.build.additional_contexts.*": r.absContextPath, + "jobs.*.build.ssh.*": r.maybeUnixPath, + "jobs.*.env_file.*.path": r.absPath, + "jobs.*.label_file.*": r.absPath, + "jobs.*.extends.file": r.absExtendsPath, + "jobs.*.volumes.*": r.absVolumeMount, + // pre_start hooks are full container specifications (compose-spec#656) + "services.*.pre_start.*.env_file.*.path": r.absPath, + "services.*.pre_start.*.label_file.*": r.absPath, + "services.*.pre_start.*.volumes.*": r.absVolumeMount, "configs.*.file": r.maybeUnixPath, "secrets.*.file": r.maybeUnixPath, "include.path": r.absPath, diff --git a/schema/compose-spec.json b/schema/compose-spec.json index fe0e45d68..7197906b7 100644 --- a/schema/compose-spec.json +++ b/schema/compose-spec.json @@ -4,19 +4,16 @@ "type": "object", "title": "Compose Specification", "description": "The Compose file is a YAML file defining a multi-containers based application.", - "properties": { "version": { "type": "string", "deprecated": true, "description": "declared for backward compatibility, ignored. Please remove it." }, - "name": { "type": "string", "description": "define the Compose project name, until user defines one explicitly." }, - "include": { "type": "array", "items": { @@ -24,7 +21,6 @@ }, "description": "compose sub-projects to be included." }, - "services": { "type": "object", "patternProperties": { @@ -35,7 +31,6 @@ "additionalProperties": false, "description": "The services that will be used by your application." }, - "models": { "type": "object", "patternProperties": { @@ -45,8 +40,6 @@ }, "description": "Language models that will be used by your application." }, - - "networks": { "type": "object", "patternProperties": { @@ -56,7 +49,6 @@ }, "description": "Networks that are shared among multiple services." }, - "volumes": { "type": "object", "patternProperties": { @@ -67,7 +59,6 @@ "additionalProperties": false, "description": "Named volumes that are shared among multiple services." }, - "secrets": { "type": "object", "patternProperties": { @@ -78,7 +69,6 @@ "additionalProperties": false, "description": "Secrets that are shared among multiple services." }, - "configs": { "type": "object", "patternProperties": { @@ -88,59 +78,29 @@ }, "additionalProperties": false, "description": "Configurations that are shared among multiple services." + }, + "jobs": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/$defs/job" + } + }, + "additionalProperties": false, + "description": "Jobs are containers that run to completion." } }, - - "patternProperties": {"^x-": {}}, + "patternProperties": { + "^x-": {} + }, "additionalProperties": false, - "$defs": { - - "service": { + "container_spec": { "type": "object", - "description": "Configuration for a service.", + "description": "Attributes of a container specification shared by anything that runs a container: services, jobs, and run-to-completion init containers (pre_start hooks).", "properties": { - "develop": {"$ref": "#/$defs/development"}, - "deploy": {"$ref": "#/$defs/deployment"}, - "annotations": {"$ref": "#/$defs/list_or_dict"}, - "attach": {"type": ["boolean", "string"]}, - "build": { - "description": "Configuration options for building the service's image.", - "oneOf": [ - {"type": "string", "description": "Path to the build context. Can be a relative path or a URL."}, - { - "type": "object", - "properties": { - "context": {"type": "string", "description": "Path to the build context. Can be a relative path or a URL."}, - "dockerfile": {"type": "string", "description": "Name of the Dockerfile to use for building the image."}, - "dockerfile_inline": {"type": "string", "description": "Inline Dockerfile content to use instead of a Dockerfile from the build context."}, - "entitlements": {"type": "array", "items": {"type": "string"}, "description": "List of extra privileged entitlements to grant to the build process."}, - "args": {"$ref": "#/$defs/list_or_dict", "description": "Build-time variables, specified as a map or a list of KEY=VAL pairs."}, - "ssh": {"$ref": "#/$defs/list_or_dict", "description": "SSH agent socket or keys to expose to the build. Format is either a string or a list of 'default|[=|[,]]'."}, - "labels": {"$ref": "#/$defs/list_or_dict", "description": "Labels to apply to the built image."}, - "cache_from": {"type": "array", "items": {"type": "string"}, "description": "List of sources the image builder should use for cache resolution"}, - "cache_to": {"type": "array", "items": {"type": "string"}, "description": "Cache destinations for the build cache."}, - "no_cache": {"type": ["boolean", "string"], "description": "Do not use cache when building the image."}, - "no_cache_filter": {"$ref": "#/$defs/string_or_list", "description": "Do not use build cache for the specified stages."}, - "additional_contexts": {"$ref": "#/$defs/list_or_dict", "description": "Additional build contexts to use, specified as a map of name to context path or URL."}, - "network": {"type": "string", "description": "Network mode to use for the build. Options include 'default', 'none', 'host', or a network name."}, - "provenance": {"type": ["string","boolean"], "description": "Add a provenance attestation"}, - "sbom": {"type": ["string","boolean"], "description": "Add a SBOM attestation"}, - "pull": {"type": ["boolean", "string"], "description": "Always attempt to pull a newer version of the image."}, - "target": {"type": "string", "description": "Build stage to target in a multi-stage Dockerfile."}, - "shm_size": {"type": ["integer", "string"], "description": "Size of /dev/shm for the build container. A string value can use suffix like '2g' for 2 gigabytes."}, - "extra_hosts": {"$ref": "#/$defs/extra_hosts", "description": "Add hostname mappings for the build container."}, - "isolation": {"type": "string", "description": "Container isolation technology to use for the build process."}, - "privileged": {"type": ["boolean", "string"], "description": "Give extended privileges to the build container."}, - "secrets": {"$ref": "#/$defs/service_config_or_secret", "description": "Secrets to expose to the build. These are accessible at build-time."}, - "tags": {"type": "array", "items": {"type": "string"}, "description": "Additional tags to apply to the built image."}, - "ulimits": {"$ref": "#/$defs/ulimits", "description": "Override the default ulimits for the build container."}, - "platforms": {"type": "array", "items": {"type": "string"}, "description": "Platforms to build for, e.g., 'linux/amd64', 'linux/arm64', or 'windows/amd64'."} - }, - "additionalProperties": false, - "patternProperties": {"^x-": {}} - } - ] + "annotations": { + "$ref": "#/$defs/list_or_dict" }, "blkio_config": { "type": "object", @@ -149,50 +109,70 @@ "device_read_bps": { "type": "array", "description": "Limit read rate (bytes per second) from a device.", - "items": {"$ref": "#/$defs/blkio_limit"} + "items": { + "$ref": "#/$defs/blkio_limit" + } }, "device_read_iops": { "type": "array", "description": "Limit read rate (IO per second) from a device.", - "items": {"$ref": "#/$defs/blkio_limit"} + "items": { + "$ref": "#/$defs/blkio_limit" + } }, "device_write_bps": { "type": "array", "description": "Limit write rate (bytes per second) to a device.", - "items": {"$ref": "#/$defs/blkio_limit"} + "items": { + "$ref": "#/$defs/blkio_limit" + } }, "device_write_iops": { "type": "array", "description": "Limit write rate (IO per second) to a device.", - "items": {"$ref": "#/$defs/blkio_limit"} + "items": { + "$ref": "#/$defs/blkio_limit" + } }, "weight": { - "type": ["integer", "string"], + "type": [ + "integer", + "string" + ], "description": "Block IO weight (relative weight) for the service, between 10 and 1000." }, "weight_device": { "type": "array", "description": "Block IO weight (relative weight) for specific devices.", - "items": {"$ref": "#/$defs/blkio_weight"} + "items": { + "$ref": "#/$defs/blkio_weight" + } } }, "additionalProperties": false }, "cap_add": { "type": "array", - "items": {"type": "string"}, + "items": { + "type": "string" + }, "uniqueItems": true, "description": "Add Linux capabilities. For example, 'CAP_SYS_ADMIN', 'SYS_ADMIN', or 'NET_ADMIN'." }, "cap_drop": { "type": "array", - "items": {"type": "string"}, + "items": { + "type": "string" + }, "uniqueItems": true, "description": "Drop Linux capabilities. For example, 'CAP_SYS_ADMIN', 'SYS_ADMIN', or 'NET_ADMIN'." }, "cgroup": { "type": "string", - "enum": ["host", "private"], + "enum": [ + "host", + "private" + ], "description": "Specify the cgroup namespace to join. Use 'host' to use the host's cgroup namespace, or 'private' to use a private cgroup namespace." }, "cgroup_parent": { @@ -207,47 +187,71 @@ "$ref": "#/$defs/service_config_or_secret", "description": "Grant access to Configs on a per-service basis." }, - "container_name": { - "type": "string", - "description": "Specify a custom container name, rather than a generated default name.", - "pattern": "[a-zA-Z0-9][a-zA-Z0-9_.-]+" - }, "cpu_count": { "oneOf": [ - {"type": "string"}, - {"type": "integer", "minimum": 0} + { + "type": "string" + }, + { + "type": "integer", + "minimum": 0 + } ], "description": "Number of usable CPUs." }, "cpu_percent": { "oneOf": [ - {"type": "string"}, - {"type": "integer", "minimum": 0, "maximum": 100} + { + "type": "string" + }, + { + "type": "integer", + "minimum": 0, + "maximum": 100 + } ], "description": "Percentage of CPU resources to use." }, "cpu_shares": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "CPU shares (relative weight) for the container." }, "cpu_quota": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Limit the CPU CFS (Completely Fair Scheduler) quota." }, "cpu_period": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Limit the CPU CFS (Completely Fair Scheduler) period." }, "cpu_rt_period": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Limit the CPU real-time period in microseconds or a duration." }, "cpu_rt_runtime": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Limit the CPU real-time runtime in microseconds or a duration." }, "cpus": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Number of CPUs to use. A floating-point value is supported to request partial CPUs." }, "cpuset": { @@ -272,41 +276,9 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} - }, - "depends_on": { - "oneOf": [ - {"$ref": "#/$defs/list_of_strings"}, - { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "type": "object", - "additionalProperties": false, - "patternProperties": {"^x-": {}}, - "properties": { - "restart": { - "type": ["boolean", "string"], - "description": "Whether to restart dependent services when this service is restarted." - }, - "required": { - "type": "boolean", - "default": true, - "description": "Whether the dependency is required for the dependent service to start." - }, - "condition": { - "type": "string", - "enum": ["service_started", "service_healthy", "service_completed_successfully"], - "description": "Condition to wait for. 'service_started' waits until the service has started, 'service_healthy' waits until the service is healthy (as defined by its healthcheck), 'service_completed_successfully' waits until the service has completed successfully." - } - }, - "required": ["condition"] - } - } - } - ], - "description": "Express dependency between services. Service dependencies cause services to be started in dependency order. The dependent service will wait for the dependency to be ready before starting." + "patternProperties": { + "^x-": {} + } }, "device_cgroup_rules": { "$ref": "#/$defs/list_of_strings", @@ -317,10 +289,14 @@ "description": "List of device mappings for the container.", "items": { "oneOf": [ - {"type": "string"}, + { + "type": "string" + }, { "type": "object", - "required": ["source"], + "required": [ + "source" + ], "properties": { "source": { "type": "string", @@ -336,7 +312,9 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } } ] } @@ -347,7 +325,9 @@ }, "dns_opt": { "type": "array", - "items": {"type": "string"}, + "items": { + "type": "string" + }, "uniqueItems": true, "description": "Custom DNS options to be passed to the container's DNS resolver." }, @@ -375,64 +355,6 @@ "$ref": "#/$defs/list_or_dict", "description": "Add environment variables. You can use either an array or a list of KEY=VAL pairs." }, - "expose": { - "type": "array", - "items": { - "type": ["string", "number"] - }, - "uniqueItems": true, - "description": "Expose ports without publishing them to the host machine - they'll only be accessible to linked services." - }, - "extends": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "service": { - "type": "string", - "description": "The name of the service to extend." - }, - "file": { - "type": "string", - "description": "The file path where the service to extend is defined." - } - }, - "required": ["service"], - "additionalProperties": false - } - ], - "description": "Extend another service, in the current file or another file." - }, - "provider": { - "type": "object", - "description": "Specify a service which will not be manage by Compose directly, and delegate its management to an external provider.", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "description": "External component used by Compose to manage setup and teardown lifecycle of the service." - }, - "options": { - "type": "object", - "description": "Provider-specific options.", - "patternProperties": { - "^.+$": {"oneOf": [ - { "type": ["string", "number", "boolean"] }, - { "type": "array", "items": {"type": ["string", "number", "boolean"]}} - ]} - } - } - }, - "additionalProperties": false, - "patternProperties": {"^x-": {}} - }, - "external_links": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true, - "description": "Link to services started outside this Compose application. Specify services as :." - }, "extra_hosts": { "$ref": "#/$defs/extra_hosts", "description": "Add hostname mappings to the container network interface configuration." @@ -444,15 +366,14 @@ "group_add": { "type": "array", "items": { - "type": ["string", "number"] + "type": [ + "string", + "number" + ] }, "uniqueItems": true, "description": "Add additional groups which user inside the container should be member of." }, - "healthcheck": { - "$ref": "#/$defs/healthcheck", - "description": "Configure a health check for the container to monitor its health status." - }, "hostname": { "type": "string", "description": "Define a custom hostname for the service container." @@ -462,7 +383,10 @@ "description": "Specify the image to start the container from. Can be a repository/tag, a digest, or a local image ID." }, "init": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Run as an init process inside the container that forwards signals and reaps processes." }, "ipc": { @@ -477,12 +401,6 @@ "$ref": "#/$defs/list_or_dict", "description": "Add metadata to containers using Docker labels. You can use either an array or a list." }, - "links": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true, - "description": "Link to containers in another service. Either specify both the service name and a link alias (SERVICE:ALIAS), or just the service name." - }, "logging": { "type": "object", "description": "Logging configuration for the service.", @@ -495,31 +413,51 @@ "type": "object", "description": "Options for the logging driver.", "patternProperties": { - "^.+$": {"type": ["string", "number", "null"]} + "^.+$": { + "type": [ + "string", + "number", + "null" + ] + } } } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "mac_address": { "type": "string", "description": "Container MAC address to set." }, "mem_limit": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Memory limit for the container. A string value can use suffix like '2g' for 2 gigabytes." }, "mem_reservation": { - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "description": "Memory reservation for the container." }, "mem_swappiness": { - "type": ["integer", "string"], + "type": [ + "integer", + "string" + ], "description": "Container memory swappiness as percentage (0 to 100)." }, "memswap_limit": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Amount of memory the container is allowed to swap to disk. Set to -1 to enable unlimited swap." }, "network_mode": { @@ -528,8 +466,11 @@ }, "models": { "oneOf": [ - {"$ref": "#/$defs/list_of_strings"}, - {"type": "object", + { + "$ref": "#/$defs/list_of_strings" + }, + { + "type": "object", "patternProperties": { "^[a-zA-Z0-9._-]+$": { "oneOf": [ @@ -546,9 +487,13 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, - {"type": "null"} + { + "type": "null" + } ] } } @@ -558,7 +503,9 @@ }, "networks": { "oneOf": [ - {"$ref": "#/$defs/list_of_strings"}, + { + "$ref": "#/$defs/list_of_strings" + }, { "type": "object", "patternProperties": { @@ -595,7 +542,12 @@ "type": "object", "description": "Driver options for this network.", "patternProperties": { - "^.+$": {"type": ["string", "number"]} + "^.+$": { + "type": [ + "string", + "number" + ] + } } }, "priority": { @@ -608,9 +560,13 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, - {"type": "null"} + { + "type": "null" + } ] } }, @@ -620,97 +576,50 @@ "description": "Networks to join, referencing entries under the top-level networks key. Can be a list of network names or a mapping of network name to network configuration." }, "oom_kill_disable": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Disable OOM Killer for the container." }, "oom_score_adj": { "oneOf": [ - {"type": "string"}, - {"type": "integer", "minimum": -1000, "maximum": 1000} + { + "type": "string" + }, + { + "type": "integer", + "minimum": -1000, + "maximum": 1000 + } ], "description": "Tune host's OOM preferences for the container (accepts -1000 to 1000)." }, "pid": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "description": "PID mode for container." }, "pids_limit": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Tune a container's PIDs limit. Set to -1 for unlimited PIDs." }, "platform": { "type": "string", "description": "Target platform to run on, e.g., 'linux/amd64', 'linux/arm64', or 'windows/amd64'." }, - "ports": { - "type": "array", - "description": "Expose container ports. Short format ([HOST:]CONTAINER[/PROTOCOL]).", - "items": { - "oneOf": [ - {"type": "number"}, - {"type": "string"}, - { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "A human-readable name for this port mapping." - }, - "mode": { - "type": "string", - "description": "The port binding mode, either 'host' for publishing a host port or 'ingress' for load balancing." - }, - "host_ip": { - "type": "string", - "description": "The host IP to bind to." - }, - "target": { - "type": ["integer", "string"], - "description": "The port inside the container." - }, - "published": { - "type": ["string", "integer"], - "description": "The publicly exposed port." - }, - "protocol": { - "type": "string", - "description": "The port protocol (tcp or udp)." - }, - "app_protocol": { - "type": "string", - "description": "Application protocol to use with the port (e.g., http, https, mysql)." - } - }, - "additionalProperties": false, - "patternProperties": {"^x-": {}} - } - ] - }, - "uniqueItems": true - }, - "pre_start": { - "type": "array", - "items": {"$ref": "#/$defs/pre_start_hook"}, - "description": "Init containers to run to completion before the service container is started. Each step runs in its own ephemeral container, in declared order; a non-zero exit fails the bring-up of the service and its dependents." - }, - "post_start": { - "type": "array", - "items": {"$ref": "#/$defs/service_hook"}, - "description": "Commands to run after the container starts. If any command fails, the container stops." - }, - "pre_stop": { - "type": "array", - "items": {"$ref": "#/$defs/service_hook"}, - "description": "Commands to run before the container stops. If any command fails, the container stop is aborted." - }, "privileged": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Give extended privileges to the service container." }, - "profiles": { - "$ref": "#/$defs/list_of_strings", - "description": "List of profiles for this service. When profiles are specified, services are only started when the profile is activated." - }, "pull_policy": { "type": "string", "pattern": "always|never|build|if_not_present|missing|refresh|daily|weekly|every_([0-9]+[wdhms])+", @@ -721,29 +630,29 @@ "description": "Time after which to refresh the image. Used with pull_policy=refresh." }, "read_only": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Mount the container's filesystem as read only." }, - "restart": { - "type": "string", - "description": "Restart policy for the service container. Options include: 'no', 'always', 'on-failure', and 'unless-stopped'." - }, "runtime": { "type": "string", "description": "Runtime to use for this container, e.g., 'runc'." }, - "scale": { - "type": ["integer", "string"], - "description": "Number of containers to deploy for this service." - }, "security_opt": { "type": "array", - "items": {"type": "string"}, + "items": { + "type": "string" + }, "uniqueItems": true, "description": "Override the default labeling scheme for each container." }, "shm_size": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Size of /dev/shm. A string value can use suffix like '2g' for 2 gigabytes." }, "secrets": { @@ -754,10 +663,6 @@ "$ref": "#/$defs/list_or_dict", "description": "Kernel parameters to set in the container. You can use either an array or a list." }, - "stdin_open": { - "type": ["boolean", "string"], - "description": "Keep STDIN open even if not attached." - }, "stop_grace_period": { "type": "string", "description": "Time to wait for the container to stop gracefully before sending SIGKILL (e.g., '1s', '1m30s')." @@ -774,10 +679,6 @@ "$ref": "#/$defs/string_or_list", "description": "Mount a temporary filesystem (tmpfs) into the container. Can be a single value or a list." }, - "tty": { - "type": ["boolean", "string"], - "description": "Allocate a pseudo-TTY to service container." - }, "ulimits": { "$ref": "#/$defs/ulimits", "description": "Override the default ulimits for a container." @@ -803,14 +704,25 @@ "description": "Mount host paths or named volumes accessible to the container. Short syntax (VOLUME:CONTAINER_PATH[:MODE])", "items": { "oneOf": [ - {"type": "string"}, + { + "type": "string" + }, { "type": "object", - "required": ["type"], + "required": [ + "type" + ], "properties": { "type": { "type": "string", - "enum": ["bind", "volume", "tmpfs", "cluster", "npipe", "image"], + "enum": [ + "bind", + "volume", + "tmpfs", + "cluster", + "npipe", + "image" + ], "description": "The mount type: bind for mounting host directories, volume for named volumes, tmpfs for temporary filesystems, cluster for cluster volumes, npipe for named pipes, or image for mounting from an image." }, "source": { @@ -822,7 +734,10 @@ "description": "The path in the container where the volume is mounted." }, "read_only": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Flag to set the volume as read-only." }, "consistency": { @@ -838,22 +753,35 @@ "description": "The propagation mode for the bind mount: 'shared', 'slave', 'private', 'rshared', 'rslave', or 'rprivate'." }, "create_host_path": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Create the host path if it doesn't exist." }, "recursive": { "type": "string", - "enum": ["enabled", "disabled", "writable", "readonly"], + "enum": [ + "enabled", + "disabled", + "writable", + "readonly" + ], "description": "Recursively mount the source directory." }, "selinux": { "type": "string", - "enum": ["z", "Z"], + "enum": [ + "z", + "Z" + ], "description": "SELinux relabeling options: 'z' for shared content, 'Z' for private unshared content." } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "volume": { "type": "object", @@ -864,7 +792,10 @@ "description": "Labels to apply to the volume." }, "nocopy": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Flag to disable copying of data from a container when a volume is created." }, "subpath": { @@ -873,7 +804,9 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "tmpfs": { "type": "object", @@ -881,18 +814,28 @@ "properties": { "size": { "oneOf": [ - {"type": "integer", "minimum": 0}, - {"type": "string"} + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string" + } ], "description": "Size of the tmpfs mount in bytes." }, "mode": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "File mode of the tmpfs in octal." } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "image": { "type": "object", @@ -904,11 +847,15 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } } ] }, @@ -916,7 +863,9 @@ }, "volumes_from": { "type": "array", - "items": {"type": "string"}, + "items": { + "type": "string" + }, "uniqueItems": true, "description": "Mount volumes from another service or container. Optionally specify read-only access (ro) or read-write (rw)." }, @@ -925,16 +874,273 @@ "description": "The working directory in which the entrypoint or command will be run" } }, - "patternProperties": {"^x-": {}}, - "additionalProperties": false + "patternProperties": { + "^x-": {} + } + }, + "service": { + "description": "Configuration for a service.", + "allOf": [ + { + "$ref": "#/$defs/container_spec" + }, + { + "$ref": "#/$defs/workload_spec" + } + ], + "properties": { + "deploy": { + "$ref": "#/$defs/deployment" + }, + "develop": { + "$ref": "#/$defs/development" + }, + "profiles": { + "$ref": "#/$defs/list_of_strings", + "description": "List of profiles for this service. When profiles are specified, services are only started when the profile is activated." + }, + "restart": { + "type": "string", + "description": "Restart policy for the service container. Options include: 'no', 'always', 'on-failure', and 'unless-stopped'." + }, + "scale": { + "type": [ + "integer", + "string" + ], + "description": "Number of containers to deploy for this service." + }, + "attach": { + "type": [ + "boolean", + "string" + ] + }, + "container_name": { + "type": "string", + "description": "Specify a custom container name, rather than a generated default name.", + "pattern": "[a-zA-Z0-9][a-zA-Z0-9_.-]+" + }, + "provider": { + "type": "object", + "description": "Specify a service which will not be manage by Compose directly, and delegate its management to an external provider.", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "description": "External component used by Compose to manage setup and teardown lifecycle of the service." + }, + "options": { + "type": "object", + "description": "Provider-specific options.", + "patternProperties": { + "^.+$": { + "oneOf": [ + { + "type": [ + "string", + "number", + "boolean" + ] + }, + { + "type": "array", + "items": { + "type": [ + "string", + "number", + "boolean" + ] + } + } + ] + } + } + } + }, + "additionalProperties": false, + "patternProperties": { + "^x-": {} + } + }, + "extends": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "service": { + "type": "string", + "description": "The name of the service to extend." + }, + "file": { + "type": "string", + "description": "The file path where the service to extend is defined." + } + }, + "required": [ + "service" + ], + "additionalProperties": false + } + ], + "description": "Extend another service, in the current file or another file." + }, + "links": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Link to containers in another service. Either specify both the service name and a link alias (SERVICE:ALIAS), or just the service name." + }, + "external_links": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Link to services started outside this Compose application. Specify services as :." + }, + "pre_start": { + "type": "array", + "items": { + "$ref": "#/$defs/pre_start_hook" + }, + "description": "Init containers to run to completion before the service container is started. Each step runs in its own ephemeral container, in declared order; a non-zero exit fails the bring-up of the service and its dependents." + }, + "post_start": { + "type": "array", + "items": { + "$ref": "#/$defs/service_hook" + }, + "description": "Commands to run after the container starts. If any command fails, the container stops." + }, + "pre_stop": { + "type": "array", + "items": { + "$ref": "#/$defs/service_hook" + }, + "description": "Commands to run before the container stops. If any command fails, the container stop is aborted." + } + }, + "unevaluatedProperties": false + }, + "job": { + "description": "Configuration for a job. Jobs are containers that run to completion.", + "allOf": [ + { + "$ref": "#/$defs/container_spec" + }, + { + "$ref": "#/$defs/workload_spec" + } + ], + "required": [ + "triggers" + ], + "properties": { + "profiles": { + "$ref": "#/$defs/list_of_strings", + "description": "List of profiles for this job. When profiles are specified, the job is only active when the profile is activated." + }, + "triggers": { + "type": "object", + "description": "Trigger conditions for the job. At least one trigger attribute must be declared. Setting manual to false forbids manual execution by an explicit run command.", + "properties": { + "manual": { + "type": [ + "boolean", + "string" + ], + "description": "Whether the job can be triggered manually by an explicit run command. Defaults to true; an explicit false forbids manual execution." + }, + "schedule": { + "type": "array", + "description": "List of schedules for the job.", + "items": { + "oneOf": [ + { + "type": "string", + "description": "Crontab expression to schedule the job (e.g. '0 * * * *' for every hour)." + }, + { + "$ref": "#/$defs/schedule" + } + ] + } + } + }, + "anyOf": [ + { + "required": [ + "manual" + ] + }, + { + "required": [ + "schedule" + ] + } + ], + "additionalProperties": false, + "patternProperties": { + "^x-": {} + } + } + }, + "unevaluatedProperties": false + }, + "schedule": { + "type": "object", + "description": "Schedule configuration for a job trigger.", + "required": [ + "cron" + ], + "properties": { + "cron": { + "type": "string", + "description": "Crontab expression to schedule the job (e.g. '0 * * * *' for every hour)." + }, + "timezone": { + "type": "string", + "description": "Timezone used to evaluate the cron expression (e.g. 'Europe/Paris'). Defaults to the platform's local timezone." + }, + "concurrency": { + "type": "string", + "enum": [ + "forbid", + "queue" + ], + "description": "Policy applied when the schedule fires while a previous run is still in progress: prevent the new run ('forbid', the default) or queue it ('queue')." + }, + "missed_fires": { + "type": "string", + "enum": [ + "one", + "skip" + ], + "description": "Policy applied to fires missed while the platform was unavailable: run a single catch-up ('one', the default) or skip them ('skip')." + } + }, + "additionalProperties": false, + "patternProperties": { + "^x-": {} + } }, - "healthcheck": { "type": "object", "description": "Configuration options to determine whether the container is healthy.", "properties": { "disable": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Disable any container-specified healthcheck. Set to true to disable." }, "interval": { @@ -942,13 +1148,23 @@ "description": "Time between running the check (e.g., '1s', '1m30s'). Default: 30s." }, "retries": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Number of consecutive failures needed to consider the container as unhealthy. Default: 3." }, "test": { "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } ], "description": "The test to perform to check container health. Can be a string or a list. The first item is either NONE, CMD, or CMD-SHELL. If it's CMD, the rest of the command is exec'd. If it's CMD-SHELL, the rest is run in the shell." }, @@ -966,10 +1182,15 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "development": { - "type": ["object", "null"], + "type": [ + "object", + "null" + ], "description": "Development configuration for the service, used for development workflows.", "properties": { "watch": { @@ -977,7 +1198,10 @@ "description": "Configure watch mode for the service, which monitors file changes and performs actions in response.", "items": { "type": "object", - "required": ["path", "action"], + "required": [ + "path", + "action" + ], "properties": { "ignore": { "$ref": "#/$defs/string_or_list", @@ -993,7 +1217,13 @@ }, "action": { "type": "string", - "enum": ["rebuild", "sync", "restart", "sync+restart", "sync+exec"], + "enum": [ + "rebuild", + "sync", + "restart", + "sync+restart", + "sync+exec" + ], "description": "Action to take when a change is detected: rebuild the container, sync files, restart the container, sync and restart, or sync and execute a command." }, "target": { @@ -1010,15 +1240,22 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } } } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "deployment": { - "type": ["object", "null"], + "type": [ + "object", + "null" + ], "description": "Deployment configuration for the service.", "properties": { "mode": { @@ -1030,7 +1267,10 @@ "description": "Endpoint mode for the service: 'vip' (default) or 'dnsrr'." }, "replicas": { - "type": ["integer", "string"], + "type": [ + "integer", + "string" + ], "description": "Number of replicas of the service container to run." }, "labels": { @@ -1042,7 +1282,10 @@ "description": "Configuration for rolling back a service update.", "properties": { "parallelism": { - "type": ["integer", "string"], + "type": [ + "integer", + "string" + ], "description": "The number of containers to rollback at a time. If set to 0, all containers rollback simultaneously." }, "delay": { @@ -1058,24 +1301,35 @@ "description": "Duration to monitor each task for failures after it is created (e.g., '1s', '1m30s')." }, "max_failure_ratio": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Failure rate to tolerate during a rollback." }, "order": { "type": "string", - "enum": ["start-first", "stop-first"], + "enum": [ + "start-first", + "stop-first" + ], "description": "Order of operations during rollbacks: 'stop-first' (default) or 'start-first'." } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "update_config": { "type": "object", "description": "Configuration for updating a service.", "properties": { "parallelism": { - "type": ["integer", "string"], + "type": [ + "integer", + "string" + ], "description": "The number of containers to update at a time." }, "delay": { @@ -1091,17 +1345,25 @@ "description": "Duration to monitor each updated task for failures after it is created (e.g., '1s', '1m30s')." }, "max_failure_ratio": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Failure rate to tolerate during an update (0 to 1)." }, "order": { "type": "string", - "enum": ["start-first", "stop-first"], + "enum": [ + "start-first", + "stop-first" + ], "description": "Order of operations during updates: 'stop-first' (default) or 'start-first'." } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "resources": { "type": "object", @@ -1112,7 +1374,10 @@ "description": "Resource limits for the service containers.", "properties": { "cpus": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Limit for how much of the available CPU resources, as number of cores, a container can use." }, "memory": { @@ -1120,19 +1385,27 @@ "description": "Limit on the amount of memory a container can allocate (e.g., '1g', '1024m')." }, "pids": { - "type": ["integer", "string"], + "type": [ + "integer", + "string" + ], "description": "Maximum number of PIDs available to the container." } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "reservations": { "type": "object", "description": "Resource reservations for the service containers.", "properties": { "cpus": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Reservation for how much of the available CPU resources, as number of cores, a container can use." }, "memory": { @@ -1149,11 +1422,15 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "restart_policy": { "type": "object", @@ -1168,7 +1445,10 @@ "description": "Delay between restart attempts (e.g., '1s', '1m30s')." }, "max_attempts": { - "type": ["integer", "string"], + "type": [ + "integer", + "string" + ], "description": "Maximum number of restart attempts before giving up." }, "window": { @@ -1177,7 +1457,9 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "placement": { "type": "object", @@ -1185,7 +1467,9 @@ "properties": { "constraints": { "type": "array", - "items": {"type": "string"}, + "items": { + "type": "string" + }, "description": "Placement constraints for the service (e.g., 'node.role==manager')." }, "preferences": { @@ -1200,22 +1484,30 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } } }, "max_replicas_per_node": { - "type": ["integer", "string"], + "type": [ + "integer", + "string" + ], "description": "Maximum number of replicas of the service." } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, - "generic_resources": { "type": "array", "description": "User-defined resources for services, allowing services to reserve specialized hardware resources.", @@ -1231,19 +1523,25 @@ "description": "Type of resource (e.g., 'GPU', 'FPGA', 'SSD')." }, "value": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "Number of resources of this kind to reserve." } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } } }, - "devices": { "type": "array", "description": "Device reservations for containers, allowing services to access specific hardware devices.", @@ -1255,7 +1553,10 @@ "description": "List of capabilities the device needs to have (e.g., 'gpu', 'compute', 'utility')." }, "count": { - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "description": "Number of devices of this type to reserve." }, "device_ids": { @@ -1272,18 +1573,21 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}}, + "patternProperties": { + "^x-": {} + }, "required": [ "capabilities" ] } }, - "gpus": { "oneOf": [ { "type": "string", - "enum": ["all"], + "enum": [ + "all" + ], "description": "Use all available GPUs." }, { @@ -1297,7 +1601,10 @@ "description": "List of capabilities the GPU needs to have (e.g., 'compute', 'utility')." }, "count": { - "type": ["string", "integer"], + "type": [ + "string", + "integer" + ], "description": "Number of GPUs to use." }, "device_ids": { @@ -1315,15 +1622,18 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } } ] }, - "include": { "description": "Compose application or sub-projects to be included.", "oneOf": [ - {"type": "string"}, + { + "type": "string" + }, { "type": "object", "properties": { @@ -1344,9 +1654,11 @@ } ] }, - "network": { - "type": ["object", "null"], + "type": [ + "object", + "null" + ], "description": "Network configuration for the Compose application.", "properties": { "name": { @@ -1361,7 +1673,12 @@ "type": "object", "description": "Specify driver-specific options defined as key/value pairs.", "patternProperties": { - "^.+$": {"type": ["string", "number"]} + "^.+$": { + "type": [ + "string", + "number" + ] + } } }, "ipam": { @@ -1394,25 +1711,41 @@ "type": "object", "description": "Auxiliary IPv4 or IPv6 addresses used by Network driver.", "additionalProperties": false, - "patternProperties": {"^.+$": {"type": "string"}} + "patternProperties": { + "^.+$": { + "type": "string" + } + } } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } } }, "options": { "type": "object", "description": "Driver-specific options for the IPAM driver.", "additionalProperties": false, - "patternProperties": {"^.+$": {"type": "string"}} + "patternProperties": { + "^.+$": { + "type": "string" + } + } } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "external": { - "type": ["boolean", "string", "object"], + "type": [ + "boolean", + "string", + "object" + ], "description": "Specifies that this network already exists and was created outside of Compose.", "properties": { "name": { @@ -1422,22 +1755,36 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "internal": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Create an externally isolated network." }, "enable_ipv4": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Enable IPv4 networking." }, "enable_ipv6": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Enable IPv6 networking." }, "attachable": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "If true, standalone containers can attach to this network." }, "labels": { @@ -1446,11 +1793,15 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, - "volume": { - "type": ["object", "null"], + "type": [ + "object", + "null" + ], "description": "Volume configuration for the Compose application.", "properties": { "name": { @@ -1465,11 +1816,20 @@ "type": "object", "description": "Specify driver-specific options.", "patternProperties": { - "^.+$": {"type": ["string", "number"]} + "^.+$": { + "type": [ + "string", + "number" + ] + } } }, "external": { - "type": ["boolean", "string", "object"], + "type": [ + "boolean", + "string", + "object" + ], "description": "Specifies that this volume already exists and was created outside of Compose.", "properties": { "name": { @@ -1479,7 +1839,9 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, "labels": { "$ref": "#/$defs/list_or_dict", @@ -1487,9 +1849,10 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, - "secret": { "type": "object", "description": "Secret configuration for the Compose application.", @@ -1507,7 +1870,11 @@ "description": "Path to a file containing the secret value." }, "external": { - "type": ["boolean", "string", "object"], + "type": [ + "boolean", + "string", + "object" + ], "description": "Specifies that this secret already exists and was created outside of Compose.", "properties": { "name": { @@ -1528,7 +1895,12 @@ "type": "object", "description": "Specify driver-specific options.", "patternProperties": { - "^.+$": {"type": ["string", "number"]} + "^.+$": { + "type": [ + "string", + "number" + ] + } } }, "template_driver": { @@ -1537,9 +1909,10 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, - "config": { "type": "object", "description": "Config configuration for the Compose application.", @@ -1561,7 +1934,11 @@ "description": "Path to a file containing the config value." }, "external": { - "type": ["boolean", "string", "object"], + "type": [ + "boolean", + "string", + "object" + ], "description": "Specifies that this config already exists and was created outside of Compose.", "properties": { "name": { @@ -1581,9 +1958,10 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, - "model": { "type": "object", "description": "Language Model for the Compose application.", @@ -1601,15 +1979,20 @@ }, "runtime_flags": { "type": "array", - "items": {"type": "string"}, + "items": { + "type": "string" + }, "description": "Raw runtime flags to pass to the inference engine." } }, - "required": ["model"], + "required": [ + "model" + ], "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } }, - "command": { "oneOf": [ { @@ -1631,7 +2014,6 @@ ], "description": "Command to run in the container, which can be specified as a string (shell form) or array (exec form)." }, - "service_hook": { "type": "object", "description": "Configuration for service lifecycle hooks, which are commands executed at specific points in a container's lifecycle.", @@ -1645,7 +2027,10 @@ "description": "User to run the command as." }, "privileged": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "description": "Whether to run the command with extended privileges." }, "working_dir": { @@ -1658,47 +2043,32 @@ } }, "additionalProperties": false, - "patternProperties": {"^x-": {}}, - "required": ["command"] + "patternProperties": { + "^x-": {} + }, + "required": [ + "command" + ] }, - "pre_start_hook": { "type": "object", - "description": "Configuration for a pre_start init container, run to completion before the service container starts.", + "description": "Configuration for a pre_start init container, run to completion before the service container starts. Accepts the full container specification; per #656, attributes not set explicitly are inherited from the service: collection attributes are completed by the hook's declarations (which win on conflicts), scalar attributes are replaced.", + "allOf": [ + { + "$ref": "#/$defs/container_spec" + } + ], + "unevaluatedProperties": false, "properties": { - "command": { - "$ref": "#/$defs/command", - "description": "Command to execute. Optional when the chosen image's entrypoint already runs the intended command." - }, - "image": { - "type": "string", - "description": "Image used for the ephemeral container. If omitted, the parent service's image is used." - }, - "user": { - "type": "string", - "description": "User to run the command as. Defaults to the user declared in image (or to the service's user when image is omitted)." - }, - "privileged": { - "type": ["boolean", "string"], - "description": "Whether to run the command with extended privileges." - }, - "working_dir": { - "type": "string", - "description": "Working directory for the command. Defaults to the service's working directory." - }, - "environment": { - "$ref": "#/$defs/list_or_dict", - "description": "Environment variables for the command. Appended to or overriding the service environment." - }, "per_replica": { - "type": ["boolean", "string"], - "description": "Whether the hook runs once per service replica (true), or once for the service as a whole before any replica starts (false, the default)." + "type": [ + "boolean", + "string" + ], + "description": "When true, the hook runs once per service replica instead of once per service." } - }, - "additionalProperties": false, - "patternProperties": {"^x-": {}} + } }, - "env_file": { "oneOf": [ { @@ -1728,7 +2098,10 @@ "description": "Format attribute lets you to use an alternative file formats for env_file. When not set, env_file is parsed according to Compose rules." }, "required": { - "type": ["boolean", "string"], + "type": [ + "boolean", + "string" + ], "default": true, "description": "Whether the file is required. If true and the file doesn't exist, an error will be raised." } @@ -1742,7 +2115,6 @@ } ] }, - "label_file": { "oneOf": [ { @@ -1759,7 +2131,6 @@ } ] }, - "string_or_list": { "oneOf": [ { @@ -1773,7 +2144,6 @@ ], "description": "Either a single string or a list of strings." }, - "list_of_strings": { "type": "array", "description": "A list of unique string values.", @@ -1783,7 +2153,6 @@ }, "uniqueItems": true }, - "list_or_dict": { "oneOf": [ { @@ -1791,7 +2160,12 @@ "description": "A dictionary mapping keys to values.", "patternProperties": { ".+": { - "type": ["string", "number", "boolean", "null"], + "type": [ + "string", + "number", + "boolean", + "null" + ], "description": "Value for the key, which can be a string, number, boolean, or null." } }, @@ -1809,7 +2183,6 @@ ], "description": "Either a dictionary mapping keys to values, or a list of strings." }, - "extra_hosts": { "oneOf": [ { @@ -1848,7 +2221,6 @@ ], "description": "Additional hostnames to be defined in the container's /etc/hosts file." }, - "blkio_limit": { "type": "object", "description": "Block IO limit for a specific device.", @@ -1858,7 +2230,10 @@ "description": "Path to the device (e.g., '/dev/sda')." }, "rate": { - "type": ["integer", "string"], + "type": [ + "integer", + "string" + ], "description": "Rate limit in bytes per second or IO operations per second." } }, @@ -1873,7 +2248,10 @@ "description": "Path to the device (e.g., '/dev/sda')." }, "weight": { - "type": ["integer", "string"], + "type": [ + "integer", + "string" + ], "description": "Relative weight for the device, between 10 and 1000." } }, @@ -1909,12 +2287,17 @@ "description": "GID of the file in the container. Default is 0 (root)." }, "mode": { - "type": ["number", "string"], + "type": [ + "number", + "string" + ], "description": "File permission mode inside the container, in octal. Default is 0444 for configs and 0400 for secrets." } }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } } ] } @@ -1926,7 +2309,10 @@ "^[a-z]+$": { "oneOf": [ { - "type": ["integer", "string"], + "type": [ + "integer", + "string" + ], "description": "Single value for both soft and hard limits." }, { @@ -1934,21 +2320,326 @@ "description": "Separate soft and hard limits.", "properties": { "hard": { - "type": ["integer", "string"], + "type": [ + "integer", + "string" + ], "description": "Hard limit for the ulimit type. This is the maximum allowed value." }, "soft": { - "type": ["integer", "string"], + "type": [ + "integer", + "string" + ], "description": "Soft limit for the ulimit type. This is the value that's actually enforced." } }, - "required": ["soft", "hard"], + "required": [ + "soft", + "hard" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": {} + } + } + ] + } + } + }, + "workload_spec": { + "type": "object", + "description": "Container attributes meaningful for orchestrated workloads (services and jobs) but not for run-to-completion init containers: build, dependency ordering, health reporting, port exposure and interactivity.", + "properties": { + "build": { + "description": "Configuration options for building the service's image.", + "oneOf": [ + { + "type": "string", + "description": "Path to the build context. Can be a relative path or a URL." + }, + { + "type": "object", + "properties": { + "context": { + "type": "string", + "description": "Path to the build context. Can be a relative path or a URL." + }, + "dockerfile": { + "type": "string", + "description": "Name of the Dockerfile to use for building the image." + }, + "dockerfile_inline": { + "type": "string", + "description": "Inline Dockerfile content to use instead of a Dockerfile from the build context." + }, + "entitlements": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of extra privileged entitlements to grant to the build process." + }, + "args": { + "$ref": "#/$defs/list_or_dict", + "description": "Build-time variables, specified as a map or a list of KEY=VAL pairs." + }, + "ssh": { + "$ref": "#/$defs/list_or_dict", + "description": "SSH agent socket or keys to expose to the build. Format is either a string or a list of 'default|[=|[,]]'." + }, + "labels": { + "$ref": "#/$defs/list_or_dict", + "description": "Labels to apply to the built image." + }, + "cache_from": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of sources the image builder should use for cache resolution" + }, + "cache_to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Cache destinations for the build cache." + }, + "no_cache": { + "type": [ + "boolean", + "string" + ], + "description": "Do not use cache when building the image." + }, + "no_cache_filter": { + "$ref": "#/$defs/string_or_list", + "description": "Do not use build cache for the specified stages." + }, + "additional_contexts": { + "$ref": "#/$defs/list_or_dict", + "description": "Additional build contexts to use, specified as a map of name to context path or URL." + }, + "network": { + "type": "string", + "description": "Network mode to use for the build. Options include 'default', 'none', 'host', or a network name." + }, + "provenance": { + "type": [ + "string", + "boolean" + ], + "description": "Add a provenance attestation" + }, + "sbom": { + "type": [ + "string", + "boolean" + ], + "description": "Add a SBOM attestation" + }, + "pull": { + "type": [ + "boolean", + "string" + ], + "description": "Always attempt to pull a newer version of the image." + }, + "target": { + "type": "string", + "description": "Build stage to target in a multi-stage Dockerfile." + }, + "shm_size": { + "type": [ + "integer", + "string" + ], + "description": "Size of /dev/shm for the build container. A string value can use suffix like '2g' for 2 gigabytes." + }, + "extra_hosts": { + "$ref": "#/$defs/extra_hosts", + "description": "Add hostname mappings for the build container." + }, + "isolation": { + "type": "string", + "description": "Container isolation technology to use for the build process." + }, + "privileged": { + "type": [ + "boolean", + "string" + ], + "description": "Give extended privileges to the build container." + }, + "secrets": { + "$ref": "#/$defs/service_config_or_secret", + "description": "Secrets to expose to the build. These are accessible at build-time." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional tags to apply to the built image." + }, + "ulimits": { + "$ref": "#/$defs/ulimits", + "description": "Override the default ulimits for the build container." + }, + "platforms": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Platforms to build for, e.g., 'linux/amd64', 'linux/arm64', or 'windows/amd64'." + } + }, "additionalProperties": false, - "patternProperties": {"^x-": {}} + "patternProperties": { + "^x-": {} + } } ] + }, + "depends_on": { + "oneOf": [ + { + "$ref": "#/$defs/list_of_strings" + }, + { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-": {} + }, + "properties": { + "restart": { + "type": [ + "boolean", + "string" + ], + "description": "Whether to restart dependent services when this service is restarted." + }, + "required": { + "type": "boolean", + "default": true, + "description": "Whether the dependency is required for the dependent service to start." + }, + "condition": { + "type": "string", + "enum": [ + "service_started", + "service_healthy", + "service_completed_successfully" + ], + "description": "Condition to wait for. 'service_started' waits until the service has started, 'service_healthy' waits until the service is healthy (as defined by its healthcheck), 'service_completed_successfully' waits until the service has completed successfully." + } + }, + "required": [ + "condition" + ] + } + } + } + ], + "description": "Express dependency between services. Service dependencies cause services to be started in dependency order. The dependent service will wait for the dependency to be ready before starting." + }, + "healthcheck": { + "$ref": "#/$defs/healthcheck", + "description": "Configure a health check for the container to monitor its health status." + }, + "ports": { + "type": "array", + "description": "Expose container ports. Short format ([HOST:]CONTAINER[/PROTOCOL]).", + "items": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "A human-readable name for this port mapping." + }, + "mode": { + "type": "string", + "description": "The port binding mode, either 'host' for publishing a host port or 'ingress' for load balancing." + }, + "host_ip": { + "type": "string", + "description": "The host IP to bind to." + }, + "target": { + "type": [ + "integer", + "string" + ], + "description": "The port inside the container." + }, + "published": { + "type": [ + "string", + "integer" + ], + "description": "The publicly exposed port." + }, + "protocol": { + "type": "string", + "description": "The port protocol (tcp or udp)." + }, + "app_protocol": { + "type": "string", + "description": "Application protocol to use with the port (e.g., http, https, mysql)." + } + }, + "additionalProperties": false, + "patternProperties": { + "^x-": {} + } + } + ] + }, + "uniqueItems": true + }, + "expose": { + "type": "array", + "items": { + "type": [ + "string", + "number" + ] + }, + "uniqueItems": true, + "description": "Expose ports without publishing them to the host machine - they'll only be accessible to linked services." + }, + "stdin_open": { + "type": [ + "boolean", + "string" + ], + "description": "Keep STDIN open even if not attached." + }, + "tty": { + "type": [ + "boolean", + "string" + ], + "description": "Allocate a pseudo-TTY to service container." } + }, + "patternProperties": { + "^x-": {} } } } -} +} \ No newline at end of file diff --git a/transform/canonical.go b/transform/canonical.go index d0525f022..65f59f9d9 100644 --- a/transform/canonical.go +++ b/transform/canonical.go @@ -28,28 +28,36 @@ type Func func(data any, p tree.Path, ignoreParseError bool) (any, error) var transformers = map[tree.Path]Func{} func init() { - transformers["services.*"] = transformService - transformers["services.*.build.secrets.*"] = transformFileMount - transformers["services.*.build.provenance"] = transformStringOrX - transformers["services.*.build.sbom"] = transformStringOrX - transformers["services.*.build.additional_contexts"] = transformKeyValue - transformers["services.*.depends_on"] = transformDependsOn - transformers["services.*.env_file"] = transformEnvFile - transformers["services.*.label_file"] = transformStringOrList - transformers["services.*.extends"] = transformExtends - transformers["services.*.gpus"] = transformGpus - transformers["services.*.networks"] = transformStringSliceToMap - transformers["services.*.models"] = transformStringSliceToMap - transformers["services.*.volumes.*"] = transformVolumeMount - transformers["services.*.dns"] = transformStringOrList - transformers["services.*.devices.*"] = transformDeviceMapping - transformers["services.*.secrets.*"] = transformFileMount - transformers["services.*.configs.*"] = transformFileMount - transformers["services.*.ports"] = transformPorts - transformers["services.*.build"] = transformBuild - transformers["services.*.build.ssh"] = transformSSH - transformers["services.*.ulimits.*"] = transformUlimits - transformers["services.*.build.ulimits.*"] = transformUlimits + // container_spec-level canonicalizations: shared by anything declaring a + // container — services, jobs, and pre_start init containers + for _, prefix := range []tree.Path{"services.*", "jobs.*", "services.*.pre_start.*"} { + transformers[prefix+".env_file"] = transformEnvFile + transformers[prefix+".label_file"] = transformStringOrList + transformers[prefix+".gpus"] = transformGpus + transformers[prefix+".networks"] = transformStringSliceToMap + transformers[prefix+".models"] = transformStringSliceToMap + transformers[prefix+".volumes.*"] = transformVolumeMount + transformers[prefix+".dns"] = transformStringOrList + transformers[prefix+".devices.*"] = transformDeviceMapping + transformers[prefix+".secrets.*"] = transformFileMount + transformers[prefix+".configs.*"] = transformFileMount + transformers[prefix+".ulimits.*"] = transformUlimits + } + // workload_spec and service-level canonicalizations + for _, prefix := range []tree.Path{"services", "jobs"} { + transformers[prefix+".*"] = transformService + transformers[prefix+".*.build.secrets.*"] = transformFileMount + transformers[prefix+".*.build.provenance"] = transformStringOrX + transformers[prefix+".*.build.sbom"] = transformStringOrX + transformers[prefix+".*.build.additional_contexts"] = transformKeyValue + transformers[prefix+".*.depends_on"] = transformDependsOn + transformers[prefix+".*.extends"] = transformExtends + transformers[prefix+".*.ports"] = transformPorts + transformers[prefix+".*.build"] = transformBuild + transformers[prefix+".*.build.ssh"] = transformSSH + transformers[prefix+".*.build.ulimits.*"] = transformUlimits + } + transformers["jobs.*.triggers.schedule.*"] = transformSchedule transformers["services.*.develop.watch.*.ignore"] = transformStringOrList transformers["services.*.develop.watch.*.include"] = transformStringOrList transformers["volumes.*"] = transformMaybeExternal diff --git a/transform/defaults.go b/transform/defaults.go index b82da6947..f8de3ff8b 100644 --- a/transform/defaults.go +++ b/transform/defaults.go @@ -24,12 +24,16 @@ import ( var DefaultValues = map[tree.Path]Func{} func init() { - DefaultValues["services.*.build"] = defaultBuildContext - DefaultValues["services.*.secrets.*"] = defaultSecretMount - DefaultValues["services.*.ports.*"] = portDefaults + // container_spec-level defaults, applied wherever a container is declared + for _, prefix := range []tree.Path{"services", "jobs"} { + DefaultValues[prefix+".*.build"] = defaultBuildContext + DefaultValues[prefix+".*.secrets.*"] = defaultSecretMount + DefaultValues[prefix+".*.ports.*"] = portDefaults + DefaultValues[prefix+".*.gpus.*"] = deviceRequestDefaults + DefaultValues[prefix+".*.volumes.*.bind"] = defaultVolumeBind + } + // deploy is service-only DefaultValues["services.*.deploy.resources.reservations.devices.*"] = deviceRequestDefaults - DefaultValues["services.*.gpus.*"] = deviceRequestDefaults - DefaultValues["services.*.volumes.*.bind"] = defaultVolumeBind } // RegisterDefaultValue registers a custom transformer for the given path pattern diff --git a/transform/schedule.go b/transform/schedule.go new file mode 100644 index 000000000..dde1e84d8 --- /dev/null +++ b/transform/schedule.go @@ -0,0 +1,39 @@ +/* + 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 transform + +import ( + "fmt" + + "github.com/compose-spec/compose-go/v2/tree" +) + +// transformSchedule canonicalizes a job trigger schedule entry: a plain +// crontab expression (short syntax) becomes a schedule object declaring only +// `cron`, following the same short/long syntax model as `volumes`. +func transformSchedule(data any, p tree.Path, _ bool) (any, error) { + switch v := data.(type) { + case string: + return map[string]any{ + "cron": v, + }, nil + case map[string]any: + return v, nil + default: + return nil, fmt.Errorf("%s: invalid type %T for schedule entry", p, v) + } +} diff --git a/transform/schedule_test.go b/transform/schedule_test.go new file mode 100644 index 000000000..b07571e9a --- /dev/null +++ b/transform/schedule_test.go @@ -0,0 +1,43 @@ +/* + 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 transform + +import ( + "testing" + + "github.com/compose-spec/compose-go/v2/tree" + "gotest.tools/v3/assert" +) + +func Test_transformSchedule(t *testing.T) { + path := tree.NewPath("jobs.test.triggers.schedule.0") + + // short syntax: a plain crontab expression becomes a schedule object + out, err := transformSchedule("0 3 * * *", path, false) + assert.NilError(t, err) + assert.DeepEqual(t, out, map[string]any{"cron": "0 3 * * *"}) + + // long syntax is preserved as-is + long := map[string]any{"cron": "0 3 * * *", "timezone": "Europe/Paris"} + out, err = transformSchedule(long, path, false) + assert.NilError(t, err) + assert.DeepEqual(t, out, long) + + // invalid entry types are rejected + _, err = transformSchedule(42, path, false) + assert.ErrorContains(t, err, "invalid type int for schedule entry") +} diff --git a/types/config_test.go b/types/config_test.go index 13962fbf2..fa7ab9568 100644 --- a/types/config_test.go +++ b/types/config_test.go @@ -27,24 +27,24 @@ func Test_WithServices(t *testing.T) { Services: Services{ "service_1": ServiceConfig{ Name: "service_1", - DependsOn: map[string]ServiceDependency{ + WorkloadSpec: WorkloadSpec{DependsOn: map[string]ServiceDependency{ "service_3": { Condition: ServiceConditionStarted, Required: true, }, - }, + }}, }, "service_2": ServiceConfig{ Name: "service_2", }, "service_3": ServiceConfig{ Name: "service_3", - DependsOn: map[string]ServiceDependency{ + WorkloadSpec: WorkloadSpec{DependsOn: map[string]ServiceDependency{ "service_2": { Condition: ServiceConditionStarted, Required: true, }, - }, + }}, }, }, } diff --git a/types/derived.gen.go b/types/derived.gen.go index c758d3ff5..6b1d3aa37 100644 --- a/types/derived.gen.go +++ b/types/derived.gen.go @@ -12,33 +12,39 @@ func deriveDeepCopyProject(dst, src *Project) { } else { dst.Services = nil } + if src.Jobs != nil { + dst.Jobs = make(map[string]JobConfig, len(src.Jobs)) + deriveDeepCopy_(dst.Jobs, src.Jobs) + } else { + dst.Jobs = nil + } if src.Networks != nil { dst.Networks = make(map[string]NetworkConfig, len(src.Networks)) - deriveDeepCopy_(dst.Networks, src.Networks) + deriveDeepCopy_1(dst.Networks, src.Networks) } else { dst.Networks = nil } if src.Volumes != nil { dst.Volumes = make(map[string]VolumeConfig, len(src.Volumes)) - deriveDeepCopy_1(dst.Volumes, src.Volumes) + deriveDeepCopy_2(dst.Volumes, src.Volumes) } else { dst.Volumes = nil } if src.Secrets != nil { dst.Secrets = make(map[string]SecretConfig, len(src.Secrets)) - deriveDeepCopy_2(dst.Secrets, src.Secrets) + deriveDeepCopy_3(dst.Secrets, src.Secrets) } else { dst.Secrets = nil } if src.Configs != nil { dst.Configs = make(map[string]ConfigObjConfig, len(src.Configs)) - deriveDeepCopy_3(dst.Configs, src.Configs) + deriveDeepCopy_4(dst.Configs, src.Configs) } else { dst.Configs = nil } if src.Models != nil { dst.Models = make(map[string]ModelConfig, len(src.Models)) - deriveDeepCopy_4(dst.Models, src.Models) + deriveDeepCopy_5(dst.Models, src.Models) } else { dst.Models = nil } @@ -68,7 +74,7 @@ func deriveDeepCopyProject(dst, src *Project) { } if src.Environment != nil { dst.Environment = make(map[string]string, len(src.Environment)) - deriveDeepCopy_5(dst.Environment, src.Environment) + deriveDeepCopy_6(dst.Environment, src.Environment) } else { dst.Environment = nil } @@ -78,6 +84,12 @@ func deriveDeepCopyProject(dst, src *Project) { } else { dst.DisabledServices = nil } + if src.DisabledJobs != nil { + dst.DisabledJobs = make(map[string]JobConfig, len(src.DisabledJobs)) + deriveDeepCopy_(dst.DisabledJobs, src.DisabledJobs) + } else { + dst.DisabledJobs = nil + } if src.Profiles == nil { dst.Profiles = nil } else { @@ -119,257 +131,571 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { } copy(dst.Profiles, src.Profiles) } - if src.Annotations != nil { - dst.Annotations = make(map[string]string, len(src.Annotations)) - deriveDeepCopy_5(dst.Annotations, src.Annotations) - } else { - dst.Annotations = nil - } - if src.Attach == nil { - dst.Attach = nil - } else { - dst.Attach = new(bool) - *dst.Attach = *src.Attach - } - if src.Build == nil { - dst.Build = nil + if src.Deploy == nil { + dst.Deploy = nil } else { - dst.Build = new(BuildConfig) - deriveDeepCopy_6(dst.Build, src.Build) + dst.Deploy = new(DeployConfig) + deriveDeepCopy_7(dst.Deploy, src.Deploy) } if src.Develop == nil { dst.Develop = nil } else { dst.Develop = new(DevelopConfig) - deriveDeepCopy_7(dst.Develop, src.Develop) - } - if src.BlkioConfig == nil { - dst.BlkioConfig = nil - } else { - dst.BlkioConfig = new(BlkioConfig) - deriveDeepCopy_8(dst.BlkioConfig, src.BlkioConfig) - } - if src.CapAdd == nil { - dst.CapAdd = nil - } else { - if dst.CapAdd != nil { - if len(src.CapAdd) > len(dst.CapAdd) { - if cap(dst.CapAdd) >= len(src.CapAdd) { - dst.CapAdd = (dst.CapAdd)[:len(src.CapAdd)] - } else { - dst.CapAdd = make([]string, len(src.CapAdd)) - } - } else if len(src.CapAdd) < len(dst.CapAdd) { - dst.CapAdd = (dst.CapAdd)[:len(src.CapAdd)] - } - } else { - dst.CapAdd = make([]string, len(src.CapAdd)) - } - copy(dst.CapAdd, src.CapAdd) - } - if src.CapDrop == nil { - dst.CapDrop = nil - } else { - if dst.CapDrop != nil { - if len(src.CapDrop) > len(dst.CapDrop) { - if cap(dst.CapDrop) >= len(src.CapDrop) { - dst.CapDrop = (dst.CapDrop)[:len(src.CapDrop)] - } else { - dst.CapDrop = make([]string, len(src.CapDrop)) - } - } else if len(src.CapDrop) < len(dst.CapDrop) { - dst.CapDrop = (dst.CapDrop)[:len(src.CapDrop)] - } - } else { - dst.CapDrop = make([]string, len(src.CapDrop)) - } - copy(dst.CapDrop, src.CapDrop) + deriveDeepCopy_8(dst.Develop, src.Develop) } - dst.CgroupParent = src.CgroupParent - dst.Cgroup = src.Cgroup - dst.CPUCount = src.CPUCount - dst.CPUPercent = src.CPUPercent - dst.CPUPeriod = src.CPUPeriod - dst.CPUQuota = src.CPUQuota - dst.CPURTPeriod = src.CPURTPeriod - dst.CPURTRuntime = src.CPURTRuntime - dst.CPUS = src.CPUS - dst.CPUSet = src.CPUSet - dst.CPUShares = src.CPUShares - if src.Command == nil { - dst.Command = nil + dst.Restart = src.Restart + if src.Scale == nil { + dst.Scale = nil } else { - if dst.Command != nil { - if len(src.Command) > len(dst.Command) { - if cap(dst.Command) >= len(src.Command) { - dst.Command = (dst.Command)[:len(src.Command)] - } else { - dst.Command = make([]string, len(src.Command)) - } - } else if len(src.Command) < len(dst.Command) { - dst.Command = (dst.Command)[:len(src.Command)] - } - } else { - dst.Command = make([]string, len(src.Command)) - } - copy(dst.Command, src.Command) + dst.Scale = new(int) + *dst.Scale = *src.Scale } - if src.Configs == nil { - dst.Configs = nil + if src.Attach == nil { + dst.Attach = nil } else { - if dst.Configs != nil { - if len(src.Configs) > len(dst.Configs) { - if cap(dst.Configs) >= len(src.Configs) { - dst.Configs = (dst.Configs)[:len(src.Configs)] - } else { - dst.Configs = make([]ServiceConfigObjConfig, len(src.Configs)) - } - } else if len(src.Configs) < len(dst.Configs) { - dst.Configs = (dst.Configs)[:len(src.Configs)] - } - } else { - dst.Configs = make([]ServiceConfigObjConfig, len(src.Configs)) - } - deriveDeepCopy_9(dst.Configs, src.Configs) + dst.Attach = new(bool) + *dst.Attach = *src.Attach } dst.ContainerName = src.ContainerName - if src.CredentialSpec == nil { - dst.CredentialSpec = nil - } else { - dst.CredentialSpec = new(CredentialSpecConfig) - deriveDeepCopy_10(dst.CredentialSpec, src.CredentialSpec) - } - if src.DependsOn != nil { - dst.DependsOn = make(map[string]ServiceDependency, len(src.DependsOn)) - deriveDeepCopy_11(dst.DependsOn, src.DependsOn) + if src.Provider == nil { + dst.Provider = nil } else { - dst.DependsOn = nil + dst.Provider = new(ServiceProviderConfig) + deriveDeepCopy_9(dst.Provider, src.Provider) } - if src.Deploy == nil { - dst.Deploy = nil + if src.Extends == nil { + dst.Extends = nil } else { - dst.Deploy = new(DeployConfig) - deriveDeepCopy_12(dst.Deploy, src.Deploy) + dst.Extends = new(ExtendsConfig) + *dst.Extends = *src.Extends } - if src.DeviceCgroupRules == nil { - dst.DeviceCgroupRules = nil + if src.ExternalLinks == nil { + dst.ExternalLinks = nil } else { - if dst.DeviceCgroupRules != nil { - if len(src.DeviceCgroupRules) > len(dst.DeviceCgroupRules) { - if cap(dst.DeviceCgroupRules) >= len(src.DeviceCgroupRules) { - dst.DeviceCgroupRules = (dst.DeviceCgroupRules)[:len(src.DeviceCgroupRules)] + if dst.ExternalLinks != nil { + if len(src.ExternalLinks) > len(dst.ExternalLinks) { + if cap(dst.ExternalLinks) >= len(src.ExternalLinks) { + dst.ExternalLinks = (dst.ExternalLinks)[:len(src.ExternalLinks)] } else { - dst.DeviceCgroupRules = make([]string, len(src.DeviceCgroupRules)) + dst.ExternalLinks = make([]string, len(src.ExternalLinks)) } - } else if len(src.DeviceCgroupRules) < len(dst.DeviceCgroupRules) { - dst.DeviceCgroupRules = (dst.DeviceCgroupRules)[:len(src.DeviceCgroupRules)] + } else if len(src.ExternalLinks) < len(dst.ExternalLinks) { + dst.ExternalLinks = (dst.ExternalLinks)[:len(src.ExternalLinks)] } } else { - dst.DeviceCgroupRules = make([]string, len(src.DeviceCgroupRules)) + dst.ExternalLinks = make([]string, len(src.ExternalLinks)) } - copy(dst.DeviceCgroupRules, src.DeviceCgroupRules) + copy(dst.ExternalLinks, src.ExternalLinks) } - if src.Devices == nil { - dst.Devices = nil + if src.Links == nil { + dst.Links = nil } else { - if dst.Devices != nil { - if len(src.Devices) > len(dst.Devices) { - if cap(dst.Devices) >= len(src.Devices) { - dst.Devices = (dst.Devices)[:len(src.Devices)] + if dst.Links != nil { + if len(src.Links) > len(dst.Links) { + if cap(dst.Links) >= len(src.Links) { + dst.Links = (dst.Links)[:len(src.Links)] } else { - dst.Devices = make([]DeviceMapping, len(src.Devices)) + dst.Links = make([]string, len(src.Links)) } - } else if len(src.Devices) < len(dst.Devices) { - dst.Devices = (dst.Devices)[:len(src.Devices)] + } else if len(src.Links) < len(dst.Links) { + dst.Links = (dst.Links)[:len(src.Links)] } } else { - dst.Devices = make([]DeviceMapping, len(src.Devices)) + dst.Links = make([]string, len(src.Links)) } - deriveDeepCopy_13(dst.Devices, src.Devices) + copy(dst.Links, src.Links) } - if src.DNS == nil { - dst.DNS = nil + dst.Net = src.Net + if src.PreStart == nil { + dst.PreStart = nil } else { - if dst.DNS != nil { - if len(src.DNS) > len(dst.DNS) { - if cap(dst.DNS) >= len(src.DNS) { - dst.DNS = (dst.DNS)[:len(src.DNS)] + if dst.PreStart != nil { + if len(src.PreStart) > len(dst.PreStart) { + if cap(dst.PreStart) >= len(src.PreStart) { + dst.PreStart = (dst.PreStart)[:len(src.PreStart)] } else { - dst.DNS = make([]string, len(src.DNS)) + dst.PreStart = make([]PreStartHook, len(src.PreStart)) } - } else if len(src.DNS) < len(dst.DNS) { - dst.DNS = (dst.DNS)[:len(src.DNS)] + } else if len(src.PreStart) < len(dst.PreStart) { + dst.PreStart = (dst.PreStart)[:len(src.PreStart)] } } else { - dst.DNS = make([]string, len(src.DNS)) + dst.PreStart = make([]PreStartHook, len(src.PreStart)) } - copy(dst.DNS, src.DNS) + deriveDeepCopy_10(dst.PreStart, src.PreStart) } - if src.DNSOpts == nil { - dst.DNSOpts = nil + if src.PostStart == nil { + dst.PostStart = nil } else { - if dst.DNSOpts != nil { - if len(src.DNSOpts) > len(dst.DNSOpts) { - if cap(dst.DNSOpts) >= len(src.DNSOpts) { - dst.DNSOpts = (dst.DNSOpts)[:len(src.DNSOpts)] + if dst.PostStart != nil { + if len(src.PostStart) > len(dst.PostStart) { + if cap(dst.PostStart) >= len(src.PostStart) { + dst.PostStart = (dst.PostStart)[:len(src.PostStart)] } else { - dst.DNSOpts = make([]string, len(src.DNSOpts)) + dst.PostStart = make([]ServiceHook, len(src.PostStart)) } - } else if len(src.DNSOpts) < len(dst.DNSOpts) { - dst.DNSOpts = (dst.DNSOpts)[:len(src.DNSOpts)] + } else if len(src.PostStart) < len(dst.PostStart) { + dst.PostStart = (dst.PostStart)[:len(src.PostStart)] } } else { - dst.DNSOpts = make([]string, len(src.DNSOpts)) + dst.PostStart = make([]ServiceHook, len(src.PostStart)) } - copy(dst.DNSOpts, src.DNSOpts) + deriveDeepCopy_11(dst.PostStart, src.PostStart) } - if src.DNSSearch == nil { - dst.DNSSearch = nil + if src.PreStop == nil { + dst.PreStop = nil } else { - if dst.DNSSearch != nil { - if len(src.DNSSearch) > len(dst.DNSSearch) { - if cap(dst.DNSSearch) >= len(src.DNSSearch) { - dst.DNSSearch = (dst.DNSSearch)[:len(src.DNSSearch)] + if dst.PreStop != nil { + if len(src.PreStop) > len(dst.PreStop) { + if cap(dst.PreStop) >= len(src.PreStop) { + dst.PreStop = (dst.PreStop)[:len(src.PreStop)] } else { - dst.DNSSearch = make([]string, len(src.DNSSearch)) + dst.PreStop = make([]ServiceHook, len(src.PreStop)) } - } else if len(src.DNSSearch) < len(dst.DNSSearch) { - dst.DNSSearch = (dst.DNSSearch)[:len(src.DNSSearch)] + } else if len(src.PreStop) < len(dst.PreStop) { + dst.PreStop = (dst.PreStop)[:len(src.PreStop)] } } else { - dst.DNSSearch = make([]string, len(src.DNSSearch)) + dst.PreStop = make([]ServiceHook, len(src.PreStop)) } - copy(dst.DNSSearch, src.DNSSearch) + deriveDeepCopy_11(dst.PreStop, src.PreStop) } - dst.Dockerfile = src.Dockerfile - dst.DomainName = src.DomainName - if src.Entrypoint == nil { - dst.Entrypoint = nil + func() { + field := new(ContainerSpec) + deriveDeepCopy_12(field, &src.ContainerSpec) + dst.ContainerSpec = *field + }() + func() { + field := new(WorkloadSpec) + deriveDeepCopy_13(field, &src.WorkloadSpec) + dst.WorkloadSpec = *field + }() + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) } else { - if dst.Entrypoint != nil { - if len(src.Entrypoint) > len(dst.Entrypoint) { - if cap(dst.Entrypoint) >= len(src.Entrypoint) { - dst.Entrypoint = (dst.Entrypoint)[:len(src.Entrypoint)] - } else { - dst.Entrypoint = make([]string, len(src.Entrypoint)) - } - } else if len(src.Entrypoint) < len(dst.Entrypoint) { - dst.Entrypoint = (dst.Entrypoint)[:len(src.Entrypoint)] + dst.Extensions = nil + } +} + +// deriveDeepCopy recursively copies the contents of src into dst. +func deriveDeepCopy(dst, src map[string]ServiceConfig) { + for src_key, src_value := range src { + func() { + field := new(ServiceConfig) + deriveDeepCopyService(field, &src_value) + dst[src_key] = *field + }() + } +} + +// deriveDeepCopy_ recursively copies the contents of src into dst. +func deriveDeepCopy_(dst, src map[string]JobConfig) { + for src_key, src_value := range src { + func() { + field := new(JobConfig) + deriveDeepCopy_14(field, &src_value) + dst[src_key] = *field + }() + } +} + +// deriveDeepCopy_1 recursively copies the contents of src into dst. +func deriveDeepCopy_1(dst, src map[string]NetworkConfig) { + for src_key, src_value := range src { + func() { + field := new(NetworkConfig) + deriveDeepCopy_15(field, &src_value) + dst[src_key] = *field + }() + } +} + +// deriveDeepCopy_2 recursively copies the contents of src into dst. +func deriveDeepCopy_2(dst, src map[string]VolumeConfig) { + for src_key, src_value := range src { + func() { + field := new(VolumeConfig) + deriveDeepCopy_16(field, &src_value) + dst[src_key] = *field + }() + } +} + +// deriveDeepCopy_3 recursively copies the contents of src into dst. +func deriveDeepCopy_3(dst, src map[string]SecretConfig) { + for src_key, src_value := range src { + func() { + field := new(SecretConfig) + deriveDeepCopy_17(field, &src_value) + dst[src_key] = *field + }() + } +} + +// deriveDeepCopy_4 recursively copies the contents of src into dst. +func deriveDeepCopy_4(dst, src map[string]ConfigObjConfig) { + for src_key, src_value := range src { + func() { + field := new(ConfigObjConfig) + deriveDeepCopy_18(field, &src_value) + dst[src_key] = *field + }() + } +} + +// deriveDeepCopy_5 recursively copies the contents of src into dst. +func deriveDeepCopy_5(dst, src map[string]ModelConfig) { + for src_key, src_value := range src { + func() { + field := new(ModelConfig) + deriveDeepCopy_19(field, &src_value) + dst[src_key] = *field + }() + } +} + +// deriveDeepCopy_6 recursively copies the contents of src into dst. +func deriveDeepCopy_6(dst, src map[string]string) { + for src_key, src_value := range src { + dst[src_key] = src_value + } +} + +// deriveDeepCopy_7 recursively copies the contents of src into dst. +func deriveDeepCopy_7(dst, src *DeployConfig) { + dst.Mode = src.Mode + if src.Replicas == nil { + dst.Replicas = nil + } else { + dst.Replicas = new(int) + *dst.Replicas = *src.Replicas + } + if src.Labels != nil { + dst.Labels = make(map[string]string, len(src.Labels)) + deriveDeepCopy_6(dst.Labels, src.Labels) + } else { + dst.Labels = nil + } + if src.UpdateConfig == nil { + dst.UpdateConfig = nil + } else { + dst.UpdateConfig = new(UpdateConfig) + deriveDeepCopy_20(dst.UpdateConfig, src.UpdateConfig) + } + if src.RollbackConfig == nil { + dst.RollbackConfig = nil + } else { + dst.RollbackConfig = new(UpdateConfig) + deriveDeepCopy_20(dst.RollbackConfig, src.RollbackConfig) + } + func() { + field := new(Resources) + deriveDeepCopy_21(field, &src.Resources) + dst.Resources = *field + }() + if src.RestartPolicy == nil { + dst.RestartPolicy = nil + } else { + dst.RestartPolicy = new(RestartPolicy) + deriveDeepCopy_22(dst.RestartPolicy, src.RestartPolicy) + } + func() { + field := new(Placement) + deriveDeepCopy_23(field, &src.Placement) + dst.Placement = *field + }() + dst.EndpointMode = src.EndpointMode + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) + } else { + dst.Extensions = nil + } +} + +// deriveDeepCopy_8 recursively copies the contents of src into dst. +func deriveDeepCopy_8(dst, src *DevelopConfig) { + if src.Watch == nil { + dst.Watch = nil + } else { + if dst.Watch != nil { + if len(src.Watch) > len(dst.Watch) { + if cap(dst.Watch) >= len(src.Watch) { + dst.Watch = (dst.Watch)[:len(src.Watch)] + } else { + dst.Watch = make([]Trigger, len(src.Watch)) + } + } else if len(src.Watch) < len(dst.Watch) { + dst.Watch = (dst.Watch)[:len(src.Watch)] + } + } else { + dst.Watch = make([]Trigger, len(src.Watch)) + } + deriveDeepCopy_24(dst.Watch, src.Watch) + } + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) + } else { + dst.Extensions = nil + } +} + +// deriveDeepCopy_9 recursively copies the contents of src into dst. +func deriveDeepCopy_9(dst, src *ServiceProviderConfig) { + dst.Type = src.Type + if src.Options != nil { + dst.Options = make(map[string][]string, len(src.Options)) + deriveDeepCopy_25(dst.Options, src.Options) + } else { + dst.Options = nil + } + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) + } else { + dst.Extensions = nil + } +} + +// deriveDeepCopy_10 recursively copies the contents of src into dst. +func deriveDeepCopy_10(dst, src []PreStartHook) { + for src_i, src_value := range src { + func() { + field := new(PreStartHook) + deriveDeepCopy_26(field, &src_value) + dst[src_i] = *field + }() + } +} + +// deriveDeepCopy_11 recursively copies the contents of src into dst. +func deriveDeepCopy_11(dst, src []ServiceHook) { + for src_i, src_value := range src { + func() { + field := new(ServiceHook) + deriveDeepCopy_27(field, &src_value) + dst[src_i] = *field + }() + } +} + +// deriveDeepCopy_12 recursively copies the contents of src into dst. +func deriveDeepCopy_12(dst, src *ContainerSpec) { + if src.Annotations != nil { + dst.Annotations = make(map[string]string, len(src.Annotations)) + deriveDeepCopy_6(dst.Annotations, src.Annotations) + } else { + dst.Annotations = nil + } + if src.BlkioConfig == nil { + dst.BlkioConfig = nil + } else { + dst.BlkioConfig = new(BlkioConfig) + deriveDeepCopy_28(dst.BlkioConfig, src.BlkioConfig) + } + if src.CapAdd == nil { + dst.CapAdd = nil + } else { + if dst.CapAdd != nil { + if len(src.CapAdd) > len(dst.CapAdd) { + if cap(dst.CapAdd) >= len(src.CapAdd) { + dst.CapAdd = (dst.CapAdd)[:len(src.CapAdd)] + } else { + dst.CapAdd = make([]string, len(src.CapAdd)) + } + } else if len(src.CapAdd) < len(dst.CapAdd) { + dst.CapAdd = (dst.CapAdd)[:len(src.CapAdd)] + } + } else { + dst.CapAdd = make([]string, len(src.CapAdd)) + } + copy(dst.CapAdd, src.CapAdd) + } + if src.CapDrop == nil { + dst.CapDrop = nil + } else { + if dst.CapDrop != nil { + if len(src.CapDrop) > len(dst.CapDrop) { + if cap(dst.CapDrop) >= len(src.CapDrop) { + dst.CapDrop = (dst.CapDrop)[:len(src.CapDrop)] + } else { + dst.CapDrop = make([]string, len(src.CapDrop)) + } + } else if len(src.CapDrop) < len(dst.CapDrop) { + dst.CapDrop = (dst.CapDrop)[:len(src.CapDrop)] + } + } else { + dst.CapDrop = make([]string, len(src.CapDrop)) + } + copy(dst.CapDrop, src.CapDrop) + } + dst.CgroupParent = src.CgroupParent + dst.Cgroup = src.Cgroup + dst.CPUCount = src.CPUCount + dst.CPUPercent = src.CPUPercent + dst.CPUPeriod = src.CPUPeriod + dst.CPUQuota = src.CPUQuota + dst.CPURTPeriod = src.CPURTPeriod + dst.CPURTRuntime = src.CPURTRuntime + dst.CPUS = src.CPUS + dst.CPUSet = src.CPUSet + dst.CPUShares = src.CPUShares + if src.Command == nil { + dst.Command = nil + } else { + if dst.Command != nil { + if len(src.Command) > len(dst.Command) { + if cap(dst.Command) >= len(src.Command) { + dst.Command = (dst.Command)[:len(src.Command)] + } else { + dst.Command = make([]string, len(src.Command)) + } + } else if len(src.Command) < len(dst.Command) { + dst.Command = (dst.Command)[:len(src.Command)] + } + } else { + dst.Command = make([]string, len(src.Command)) + } + copy(dst.Command, src.Command) + } + if src.Configs == nil { + dst.Configs = nil + } else { + if dst.Configs != nil { + if len(src.Configs) > len(dst.Configs) { + if cap(dst.Configs) >= len(src.Configs) { + dst.Configs = (dst.Configs)[:len(src.Configs)] + } else { + dst.Configs = make([]ServiceConfigObjConfig, len(src.Configs)) + } + } else if len(src.Configs) < len(dst.Configs) { + dst.Configs = (dst.Configs)[:len(src.Configs)] + } + } else { + dst.Configs = make([]ServiceConfigObjConfig, len(src.Configs)) + } + deriveDeepCopy_29(dst.Configs, src.Configs) + } + if src.CredentialSpec == nil { + dst.CredentialSpec = nil + } else { + dst.CredentialSpec = new(CredentialSpecConfig) + deriveDeepCopy_30(dst.CredentialSpec, src.CredentialSpec) + } + if src.DeviceCgroupRules == nil { + dst.DeviceCgroupRules = nil + } else { + if dst.DeviceCgroupRules != nil { + if len(src.DeviceCgroupRules) > len(dst.DeviceCgroupRules) { + if cap(dst.DeviceCgroupRules) >= len(src.DeviceCgroupRules) { + dst.DeviceCgroupRules = (dst.DeviceCgroupRules)[:len(src.DeviceCgroupRules)] + } else { + dst.DeviceCgroupRules = make([]string, len(src.DeviceCgroupRules)) + } + } else if len(src.DeviceCgroupRules) < len(dst.DeviceCgroupRules) { + dst.DeviceCgroupRules = (dst.DeviceCgroupRules)[:len(src.DeviceCgroupRules)] + } + } else { + dst.DeviceCgroupRules = make([]string, len(src.DeviceCgroupRules)) + } + copy(dst.DeviceCgroupRules, src.DeviceCgroupRules) + } + if src.Devices == nil { + dst.Devices = nil + } else { + if dst.Devices != nil { + if len(src.Devices) > len(dst.Devices) { + if cap(dst.Devices) >= len(src.Devices) { + dst.Devices = (dst.Devices)[:len(src.Devices)] + } else { + dst.Devices = make([]DeviceMapping, len(src.Devices)) + } + } else if len(src.Devices) < len(dst.Devices) { + dst.Devices = (dst.Devices)[:len(src.Devices)] + } + } else { + dst.Devices = make([]DeviceMapping, len(src.Devices)) + } + deriveDeepCopy_31(dst.Devices, src.Devices) + } + if src.DNS == nil { + dst.DNS = nil + } else { + if dst.DNS != nil { + if len(src.DNS) > len(dst.DNS) { + if cap(dst.DNS) >= len(src.DNS) { + dst.DNS = (dst.DNS)[:len(src.DNS)] + } else { + dst.DNS = make([]string, len(src.DNS)) + } + } else if len(src.DNS) < len(dst.DNS) { + dst.DNS = (dst.DNS)[:len(src.DNS)] + } + } else { + dst.DNS = make([]string, len(src.DNS)) + } + copy(dst.DNS, src.DNS) + } + if src.DNSOpts == nil { + dst.DNSOpts = nil + } else { + if dst.DNSOpts != nil { + if len(src.DNSOpts) > len(dst.DNSOpts) { + if cap(dst.DNSOpts) >= len(src.DNSOpts) { + dst.DNSOpts = (dst.DNSOpts)[:len(src.DNSOpts)] + } else { + dst.DNSOpts = make([]string, len(src.DNSOpts)) + } + } else if len(src.DNSOpts) < len(dst.DNSOpts) { + dst.DNSOpts = (dst.DNSOpts)[:len(src.DNSOpts)] } } else { - dst.Entrypoint = make([]string, len(src.Entrypoint)) + dst.DNSOpts = make([]string, len(src.DNSOpts)) } - copy(dst.Entrypoint, src.Entrypoint) + copy(dst.DNSOpts, src.DNSOpts) } - if src.Provider == nil { - dst.Provider = nil + if src.DNSSearch == nil { + dst.DNSSearch = nil } else { - dst.Provider = new(ServiceProviderConfig) - deriveDeepCopy_14(dst.Provider, src.Provider) + if dst.DNSSearch != nil { + if len(src.DNSSearch) > len(dst.DNSSearch) { + if cap(dst.DNSSearch) >= len(src.DNSSearch) { + dst.DNSSearch = (dst.DNSSearch)[:len(src.DNSSearch)] + } else { + dst.DNSSearch = make([]string, len(src.DNSSearch)) + } + } else if len(src.DNSSearch) < len(dst.DNSSearch) { + dst.DNSSearch = (dst.DNSSearch)[:len(src.DNSSearch)] + } + } else { + dst.DNSSearch = make([]string, len(src.DNSSearch)) + } + copy(dst.DNSSearch, src.DNSSearch) + } + dst.DomainName = src.DomainName + if src.Entrypoint == nil { + dst.Entrypoint = nil + } else { + if dst.Entrypoint != nil { + if len(src.Entrypoint) > len(dst.Entrypoint) { + if cap(dst.Entrypoint) >= len(src.Entrypoint) { + dst.Entrypoint = (dst.Entrypoint)[:len(src.Entrypoint)] + } else { + dst.Entrypoint = make([]string, len(src.Entrypoint)) + } + } else if len(src.Entrypoint) < len(dst.Entrypoint) { + dst.Entrypoint = (dst.Entrypoint)[:len(src.Entrypoint)] + } + } else { + dst.Entrypoint = make([]string, len(src.Entrypoint)) + } + copy(dst.Entrypoint, src.Entrypoint) } if src.Environment != nil { dst.Environment = make(map[string]*string, len(src.Environment)) - deriveDeepCopy_15(dst.Environment, src.Environment) + deriveDeepCopy_32(dst.Environment, src.Environment) } else { dst.Environment = nil } @@ -391,51 +717,9 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { } copy(dst.EnvFiles, src.EnvFiles) } - if src.Expose == nil { - dst.Expose = nil - } else { - if dst.Expose != nil { - if len(src.Expose) > len(dst.Expose) { - if cap(dst.Expose) >= len(src.Expose) { - dst.Expose = (dst.Expose)[:len(src.Expose)] - } else { - dst.Expose = make([]string, len(src.Expose)) - } - } else if len(src.Expose) < len(dst.Expose) { - dst.Expose = (dst.Expose)[:len(src.Expose)] - } - } else { - dst.Expose = make([]string, len(src.Expose)) - } - copy(dst.Expose, src.Expose) - } - if src.Extends == nil { - dst.Extends = nil - } else { - dst.Extends = new(ExtendsConfig) - *dst.Extends = *src.Extends - } - if src.ExternalLinks == nil { - dst.ExternalLinks = nil - } else { - if dst.ExternalLinks != nil { - if len(src.ExternalLinks) > len(dst.ExternalLinks) { - if cap(dst.ExternalLinks) >= len(src.ExternalLinks) { - dst.ExternalLinks = (dst.ExternalLinks)[:len(src.ExternalLinks)] - } else { - dst.ExternalLinks = make([]string, len(src.ExternalLinks)) - } - } else if len(src.ExternalLinks) < len(dst.ExternalLinks) { - dst.ExternalLinks = (dst.ExternalLinks)[:len(src.ExternalLinks)] - } - } else { - dst.ExternalLinks = make([]string, len(src.ExternalLinks)) - } - copy(dst.ExternalLinks, src.ExternalLinks) - } if src.ExtraHosts != nil { dst.ExtraHosts = make(map[string][]string, len(src.ExtraHosts)) - deriveDeepCopy_16(dst.ExtraHosts, src.ExtraHosts) + deriveDeepCopy_25(dst.ExtraHosts, src.ExtraHosts) } else { dst.ExtraHosts = nil } @@ -473,15 +757,9 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { } else { dst.Gpus = make([]DeviceRequest, len(src.Gpus)) } - deriveDeepCopy_17(dst.Gpus, src.Gpus) + deriveDeepCopy_33(dst.Gpus, src.Gpus) } dst.Hostname = src.Hostname - if src.HealthCheck == nil { - dst.HealthCheck = nil - } else { - dst.HealthCheck = new(HealthCheckConfig) - deriveDeepCopy_18(dst.HealthCheck, src.HealthCheck) - } dst.Image = src.Image if src.Init == nil { dst.Init = nil @@ -493,7 +771,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { dst.Isolation = src.Isolation if src.Labels != nil { dst.Labels = make(map[string]string, len(src.Labels)) - deriveDeepCopy_5(dst.Labels, src.Labels) + deriveDeepCopy_6(dst.Labels, src.Labels) } else { dst.Labels = nil } @@ -517,38 +795,20 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { } if src.CustomLabels != nil { dst.CustomLabels = make(map[string]string, len(src.CustomLabels)) - deriveDeepCopy_5(dst.CustomLabels, src.CustomLabels) + deriveDeepCopy_6(dst.CustomLabels, src.CustomLabels) } else { dst.CustomLabels = nil } - if src.Links == nil { - dst.Links = nil - } else { - if dst.Links != nil { - if len(src.Links) > len(dst.Links) { - if cap(dst.Links) >= len(src.Links) { - dst.Links = (dst.Links)[:len(src.Links)] - } else { - dst.Links = make([]string, len(src.Links)) - } - } else if len(src.Links) < len(dst.Links) { - dst.Links = (dst.Links)[:len(src.Links)] - } - } else { - dst.Links = make([]string, len(src.Links)) - } - copy(dst.Links, src.Links) - } if src.Logging == nil { dst.Logging = nil } else { dst.Logging = new(LoggingConfig) - deriveDeepCopy_19(dst.Logging, src.Logging) + deriveDeepCopy_34(dst.Logging, src.Logging) } dst.LogDriver = src.LogDriver if src.LogOpt != nil { dst.LogOpt = make(map[string]string, len(src.LogOpt)) - deriveDeepCopy_5(dst.LogOpt, src.LogOpt) + deriveDeepCopy_6(dst.LogOpt, src.LogOpt) } else { dst.LogOpt = nil } @@ -559,15 +819,14 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { dst.MacAddress = src.MacAddress if src.Models != nil { dst.Models = make(map[string]*ServiceModelConfig, len(src.Models)) - deriveDeepCopy_20(dst.Models, src.Models) + deriveDeepCopy_35(dst.Models, src.Models) } else { dst.Models = nil } - dst.Net = src.Net dst.NetworkMode = src.NetworkMode if src.Networks != nil { dst.Networks = make(map[string]*ServiceNetworkConfig, len(src.Networks)) - deriveDeepCopy_21(dst.Networks, src.Networks) + deriveDeepCopy_36(dst.Networks, src.Networks) } else { dst.Networks = nil } @@ -576,35 +835,11 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { dst.Pid = src.Pid dst.PidsLimit = src.PidsLimit dst.Platform = src.Platform - if src.Ports == nil { - dst.Ports = nil - } else { - if dst.Ports != nil { - if len(src.Ports) > len(dst.Ports) { - if cap(dst.Ports) >= len(src.Ports) { - dst.Ports = (dst.Ports)[:len(src.Ports)] - } else { - dst.Ports = make([]ServicePortConfig, len(src.Ports)) - } - } else if len(src.Ports) < len(dst.Ports) { - dst.Ports = (dst.Ports)[:len(src.Ports)] - } - } else { - dst.Ports = make([]ServicePortConfig, len(src.Ports)) - } - deriveDeepCopy_22(dst.Ports, src.Ports) - } dst.Privileged = src.Privileged dst.PullPolicy = src.PullPolicy + dst.PullRefreshAfter = src.PullRefreshAfter dst.ReadOnly = src.ReadOnly - dst.Restart = src.Restart dst.Runtime = src.Runtime - if src.Scale == nil { - dst.Scale = nil - } else { - dst.Scale = new(int) - *dst.Scale = *src.Scale - } if src.Secrets == nil { dst.Secrets = nil } else { @@ -621,7 +856,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { } else { dst.Secrets = make([]ServiceSecretConfig, len(src.Secrets)) } - deriveDeepCopy_23(dst.Secrets, src.Secrets) + deriveDeepCopy_37(dst.Secrets, src.Secrets) } if src.SecurityOpt == nil { dst.SecurityOpt = nil @@ -642,7 +877,6 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { copy(dst.SecurityOpt, src.SecurityOpt) } dst.ShmSize = src.ShmSize - dst.StdinOpen = src.StdinOpen if src.StopGracePeriod == nil { dst.StopGracePeriod = nil } else { @@ -652,13 +886,13 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { dst.StopSignal = src.StopSignal if src.StorageOpt != nil { dst.StorageOpt = make(map[string]string, len(src.StorageOpt)) - deriveDeepCopy_5(dst.StorageOpt, src.StorageOpt) + deriveDeepCopy_6(dst.StorageOpt, src.StorageOpt) } else { dst.StorageOpt = nil } if src.Sysctls != nil { dst.Sysctls = make(map[string]string, len(src.Sysctls)) - deriveDeepCopy_5(dst.Sysctls, src.Sysctls) + deriveDeepCopy_6(dst.Sysctls, src.Sysctls) } else { dst.Sysctls = nil } @@ -680,10 +914,9 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { } copy(dst.Tmpfs, src.Tmpfs) } - dst.Tty = src.Tty if src.Ulimits != nil { dst.Ulimits = make(map[string]*UlimitsConfig, len(src.Ulimits)) - deriveDeepCopy_24(dst.Ulimits, src.Ulimits) + deriveDeepCopy_38(dst.Ulimits, src.Ulimits) } else { dst.Ulimits = nil } @@ -708,7 +941,7 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { } else { dst.Volumes = make([]ServiceVolumeConfig, len(src.Volumes)) } - deriveDeepCopy_25(dst.Volumes, src.Volumes) + deriveDeepCopy_39(dst.Volumes, src.Volumes) } if src.VolumesFrom == nil { dst.VolumesFrom = nil @@ -729,59 +962,155 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { copy(dst.VolumesFrom, src.VolumesFrom) } dst.WorkingDir = src.WorkingDir - if src.PreStart == nil { - dst.PreStart = nil +} + +// deriveDeepCopy_13 recursively copies the contents of src into dst. +func deriveDeepCopy_13(dst, src *WorkloadSpec) { + if src.Build == nil { + dst.Build = nil } else { - if dst.PreStart != nil { - if len(src.PreStart) > len(dst.PreStart) { - if cap(dst.PreStart) >= len(src.PreStart) { - dst.PreStart = (dst.PreStart)[:len(src.PreStart)] + dst.Build = new(BuildConfig) + deriveDeepCopy_40(dst.Build, src.Build) + } + if src.DependsOn != nil { + dst.DependsOn = make(map[string]ServiceDependency, len(src.DependsOn)) + deriveDeepCopy_41(dst.DependsOn, src.DependsOn) + } else { + dst.DependsOn = nil + } + dst.Dockerfile = src.Dockerfile + if src.Expose == nil { + dst.Expose = nil + } else { + if dst.Expose != nil { + if len(src.Expose) > len(dst.Expose) { + if cap(dst.Expose) >= len(src.Expose) { + dst.Expose = (dst.Expose)[:len(src.Expose)] } else { - dst.PreStart = make([]ServiceHook, len(src.PreStart)) + dst.Expose = make([]string, len(src.Expose)) } - } else if len(src.PreStart) < len(dst.PreStart) { - dst.PreStart = (dst.PreStart)[:len(src.PreStart)] + } else if len(src.Expose) < len(dst.Expose) { + dst.Expose = (dst.Expose)[:len(src.Expose)] } } else { - dst.PreStart = make([]ServiceHook, len(src.PreStart)) + dst.Expose = make([]string, len(src.Expose)) } - deriveDeepCopy_26(dst.PreStart, src.PreStart) + copy(dst.Expose, src.Expose) } - if src.PostStart == nil { - dst.PostStart = nil + if src.HealthCheck == nil { + dst.HealthCheck = nil } else { - if dst.PostStart != nil { - if len(src.PostStart) > len(dst.PostStart) { - if cap(dst.PostStart) >= len(src.PostStart) { - dst.PostStart = (dst.PostStart)[:len(src.PostStart)] + dst.HealthCheck = new(HealthCheckConfig) + deriveDeepCopy_42(dst.HealthCheck, src.HealthCheck) + } + if src.Ports == nil { + dst.Ports = nil + } else { + if dst.Ports != nil { + if len(src.Ports) > len(dst.Ports) { + if cap(dst.Ports) >= len(src.Ports) { + dst.Ports = (dst.Ports)[:len(src.Ports)] } else { - dst.PostStart = make([]ServiceHook, len(src.PostStart)) + dst.Ports = make([]ServicePortConfig, len(src.Ports)) } - } else if len(src.PostStart) < len(dst.PostStart) { - dst.PostStart = (dst.PostStart)[:len(src.PostStart)] + } else if len(src.Ports) < len(dst.Ports) { + dst.Ports = (dst.Ports)[:len(src.Ports)] } } else { - dst.PostStart = make([]ServiceHook, len(src.PostStart)) + dst.Ports = make([]ServicePortConfig, len(src.Ports)) + } + deriveDeepCopy_43(dst.Ports, src.Ports) + } + dst.StdinOpen = src.StdinOpen + dst.Tty = src.Tty +} + +// deriveDeepCopy_14 recursively copies the contents of src into dst. +func deriveDeepCopy_14(dst, src *JobConfig) { + dst.Name = src.Name + if src.Profiles == nil { + dst.Profiles = nil + } else { + if dst.Profiles != nil { + if len(src.Profiles) > len(dst.Profiles) { + if cap(dst.Profiles) >= len(src.Profiles) { + dst.Profiles = (dst.Profiles)[:len(src.Profiles)] + } else { + dst.Profiles = make([]string, len(src.Profiles)) + } + } else if len(src.Profiles) < len(dst.Profiles) { + dst.Profiles = (dst.Profiles)[:len(src.Profiles)] + } + } else { + dst.Profiles = make([]string, len(src.Profiles)) } - deriveDeepCopy_26(dst.PostStart, src.PostStart) + copy(dst.Profiles, src.Profiles) + } + if src.Triggers == nil { + dst.Triggers = nil + } else { + dst.Triggers = new(TriggerConfig) + deriveDeepCopy_44(dst.Triggers, src.Triggers) + } + func() { + field := new(ContainerSpec) + deriveDeepCopy_12(field, &src.ContainerSpec) + dst.ContainerSpec = *field + }() + func() { + field := new(WorkloadSpec) + deriveDeepCopy_13(field, &src.WorkloadSpec) + dst.WorkloadSpec = *field + }() + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) + } else { + dst.Extensions = nil + } +} + +// deriveDeepCopy_15 recursively copies the contents of src into dst. +func deriveDeepCopy_15(dst, src *NetworkConfig) { + dst.Name = src.Name + dst.Driver = src.Driver + if src.DriverOpts != nil { + dst.DriverOpts = make(map[string]string, len(src.DriverOpts)) + deriveDeepCopy_6(dst.DriverOpts, src.DriverOpts) + } else { + dst.DriverOpts = nil + } + func() { + field := new(IPAMConfig) + deriveDeepCopy_45(field, &src.Ipam) + dst.Ipam = *field + }() + dst.External = src.External + dst.Internal = src.Internal + dst.Attachable = src.Attachable + if src.Labels != nil { + dst.Labels = make(map[string]string, len(src.Labels)) + deriveDeepCopy_6(dst.Labels, src.Labels) + } else { + dst.Labels = nil + } + if src.CustomLabels != nil { + dst.CustomLabels = make(map[string]string, len(src.CustomLabels)) + deriveDeepCopy_6(dst.CustomLabels, src.CustomLabels) + } else { + dst.CustomLabels = nil } - if src.PreStop == nil { - dst.PreStop = nil + if src.EnableIPv4 == nil { + dst.EnableIPv4 = nil } else { - if dst.PreStop != nil { - if len(src.PreStop) > len(dst.PreStop) { - if cap(dst.PreStop) >= len(src.PreStop) { - dst.PreStop = (dst.PreStop)[:len(src.PreStop)] - } else { - dst.PreStop = make([]ServiceHook, len(src.PreStop)) - } - } else if len(src.PreStop) < len(dst.PreStop) { - dst.PreStop = (dst.PreStop)[:len(src.PreStop)] - } - } else { - dst.PreStop = make([]ServiceHook, len(src.PreStop)) - } - deriveDeepCopy_26(dst.PreStop, src.PreStop) + dst.EnableIPv4 = new(bool) + *dst.EnableIPv4 = *src.EnableIPv4 + } + if src.EnableIPv6 == nil { + dst.EnableIPv6 = nil + } else { + dst.EnableIPv6 = new(bool) + *dst.EnableIPv6 = *src.EnableIPv6 } if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) @@ -791,267 +1120,293 @@ func deriveDeepCopyService(dst, src *ServiceConfig) { } } -// deriveDeepCopy recursively copies the contents of src into dst. -func deriveDeepCopy(dst, src map[string]ServiceConfig) { - for src_key, src_value := range src { - func() { - field := new(ServiceConfig) - deriveDeepCopyService(field, &src_value) - dst[src_key] = *field - }() +// deriveDeepCopy_16 recursively copies the contents of src into dst. +func deriveDeepCopy_16(dst, src *VolumeConfig) { + dst.Name = src.Name + dst.Driver = src.Driver + if src.DriverOpts != nil { + dst.DriverOpts = make(map[string]string, len(src.DriverOpts)) + deriveDeepCopy_6(dst.DriverOpts, src.DriverOpts) + } else { + dst.DriverOpts = nil } -} - -// deriveDeepCopy_ recursively copies the contents of src into dst. -func deriveDeepCopy_(dst, src map[string]NetworkConfig) { - for src_key, src_value := range src { - func() { - field := new(NetworkConfig) - deriveDeepCopy_27(field, &src_value) - dst[src_key] = *field - }() + dst.External = src.External + if src.Labels != nil { + dst.Labels = make(map[string]string, len(src.Labels)) + deriveDeepCopy_6(dst.Labels, src.Labels) + } else { + dst.Labels = nil } -} - -// deriveDeepCopy_1 recursively copies the contents of src into dst. -func deriveDeepCopy_1(dst, src map[string]VolumeConfig) { - for src_key, src_value := range src { - func() { - field := new(VolumeConfig) - deriveDeepCopy_28(field, &src_value) - dst[src_key] = *field - }() + if src.CustomLabels != nil { + dst.CustomLabels = make(map[string]string, len(src.CustomLabels)) + deriveDeepCopy_6(dst.CustomLabels, src.CustomLabels) + } else { + dst.CustomLabels = nil } -} - -// deriveDeepCopy_2 recursively copies the contents of src into dst. -func deriveDeepCopy_2(dst, src map[string]SecretConfig) { - for src_key, src_value := range src { - func() { - field := new(SecretConfig) - deriveDeepCopy_29(field, &src_value) - dst[src_key] = *field - }() + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) + } else { + dst.Extensions = nil } } -// deriveDeepCopy_3 recursively copies the contents of src into dst. -func deriveDeepCopy_3(dst, src map[string]ConfigObjConfig) { - for src_key, src_value := range src { - func() { - field := new(ConfigObjConfig) - deriveDeepCopy_30(field, &src_value) - dst[src_key] = *field - }() +// deriveDeepCopy_17 recursively copies the contents of src into dst. +func deriveDeepCopy_17(dst, src *SecretConfig) { + dst.Name = src.Name + dst.File = src.File + dst.Environment = src.Environment + dst.Content = src.Content + dst.marshallContent = src.marshallContent + dst.External = src.External + if src.Labels != nil { + dst.Labels = make(map[string]string, len(src.Labels)) + deriveDeepCopy_6(dst.Labels, src.Labels) + } else { + dst.Labels = nil } -} - -// deriveDeepCopy_4 recursively copies the contents of src into dst. -func deriveDeepCopy_4(dst, src map[string]ModelConfig) { - for src_key, src_value := range src { - func() { - field := new(ModelConfig) - deriveDeepCopy_31(field, &src_value) - dst[src_key] = *field - }() + dst.Driver = src.Driver + if src.DriverOpts != nil { + dst.DriverOpts = make(map[string]string, len(src.DriverOpts)) + deriveDeepCopy_6(dst.DriverOpts, src.DriverOpts) + } else { + dst.DriverOpts = nil + } + dst.TemplateDriver = src.TemplateDriver + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) + } else { + dst.Extensions = nil } } -// deriveDeepCopy_5 recursively copies the contents of src into dst. -func deriveDeepCopy_5(dst, src map[string]string) { - for src_key, src_value := range src { - dst[src_key] = src_value +// deriveDeepCopy_18 recursively copies the contents of src into dst. +func deriveDeepCopy_18(dst, src *ConfigObjConfig) { + dst.Name = src.Name + dst.File = src.File + dst.Environment = src.Environment + dst.Content = src.Content + dst.marshallContent = src.marshallContent + dst.External = src.External + if src.Labels != nil { + dst.Labels = make(map[string]string, len(src.Labels)) + deriveDeepCopy_6(dst.Labels, src.Labels) + } else { + dst.Labels = nil + } + dst.Driver = src.Driver + if src.DriverOpts != nil { + dst.DriverOpts = make(map[string]string, len(src.DriverOpts)) + deriveDeepCopy_6(dst.DriverOpts, src.DriverOpts) + } else { + dst.DriverOpts = nil + } + dst.TemplateDriver = src.TemplateDriver + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) + } else { + dst.Extensions = nil } } -// deriveDeepCopy_6 recursively copies the contents of src into dst. -func deriveDeepCopy_6(dst, src *BuildConfig) { - dst.Context = src.Context - dst.Dockerfile = src.Dockerfile - dst.DockerfileInline = src.DockerfileInline - if src.Entitlements == nil { - dst.Entitlements = nil +// deriveDeepCopy_19 recursively copies the contents of src into dst. +func deriveDeepCopy_19(dst, src *ModelConfig) { + dst.Name = src.Name + dst.Model = src.Model + dst.ContextSize = src.ContextSize + if src.RuntimeFlags == nil { + dst.RuntimeFlags = nil } else { - if dst.Entitlements != nil { - if len(src.Entitlements) > len(dst.Entitlements) { - if cap(dst.Entitlements) >= len(src.Entitlements) { - dst.Entitlements = (dst.Entitlements)[:len(src.Entitlements)] + if dst.RuntimeFlags != nil { + if len(src.RuntimeFlags) > len(dst.RuntimeFlags) { + if cap(dst.RuntimeFlags) >= len(src.RuntimeFlags) { + dst.RuntimeFlags = (dst.RuntimeFlags)[:len(src.RuntimeFlags)] } else { - dst.Entitlements = make([]string, len(src.Entitlements)) + dst.RuntimeFlags = make([]string, len(src.RuntimeFlags)) } - } else if len(src.Entitlements) < len(dst.Entitlements) { - dst.Entitlements = (dst.Entitlements)[:len(src.Entitlements)] + } else if len(src.RuntimeFlags) < len(dst.RuntimeFlags) { + dst.RuntimeFlags = (dst.RuntimeFlags)[:len(src.RuntimeFlags)] } } else { - dst.Entitlements = make([]string, len(src.Entitlements)) + dst.RuntimeFlags = make([]string, len(src.RuntimeFlags)) } - copy(dst.Entitlements, src.Entitlements) + copy(dst.RuntimeFlags, src.RuntimeFlags) } - if src.Args != nil { - dst.Args = make(map[string]*string, len(src.Args)) - deriveDeepCopy_15(dst.Args, src.Args) + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) } else { - dst.Args = nil + dst.Extensions = nil } - dst.Provenance = src.Provenance - dst.SBOM = src.SBOM - if src.SSH == nil { - dst.SSH = nil +} + +// deriveDeepCopy_20 recursively copies the contents of src into dst. +func deriveDeepCopy_20(dst, src *UpdateConfig) { + if src.Parallelism == nil { + dst.Parallelism = nil } else { - if dst.SSH != nil { - if len(src.SSH) > len(dst.SSH) { - if cap(dst.SSH) >= len(src.SSH) { - dst.SSH = (dst.SSH)[:len(src.SSH)] - } else { - dst.SSH = make([]SSHKey, len(src.SSH)) - } - } else if len(src.SSH) < len(dst.SSH) { - dst.SSH = (dst.SSH)[:len(src.SSH)] - } - } else { - dst.SSH = make([]SSHKey, len(src.SSH)) - } - copy(dst.SSH, src.SSH) + dst.Parallelism = new(uint64) + *dst.Parallelism = *src.Parallelism } - if src.Labels != nil { - dst.Labels = make(map[string]string, len(src.Labels)) - deriveDeepCopy_5(dst.Labels, src.Labels) + dst.Delay = src.Delay + dst.FailureAction = src.FailureAction + dst.Monitor = src.Monitor + dst.MaxFailureRatio = src.MaxFailureRatio + dst.Order = src.Order + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) } else { - dst.Labels = nil + dst.Extensions = nil } - if src.CacheFrom == nil { - dst.CacheFrom = nil +} + +// deriveDeepCopy_21 recursively copies the contents of src into dst. +func deriveDeepCopy_21(dst, src *Resources) { + if src.Limits == nil { + dst.Limits = nil } else { - if dst.CacheFrom != nil { - if len(src.CacheFrom) > len(dst.CacheFrom) { - if cap(dst.CacheFrom) >= len(src.CacheFrom) { - dst.CacheFrom = (dst.CacheFrom)[:len(src.CacheFrom)] - } else { - dst.CacheFrom = make([]string, len(src.CacheFrom)) - } - } else if len(src.CacheFrom) < len(dst.CacheFrom) { - dst.CacheFrom = (dst.CacheFrom)[:len(src.CacheFrom)] - } - } else { - dst.CacheFrom = make([]string, len(src.CacheFrom)) - } - copy(dst.CacheFrom, src.CacheFrom) + dst.Limits = new(Resource) + deriveDeepCopy_46(dst.Limits, src.Limits) } - if src.CacheTo == nil { - dst.CacheTo = nil + if src.Reservations == nil { + dst.Reservations = nil + } else { + dst.Reservations = new(Resource) + deriveDeepCopy_46(dst.Reservations, src.Reservations) + } + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) + } else { + dst.Extensions = nil + } +} + +// deriveDeepCopy_22 recursively copies the contents of src into dst. +func deriveDeepCopy_22(dst, src *RestartPolicy) { + dst.Condition = src.Condition + if src.Delay == nil { + dst.Delay = nil } else { - if dst.CacheTo != nil { - if len(src.CacheTo) > len(dst.CacheTo) { - if cap(dst.CacheTo) >= len(src.CacheTo) { - dst.CacheTo = (dst.CacheTo)[:len(src.CacheTo)] - } else { - dst.CacheTo = make([]string, len(src.CacheTo)) - } - } else if len(src.CacheTo) < len(dst.CacheTo) { - dst.CacheTo = (dst.CacheTo)[:len(src.CacheTo)] - } - } else { - dst.CacheTo = make([]string, len(src.CacheTo)) - } - copy(dst.CacheTo, src.CacheTo) + dst.Delay = new(Duration) + *dst.Delay = *src.Delay } - dst.NoCache = src.NoCache - if src.NoCacheFilter == nil { - dst.NoCacheFilter = nil + if src.MaxAttempts == nil { + dst.MaxAttempts = nil } else { - if dst.NoCacheFilter != nil { - if len(src.NoCacheFilter) > len(dst.NoCacheFilter) { - if cap(dst.NoCacheFilter) >= len(src.NoCacheFilter) { - dst.NoCacheFilter = (dst.NoCacheFilter)[:len(src.NoCacheFilter)] - } else { - dst.NoCacheFilter = make([]string, len(src.NoCacheFilter)) - } - } else if len(src.NoCacheFilter) < len(dst.NoCacheFilter) { - dst.NoCacheFilter = (dst.NoCacheFilter)[:len(src.NoCacheFilter)] - } - } else { - dst.NoCacheFilter = make([]string, len(src.NoCacheFilter)) - } - copy(dst.NoCacheFilter, src.NoCacheFilter) + dst.MaxAttempts = new(uint64) + *dst.MaxAttempts = *src.MaxAttempts } - if src.AdditionalContexts != nil { - dst.AdditionalContexts = make(map[string]string, len(src.AdditionalContexts)) - deriveDeepCopy_5(dst.AdditionalContexts, src.AdditionalContexts) + if src.Window == nil { + dst.Window = nil } else { - dst.AdditionalContexts = nil + dst.Window = new(Duration) + *dst.Window = *src.Window } - dst.Pull = src.Pull - if src.ExtraHosts != nil { - dst.ExtraHosts = make(map[string][]string, len(src.ExtraHosts)) - deriveDeepCopy_16(dst.ExtraHosts, src.ExtraHosts) + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) } else { - dst.ExtraHosts = nil + dst.Extensions = nil } - dst.Isolation = src.Isolation - dst.Network = src.Network - dst.Target = src.Target - if src.Secrets == nil { - dst.Secrets = nil +} + +// deriveDeepCopy_23 recursively copies the contents of src into dst. +func deriveDeepCopy_23(dst, src *Placement) { + if src.Constraints == nil { + dst.Constraints = nil } else { - if dst.Secrets != nil { - if len(src.Secrets) > len(dst.Secrets) { - if cap(dst.Secrets) >= len(src.Secrets) { - dst.Secrets = (dst.Secrets)[:len(src.Secrets)] + if dst.Constraints != nil { + if len(src.Constraints) > len(dst.Constraints) { + if cap(dst.Constraints) >= len(src.Constraints) { + dst.Constraints = (dst.Constraints)[:len(src.Constraints)] } else { - dst.Secrets = make([]ServiceSecretConfig, len(src.Secrets)) + dst.Constraints = make([]string, len(src.Constraints)) } - } else if len(src.Secrets) < len(dst.Secrets) { - dst.Secrets = (dst.Secrets)[:len(src.Secrets)] + } else if len(src.Constraints) < len(dst.Constraints) { + dst.Constraints = (dst.Constraints)[:len(src.Constraints)] } } else { - dst.Secrets = make([]ServiceSecretConfig, len(src.Secrets)) + dst.Constraints = make([]string, len(src.Constraints)) } - deriveDeepCopy_23(dst.Secrets, src.Secrets) + copy(dst.Constraints, src.Constraints) } - dst.ShmSize = src.ShmSize - if src.Tags == nil { - dst.Tags = nil + if src.Preferences == nil { + dst.Preferences = nil } else { - if dst.Tags != nil { - if len(src.Tags) > len(dst.Tags) { - if cap(dst.Tags) >= len(src.Tags) { - dst.Tags = (dst.Tags)[:len(src.Tags)] + if dst.Preferences != nil { + if len(src.Preferences) > len(dst.Preferences) { + if cap(dst.Preferences) >= len(src.Preferences) { + dst.Preferences = (dst.Preferences)[:len(src.Preferences)] } else { - dst.Tags = make([]string, len(src.Tags)) + dst.Preferences = make([]PlacementPreferences, len(src.Preferences)) } - } else if len(src.Tags) < len(dst.Tags) { - dst.Tags = (dst.Tags)[:len(src.Tags)] + } else if len(src.Preferences) < len(dst.Preferences) { + dst.Preferences = (dst.Preferences)[:len(src.Preferences)] } } else { - dst.Tags = make([]string, len(src.Tags)) + dst.Preferences = make([]PlacementPreferences, len(src.Preferences)) } - copy(dst.Tags, src.Tags) + deriveDeepCopy_47(dst.Preferences, src.Preferences) } - if src.Ulimits != nil { - dst.Ulimits = make(map[string]*UlimitsConfig, len(src.Ulimits)) - deriveDeepCopy_24(dst.Ulimits, src.Ulimits) + dst.MaxReplicas = src.MaxReplicas + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) } else { - dst.Ulimits = nil + dst.Extensions = nil } - if src.Platforms == nil { - dst.Platforms = nil - } else { - if dst.Platforms != nil { - if len(src.Platforms) > len(dst.Platforms) { - if cap(dst.Platforms) >= len(src.Platforms) { - dst.Platforms = (dst.Platforms)[:len(src.Platforms)] - } else { - dst.Platforms = make([]string, len(src.Platforms)) +} + +// deriveDeepCopy_24 recursively copies the contents of src into dst. +func deriveDeepCopy_24(dst, src []Trigger) { + for src_i, src_value := range src { + func() { + field := new(Trigger) + deriveDeepCopy_48(field, &src_value) + dst[src_i] = *field + }() + } +} + +// deriveDeepCopy_25 recursively copies the contents of src into dst. +func deriveDeepCopy_25(dst, src map[string][]string) { + for src_key, src_value := range src { + if src_value == nil { + dst[src_key] = nil + } + if src_value == nil { + dst[src_key] = nil + } else { + if dst[src_key] != nil { + if len(src_value) > len(dst[src_key]) { + if cap(dst[src_key]) >= len(src_value) { + dst[src_key] = (dst[src_key])[:len(src_value)] + } else { + dst[src_key] = make([]string, len(src_value)) + } + } else if len(src_value) < len(dst[src_key]) { + dst[src_key] = (dst[src_key])[:len(src_value)] } - } else if len(src.Platforms) < len(dst.Platforms) { - dst.Platforms = (dst.Platforms)[:len(src.Platforms)] + } else { + dst[src_key] = make([]string, len(src_value)) } - } else { - dst.Platforms = make([]string, len(src.Platforms)) + copy(dst[src_key], src_value) } - copy(dst.Platforms, src.Platforms) } - dst.Privileged = src.Privileged +} + +// deriveDeepCopy_26 recursively copies the contents of src into dst. +func deriveDeepCopy_26(dst, src *PreStartHook) { + func() { + field := new(ContainerSpec) + deriveDeepCopy_12(field, &src.ContainerSpec) + dst.ContainerSpec = *field + }() + dst.PerReplica = src.PerReplica if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) src.Extensions.DeepCopy(dst.Extensions) @@ -1060,25 +1415,34 @@ func deriveDeepCopy_6(dst, src *BuildConfig) { } } -// deriveDeepCopy_7 recursively copies the contents of src into dst. -func deriveDeepCopy_7(dst, src *DevelopConfig) { - if src.Watch == nil { - dst.Watch = nil +// deriveDeepCopy_27 recursively copies the contents of src into dst. +func deriveDeepCopy_27(dst, src *ServiceHook) { + if src.Command == nil { + dst.Command = nil } else { - if dst.Watch != nil { - if len(src.Watch) > len(dst.Watch) { - if cap(dst.Watch) >= len(src.Watch) { - dst.Watch = (dst.Watch)[:len(src.Watch)] + if dst.Command != nil { + if len(src.Command) > len(dst.Command) { + if cap(dst.Command) >= len(src.Command) { + dst.Command = (dst.Command)[:len(src.Command)] } else { - dst.Watch = make([]Trigger, len(src.Watch)) + dst.Command = make([]string, len(src.Command)) } - } else if len(src.Watch) < len(dst.Watch) { - dst.Watch = (dst.Watch)[:len(src.Watch)] + } else if len(src.Command) < len(dst.Command) { + dst.Command = (dst.Command)[:len(src.Command)] } } else { - dst.Watch = make([]Trigger, len(src.Watch)) + dst.Command = make([]string, len(src.Command)) } - deriveDeepCopy_32(dst.Watch, src.Watch) + copy(dst.Command, src.Command) + } + dst.User = src.User + dst.Privileged = src.Privileged + dst.WorkingDir = src.WorkingDir + if src.Environment != nil { + dst.Environment = make(map[string]*string, len(src.Environment)) + deriveDeepCopy_32(dst.Environment, src.Environment) + } else { + dst.Environment = nil } if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) @@ -1088,8 +1452,8 @@ func deriveDeepCopy_7(dst, src *DevelopConfig) { } } -// deriveDeepCopy_8 recursively copies the contents of src into dst. -func deriveDeepCopy_8(dst, src *BlkioConfig) { +// deriveDeepCopy_28 recursively copies the contents of src into dst. +func deriveDeepCopy_28(dst, src *BlkioConfig) { dst.Weight = src.Weight if src.WeightDevice == nil { dst.WeightDevice = nil @@ -1107,7 +1471,7 @@ func deriveDeepCopy_8(dst, src *BlkioConfig) { } else { dst.WeightDevice = make([]WeightDevice, len(src.WeightDevice)) } - deriveDeepCopy_33(dst.WeightDevice, src.WeightDevice) + deriveDeepCopy_49(dst.WeightDevice, src.WeightDevice) } if src.DeviceReadBps == nil { dst.DeviceReadBps = nil @@ -1125,7 +1489,7 @@ func deriveDeepCopy_8(dst, src *BlkioConfig) { } else { dst.DeviceReadBps = make([]ThrottleDevice, len(src.DeviceReadBps)) } - deriveDeepCopy_34(dst.DeviceReadBps, src.DeviceReadBps) + deriveDeepCopy_50(dst.DeviceReadBps, src.DeviceReadBps) } if src.DeviceReadIOps == nil { dst.DeviceReadIOps = nil @@ -1143,7 +1507,7 @@ func deriveDeepCopy_8(dst, src *BlkioConfig) { } else { dst.DeviceReadIOps = make([]ThrottleDevice, len(src.DeviceReadIOps)) } - deriveDeepCopy_34(dst.DeviceReadIOps, src.DeviceReadIOps) + deriveDeepCopy_50(dst.DeviceReadIOps, src.DeviceReadIOps) } if src.DeviceWriteBps == nil { dst.DeviceWriteBps = nil @@ -1161,7 +1525,7 @@ func deriveDeepCopy_8(dst, src *BlkioConfig) { } else { dst.DeviceWriteBps = make([]ThrottleDevice, len(src.DeviceWriteBps)) } - deriveDeepCopy_34(dst.DeviceWriteBps, src.DeviceWriteBps) + deriveDeepCopy_50(dst.DeviceWriteBps, src.DeviceWriteBps) } if src.DeviceWriteIOps == nil { dst.DeviceWriteIOps = nil @@ -1179,7 +1543,7 @@ func deriveDeepCopy_8(dst, src *BlkioConfig) { } else { dst.DeviceWriteIOps = make([]ThrottleDevice, len(src.DeviceWriteIOps)) } - deriveDeepCopy_34(dst.DeviceWriteIOps, src.DeviceWriteIOps) + deriveDeepCopy_50(dst.DeviceWriteIOps, src.DeviceWriteIOps) } if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) @@ -1189,19 +1553,19 @@ func deriveDeepCopy_8(dst, src *BlkioConfig) { } } -// deriveDeepCopy_9 recursively copies the contents of src into dst. -func deriveDeepCopy_9(dst, src []ServiceConfigObjConfig) { +// deriveDeepCopy_29 recursively copies the contents of src into dst. +func deriveDeepCopy_29(dst, src []ServiceConfigObjConfig) { for src_i, src_value := range src { func() { field := new(ServiceConfigObjConfig) - deriveDeepCopy_35(field, &src_value) + deriveDeepCopy_51(field, &src_value) dst[src_i] = *field }() } } -// deriveDeepCopy_10 recursively copies the contents of src into dst. -func deriveDeepCopy_10(dst, src *CredentialSpecConfig) { +// deriveDeepCopy_30 recursively copies the contents of src into dst. +func deriveDeepCopy_30(dst, src *CredentialSpecConfig) { dst.Config = src.Config dst.File = src.File dst.Registry = src.Registry @@ -1213,86 +1577,49 @@ func deriveDeepCopy_10(dst, src *CredentialSpecConfig) { } } -// deriveDeepCopy_11 recursively copies the contents of src into dst. -func deriveDeepCopy_11(dst, src map[string]ServiceDependency) { - for src_key, src_value := range src { +// deriveDeepCopy_31 recursively copies the contents of src into dst. +func deriveDeepCopy_31(dst, src []DeviceMapping) { + for src_i, src_value := range src { func() { - field := new(ServiceDependency) - deriveDeepCopy_36(field, &src_value) - dst[src_key] = *field + field := new(DeviceMapping) + deriveDeepCopy_52(field, &src_value) + dst[src_i] = *field }() } } -// deriveDeepCopy_12 recursively copies the contents of src into dst. -func deriveDeepCopy_12(dst, src *DeployConfig) { - dst.Mode = src.Mode - if src.Replicas == nil { - dst.Replicas = nil - } else { - dst.Replicas = new(int) - *dst.Replicas = *src.Replicas - } - if src.Labels != nil { - dst.Labels = make(map[string]string, len(src.Labels)) - deriveDeepCopy_5(dst.Labels, src.Labels) - } else { - dst.Labels = nil - } - if src.UpdateConfig == nil { - dst.UpdateConfig = nil - } else { - dst.UpdateConfig = new(UpdateConfig) - deriveDeepCopy_37(dst.UpdateConfig, src.UpdateConfig) - } - if src.RollbackConfig == nil { - dst.RollbackConfig = nil - } else { - dst.RollbackConfig = new(UpdateConfig) - deriveDeepCopy_37(dst.RollbackConfig, src.RollbackConfig) - } - func() { - field := new(Resources) - deriveDeepCopy_38(field, &src.Resources) - dst.Resources = *field - }() - if src.RestartPolicy == nil { - dst.RestartPolicy = nil - } else { - dst.RestartPolicy = new(RestartPolicy) - deriveDeepCopy_39(dst.RestartPolicy, src.RestartPolicy) - } - func() { - field := new(Placement) - deriveDeepCopy_40(field, &src.Placement) - dst.Placement = *field - }() - dst.EndpointMode = src.EndpointMode - if src.Extensions != nil { - dst.Extensions = make(map[string]any, len(src.Extensions)) - src.Extensions.DeepCopy(dst.Extensions) - } else { - dst.Extensions = nil - } -} - -// deriveDeepCopy_13 recursively copies the contents of src into dst. -func deriveDeepCopy_13(dst, src []DeviceMapping) { +// deriveDeepCopy_32 recursively copies the contents of src into dst. +func deriveDeepCopy_32(dst, src map[string]*string) { + for src_key, src_value := range src { + if src_value == nil { + dst[src_key] = nil + } + if src_value == nil { + dst[src_key] = nil + } else { + dst[src_key] = new(string) + *dst[src_key] = *src_value + } + } +} + +// deriveDeepCopy_33 recursively copies the contents of src into dst. +func deriveDeepCopy_33(dst, src []DeviceRequest) { for src_i, src_value := range src { func() { - field := new(DeviceMapping) - deriveDeepCopy_41(field, &src_value) + field := new(DeviceRequest) + deriveDeepCopy_53(field, &src_value) dst[src_i] = *field }() } } -// deriveDeepCopy_14 recursively copies the contents of src into dst. -func deriveDeepCopy_14(dst, src *ServiceProviderConfig) { - dst.Type = src.Type +// deriveDeepCopy_34 recursively copies the contents of src into dst. +func deriveDeepCopy_34(dst, src *LoggingConfig) { + dst.Driver = src.Driver if src.Options != nil { - dst.Options = make(map[string][]string, len(src.Options)) - deriveDeepCopy_16(dst.Options, src.Options) + dst.Options = make(map[string]string, len(src.Options)) + deriveDeepCopy_6(dst.Options, src.Options) } else { dst.Options = nil } @@ -1304,8 +1631,8 @@ func deriveDeepCopy_14(dst, src *ServiceProviderConfig) { } } -// deriveDeepCopy_15 recursively copies the contents of src into dst. -func deriveDeepCopy_15(dst, src map[string]*string) { +// deriveDeepCopy_35 recursively copies the contents of src into dst. +func deriveDeepCopy_35(dst, src map[string]*ServiceModelConfig) { for src_key, src_value := range src { if src_value == nil { dst[src_key] = nil @@ -1313,14 +1640,14 @@ func deriveDeepCopy_15(dst, src map[string]*string) { if src_value == nil { dst[src_key] = nil } else { - dst[src_key] = new(string) - *dst[src_key] = *src_value + dst[src_key] = new(ServiceModelConfig) + deriveDeepCopy_54(dst[src_key], src_value) } } } -// deriveDeepCopy_16 recursively copies the contents of src into dst. -func deriveDeepCopy_16(dst, src map[string][]string) { +// deriveDeepCopy_36 recursively copies the contents of src into dst. +func deriveDeepCopy_36(dst, src map[string]*ServiceNetworkConfig) { for src_key, src_value := range src { if src_value == nil { dst[src_key] = nil @@ -1328,37 +1655,258 @@ func deriveDeepCopy_16(dst, src map[string][]string) { if src_value == nil { dst[src_key] = nil } else { - if dst[src_key] != nil { - if len(src_value) > len(dst[src_key]) { - if cap(dst[src_key]) >= len(src_value) { - dst[src_key] = (dst[src_key])[:len(src_value)] - } else { - dst[src_key] = make([]string, len(src_value)) - } - } else if len(src_value) < len(dst[src_key]) { - dst[src_key] = (dst[src_key])[:len(src_value)] + dst[src_key] = new(ServiceNetworkConfig) + deriveDeepCopy_55(dst[src_key], src_value) + } + } +} + +// deriveDeepCopy_37 recursively copies the contents of src into dst. +func deriveDeepCopy_37(dst, src []ServiceSecretConfig) { + for src_i, src_value := range src { + func() { + field := new(ServiceSecretConfig) + deriveDeepCopy_56(field, &src_value) + dst[src_i] = *field + }() + } +} + +// deriveDeepCopy_38 recursively copies the contents of src into dst. +func deriveDeepCopy_38(dst, src map[string]*UlimitsConfig) { + for src_key, src_value := range src { + if src_value == nil { + dst[src_key] = nil + } + if src_value == nil { + dst[src_key] = nil + } else { + dst[src_key] = new(UlimitsConfig) + deriveDeepCopy_57(dst[src_key], src_value) + } + } +} + +// deriveDeepCopy_39 recursively copies the contents of src into dst. +func deriveDeepCopy_39(dst, src []ServiceVolumeConfig) { + for src_i, src_value := range src { + func() { + field := new(ServiceVolumeConfig) + deriveDeepCopy_58(field, &src_value) + dst[src_i] = *field + }() + } +} + +// deriveDeepCopy_40 recursively copies the contents of src into dst. +func deriveDeepCopy_40(dst, src *BuildConfig) { + dst.Context = src.Context + dst.Dockerfile = src.Dockerfile + dst.DockerfileInline = src.DockerfileInline + if src.Entitlements == nil { + dst.Entitlements = nil + } else { + if dst.Entitlements != nil { + if len(src.Entitlements) > len(dst.Entitlements) { + if cap(dst.Entitlements) >= len(src.Entitlements) { + dst.Entitlements = (dst.Entitlements)[:len(src.Entitlements)] + } else { + dst.Entitlements = make([]string, len(src.Entitlements)) } - } else { - dst[src_key] = make([]string, len(src_value)) + } else if len(src.Entitlements) < len(dst.Entitlements) { + dst.Entitlements = (dst.Entitlements)[:len(src.Entitlements)] } - copy(dst[src_key], src_value) + } else { + dst.Entitlements = make([]string, len(src.Entitlements)) + } + copy(dst.Entitlements, src.Entitlements) + } + if src.Args != nil { + dst.Args = make(map[string]*string, len(src.Args)) + deriveDeepCopy_32(dst.Args, src.Args) + } else { + dst.Args = nil + } + dst.Provenance = src.Provenance + dst.SBOM = src.SBOM + if src.SSH == nil { + dst.SSH = nil + } else { + if dst.SSH != nil { + if len(src.SSH) > len(dst.SSH) { + if cap(dst.SSH) >= len(src.SSH) { + dst.SSH = (dst.SSH)[:len(src.SSH)] + } else { + dst.SSH = make([]SSHKey, len(src.SSH)) + } + } else if len(src.SSH) < len(dst.SSH) { + dst.SSH = (dst.SSH)[:len(src.SSH)] + } + } else { + dst.SSH = make([]SSHKey, len(src.SSH)) + } + copy(dst.SSH, src.SSH) + } + if src.Labels != nil { + dst.Labels = make(map[string]string, len(src.Labels)) + deriveDeepCopy_6(dst.Labels, src.Labels) + } else { + dst.Labels = nil + } + if src.CacheFrom == nil { + dst.CacheFrom = nil + } else { + if dst.CacheFrom != nil { + if len(src.CacheFrom) > len(dst.CacheFrom) { + if cap(dst.CacheFrom) >= len(src.CacheFrom) { + dst.CacheFrom = (dst.CacheFrom)[:len(src.CacheFrom)] + } else { + dst.CacheFrom = make([]string, len(src.CacheFrom)) + } + } else if len(src.CacheFrom) < len(dst.CacheFrom) { + dst.CacheFrom = (dst.CacheFrom)[:len(src.CacheFrom)] + } + } else { + dst.CacheFrom = make([]string, len(src.CacheFrom)) + } + copy(dst.CacheFrom, src.CacheFrom) + } + if src.CacheTo == nil { + dst.CacheTo = nil + } else { + if dst.CacheTo != nil { + if len(src.CacheTo) > len(dst.CacheTo) { + if cap(dst.CacheTo) >= len(src.CacheTo) { + dst.CacheTo = (dst.CacheTo)[:len(src.CacheTo)] + } else { + dst.CacheTo = make([]string, len(src.CacheTo)) + } + } else if len(src.CacheTo) < len(dst.CacheTo) { + dst.CacheTo = (dst.CacheTo)[:len(src.CacheTo)] + } + } else { + dst.CacheTo = make([]string, len(src.CacheTo)) + } + copy(dst.CacheTo, src.CacheTo) + } + dst.NoCache = src.NoCache + if src.NoCacheFilter == nil { + dst.NoCacheFilter = nil + } else { + if dst.NoCacheFilter != nil { + if len(src.NoCacheFilter) > len(dst.NoCacheFilter) { + if cap(dst.NoCacheFilter) >= len(src.NoCacheFilter) { + dst.NoCacheFilter = (dst.NoCacheFilter)[:len(src.NoCacheFilter)] + } else { + dst.NoCacheFilter = make([]string, len(src.NoCacheFilter)) + } + } else if len(src.NoCacheFilter) < len(dst.NoCacheFilter) { + dst.NoCacheFilter = (dst.NoCacheFilter)[:len(src.NoCacheFilter)] + } + } else { + dst.NoCacheFilter = make([]string, len(src.NoCacheFilter)) + } + copy(dst.NoCacheFilter, src.NoCacheFilter) + } + if src.AdditionalContexts != nil { + dst.AdditionalContexts = make(map[string]string, len(src.AdditionalContexts)) + deriveDeepCopy_6(dst.AdditionalContexts, src.AdditionalContexts) + } else { + dst.AdditionalContexts = nil + } + dst.Pull = src.Pull + if src.ExtraHosts != nil { + dst.ExtraHosts = make(map[string][]string, len(src.ExtraHosts)) + deriveDeepCopy_25(dst.ExtraHosts, src.ExtraHosts) + } else { + dst.ExtraHosts = nil + } + dst.Isolation = src.Isolation + dst.Network = src.Network + dst.Target = src.Target + if src.Secrets == nil { + dst.Secrets = nil + } else { + if dst.Secrets != nil { + if len(src.Secrets) > len(dst.Secrets) { + if cap(dst.Secrets) >= len(src.Secrets) { + dst.Secrets = (dst.Secrets)[:len(src.Secrets)] + } else { + dst.Secrets = make([]ServiceSecretConfig, len(src.Secrets)) + } + } else if len(src.Secrets) < len(dst.Secrets) { + dst.Secrets = (dst.Secrets)[:len(src.Secrets)] + } + } else { + dst.Secrets = make([]ServiceSecretConfig, len(src.Secrets)) + } + deriveDeepCopy_37(dst.Secrets, src.Secrets) + } + dst.ShmSize = src.ShmSize + if src.Tags == nil { + dst.Tags = nil + } else { + if dst.Tags != nil { + if len(src.Tags) > len(dst.Tags) { + if cap(dst.Tags) >= len(src.Tags) { + dst.Tags = (dst.Tags)[:len(src.Tags)] + } else { + dst.Tags = make([]string, len(src.Tags)) + } + } else if len(src.Tags) < len(dst.Tags) { + dst.Tags = (dst.Tags)[:len(src.Tags)] + } + } else { + dst.Tags = make([]string, len(src.Tags)) + } + copy(dst.Tags, src.Tags) + } + if src.Ulimits != nil { + dst.Ulimits = make(map[string]*UlimitsConfig, len(src.Ulimits)) + deriveDeepCopy_38(dst.Ulimits, src.Ulimits) + } else { + dst.Ulimits = nil + } + if src.Platforms == nil { + dst.Platforms = nil + } else { + if dst.Platforms != nil { + if len(src.Platforms) > len(dst.Platforms) { + if cap(dst.Platforms) >= len(src.Platforms) { + dst.Platforms = (dst.Platforms)[:len(src.Platforms)] + } else { + dst.Platforms = make([]string, len(src.Platforms)) + } + } else if len(src.Platforms) < len(dst.Platforms) { + dst.Platforms = (dst.Platforms)[:len(src.Platforms)] + } + } else { + dst.Platforms = make([]string, len(src.Platforms)) } + copy(dst.Platforms, src.Platforms) + } + dst.Privileged = src.Privileged + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) + } else { + dst.Extensions = nil } } -// deriveDeepCopy_17 recursively copies the contents of src into dst. -func deriveDeepCopy_17(dst, src []DeviceRequest) { - for src_i, src_value := range src { +// deriveDeepCopy_41 recursively copies the contents of src into dst. +func deriveDeepCopy_41(dst, src map[string]ServiceDependency) { + for src_key, src_value := range src { func() { - field := new(DeviceRequest) - deriveDeepCopy_42(field, &src_value) - dst[src_i] = *field + field := new(ServiceDependency) + deriveDeepCopy_59(field, &src_value) + dst[src_key] = *field }() } } -// deriveDeepCopy_18 recursively copies the contents of src into dst. -func deriveDeepCopy_18(dst, src *HealthCheckConfig) { +// deriveDeepCopy_42 recursively copies the contents of src into dst. +func deriveDeepCopy_42(dst, src *HealthCheckConfig) { if src.Test == nil { dst.Test = nil } else { @@ -1400,292 +1948,14 @@ func deriveDeepCopy_18(dst, src *HealthCheckConfig) { } else { dst.StartPeriod = new(Duration) *dst.StartPeriod = *src.StartPeriod - } - if src.StartInterval == nil { - dst.StartInterval = nil - } else { - dst.StartInterval = new(Duration) - *dst.StartInterval = *src.StartInterval - } - dst.Disable = src.Disable - if src.Extensions != nil { - dst.Extensions = make(map[string]any, len(src.Extensions)) - src.Extensions.DeepCopy(dst.Extensions) - } else { - dst.Extensions = nil - } -} - -// deriveDeepCopy_19 recursively copies the contents of src into dst. -func deriveDeepCopy_19(dst, src *LoggingConfig) { - dst.Driver = src.Driver - if src.Options != nil { - dst.Options = make(map[string]string, len(src.Options)) - deriveDeepCopy_5(dst.Options, src.Options) - } else { - dst.Options = nil - } - if src.Extensions != nil { - dst.Extensions = make(map[string]any, len(src.Extensions)) - src.Extensions.DeepCopy(dst.Extensions) - } else { - dst.Extensions = nil - } -} - -// deriveDeepCopy_20 recursively copies the contents of src into dst. -func deriveDeepCopy_20(dst, src map[string]*ServiceModelConfig) { - for src_key, src_value := range src { - if src_value == nil { - dst[src_key] = nil - } - if src_value == nil { - dst[src_key] = nil - } else { - dst[src_key] = new(ServiceModelConfig) - deriveDeepCopy_43(dst[src_key], src_value) - } - } -} - -// deriveDeepCopy_21 recursively copies the contents of src into dst. -func deriveDeepCopy_21(dst, src map[string]*ServiceNetworkConfig) { - for src_key, src_value := range src { - if src_value == nil { - dst[src_key] = nil - } - if src_value == nil { - dst[src_key] = nil - } else { - dst[src_key] = new(ServiceNetworkConfig) - deriveDeepCopy_44(dst[src_key], src_value) - } - } -} - -// deriveDeepCopy_22 recursively copies the contents of src into dst. -func deriveDeepCopy_22(dst, src []ServicePortConfig) { - for src_i, src_value := range src { - func() { - field := new(ServicePortConfig) - deriveDeepCopy_45(field, &src_value) - dst[src_i] = *field - }() - } -} - -// deriveDeepCopy_23 recursively copies the contents of src into dst. -func deriveDeepCopy_23(dst, src []ServiceSecretConfig) { - for src_i, src_value := range src { - func() { - field := new(ServiceSecretConfig) - deriveDeepCopy_46(field, &src_value) - dst[src_i] = *field - }() - } -} - -// deriveDeepCopy_24 recursively copies the contents of src into dst. -func deriveDeepCopy_24(dst, src map[string]*UlimitsConfig) { - for src_key, src_value := range src { - if src_value == nil { - dst[src_key] = nil - } - if src_value == nil { - dst[src_key] = nil - } else { - dst[src_key] = new(UlimitsConfig) - deriveDeepCopy_47(dst[src_key], src_value) - } - } -} - -// deriveDeepCopy_25 recursively copies the contents of src into dst. -func deriveDeepCopy_25(dst, src []ServiceVolumeConfig) { - for src_i, src_value := range src { - func() { - field := new(ServiceVolumeConfig) - deriveDeepCopy_48(field, &src_value) - dst[src_i] = *field - }() - } -} - -// deriveDeepCopy_26 recursively copies the contents of src into dst. -func deriveDeepCopy_26(dst, src []ServiceHook) { - for src_i, src_value := range src { - func() { - field := new(ServiceHook) - deriveDeepCopy_49(field, &src_value) - dst[src_i] = *field - }() - } -} - -// deriveDeepCopy_27 recursively copies the contents of src into dst. -func deriveDeepCopy_27(dst, src *NetworkConfig) { - dst.Name = src.Name - dst.Driver = src.Driver - if src.DriverOpts != nil { - dst.DriverOpts = make(map[string]string, len(src.DriverOpts)) - deriveDeepCopy_5(dst.DriverOpts, src.DriverOpts) - } else { - dst.DriverOpts = nil - } - func() { - field := new(IPAMConfig) - deriveDeepCopy_50(field, &src.Ipam) - dst.Ipam = *field - }() - dst.External = src.External - dst.Internal = src.Internal - dst.Attachable = src.Attachable - if src.Labels != nil { - dst.Labels = make(map[string]string, len(src.Labels)) - deriveDeepCopy_5(dst.Labels, src.Labels) - } else { - dst.Labels = nil - } - if src.CustomLabels != nil { - dst.CustomLabels = make(map[string]string, len(src.CustomLabels)) - deriveDeepCopy_5(dst.CustomLabels, src.CustomLabels) - } else { - dst.CustomLabels = nil - } - if src.EnableIPv4 == nil { - dst.EnableIPv4 = nil - } else { - dst.EnableIPv4 = new(bool) - *dst.EnableIPv4 = *src.EnableIPv4 - } - if src.EnableIPv6 == nil { - dst.EnableIPv6 = nil - } else { - dst.EnableIPv6 = new(bool) - *dst.EnableIPv6 = *src.EnableIPv6 - } - if src.Extensions != nil { - dst.Extensions = make(map[string]any, len(src.Extensions)) - src.Extensions.DeepCopy(dst.Extensions) - } else { - dst.Extensions = nil - } -} - -// deriveDeepCopy_28 recursively copies the contents of src into dst. -func deriveDeepCopy_28(dst, src *VolumeConfig) { - dst.Name = src.Name - dst.Driver = src.Driver - if src.DriverOpts != nil { - dst.DriverOpts = make(map[string]string, len(src.DriverOpts)) - deriveDeepCopy_5(dst.DriverOpts, src.DriverOpts) - } else { - dst.DriverOpts = nil - } - dst.External = src.External - if src.Labels != nil { - dst.Labels = make(map[string]string, len(src.Labels)) - deriveDeepCopy_5(dst.Labels, src.Labels) - } else { - dst.Labels = nil - } - if src.CustomLabels != nil { - dst.CustomLabels = make(map[string]string, len(src.CustomLabels)) - deriveDeepCopy_5(dst.CustomLabels, src.CustomLabels) - } else { - dst.CustomLabels = nil - } - if src.Extensions != nil { - dst.Extensions = make(map[string]any, len(src.Extensions)) - src.Extensions.DeepCopy(dst.Extensions) - } else { - dst.Extensions = nil - } -} - -// deriveDeepCopy_29 recursively copies the contents of src into dst. -func deriveDeepCopy_29(dst, src *SecretConfig) { - dst.Name = src.Name - dst.File = src.File - dst.Environment = src.Environment - dst.Content = src.Content - dst.marshallContent = src.marshallContent - dst.External = src.External - if src.Labels != nil { - dst.Labels = make(map[string]string, len(src.Labels)) - deriveDeepCopy_5(dst.Labels, src.Labels) - } else { - dst.Labels = nil - } - dst.Driver = src.Driver - if src.DriverOpts != nil { - dst.DriverOpts = make(map[string]string, len(src.DriverOpts)) - deriveDeepCopy_5(dst.DriverOpts, src.DriverOpts) - } else { - dst.DriverOpts = nil - } - dst.TemplateDriver = src.TemplateDriver - if src.Extensions != nil { - dst.Extensions = make(map[string]any, len(src.Extensions)) - src.Extensions.DeepCopy(dst.Extensions) - } else { - dst.Extensions = nil - } -} - -// deriveDeepCopy_30 recursively copies the contents of src into dst. -func deriveDeepCopy_30(dst, src *ConfigObjConfig) { - dst.Name = src.Name - dst.File = src.File - dst.Environment = src.Environment - dst.Content = src.Content - dst.marshallContent = src.marshallContent - dst.External = src.External - if src.Labels != nil { - dst.Labels = make(map[string]string, len(src.Labels)) - deriveDeepCopy_5(dst.Labels, src.Labels) - } else { - dst.Labels = nil - } - dst.Driver = src.Driver - if src.DriverOpts != nil { - dst.DriverOpts = make(map[string]string, len(src.DriverOpts)) - deriveDeepCopy_5(dst.DriverOpts, src.DriverOpts) - } else { - dst.DriverOpts = nil - } - dst.TemplateDriver = src.TemplateDriver - if src.Extensions != nil { - dst.Extensions = make(map[string]any, len(src.Extensions)) - src.Extensions.DeepCopy(dst.Extensions) - } else { - dst.Extensions = nil - } -} - -// deriveDeepCopy_31 recursively copies the contents of src into dst. -func deriveDeepCopy_31(dst, src *ModelConfig) { - dst.Name = src.Name - dst.Model = src.Model - dst.ContextSize = src.ContextSize - if src.RuntimeFlags == nil { - dst.RuntimeFlags = nil + } + if src.StartInterval == nil { + dst.StartInterval = nil } else { - if dst.RuntimeFlags != nil { - if len(src.RuntimeFlags) > len(dst.RuntimeFlags) { - if cap(dst.RuntimeFlags) >= len(src.RuntimeFlags) { - dst.RuntimeFlags = (dst.RuntimeFlags)[:len(src.RuntimeFlags)] - } else { - dst.RuntimeFlags = make([]string, len(src.RuntimeFlags)) - } - } else if len(src.RuntimeFlags) < len(dst.RuntimeFlags) { - dst.RuntimeFlags = (dst.RuntimeFlags)[:len(src.RuntimeFlags)] - } - } else { - dst.RuntimeFlags = make([]string, len(src.RuntimeFlags)) - } - copy(dst.RuntimeFlags, src.RuntimeFlags) + dst.StartInterval = new(Duration) + *dst.StartInterval = *src.StartInterval } + dst.Disable = src.Disable if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) src.Extensions.DeepCopy(dst.Extensions) @@ -1694,50 +1964,42 @@ func deriveDeepCopy_31(dst, src *ModelConfig) { } } -// deriveDeepCopy_32 recursively copies the contents of src into dst. -func deriveDeepCopy_32(dst, src []Trigger) { - for src_i, src_value := range src { - func() { - field := new(Trigger) - deriveDeepCopy_51(field, &src_value) - dst[src_i] = *field - }() - } -} - -// deriveDeepCopy_33 recursively copies the contents of src into dst. -func deriveDeepCopy_33(dst, src []WeightDevice) { +// deriveDeepCopy_43 recursively copies the contents of src into dst. +func deriveDeepCopy_43(dst, src []ServicePortConfig) { for src_i, src_value := range src { func() { - field := new(WeightDevice) - deriveDeepCopy_52(field, &src_value) + field := new(ServicePortConfig) + deriveDeepCopy_60(field, &src_value) dst[src_i] = *field }() } } -// deriveDeepCopy_34 recursively copies the contents of src into dst. -func deriveDeepCopy_34(dst, src []ThrottleDevice) { - for src_i, src_value := range src { - func() { - field := new(ThrottleDevice) - deriveDeepCopy_53(field, &src_value) - dst[src_i] = *field - }() +// deriveDeepCopy_44 recursively copies the contents of src into dst. +func deriveDeepCopy_44(dst, src *TriggerConfig) { + if src.Manual == nil { + dst.Manual = nil + } else { + dst.Manual = new(bool) + *dst.Manual = *src.Manual } -} - -// deriveDeepCopy_35 recursively copies the contents of src into dst. -func deriveDeepCopy_35(dst, src *ServiceConfigObjConfig) { - dst.Source = src.Source - dst.Target = src.Target - dst.UID = src.UID - dst.GID = src.GID - if src.Mode == nil { - dst.Mode = nil + if src.Schedule == nil { + dst.Schedule = nil } else { - dst.Mode = new(FileMode) - *dst.Mode = *src.Mode + if dst.Schedule != nil { + if len(src.Schedule) > len(dst.Schedule) { + if cap(dst.Schedule) >= len(src.Schedule) { + dst.Schedule = (dst.Schedule)[:len(src.Schedule)] + } else { + dst.Schedule = make([]ScheduleConfig, len(src.Schedule)) + } + } else if len(src.Schedule) < len(dst.Schedule) { + dst.Schedule = (dst.Schedule)[:len(src.Schedule)] + } + } else { + dst.Schedule = make([]ScheduleConfig, len(src.Schedule)) + } + deriveDeepCopy_61(dst.Schedule, src.Schedule) } if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) @@ -1747,32 +2009,33 @@ func deriveDeepCopy_35(dst, src *ServiceConfigObjConfig) { } } -// deriveDeepCopy_36 recursively copies the contents of src into dst. -func deriveDeepCopy_36(dst, src *ServiceDependency) { - dst.Condition = src.Condition - dst.Restart = src.Restart - if src.Extensions != nil { - dst.Extensions = make(map[string]any, len(src.Extensions)) - src.Extensions.DeepCopy(dst.Extensions) +// deriveDeepCopy_45 recursively copies the contents of src into dst. +func deriveDeepCopy_45(dst, src *IPAMConfig) { + dst.Driver = src.Driver + if src.Config == nil { + dst.Config = nil } else { - dst.Extensions = nil + if dst.Config != nil { + if len(src.Config) > len(dst.Config) { + if cap(dst.Config) >= len(src.Config) { + dst.Config = (dst.Config)[:len(src.Config)] + } else { + dst.Config = make([]*IPAMPool, len(src.Config)) + } + } else if len(src.Config) < len(dst.Config) { + dst.Config = (dst.Config)[:len(src.Config)] + } + } else { + dst.Config = make([]*IPAMPool, len(src.Config)) + } + deriveDeepCopy_62(dst.Config, src.Config) } - dst.Required = src.Required -} - -// deriveDeepCopy_37 recursively copies the contents of src into dst. -func deriveDeepCopy_37(dst, src *UpdateConfig) { - if src.Parallelism == nil { - dst.Parallelism = nil + if src.Options != nil { + dst.Options = make(map[string]string, len(src.Options)) + deriveDeepCopy_6(dst.Options, src.Options) } else { - dst.Parallelism = new(uint64) - *dst.Parallelism = *src.Parallelism + dst.Options = nil } - dst.Delay = src.Delay - dst.FailureAction = src.FailureAction - dst.Monitor = src.Monitor - dst.MaxFailureRatio = src.MaxFailureRatio - dst.Order = src.Order if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) src.Extensions.DeepCopy(dst.Extensions) @@ -1781,19 +2044,46 @@ func deriveDeepCopy_37(dst, src *UpdateConfig) { } } -// deriveDeepCopy_38 recursively copies the contents of src into dst. -func deriveDeepCopy_38(dst, src *Resources) { - if src.Limits == nil { - dst.Limits = nil +// deriveDeepCopy_46 recursively copies the contents of src into dst. +func deriveDeepCopy_46(dst, src *Resource) { + dst.NanoCPUs = src.NanoCPUs + dst.MemoryBytes = src.MemoryBytes + dst.Pids = src.Pids + if src.Devices == nil { + dst.Devices = nil } else { - dst.Limits = new(Resource) - deriveDeepCopy_54(dst.Limits, src.Limits) + if dst.Devices != nil { + if len(src.Devices) > len(dst.Devices) { + if cap(dst.Devices) >= len(src.Devices) { + dst.Devices = (dst.Devices)[:len(src.Devices)] + } else { + dst.Devices = make([]DeviceRequest, len(src.Devices)) + } + } else if len(src.Devices) < len(dst.Devices) { + dst.Devices = (dst.Devices)[:len(src.Devices)] + } + } else { + dst.Devices = make([]DeviceRequest, len(src.Devices)) + } + deriveDeepCopy_33(dst.Devices, src.Devices) } - if src.Reservations == nil { - dst.Reservations = nil + if src.GenericResources == nil { + dst.GenericResources = nil } else { - dst.Reservations = new(Resource) - deriveDeepCopy_54(dst.Reservations, src.Reservations) + if dst.GenericResources != nil { + if len(src.GenericResources) > len(dst.GenericResources) { + if cap(dst.GenericResources) >= len(src.GenericResources) { + dst.GenericResources = (dst.GenericResources)[:len(src.GenericResources)] + } else { + dst.GenericResources = make([]GenericResource, len(src.GenericResources)) + } + } else if len(src.GenericResources) < len(dst.GenericResources) { + dst.GenericResources = (dst.GenericResources)[:len(src.GenericResources)] + } + } else { + dst.GenericResources = make([]GenericResource, len(src.GenericResources)) + } + deriveDeepCopy_63(dst.GenericResources, src.GenericResources) } if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) @@ -1803,74 +2093,106 @@ func deriveDeepCopy_38(dst, src *Resources) { } } -// deriveDeepCopy_39 recursively copies the contents of src into dst. -func deriveDeepCopy_39(dst, src *RestartPolicy) { - dst.Condition = src.Condition - if src.Delay == nil { - dst.Delay = nil - } else { - dst.Delay = new(Duration) - *dst.Delay = *src.Delay - } - if src.MaxAttempts == nil { - dst.MaxAttempts = nil - } else { - dst.MaxAttempts = new(uint64) - *dst.MaxAttempts = *src.MaxAttempts - } - if src.Window == nil { - dst.Window = nil - } else { - dst.Window = new(Duration) - *dst.Window = *src.Window - } - if src.Extensions != nil { - dst.Extensions = make(map[string]any, len(src.Extensions)) - src.Extensions.DeepCopy(dst.Extensions) - } else { - dst.Extensions = nil +// deriveDeepCopy_47 recursively copies the contents of src into dst. +func deriveDeepCopy_47(dst, src []PlacementPreferences) { + for src_i, src_value := range src { + func() { + field := new(PlacementPreferences) + deriveDeepCopy_64(field, &src_value) + dst[src_i] = *field + }() } } -// deriveDeepCopy_40 recursively copies the contents of src into dst. -func deriveDeepCopy_40(dst, src *Placement) { - if src.Constraints == nil { - dst.Constraints = nil +// deriveDeepCopy_48 recursively copies the contents of src into dst. +func deriveDeepCopy_48(dst, src *Trigger) { + dst.Path = src.Path + dst.Action = src.Action + dst.Target = src.Target + func() { + field := new(ServiceHook) + deriveDeepCopy_27(field, &src.Exec) + dst.Exec = *field + }() + if src.Include == nil { + dst.Include = nil } else { - if dst.Constraints != nil { - if len(src.Constraints) > len(dst.Constraints) { - if cap(dst.Constraints) >= len(src.Constraints) { - dst.Constraints = (dst.Constraints)[:len(src.Constraints)] + if dst.Include != nil { + if len(src.Include) > len(dst.Include) { + if cap(dst.Include) >= len(src.Include) { + dst.Include = (dst.Include)[:len(src.Include)] } else { - dst.Constraints = make([]string, len(src.Constraints)) + dst.Include = make([]string, len(src.Include)) } - } else if len(src.Constraints) < len(dst.Constraints) { - dst.Constraints = (dst.Constraints)[:len(src.Constraints)] + } else if len(src.Include) < len(dst.Include) { + dst.Include = (dst.Include)[:len(src.Include)] } } else { - dst.Constraints = make([]string, len(src.Constraints)) + dst.Include = make([]string, len(src.Include)) } - copy(dst.Constraints, src.Constraints) + copy(dst.Include, src.Include) } - if src.Preferences == nil { - dst.Preferences = nil + if src.Ignore == nil { + dst.Ignore = nil } else { - if dst.Preferences != nil { - if len(src.Preferences) > len(dst.Preferences) { - if cap(dst.Preferences) >= len(src.Preferences) { - dst.Preferences = (dst.Preferences)[:len(src.Preferences)] + if dst.Ignore != nil { + if len(src.Ignore) > len(dst.Ignore) { + if cap(dst.Ignore) >= len(src.Ignore) { + dst.Ignore = (dst.Ignore)[:len(src.Ignore)] } else { - dst.Preferences = make([]PlacementPreferences, len(src.Preferences)) + dst.Ignore = make([]string, len(src.Ignore)) } - } else if len(src.Preferences) < len(dst.Preferences) { - dst.Preferences = (dst.Preferences)[:len(src.Preferences)] + } else if len(src.Ignore) < len(dst.Ignore) { + dst.Ignore = (dst.Ignore)[:len(src.Ignore)] } } else { - dst.Preferences = make([]PlacementPreferences, len(src.Preferences)) + dst.Ignore = make([]string, len(src.Ignore)) } - deriveDeepCopy_55(dst.Preferences, src.Preferences) + copy(dst.Ignore, src.Ignore) + } + dst.InitialSync = src.InitialSync + if src.Extensions != nil { + dst.Extensions = make(map[string]any, len(src.Extensions)) + src.Extensions.DeepCopy(dst.Extensions) + } else { + dst.Extensions = nil + } +} + +// deriveDeepCopy_49 recursively copies the contents of src into dst. +func deriveDeepCopy_49(dst, src []WeightDevice) { + for src_i, src_value := range src { + func() { + field := new(WeightDevice) + deriveDeepCopy_65(field, &src_value) + dst[src_i] = *field + }() + } +} + +// deriveDeepCopy_50 recursively copies the contents of src into dst. +func deriveDeepCopy_50(dst, src []ThrottleDevice) { + for src_i, src_value := range src { + func() { + field := new(ThrottleDevice) + deriveDeepCopy_66(field, &src_value) + dst[src_i] = *field + }() + } +} + +// deriveDeepCopy_51 recursively copies the contents of src into dst. +func deriveDeepCopy_51(dst, src *ServiceConfigObjConfig) { + dst.Source = src.Source + dst.Target = src.Target + dst.UID = src.UID + dst.GID = src.GID + if src.Mode == nil { + dst.Mode = nil + } else { + dst.Mode = new(FileMode) + *dst.Mode = *src.Mode } - dst.MaxReplicas = src.MaxReplicas if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) src.Extensions.DeepCopy(dst.Extensions) @@ -1879,8 +2201,8 @@ func deriveDeepCopy_40(dst, src *Placement) { } } -// deriveDeepCopy_41 recursively copies the contents of src into dst. -func deriveDeepCopy_41(dst, src *DeviceMapping) { +// deriveDeepCopy_52 recursively copies the contents of src into dst. +func deriveDeepCopy_52(dst, src *DeviceMapping) { dst.Source = src.Source dst.Target = src.Target dst.Permissions = src.Permissions @@ -1892,8 +2214,8 @@ func deriveDeepCopy_41(dst, src *DeviceMapping) { } } -// deriveDeepCopy_42 recursively copies the contents of src into dst. -func deriveDeepCopy_42(dst, src *DeviceRequest) { +// deriveDeepCopy_53 recursively copies the contents of src into dst. +func deriveDeepCopy_53(dst, src *DeviceRequest) { if src.Capabilities == nil { dst.Capabilities = nil } else { @@ -1934,14 +2256,14 @@ func deriveDeepCopy_42(dst, src *DeviceRequest) { } if src.Options != nil { dst.Options = make(map[string]string, len(src.Options)) - deriveDeepCopy_5(dst.Options, src.Options) + deriveDeepCopy_6(dst.Options, src.Options) } else { dst.Options = nil } } -// deriveDeepCopy_43 recursively copies the contents of src into dst. -func deriveDeepCopy_43(dst, src *ServiceModelConfig) { +// deriveDeepCopy_54 recursively copies the contents of src into dst. +func deriveDeepCopy_54(dst, src *ServiceModelConfig) { dst.EndpointVariable = src.EndpointVariable dst.ModelVariable = src.ModelVariable if src.Extensions != nil { @@ -1952,8 +2274,8 @@ func deriveDeepCopy_43(dst, src *ServiceModelConfig) { } } -// deriveDeepCopy_44 recursively copies the contents of src into dst. -func deriveDeepCopy_44(dst, src *ServiceNetworkConfig) { +// deriveDeepCopy_55 recursively copies the contents of src into dst. +func deriveDeepCopy_55(dst, src *ServiceNetworkConfig) { if src.Aliases == nil { dst.Aliases = nil } else { @@ -1974,7 +2296,7 @@ func deriveDeepCopy_44(dst, src *ServiceNetworkConfig) { } if src.DriverOpts != nil { dst.DriverOpts = make(map[string]string, len(src.DriverOpts)) - deriveDeepCopy_5(dst.DriverOpts, src.DriverOpts) + deriveDeepCopy_6(dst.DriverOpts, src.DriverOpts) } else { dst.DriverOpts = nil } @@ -2010,25 +2332,8 @@ func deriveDeepCopy_44(dst, src *ServiceNetworkConfig) { } } -// deriveDeepCopy_45 recursively copies the contents of src into dst. -func deriveDeepCopy_45(dst, src *ServicePortConfig) { - dst.Name = src.Name - dst.Mode = src.Mode - dst.HostIP = src.HostIP - dst.Target = src.Target - dst.Published = src.Published - dst.Protocol = src.Protocol - dst.AppProtocol = src.AppProtocol - if src.Extensions != nil { - dst.Extensions = make(map[string]any, len(src.Extensions)) - src.Extensions.DeepCopy(dst.Extensions) - } else { - dst.Extensions = nil - } -} - -// deriveDeepCopy_46 recursively copies the contents of src into dst. -func deriveDeepCopy_46(dst, src *ServiceSecretConfig) { +// deriveDeepCopy_56 recursively copies the contents of src into dst. +func deriveDeepCopy_56(dst, src *ServiceSecretConfig) { dst.Source = src.Source dst.Target = src.Target dst.UID = src.UID @@ -2047,8 +2352,8 @@ func deriveDeepCopy_46(dst, src *ServiceSecretConfig) { } } -// deriveDeepCopy_47 recursively copies the contents of src into dst. -func deriveDeepCopy_47(dst, src *UlimitsConfig) { +// deriveDeepCopy_57 recursively copies the contents of src into dst. +func deriveDeepCopy_57(dst, src *UlimitsConfig) { dst.Single = src.Single dst.Soft = src.Soft dst.Hard = src.Hard @@ -2060,8 +2365,8 @@ func deriveDeepCopy_47(dst, src *UlimitsConfig) { } } -// deriveDeepCopy_48 recursively copies the contents of src into dst. -func deriveDeepCopy_48(dst, src *ServiceVolumeConfig) { +// deriveDeepCopy_58 recursively copies the contents of src into dst. +func deriveDeepCopy_58(dst, src *ServiceVolumeConfig) { dst.Type = src.Type dst.Source = src.Source dst.Target = src.Target @@ -2071,25 +2376,25 @@ func deriveDeepCopy_48(dst, src *ServiceVolumeConfig) { dst.Bind = nil } else { dst.Bind = new(ServiceVolumeBind) - deriveDeepCopy_56(dst.Bind, src.Bind) + deriveDeepCopy_67(dst.Bind, src.Bind) } if src.Volume == nil { dst.Volume = nil } else { dst.Volume = new(ServiceVolumeVolume) - deriveDeepCopy_57(dst.Volume, src.Volume) + deriveDeepCopy_68(dst.Volume, src.Volume) } if src.Tmpfs == nil { dst.Tmpfs = nil } else { dst.Tmpfs = new(ServiceVolumeTmpfs) - deriveDeepCopy_58(dst.Tmpfs, src.Tmpfs) + deriveDeepCopy_69(dst.Tmpfs, src.Tmpfs) } if src.Image == nil { dst.Image = nil } else { dst.Image = new(ServiceVolumeImage) - deriveDeepCopy_59(dst.Image, src.Image) + deriveDeepCopy_70(dst.Image, src.Image) } if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) @@ -2099,72 +2404,28 @@ func deriveDeepCopy_48(dst, src *ServiceVolumeConfig) { } } -// deriveDeepCopy_49 recursively copies the contents of src into dst. -func deriveDeepCopy_49(dst, src *ServiceHook) { - if src.Command == nil { - dst.Command = nil - } else { - if dst.Command != nil { - if len(src.Command) > len(dst.Command) { - if cap(dst.Command) >= len(src.Command) { - dst.Command = (dst.Command)[:len(src.Command)] - } else { - dst.Command = make([]string, len(src.Command)) - } - } else if len(src.Command) < len(dst.Command) { - dst.Command = (dst.Command)[:len(src.Command)] - } - } else { - dst.Command = make([]string, len(src.Command)) - } - copy(dst.Command, src.Command) - } - dst.Image = src.Image - dst.User = src.User - dst.Privileged = src.Privileged - dst.WorkingDir = src.WorkingDir - if src.Environment != nil { - dst.Environment = make(map[string]*string, len(src.Environment)) - deriveDeepCopy_15(dst.Environment, src.Environment) - } else { - dst.Environment = nil - } - dst.PerReplica = src.PerReplica +// deriveDeepCopy_59 recursively copies the contents of src into dst. +func deriveDeepCopy_59(dst, src *ServiceDependency) { + dst.Condition = src.Condition + dst.Restart = src.Restart if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) src.Extensions.DeepCopy(dst.Extensions) } else { dst.Extensions = nil } + dst.Required = src.Required } -// deriveDeepCopy_50 recursively copies the contents of src into dst. -func deriveDeepCopy_50(dst, src *IPAMConfig) { - dst.Driver = src.Driver - if src.Config == nil { - dst.Config = nil - } else { - if dst.Config != nil { - if len(src.Config) > len(dst.Config) { - if cap(dst.Config) >= len(src.Config) { - dst.Config = (dst.Config)[:len(src.Config)] - } else { - dst.Config = make([]*IPAMPool, len(src.Config)) - } - } else if len(src.Config) < len(dst.Config) { - dst.Config = (dst.Config)[:len(src.Config)] - } - } else { - dst.Config = make([]*IPAMPool, len(src.Config)) - } - deriveDeepCopy_60(dst.Config, src.Config) - } - if src.Options != nil { - dst.Options = make(map[string]string, len(src.Options)) - deriveDeepCopy_5(dst.Options, src.Options) - } else { - dst.Options = nil - } +// deriveDeepCopy_60 recursively copies the contents of src into dst. +func deriveDeepCopy_60(dst, src *ServicePortConfig) { + dst.Name = src.Name + dst.Mode = src.Mode + dst.HostIP = src.HostIP + dst.Target = src.Target + dst.Published = src.Published + dst.Protocol = src.Protocol + dst.AppProtocol = src.AppProtocol if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) src.Extensions.DeepCopy(dst.Extensions) @@ -2173,65 +2434,43 @@ func deriveDeepCopy_50(dst, src *IPAMConfig) { } } -// deriveDeepCopy_51 recursively copies the contents of src into dst. -func deriveDeepCopy_51(dst, src *Trigger) { - dst.Path = src.Path - dst.Action = src.Action - dst.Target = src.Target - func() { - field := new(ServiceHook) - deriveDeepCopy_49(field, &src.Exec) - dst.Exec = *field - }() - if src.Include == nil { - dst.Include = nil - } else { - if dst.Include != nil { - if len(src.Include) > len(dst.Include) { - if cap(dst.Include) >= len(src.Include) { - dst.Include = (dst.Include)[:len(src.Include)] - } else { - dst.Include = make([]string, len(src.Include)) - } - } else if len(src.Include) < len(dst.Include) { - dst.Include = (dst.Include)[:len(src.Include)] - } - } else { - dst.Include = make([]string, len(src.Include)) - } - copy(dst.Include, src.Include) +// deriveDeepCopy_61 recursively copies the contents of src into dst. +func deriveDeepCopy_61(dst, src []ScheduleConfig) { + for src_i, src_value := range src { + func() { + field := new(ScheduleConfig) + deriveDeepCopy_71(field, &src_value) + dst[src_i] = *field + }() } - if src.Ignore == nil { - dst.Ignore = nil - } else { - if dst.Ignore != nil { - if len(src.Ignore) > len(dst.Ignore) { - if cap(dst.Ignore) >= len(src.Ignore) { - dst.Ignore = (dst.Ignore)[:len(src.Ignore)] - } else { - dst.Ignore = make([]string, len(src.Ignore)) - } - } else if len(src.Ignore) < len(dst.Ignore) { - dst.Ignore = (dst.Ignore)[:len(src.Ignore)] - } +} + +// deriveDeepCopy_62 recursively copies the contents of src into dst. +func deriveDeepCopy_62(dst, src []*IPAMPool) { + for src_i, src_value := range src { + if src_value == nil { + dst[src_i] = nil } else { - dst.Ignore = make([]string, len(src.Ignore)) + dst[src_i] = new(IPAMPool) + deriveDeepCopy_72(dst[src_i], src_value) } - copy(dst.Ignore, src.Ignore) } - dst.InitialSync = src.InitialSync - if src.Extensions != nil { - dst.Extensions = make(map[string]any, len(src.Extensions)) - src.Extensions.DeepCopy(dst.Extensions) - } else { - dst.Extensions = nil +} + +// deriveDeepCopy_63 recursively copies the contents of src into dst. +func deriveDeepCopy_63(dst, src []GenericResource) { + for src_i, src_value := range src { + func() { + field := new(GenericResource) + deriveDeepCopy_73(field, &src_value) + dst[src_i] = *field + }() } } -// deriveDeepCopy_52 recursively copies the contents of src into dst. -func deriveDeepCopy_52(dst, src *WeightDevice) { - dst.Path = src.Path - dst.Weight = src.Weight +// deriveDeepCopy_64 recursively copies the contents of src into dst. +func deriveDeepCopy_64(dst, src *PlacementPreferences) { + dst.Spread = src.Spread if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) src.Extensions.DeepCopy(dst.Extensions) @@ -2240,10 +2479,10 @@ func deriveDeepCopy_52(dst, src *WeightDevice) { } } -// deriveDeepCopy_53 recursively copies the contents of src into dst. -func deriveDeepCopy_53(dst, src *ThrottleDevice) { +// deriveDeepCopy_65 recursively copies the contents of src into dst. +func deriveDeepCopy_65(dst, src *WeightDevice) { dst.Path = src.Path - dst.Rate = src.Rate + dst.Weight = src.Weight if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) src.Extensions.DeepCopy(dst.Extensions) @@ -2252,47 +2491,10 @@ func deriveDeepCopy_53(dst, src *ThrottleDevice) { } } -// deriveDeepCopy_54 recursively copies the contents of src into dst. -func deriveDeepCopy_54(dst, src *Resource) { - dst.NanoCPUs = src.NanoCPUs - dst.MemoryBytes = src.MemoryBytes - dst.Pids = src.Pids - if src.Devices == nil { - dst.Devices = nil - } else { - if dst.Devices != nil { - if len(src.Devices) > len(dst.Devices) { - if cap(dst.Devices) >= len(src.Devices) { - dst.Devices = (dst.Devices)[:len(src.Devices)] - } else { - dst.Devices = make([]DeviceRequest, len(src.Devices)) - } - } else if len(src.Devices) < len(dst.Devices) { - dst.Devices = (dst.Devices)[:len(src.Devices)] - } - } else { - dst.Devices = make([]DeviceRequest, len(src.Devices)) - } - deriveDeepCopy_17(dst.Devices, src.Devices) - } - if src.GenericResources == nil { - dst.GenericResources = nil - } else { - if dst.GenericResources != nil { - if len(src.GenericResources) > len(dst.GenericResources) { - if cap(dst.GenericResources) >= len(src.GenericResources) { - dst.GenericResources = (dst.GenericResources)[:len(src.GenericResources)] - } else { - dst.GenericResources = make([]GenericResource, len(src.GenericResources)) - } - } else if len(src.GenericResources) < len(dst.GenericResources) { - dst.GenericResources = (dst.GenericResources)[:len(src.GenericResources)] - } - } else { - dst.GenericResources = make([]GenericResource, len(src.GenericResources)) - } - deriveDeepCopy_61(dst.GenericResources, src.GenericResources) - } +// deriveDeepCopy_66 recursively copies the contents of src into dst. +func deriveDeepCopy_66(dst, src *ThrottleDevice) { + dst.Path = src.Path + dst.Rate = src.Rate if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) src.Extensions.DeepCopy(dst.Extensions) @@ -2301,19 +2503,8 @@ func deriveDeepCopy_54(dst, src *Resource) { } } -// deriveDeepCopy_55 recursively copies the contents of src into dst. -func deriveDeepCopy_55(dst, src []PlacementPreferences) { - for src_i, src_value := range src { - func() { - field := new(PlacementPreferences) - deriveDeepCopy_62(field, &src_value) - dst[src_i] = *field - }() - } -} - -// deriveDeepCopy_56 recursively copies the contents of src into dst. -func deriveDeepCopy_56(dst, src *ServiceVolumeBind) { +// deriveDeepCopy_67 recursively copies the contents of src into dst. +func deriveDeepCopy_67(dst, src *ServiceVolumeBind) { dst.SELinux = src.SELinux dst.Propagation = src.Propagation dst.CreateHostPath = src.CreateHostPath @@ -2326,11 +2517,11 @@ func deriveDeepCopy_56(dst, src *ServiceVolumeBind) { } } -// deriveDeepCopy_57 recursively copies the contents of src into dst. -func deriveDeepCopy_57(dst, src *ServiceVolumeVolume) { +// deriveDeepCopy_68 recursively copies the contents of src into dst. +func deriveDeepCopy_68(dst, src *ServiceVolumeVolume) { if src.Labels != nil { dst.Labels = make(map[string]string, len(src.Labels)) - deriveDeepCopy_5(dst.Labels, src.Labels) + deriveDeepCopy_6(dst.Labels, src.Labels) } else { dst.Labels = nil } @@ -2344,8 +2535,8 @@ func deriveDeepCopy_57(dst, src *ServiceVolumeVolume) { } } -// deriveDeepCopy_58 recursively copies the contents of src into dst. -func deriveDeepCopy_58(dst, src *ServiceVolumeTmpfs) { +// deriveDeepCopy_69 recursively copies the contents of src into dst. +func deriveDeepCopy_69(dst, src *ServiceVolumeTmpfs) { dst.Size = src.Size dst.Mode = src.Mode if src.Extensions != nil { @@ -2356,8 +2547,8 @@ func deriveDeepCopy_58(dst, src *ServiceVolumeTmpfs) { } } -// deriveDeepCopy_59 recursively copies the contents of src into dst. -func deriveDeepCopy_59(dst, src *ServiceVolumeImage) { +// deriveDeepCopy_70 recursively copies the contents of src into dst. +func deriveDeepCopy_70(dst, src *ServiceVolumeImage) { dst.SubPath = src.SubPath if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) @@ -2367,32 +2558,12 @@ func deriveDeepCopy_59(dst, src *ServiceVolumeImage) { } } -// deriveDeepCopy_60 recursively copies the contents of src into dst. -func deriveDeepCopy_60(dst, src []*IPAMPool) { - for src_i, src_value := range src { - if src_value == nil { - dst[src_i] = nil - } else { - dst[src_i] = new(IPAMPool) - deriveDeepCopy_63(dst[src_i], src_value) - } - } -} - -// deriveDeepCopy_61 recursively copies the contents of src into dst. -func deriveDeepCopy_61(dst, src []GenericResource) { - for src_i, src_value := range src { - func() { - field := new(GenericResource) - deriveDeepCopy_64(field, &src_value) - dst[src_i] = *field - }() - } -} - -// deriveDeepCopy_62 recursively copies the contents of src into dst. -func deriveDeepCopy_62(dst, src *PlacementPreferences) { - dst.Spread = src.Spread +// deriveDeepCopy_71 recursively copies the contents of src into dst. +func deriveDeepCopy_71(dst, src *ScheduleConfig) { + dst.Cron = src.Cron + dst.Timezone = src.Timezone + dst.Concurrency = src.Concurrency + dst.MissedFires = src.MissedFires if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) src.Extensions.DeepCopy(dst.Extensions) @@ -2401,14 +2572,14 @@ func deriveDeepCopy_62(dst, src *PlacementPreferences) { } } -// deriveDeepCopy_63 recursively copies the contents of src into dst. -func deriveDeepCopy_63(dst, src *IPAMPool) { +// deriveDeepCopy_72 recursively copies the contents of src into dst. +func deriveDeepCopy_72(dst, src *IPAMPool) { dst.Subnet = src.Subnet dst.Gateway = src.Gateway dst.IPRange = src.IPRange if src.AuxiliaryAddresses != nil { dst.AuxiliaryAddresses = make(map[string]string, len(src.AuxiliaryAddresses)) - deriveDeepCopy_5(dst.AuxiliaryAddresses, src.AuxiliaryAddresses) + deriveDeepCopy_6(dst.AuxiliaryAddresses, src.AuxiliaryAddresses) } else { dst.AuxiliaryAddresses = nil } @@ -2420,13 +2591,13 @@ func deriveDeepCopy_63(dst, src *IPAMPool) { } } -// deriveDeepCopy_64 recursively copies the contents of src into dst. -func deriveDeepCopy_64(dst, src *GenericResource) { +// deriveDeepCopy_73 recursively copies the contents of src into dst. +func deriveDeepCopy_73(dst, src *GenericResource) { if src.DiscreteResourceSpec == nil { dst.DiscreteResourceSpec = nil } else { dst.DiscreteResourceSpec = new(DiscreteGenericResource) - deriveDeepCopy_65(dst.DiscreteResourceSpec, src.DiscreteResourceSpec) + deriveDeepCopy_74(dst.DiscreteResourceSpec, src.DiscreteResourceSpec) } if src.Extensions != nil { dst.Extensions = make(map[string]any, len(src.Extensions)) @@ -2436,8 +2607,8 @@ func deriveDeepCopy_64(dst, src *GenericResource) { } } -// deriveDeepCopy_65 recursively copies the contents of src into dst. -func deriveDeepCopy_65(dst, src *DiscreteGenericResource) { +// deriveDeepCopy_74 recursively copies the contents of src into dst. +func deriveDeepCopy_74(dst, src *DiscreteGenericResource) { dst.Kind = src.Kind dst.Value = src.Value if src.Extensions != nil { diff --git a/types/hooks.go b/types/hooks.go index 6eca7c94d..5ca5d3f58 100644 --- a/types/hooks.go +++ b/types/hooks.go @@ -16,17 +16,30 @@ package types -// ServiceHook is a hook executed at a service lifecycle event: a command exec'd -// inside the service container for post_start/pre_stop, or an ephemeral -// container run before the service starts for pre_start. +// ServiceHook is a command exec'd inside the service container at a lifecycle +// event (post_start, pre_stop). type ServiceHook struct { Command ShellCommand `yaml:"command,omitempty" json:"command"` - Image string `yaml:"image,omitempty" json:"image,omitempty"` User string `yaml:"user,omitempty" json:"user,omitempty"` Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"` WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"` Environment MappingWithEquals `yaml:"environment,omitempty" json:"environment,omitempty"` - PerReplica bool `yaml:"per_replica,omitempty" json:"per_replica,omitempty"` + + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` +} + +// PreStartHook is an init container run to completion before the service +// starts. It accepts the full container specification, inheriting from the +// service in the spirit of the yaml merge rules: collection attributes +// complete the inherited value with the hook's declarations winning on +// conflicts, scalar attributes replace it (image is inherited via +// normalization when undeclared). +type PreStartHook struct { + ContainerSpec `yaml:",inline" mapstructure:",squash"` + + // PerReplica runs the hook once per service replica instead of once per + // service. + PerReplica bool `yaml:"per_replica,omitempty" json:"per_replica,omitempty"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } diff --git a/types/jobs.go b/types/jobs.go new file mode 100644 index 000000000..163444233 --- /dev/null +++ b/types/jobs.go @@ -0,0 +1,53 @@ +/* + 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 + +// JobConfig is the configuration of one job +type JobConfig struct { + Name string `yaml:"name,omitempty" json:"-"` + Profiles []string `yaml:"profiles,omitempty" json:"profiles,omitempty"` + Triggers *TriggerConfig `yaml:"triggers,omitempty" json:"triggers,omitempty"` + + ContainerSpec `yaml:",inline" mapstructure:",squash"` + WorkloadSpec `yaml:",inline" mapstructure:",squash"` + + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` +} + +// TriggerConfig defines trigger conditions for a job. +// Manual is tri-state: nil (unset) leaves manual execution allowed — any job +// can be triggered by an explicit run command; an explicit false forbids it; +// an explicit true declares the job as manual-only intent. +type TriggerConfig struct { + Manual *bool `yaml:"manual,omitempty" json:"manual,omitempty"` + Schedule []ScheduleConfig `yaml:"schedule,omitempty" json:"schedule,omitempty"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` +} + +// ScheduleConfig defines a schedule for a job trigger. +// A plain crontab expression in yaml is canonicalized into a ScheduleConfig +// with only Cron set. +type ScheduleConfig struct { + Cron string `yaml:"cron,omitempty" json:"cron,omitempty"` + Timezone string `yaml:"timezone,omitempty" json:"timezone,omitempty"` + Concurrency string `yaml:"concurrency,omitempty" json:"concurrency,omitempty"` + MissedFires string `yaml:"missed_fires,omitempty" json:"missed_fires,omitempty"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` +} + +// Jobs is a mapping of job names to job configurations +type Jobs map[string]JobConfig diff --git a/types/project.go b/types/project.go index 66365faed..0db54621e 100644 --- a/types/project.go +++ b/types/project.go @@ -45,6 +45,7 @@ type Project struct { Name string `yaml:"name,omitempty" json:"name,omitempty"` WorkingDir string `yaml:"-" json:"-"` Services Services `yaml:"services" json:"services"` + Jobs Jobs `yaml:"jobs,omitempty" json:"jobs,omitempty"` Networks Networks `yaml:"networks,omitempty" json:"networks,omitempty"` Volumes Volumes `yaml:"volumes,omitempty" json:"volumes,omitempty"` Secrets Secrets `yaml:"secrets,omitempty" json:"secrets,omitempty"` @@ -57,7 +58,9 @@ type Project struct { // DisabledServices track services which have been disable as profile is not active DisabledServices Services `yaml:"-" json:"-"` - Profiles []string `yaml:"-" json:"-"` + // DisabledJobs track jobs which have been disabled as profile is not active + DisabledJobs Jobs `yaml:"-" json:"-"` + Profiles []string `yaml:"-" json:"-"` } // ServiceNames return names for all services in this Compose config @@ -263,6 +266,18 @@ func (p *Project) AllServices() Services { return all } +// AllJobs returns all the project jobs, enabled or not +func (p *Project) AllJobs() Jobs { + all := Jobs{} + for name, job := range p.Jobs { + all[name] = job + } + for name, job := range p.DisabledJobs { + all[name] = job + } + return all +} + type ServiceFunc func(name string, service *ServiceConfig) error // ForEachService runs ServiceFunc on each service and dependencies according to DependencyPolicy @@ -366,14 +381,23 @@ func (p *Project) RelativePath(path string) string { // HasProfile return true if service has no profile declared or has at least one profile matching func (s ServiceConfig) HasProfile(profiles []string) bool { - if len(s.Profiles) == 0 { + return matchesProfiles(s.Profiles, profiles) +} + +// HasProfile return true if job has no profile declared or has at least one profile matching +func (j JobConfig) HasProfile(profiles []string) bool { + return matchesProfiles(j.Profiles, profiles) +} + +func matchesProfiles(declared, active []string) bool { + if len(declared) == 0 { return true } - for _, p := range profiles { + for _, p := range active { if p == "*" { return true } - for _, sp := range s.Profiles { + for _, sp := range declared { if sp == p { return true } @@ -397,6 +421,21 @@ func (p *Project) WithProfiles(profiles []string) (*Project, error) { } newProject.Services = enabled newProject.DisabledServices = disabled + + if newProject.Jobs != nil || newProject.DisabledJobs != nil { + enabledJobs := Jobs{} + disabledJobs := Jobs{} + for name, job := range newProject.AllJobs() { + if job.HasProfile(profiles) { + enabledJobs[name] = job + } else { + disabledJobs[name] = job + } + } + newProject.Jobs = enabledJobs + newProject.DisabledJobs = disabledJobs + } + newProject.Profiles = profiles return newProject, nil } @@ -551,6 +590,40 @@ func (p *Project) WithSelectedServices(names []string, options ...DependencyOpti return newProject, nil } +// WithSelectedJob returns a new Project containing only the services required +// by the named job's DependsOn. The job itself is NOT added to Services. +func (p *Project) WithSelectedJob(name string, options ...DependencyOption) (*Project, error) { + job, ok := p.Jobs[name] + if !ok { + if disabled, exists := p.DisabledJobs[name]; exists { + // a profile-disabled job is enabled when explicitly selected, + // and its profiles are added to the set of active profiles + enabled, err := p.WithProfiles(append(append([]string{}, p.Profiles...), disabled.Profiles...)) + if err != nil { + return nil, err + } + return enabled.WithSelectedJob(name, options...) + } + return nil, fmt.Errorf("no such job: %s", name) + } + + var deps []string + for dep := range job.DependsOn { + deps = append(deps, dep) + } + + if len(deps) == 0 { + // Job has no service dependencies: return project with all services disabled + newProject := p.deepCopy() + for name := range newProject.Services { + newProject = newProject.WithServicesDisabled(name) + } + return newProject, nil + } + + return p.WithSelectedServices(deps, options...) +} + // WithServicesDisabled removes from the project model the given services and their references in all dependencies // It returns a new Project instance with the changes and keep the original Project unchanged func (p *Project) WithServicesDisabled(names ...string) *Project { @@ -739,6 +812,9 @@ func (p *Project) MarshalJSON(options ...func(*marshallOptions)) ([]byte, error) if len(src.Configs) > 0 { m["configs"] = src.Configs } + if len(src.Jobs) > 0 { + m["jobs"] = src.Jobs + } for k, v := range src.Extensions { m[k] = v } diff --git a/types/project_test.go b/types/project_test.go index 1abd5ca61..1d96b5fad 100644 --- a/types/project_test.go +++ b/types/project_test.go @@ -47,6 +47,34 @@ func Test_ApplyProfiles(t *testing.T) { assert.DeepEqual(t, p.DisabledServiceNames(), []string{"service_3"}) } +func Test_ApplyProfilesToJobs(t *testing.T) { + p := &Project{ + Jobs: Jobs{ + "job_1": {Name: "job_1"}, + "job_2": {Name: "job_2", Profiles: []string{"foo"}}, + "job_3": {Name: "job_3", Profiles: []string{"bar"}}, + }, + } + + // jobs with a profile are inactive by default + p, err := p.WithProfiles(nil) + assert.NilError(t, err) + assert.Equal(t, len(p.Jobs), 1) + assert.Equal(t, p.Jobs["job_1"].Name, "job_1") + assert.Equal(t, len(p.DisabledJobs), 2) + + p, err = p.WithProfiles([]string{"foo"}) + assert.NilError(t, err) + assert.Equal(t, len(p.Jobs), 2) + assert.Equal(t, p.Jobs["job_2"].Name, "job_2") + assert.Equal(t, len(p.DisabledJobs), 1) + + p, err = p.WithProfiles([]string{"*"}) + assert.NilError(t, err) + assert.Equal(t, len(p.Jobs), 3) + assert.Equal(t, len(p.DisabledJobs), 0) +} + func Test_WithoutUnnecessaryResources(t *testing.T) { p := makeProject() p.Networks["unused"] = NetworkConfig{} @@ -139,19 +167,19 @@ func makeProject() *Project { Name: "service_1", }, "service_2": ServiceConfig{ - Name: "service_2", - Profiles: []string{"foo"}, - DependsOn: map[string]ServiceDependency{"service_1": {Required: true}}, + Name: "service_2", + Profiles: []string{"foo"}, + WorkloadSpec: WorkloadSpec{DependsOn: map[string]ServiceDependency{"service_1": {Required: true}}}, }, "service_3": ServiceConfig{ - Name: "service_3", - Profiles: []string{"bar"}, - DependsOn: map[string]ServiceDependency{"service_2": {Required: true}}, + Name: "service_3", + Profiles: []string{"bar"}, + WorkloadSpec: WorkloadSpec{DependsOn: map[string]ServiceDependency{"service_2": {Required: true}}}, }, "service_4": ServiceConfig{ - Name: "service_4", - Profiles: []string{"zot"}, - DependsOn: map[string]ServiceDependency{"service_2": {Required: false}}, + Name: "service_4", + Profiles: []string{"zot"}, + WorkloadSpec: WorkloadSpec{DependsOn: map[string]ServiceDependency{"service_2": {Required: false}}}, }, "service_5": ServiceConfig{ Name: "service_5", @@ -219,14 +247,14 @@ func Test_ResolveImages_preStartHooks(t *testing.T) { p := &Project{ Services: Services{ "service_1": { - Name: "service_1", - Image: "alpine:3.20", - PreStart: []ServiceHook{ - {Image: "alpine:3.19", Command: ShellCommand{"echo", "init"}}, + Name: "service_1", + PreStart: []PreStartHook{ + {ContainerSpec: ContainerSpec{Image: "alpine:3.19", Command: ShellCommand{"echo", "init"}}}, // hook without an explicit image falls back to the service // image at runtime and must be left untouched here - {Command: ShellCommand{"echo", "noimage"}}, + {ContainerSpec: ContainerSpec{Command: ShellCommand{"echo", "noimage"}}}, }, + ContainerSpec: ContainerSpec{Image: "alpine:3.20"}, }, }, } @@ -248,19 +276,23 @@ func Test_ResolveImages_imageVolumes(t *testing.T) { p := &Project{ Services: Services{ "builder": { - Name: "builder", - Image: "docker.io/library/alpine:latest@" + digested, + Name: "builder", + ContainerSpec: ContainerSpec{ + Image: "docker.io/library/alpine:latest@" + digested, + }, }, "service_1": { - Name: "service_1", - Image: "alpine:3.20", - Volumes: []ServiceVolumeConfig{ - // external image reference: must be resolved to a digest - {Type: VolumeTypeImage, Source: "alpine:3.19", Target: "/data"}, - // reference to another service: resolved to a local image, left untouched - {Type: VolumeTypeImage, Source: "builder", Target: "/from-builder"}, - // regular named volume: left untouched - {Type: VolumeTypeVolume, Source: "vol", Target: "/vol"}, + Name: "service_1", + ContainerSpec: ContainerSpec{ + Image: "alpine:3.20", + Volumes: []ServiceVolumeConfig{ + // external image reference: must be resolved to a digest + {Type: VolumeTypeImage, Source: "alpine:3.19", Target: "/data"}, + // reference to another service: resolved to a local image, left untouched + {Type: VolumeTypeImage, Source: "builder", Target: "/from-builder"}, + // regular named volume: left untouched + {Type: VolumeTypeVolume, Source: "vol", Target: "/vol"}, + }, }, }, }, @@ -284,11 +316,11 @@ func Test_ResolveImages_preStartHookError(t *testing.T) { p := &Project{ Services: Services{ "service_1": { - Name: "service_1", - Image: "docker.io/library/alpine:3.20@sha256:1234567890123456789012345678901234567890123456789012345678901234", - PreStart: []ServiceHook{ - {Image: "alpine:3.19", Command: ShellCommand{"echo", "init"}}, + Name: "service_1", + PreStart: []PreStartHook{ + {ContainerSpec: ContainerSpec{Image: "alpine:3.19", Command: ShellCommand{"echo", "init"}}}, }, + ContainerSpec: ContainerSpec{Image: "docker.io/library/alpine:3.20@sha256:1234567890123456789012345678901234567890123456789012345678901234"}, }, }, } @@ -308,13 +340,15 @@ func Test_ResolveImages_imageVolumeDisabledService(t *testing.T) { Services: Services{ "service_1": { Name: "service_1", - Volumes: []ServiceVolumeConfig{ - {Type: VolumeTypeImage, Source: "Builder", Target: "/from-builder"}, + ContainerSpec: ContainerSpec{ + Volumes: []ServiceVolumeConfig{ + {Type: VolumeTypeImage, Source: "Builder", Target: "/from-builder"}, + }, }, }, }, DisabledServices: Services{ - "Builder": {Name: "Builder", Image: "alpine:3.19"}, + "Builder": {Name: "Builder", ContainerSpec: ContainerSpec{Image: "alpine:3.19"}}, }, } @@ -337,16 +371,15 @@ func Test_ResolveImages_deduplicated(t *testing.T) { // transform (image, hook, volume) — the sequential lookups must // collapse to a single resolver call. "service_1": { - Name: "service_1", - Image: "alpine:3.20", - PreStart: []ServiceHook{{Image: "alpine:3.20", Command: ShellCommand{"echo"}}}, - Volumes: []ServiceVolumeConfig{{Type: VolumeTypeImage, Source: "alpine:3.20", Target: "/data"}}, + Name: "service_1", + PreStart: []PreStartHook{{ContainerSpec: ContainerSpec{Image: "alpine:3.20", Command: ShellCommand{"echo"}}}}, + ContainerSpec: ContainerSpec{Image: "alpine:3.20", Volumes: []ServiceVolumeConfig{{Type: VolumeTypeImage, Source: "alpine:3.20", Target: "/data"}}}, }, // service_2 shares the same image, resolved from a concurrent // transform — must also be served from the shared cache. "service_2": { - Name: "service_2", - Image: "alpine:3.20", + Name: "service_2", + ContainerSpec: ContainerSpec{Image: "alpine:3.20"}, }, }, } @@ -369,7 +402,9 @@ func Test_ResolveImages_concurrent(t *testing.T) { } for i := 0; i < 1000; i++ { p.Services[fmt.Sprintf("service_%d", i)] = ServiceConfig{ - Image: fmt.Sprintf("image_%d", i), + ContainerSpec: ContainerSpec{ + Image: fmt.Sprintf("image_%d", i), + }, } } p, err := p.WithImagesResolved(resolver) @@ -389,7 +424,9 @@ func Test_ResolveImages_concurrent_interrupted(t *testing.T) { } for i := 0; i < 10; i++ { p.Services[fmt.Sprintf("service_%d", i)] = ServiceConfig{ - Image: fmt.Sprintf("image_%d", i), + ContainerSpec: ContainerSpec{ + Image: fmt.Sprintf("image_%d", i), + }, } } _, err := p.WithImagesResolved(resolver) @@ -600,22 +637,26 @@ func TestProject_WithServicesEnvironmentResolved(t *testing.T) { p := &Project{ Services: Services{ "base": ServiceConfig{ - Environment: MappingWithEquals{ - "FOO": ptr("foo_from_environment"), - "BAR": ptr("bar_from_environment"), - "QIX": nil, - }, - EnvFiles: []EnvFile{ - {Path: "fixtures/base.env"}, + ContainerSpec: ContainerSpec{ + Environment: MappingWithEquals{ + "FOO": ptr("foo_from_environment"), + "BAR": ptr("bar_from_environment"), + "QIX": nil, + }, + EnvFiles: []EnvFile{ + {Path: "fixtures/base.env"}, + }, }, }, "override": ServiceConfig{ - Environment: MappingWithEquals{ - "FOO": ptr("foo_from_environment"), - }, - EnvFiles: []EnvFile{ - {Path: "fixtures/base.env"}, - {Path: "fixtures/override.env"}, + ContainerSpec: ContainerSpec{ + Environment: MappingWithEquals{ + "FOO": ptr("foo_from_environment"), + }, + EnvFiles: []EnvFile{ + {Path: "fixtures/base.env"}, + {Path: "fixtures/override.env"}, + }, }, }, }, @@ -656,6 +697,90 @@ func TestProject_WithServicesEnvironmentResolved(t *testing.T) { }) } +func TestWithSelectedJob(t *testing.T) { + project := &Project{ + Services: Services{ + "db": { + Name: "db", + ContainerSpec: ContainerSpec{ + Image: "postgres", + }, + }, + "redis": { + Name: "redis", + ContainerSpec: ContainerSpec{ + Image: "redis", + }, + }, + "web": { + Name: "web", + ContainerSpec: ContainerSpec{Image: "myapp"}, + WorkloadSpec: WorkloadSpec{DependsOn: DependsOnConfig{ + "db": {Condition: "service_healthy"}, + }}, + }, + }, + Jobs: Jobs{ + "migrate": { + Name: "migrate", + ContainerSpec: ContainerSpec{Image: "myapp", Command: ShellCommand{"migrate"}}, + WorkloadSpec: WorkloadSpec{DependsOn: DependsOnConfig{ + "db": {Condition: "service_healthy"}, + }}, + }, + "seed": { + Name: "seed", + ContainerSpec: ContainerSpec{ + Image: "myapp", + }, + }, + }, + } + + t.Run("job with dependencies includes only required services", func(t *testing.T) { + result, err := project.WithSelectedJob("migrate") + assert.NilError(t, err) + assert.Equal(t, len(result.Services), 1) + _, hasDB := result.Services["db"] + assert.Assert(t, hasDB) + _, hasRedis := result.Services["redis"] + assert.Assert(t, !hasRedis) + _, hasWeb := result.Services["web"] + assert.Assert(t, !hasWeb) + }) + + t.Run("job without dependencies returns empty services", func(t *testing.T) { + result, err := project.WithSelectedJob("seed") + assert.NilError(t, err) + assert.Equal(t, len(result.Services), 0) + }) + + t.Run("unknown job returns error", func(t *testing.T) { + _, err := project.WithSelectedJob("nonexistent") + assert.ErrorContains(t, err, "no such job: nonexistent") + }) + + t.Run("profile-disabled job is enabled when selected", func(t *testing.T) { + p := project.deepCopy() + p.Jobs["cleanup"] = JobConfig{ + Name: "cleanup", + Profiles: []string{"maintenance"}, + ContainerSpec: ContainerSpec{ + Image: "busybox", + }, + } + p, err := p.WithProfiles(nil) + assert.NilError(t, err) + _, disabled := p.DisabledJobs["cleanup"] + assert.Assert(t, disabled) + + result, err := p.WithSelectedJob("cleanup") + assert.NilError(t, err) + _, enabled := result.Jobs["cleanup"] + assert.Assert(t, enabled) + }) +} + func ptr[T any](s T) *T { return &s } diff --git a/types/types.go b/types/types.go index 66987e9fe..eca2e799b 100644 --- a/types/types.go +++ b/types/types.go @@ -28,29 +28,26 @@ import ( "github.com/xhit/go-str2duration/v2" ) -// ServiceConfig is the configuration of one service -type ServiceConfig struct { - Name string `yaml:"name,omitempty" json:"-"` - Profiles []string `yaml:"profiles,omitempty" json:"profiles,omitempty"` - - Annotations Mapping `yaml:"annotations,omitempty" json:"annotations,omitempty"` - Attach *bool `yaml:"attach,omitempty" json:"attach,omitempty"` - Build *BuildConfig `yaml:"build,omitempty" json:"build,omitempty"` - Develop *DevelopConfig `yaml:"develop,omitempty" json:"develop,omitempty"` - BlkioConfig *BlkioConfig `yaml:"blkio_config,omitempty" json:"blkio_config,omitempty"` - CapAdd []string `yaml:"cap_add,omitempty" json:"cap_add,omitempty"` - CapDrop []string `yaml:"cap_drop,omitempty" json:"cap_drop,omitempty"` - CgroupParent string `yaml:"cgroup_parent,omitempty" json:"cgroup_parent,omitempty"` - Cgroup string `yaml:"cgroup,omitempty" json:"cgroup,omitempty"` - CPUCount int64 `yaml:"cpu_count,omitempty" json:"cpu_count,omitempty"` - CPUPercent float32 `yaml:"cpu_percent,omitempty" json:"cpu_percent,omitempty"` - CPUPeriod int64 `yaml:"cpu_period,omitempty" json:"cpu_period,omitempty"` - CPUQuota int64 `yaml:"cpu_quota,omitempty" json:"cpu_quota,omitempty"` - CPURTPeriod int64 `yaml:"cpu_rt_period,omitempty" json:"cpu_rt_period,omitempty"` - CPURTRuntime int64 `yaml:"cpu_rt_runtime,omitempty" json:"cpu_rt_runtime,omitempty"` - CPUS float32 `yaml:"cpus,omitempty" json:"cpus,omitempty"` - CPUSet string `yaml:"cpuset,omitempty" json:"cpuset,omitempty"` - CPUShares int64 `yaml:"cpu_shares,omitempty" json:"cpu_shares,omitempty"` +// ContainerSpec defines the runtime configuration for a container. +// It is the common set of attributes shared by services, jobs, and other container-based elements. +// ContainerSpec holds the attributes shared by anything that runs a container: +// services, jobs, and run-to-completion init containers (pre_start hooks). +type ContainerSpec struct { + Annotations Mapping `yaml:"annotations,omitempty" json:"annotations,omitempty"` + BlkioConfig *BlkioConfig `yaml:"blkio_config,omitempty" json:"blkio_config,omitempty"` + CapAdd []string `yaml:"cap_add,omitempty" json:"cap_add,omitempty"` + CapDrop []string `yaml:"cap_drop,omitempty" json:"cap_drop,omitempty"` + CgroupParent string `yaml:"cgroup_parent,omitempty" json:"cgroup_parent,omitempty"` + Cgroup string `yaml:"cgroup,omitempty" json:"cgroup,omitempty"` + CPUCount int64 `yaml:"cpu_count,omitempty" json:"cpu_count,omitempty"` + CPUPercent float32 `yaml:"cpu_percent,omitempty" json:"cpu_percent,omitempty"` + CPUPeriod int64 `yaml:"cpu_period,omitempty" json:"cpu_period,omitempty"` + CPUQuota int64 `yaml:"cpu_quota,omitempty" json:"cpu_quota,omitempty"` + CPURTPeriod int64 `yaml:"cpu_rt_period,omitempty" json:"cpu_rt_period,omitempty"` + CPURTRuntime int64 `yaml:"cpu_rt_runtime,omitempty" json:"cpu_rt_runtime,omitempty"` + CPUS float32 `yaml:"cpus,omitempty" json:"cpus,omitempty"` + CPUSet string `yaml:"cpuset,omitempty" json:"cpuset,omitempty"` + CPUShares int64 `yaml:"cpu_shares,omitempty" json:"cpu_shares,omitempty"` // Command for the service containers. // If set, overrides COMMAND from the image. @@ -59,88 +56,109 @@ type ServiceConfig struct { Command ShellCommand `yaml:"command,omitempty" json:"command"` // NOTE: we can NOT omitempty for JSON! see ShellCommand type for details. Configs []ServiceConfigObjConfig `yaml:"configs,omitempty" json:"configs,omitempty"` - ContainerName string `yaml:"container_name,omitempty" json:"container_name,omitempty"` CredentialSpec *CredentialSpecConfig `yaml:"credential_spec,omitempty" json:"credential_spec,omitempty"` - DependsOn DependsOnConfig `yaml:"depends_on,omitempty" json:"depends_on,omitempty"` - Deploy *DeployConfig `yaml:"deploy,omitempty" json:"deploy,omitempty"` DeviceCgroupRules []string `yaml:"device_cgroup_rules,omitempty" json:"device_cgroup_rules,omitempty"` Devices []DeviceMapping `yaml:"devices,omitempty" json:"devices,omitempty"` DNS StringList `yaml:"dns,omitempty" json:"dns,omitempty"` DNSOpts []string `yaml:"dns_opt,omitempty" json:"dns_opt,omitempty"` DNSSearch StringList `yaml:"dns_search,omitempty" json:"dns_search,omitempty"` - Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"` DomainName string `yaml:"domainname,omitempty" json:"domainname,omitempty"` // Entrypoint for the service containers. // If set, overrides ENTRYPOINT from the image. // // Set to `[]` or an empty string to clear the entrypoint from the image. - Entrypoint ShellCommand `yaml:"entrypoint,omitempty" json:"entrypoint"` // NOTE: we can NOT omitempty for JSON! see ShellCommand type for details. - Provider *ServiceProviderConfig `yaml:"provider,omitempty" json:"provider,omitempty"` - Environment MappingWithEquals `yaml:"environment,omitempty" json:"environment,omitempty"` - EnvFiles []EnvFile `yaml:"env_file,omitempty" json:"env_file,omitempty"` - Expose StringOrNumberList `yaml:"expose,omitempty" json:"expose,omitempty"` - Extends *ExtendsConfig `yaml:"extends,omitempty" json:"extends,omitempty"` - ExternalLinks []string `yaml:"external_links,omitempty" json:"external_links,omitempty"` - ExtraHosts HostsList `yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"` - GroupAdd []string `yaml:"group_add,omitempty" json:"group_add,omitempty"` - Gpus []DeviceRequest `yaml:"gpus,omitempty" json:"gpus,omitempty"` - Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"` - HealthCheck *HealthCheckConfig `yaml:"healthcheck,omitempty" json:"healthcheck,omitempty"` - Image string `yaml:"image,omitempty" json:"image,omitempty"` - Init *bool `yaml:"init,omitempty" json:"init,omitempty"` - Ipc string `yaml:"ipc,omitempty" json:"ipc,omitempty"` - Isolation string `yaml:"isolation,omitempty" json:"isolation,omitempty"` - Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` - LabelFiles []string `yaml:"label_file,omitempty" json:"label_file,omitempty"` - CustomLabels Labels `yaml:"-" json:"-"` - Links []string `yaml:"links,omitempty" json:"links,omitempty"` - Logging *LoggingConfig `yaml:"logging,omitempty" json:"logging,omitempty"` - LogDriver string `yaml:"log_driver,omitempty" json:"log_driver,omitempty"` - LogOpt map[string]string `yaml:"log_opt,omitempty" json:"log_opt,omitempty"` - MemLimit UnitBytes `yaml:"mem_limit,omitempty" json:"mem_limit,omitempty"` - MemReservation UnitBytes `yaml:"mem_reservation,omitempty" json:"mem_reservation,omitempty"` - MemSwapLimit UnitBytes `yaml:"memswap_limit,omitempty" json:"memswap_limit,omitempty"` - MemSwappiness UnitBytes `yaml:"mem_swappiness,omitempty" json:"mem_swappiness,omitempty"` - MacAddress string `yaml:"mac_address,omitempty" json:"mac_address,omitempty"` - Models map[string]*ServiceModelConfig `yaml:"models,omitempty" json:"models,omitempty"` - Net string `yaml:"net,omitempty" json:"net,omitempty"` - NetworkMode string `yaml:"network_mode,omitempty" json:"network_mode,omitempty"` - Networks map[string]*ServiceNetworkConfig `yaml:"networks,omitempty" json:"networks,omitempty"` - OomKillDisable bool `yaml:"oom_kill_disable,omitempty" json:"oom_kill_disable,omitempty"` - OomScoreAdj int64 `yaml:"oom_score_adj,omitempty" json:"oom_score_adj,omitempty"` - Pid string `yaml:"pid,omitempty" json:"pid,omitempty"` - PidsLimit int64 `yaml:"pids_limit,omitempty" json:"pids_limit,omitempty"` - Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` - Ports []ServicePortConfig `yaml:"ports,omitempty" json:"ports,omitempty"` - Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"` - PullPolicy string `yaml:"pull_policy,omitempty" json:"pull_policy,omitempty"` - ReadOnly bool `yaml:"read_only,omitempty" json:"read_only,omitempty"` - Restart string `yaml:"restart,omitempty" json:"restart,omitempty"` - Runtime string `yaml:"runtime,omitempty" json:"runtime,omitempty"` - Scale *int `yaml:"scale,omitempty" json:"scale,omitempty"` - Secrets []ServiceSecretConfig `yaml:"secrets,omitempty" json:"secrets,omitempty"` - SecurityOpt []string `yaml:"security_opt,omitempty" json:"security_opt,omitempty"` - ShmSize UnitBytes `yaml:"shm_size,omitempty" json:"shm_size,omitempty"` - StdinOpen bool `yaml:"stdin_open,omitempty" json:"stdin_open,omitempty"` - StopGracePeriod *Duration `yaml:"stop_grace_period,omitempty" json:"stop_grace_period,omitempty"` - StopSignal string `yaml:"stop_signal,omitempty" json:"stop_signal,omitempty"` - StorageOpt map[string]string `yaml:"storage_opt,omitempty" json:"storage_opt,omitempty"` - Sysctls Mapping `yaml:"sysctls,omitempty" json:"sysctls,omitempty"` - Tmpfs StringList `yaml:"tmpfs,omitempty" json:"tmpfs,omitempty"` - Tty bool `yaml:"tty,omitempty" json:"tty,omitempty"` - Ulimits map[string]*UlimitsConfig `yaml:"ulimits,omitempty" json:"ulimits,omitempty"` - UseAPISocket bool `yaml:"use_api_socket,omitempty" json:"use_api_socket,omitempty"` - User string `yaml:"user,omitempty" json:"user,omitempty"` - UserNSMode string `yaml:"userns_mode,omitempty" json:"userns_mode,omitempty"` - Uts string `yaml:"uts,omitempty" json:"uts,omitempty"` - VolumeDriver string `yaml:"volume_driver,omitempty" json:"volume_driver,omitempty"` - Volumes []ServiceVolumeConfig `yaml:"volumes,omitempty" json:"volumes,omitempty"` - VolumesFrom []string `yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"` - WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"` - PreStart []ServiceHook `yaml:"pre_start,omitempty" json:"pre_start,omitempty"` - PostStart []ServiceHook `yaml:"post_start,omitempty" json:"post_start,omitempty"` - PreStop []ServiceHook `yaml:"pre_stop,omitempty" json:"pre_stop,omitempty"` + Entrypoint ShellCommand `yaml:"entrypoint,omitempty" json:"entrypoint"` // NOTE: we can NOT omitempty for JSON! see ShellCommand type for details. + Environment MappingWithEquals `yaml:"environment,omitempty" json:"environment,omitempty"` + EnvFiles []EnvFile `yaml:"env_file,omitempty" json:"env_file,omitempty"` + ExtraHosts HostsList `yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"` + GroupAdd []string `yaml:"group_add,omitempty" json:"group_add,omitempty"` + Gpus []DeviceRequest `yaml:"gpus,omitempty" json:"gpus,omitempty"` + Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"` + Image string `yaml:"image,omitempty" json:"image,omitempty"` + Init *bool `yaml:"init,omitempty" json:"init,omitempty"` + Ipc string `yaml:"ipc,omitempty" json:"ipc,omitempty"` + Isolation string `yaml:"isolation,omitempty" json:"isolation,omitempty"` + Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` + LabelFiles []string `yaml:"label_file,omitempty" json:"label_file,omitempty"` + CustomLabels Labels `yaml:"-" json:"-"` + Logging *LoggingConfig `yaml:"logging,omitempty" json:"logging,omitempty"` + LogDriver string `yaml:"log_driver,omitempty" json:"log_driver,omitempty"` + LogOpt map[string]string `yaml:"log_opt,omitempty" json:"log_opt,omitempty"` + MemLimit UnitBytes `yaml:"mem_limit,omitempty" json:"mem_limit,omitempty"` + MemReservation UnitBytes `yaml:"mem_reservation,omitempty" json:"mem_reservation,omitempty"` + MemSwapLimit UnitBytes `yaml:"memswap_limit,omitempty" json:"memswap_limit,omitempty"` + MemSwappiness UnitBytes `yaml:"mem_swappiness,omitempty" json:"mem_swappiness,omitempty"` + MacAddress string `yaml:"mac_address,omitempty" json:"mac_address,omitempty"` + Models map[string]*ServiceModelConfig `yaml:"models,omitempty" json:"models,omitempty"` + NetworkMode string `yaml:"network_mode,omitempty" json:"network_mode,omitempty"` + Networks map[string]*ServiceNetworkConfig `yaml:"networks,omitempty" json:"networks,omitempty"` + OomKillDisable bool `yaml:"oom_kill_disable,omitempty" json:"oom_kill_disable,omitempty"` + OomScoreAdj int64 `yaml:"oom_score_adj,omitempty" json:"oom_score_adj,omitempty"` + Pid string `yaml:"pid,omitempty" json:"pid,omitempty"` + PidsLimit int64 `yaml:"pids_limit,omitempty" json:"pids_limit,omitempty"` + Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` + Privileged bool `yaml:"privileged,omitempty" json:"privileged,omitempty"` + PullPolicy string `yaml:"pull_policy,omitempty" json:"pull_policy,omitempty"` + PullRefreshAfter string `yaml:"pull_refresh_after,omitempty" json:"pull_refresh_after,omitempty"` + ReadOnly bool `yaml:"read_only,omitempty" json:"read_only,omitempty"` + Runtime string `yaml:"runtime,omitempty" json:"runtime,omitempty"` + Secrets []ServiceSecretConfig `yaml:"secrets,omitempty" json:"secrets,omitempty"` + SecurityOpt []string `yaml:"security_opt,omitempty" json:"security_opt,omitempty"` + ShmSize UnitBytes `yaml:"shm_size,omitempty" json:"shm_size,omitempty"` + StopGracePeriod *Duration `yaml:"stop_grace_period,omitempty" json:"stop_grace_period,omitempty"` + StopSignal string `yaml:"stop_signal,omitempty" json:"stop_signal,omitempty"` + StorageOpt map[string]string `yaml:"storage_opt,omitempty" json:"storage_opt,omitempty"` + Sysctls Mapping `yaml:"sysctls,omitempty" json:"sysctls,omitempty"` + Tmpfs StringList `yaml:"tmpfs,omitempty" json:"tmpfs,omitempty"` + Ulimits map[string]*UlimitsConfig `yaml:"ulimits,omitempty" json:"ulimits,omitempty"` + UseAPISocket bool `yaml:"use_api_socket,omitempty" json:"use_api_socket,omitempty"` + User string `yaml:"user,omitempty" json:"user,omitempty"` + UserNSMode string `yaml:"userns_mode,omitempty" json:"userns_mode,omitempty"` + Uts string `yaml:"uts,omitempty" json:"uts,omitempty"` + VolumeDriver string `yaml:"volume_driver,omitempty" json:"volume_driver,omitempty"` + Volumes []ServiceVolumeConfig `yaml:"volumes,omitempty" json:"volumes,omitempty"` + VolumesFrom []string `yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"` + WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"` +} + +// WorkloadSpec holds container attributes meaningful for orchestrated +// workloads (services and jobs) but not for run-to-completion init +// containers: build, dependency ordering, health reporting, port exposure +// and interactivity. +type WorkloadSpec struct { + Build *BuildConfig `yaml:"build,omitempty" json:"build,omitempty"` + DependsOn DependsOnConfig `yaml:"depends_on,omitempty" json:"depends_on,omitempty"` + Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"` + Expose StringOrNumberList `yaml:"expose,omitempty" json:"expose,omitempty"` + HealthCheck *HealthCheckConfig `yaml:"healthcheck,omitempty" json:"healthcheck,omitempty"` + Ports []ServicePortConfig `yaml:"ports,omitempty" json:"ports,omitempty"` + StdinOpen bool `yaml:"stdin_open,omitempty" json:"stdin_open,omitempty"` + Tty bool `yaml:"tty,omitempty" json:"tty,omitempty"` +} + +// ServiceConfig is the configuration of one service +type ServiceConfig struct { + Name string `yaml:"name,omitempty" json:"-"` + Profiles []string `yaml:"profiles,omitempty" json:"profiles,omitempty"` + Deploy *DeployConfig `yaml:"deploy,omitempty" json:"deploy,omitempty"` + Develop *DevelopConfig `yaml:"develop,omitempty" json:"develop,omitempty"` + Restart string `yaml:"restart,omitempty" json:"restart,omitempty"` + Scale *int `yaml:"scale,omitempty" json:"scale,omitempty"` + + Attach *bool `yaml:"attach,omitempty" json:"attach,omitempty"` + ContainerName string `yaml:"container_name,omitempty" json:"container_name,omitempty"` + Provider *ServiceProviderConfig `yaml:"provider,omitempty" json:"provider,omitempty"` + Extends *ExtendsConfig `yaml:"extends,omitempty" json:"extends,omitempty"` + ExternalLinks []string `yaml:"external_links,omitempty" json:"external_links,omitempty"` + Links []string `yaml:"links,omitempty" json:"links,omitempty"` + Net string `yaml:"net,omitempty" json:"net,omitempty"` + PreStart []PreStartHook `yaml:"pre_start,omitempty" json:"pre_start,omitempty"` + PostStart []ServiceHook `yaml:"post_start,omitempty" json:"post_start,omitempty"` + PreStop []ServiceHook `yaml:"pre_stop,omitempty" json:"pre_stop,omitempty"` + + ContainerSpec `yaml:",inline" mapstructure:",squash"` + WorkloadSpec `yaml:",inline" mapstructure:",squash"` Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } @@ -160,7 +178,7 @@ func (s ServiceConfig) MarshalYAML() (interface{}, error) { } // NetworksByPriority return the service networks IDs sorted according to Priority -func (s *ServiceConfig) NetworksByPriority() []string { +func (s *ContainerSpec) NetworksByPriority() []string { type key struct { name string priority int diff --git a/types/types_test.go b/types/types_test.go index 2d03daa73..4759315e5 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -253,16 +253,18 @@ func TestNewMapping(t *testing.T) { func TestNetworksByPriority(t *testing.T) { s := ServiceConfig{ - Networks: map[string]*ServiceNetworkConfig{ - "foo": nil, - "bar": { - Priority: 10, - }, - "zot": { - Priority: 100, - }, - "qix": { - Priority: 1000, + ContainerSpec: ContainerSpec{ + Networks: map[string]*ServiceNetworkConfig{ + "foo": nil, + "bar": { + Priority: 10, + }, + "zot": { + Priority: 100, + }, + "qix": { + Priority: 1000, + }, }, }, } @@ -271,11 +273,13 @@ func TestNetworksByPriority(t *testing.T) { func TestNetworksByPriorityWithEqualPriorities(t *testing.T) { s := ServiceConfig{ - Networks: map[string]*ServiceNetworkConfig{ - "foo": nil, - "bar": nil, - "zot": nil, - "qix": nil, + ContainerSpec: ContainerSpec{ + Networks: map[string]*ServiceNetworkConfig{ + "foo": nil, + "bar": nil, + "zot": nil, + "qix": nil, + }, }, } assert.DeepEqual(t, s.NetworksByPriority(), []string{"bar", "foo", "qix", "zot"}) @@ -321,7 +325,7 @@ func TestMarshalServiceEntrypoint(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - s := ServiceConfig{Entrypoint: tc.entrypoint} + s := ServiceConfig{ContainerSpec: ContainerSpec{Entrypoint: tc.entrypoint}} actualYAML, err := yaml.Marshal(s) assert.NilError(t, err, "YAML marshal failed") assertEqual(t, actualYAML, tc.expectedYAML) @@ -385,17 +389,19 @@ func TestMarhsall(t *testing.T) { p := Project{ Services: Services{ "test": ServiceConfig{ - Volumes: []ServiceVolumeConfig{ - { - Type: "bind", - Bind: &ServiceVolumeBind{ - CreateHostPath: true, // default + ContainerSpec: ContainerSpec{ + Volumes: []ServiceVolumeConfig{ + { + Type: "bind", + Bind: &ServiceVolumeBind{ + CreateHostPath: true, // default + }, }, - }, - { - Type: "bind", - Bind: &ServiceVolumeBind{ - CreateHostPath: false, + { + Type: "bind", + Bind: &ServiceVolumeBind{ + CreateHostPath: false, + }, }, }, },