Skip to content
Open

Jobs #866

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
62 changes: 31 additions & 31 deletions graph/graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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]{
Expand All @@ -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]{
Expand All @@ -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]{
Expand All @@ -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]{
Expand All @@ -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"`,
Expand All @@ -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]`,
Expand All @@ -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]{
Expand Down Expand Up @@ -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",
Expand Down
21 changes: 13 additions & 8 deletions loader/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
30 changes: 16 additions & 14 deletions loader/extends.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
98 changes: 98 additions & 0 deletions loader/golden_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
21 changes: 4 additions & 17 deletions loader/include.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
10 changes: 6 additions & 4 deletions loader/include_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
},
},
})
}
Expand Down
Loading