Skip to content

Commit

Permalink
re-implemented ResetProcessor to apply on map[string]any
Browse files Browse the repository at this point in the history
Signed-off-by: Nicolas De Loof <[email protected]>
  • Loading branch information
ndeloof committed Oct 10, 2023
1 parent 963c38b commit 9b8be2a
Show file tree
Hide file tree
Showing 26 changed files with 691 additions and 198 deletions.
42 changes: 27 additions & 15 deletions loader/extends.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,22 @@ package loader
import (
"context"
"fmt"
"path/filepath"

"github.com/compose-spec/compose-go/override"
"github.com/compose-spec/compose-go/types"
)

func ApplyExtends(ctx context.Context, dict map[string]interface{}, opts *Options) error {
services := dict["services"].(map[string]interface{})
func ApplyExtends(ctx context.Context, dict map[string]any, opts *Options, post ...PostProcessor) error {
services := dict["services"].(map[string]any)
for name, s := range services {
service := s.(map[string]interface{})
service := s.(map[string]any)
x, ok := service["extends"]
if !ok {
continue
}
extends := x.(map[string]interface{})
var base interface{}
extends := x.(map[string]any)
var base any
ref := extends["service"].(string)
if file, ok := extends["file"]; ok {
path := file.(string)
Expand All @@ -43,15 +44,18 @@ func ApplyExtends(ctx context.Context, dict map[string]interface{}, opts *Option
if err != nil {
return err
}
source, err := loadYamlModel(ctx, []types.ConfigFile{
{Filename: local},
source, err := loadYamlModel(ctx, types.ConfigDetails{
WorkingDir: filepath.Dir(path),
ConfigFiles: []types.ConfigFile{
{Filename: local},
},
}, opts)
if err != nil {
return err
}
services := source["services"].([]interface{})
services := source["services"].([]any)
for _, s := range services {
service := s.(map[string]interface{})
service := s.(map[string]any)
if service["name"] == ref {
base = service
break
Expand All @@ -71,7 +75,15 @@ func ApplyExtends(ctx context.Context, dict map[string]interface{}, opts *Option
return fmt.Errorf("cannot extend service %q in %s: service not found", name, "filename") //TODO track filename
}
}
merged, err := override.ExtendService(deepClone(base).(map[string]interface{}), service)
source := deepClone(base).(map[string]any)
for _, processor := range post {
processor.Apply(map[string]any{
"services": map[string]any{
name: source,
},
})
}
merged, err := override.ExtendService(source, service)
if err != nil {
return err
}
Expand All @@ -81,16 +93,16 @@ func ApplyExtends(ctx context.Context, dict map[string]interface{}, opts *Option
return nil
}

func deepClone(value interface{}) interface{} {
func deepClone(value any) any {
switch v := value.(type) {
case []interface{}:
cp := make([]interface{}, len(v))
case []any:
cp := make([]any, len(v))
for i, e := range v {
cp[i] = deepClone(e)
}
return cp
case map[string]interface{}:
cp := make(map[string]interface{}, len(v))
case map[string]any:
cp := make(map[string]any, len(v))
for k, e := range v {
cp[k] = deepClone(e)
}
Expand Down
36 changes: 36 additions & 0 deletions loader/fix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
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

// fixEmptyNotNull is a workaround for https://github.com/xeipuuv/gojsonschema/issues/141
// as go-yaml `[]` will load as a `[]any(nil)`, which is not the same as an empty array
func fixEmptyNotNull(value any) interface{} {
switch v := value.(type) {
case []any:
if v == nil {
return []any{}
}
for i, e := range v {
v[i] = fixEmptyNotNull(e)
}
case map[string]any:
for k, e := range v {
v[k] = fixEmptyNotNull(e)
}
}
return value
}
97 changes: 63 additions & 34 deletions loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
"fmt"
"io"
"os"
paths "path"
"path"
"path/filepath"
"reflect"
"regexp"
Expand All @@ -33,6 +33,7 @@ import (
"github.com/compose-spec/compose-go/format"
interp "github.com/compose-spec/compose-go/interpolation"
"github.com/compose-spec/compose-go/override"
"github.com/compose-spec/compose-go/paths"
"github.com/compose-spec/compose-go/schema"
"github.com/compose-spec/compose-go/template"
"github.com/compose-spec/compose-go/transform"
Expand All @@ -51,9 +52,9 @@ type Options struct {
SkipInterpolation bool
// Skip normalization
SkipNormalization bool
// Resolve paths
// Resolve path
ResolvePaths bool
// Convert Windows paths
// Convert Windows path
ConvertWindowsPaths bool
// Skip consistency check
SkipConsistencyCheck bool
Expand Down Expand Up @@ -265,12 +266,12 @@ func LoadWithContext(ctx context.Context, configDetails types.ConfigDetails, opt
return load(ctx, configDetails, opts, nil)
}

func loadYamlModel(ctx context.Context, configFiles []types.ConfigFile, opts *Options) (map[string]interface{}, error) {
func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Options) (map[string]interface{}, error) {
var (
dict = map[string]interface{}{}
err error
)
for _, file := range configFiles {
for _, file := range config.ConfigFiles {
if len(file.Content) == 0 {
content, err := os.ReadFile(file.Filename)
if err != nil {
Expand All @@ -282,8 +283,8 @@ func loadYamlModel(ctx context.Context, configFiles []types.ConfigFile, opts *Op
r := bytes.NewReader(file.Content)
decoder := yaml.NewDecoder(r)
for {
var cfg map[string]interface{}
processor := ResetProcessor{target: &cfg}
var raw interface{}
processor := ResetProcessor{target: &raw}
err := decoder.Decode(&processor)
if err == io.EOF {
break
Expand All @@ -292,6 +293,24 @@ func loadYamlModel(ctx context.Context, configFiles []types.ConfigFile, opts *Op
return nil, err
}

converted, err := convertToStringKeysRecursive(raw, "")
if err != nil {
return nil, err
}
cfg, ok := converted.(map[string]interface{})
if !ok {
return nil, errors.New("Top-level object must be a mapping")
}

if opts.Interpolate != nil && !opts.SkipInterpolation {
cfg, err = interp.Interpolate(cfg, *opts.Interpolate)
if err != nil {
return nil, err
}
}

fixEmptyNotNull(cfg)

if !opts.SkipValidation {
if err := schema.Validate(cfg); err != nil {
return nil, fmt.Errorf("validating %s: %w", file.Filename, err)
Expand All @@ -302,39 +321,42 @@ func loadYamlModel(ctx context.Context, configFiles []types.ConfigFile, opts *Op
if err != nil {
return nil, err
}

if !opts.SkipExtends {
err = ApplyExtends(ctx, cfg, opts, &processor)
if err != nil {
return nil, err
}
}

dict, err = override.Merge(dict, cfg)
if err != nil {
return nil, err
}
}
}

if opts.Interpolate != nil && !opts.SkipInterpolation {
dict, err = interp.Interpolate(dict, *opts.Interpolate)
if err != nil {
return nil, err
}
}

if !opts.SkipExtends {
err = ApplyExtends(ctx, dict, opts)
if err != nil {
return nil, err
}
dict, err = override.EnforceUnicity(dict)
if err != nil {
return nil, err
}

dict, err = override.EnforceUnicity(dict)
dict, err = transform.Canonical(dict)
if err != nil {
return nil, err
}

dict = groupXFieldsIntoExtensions(dict)

dict, err = transform.Canonical(dict)
if err != nil {
// TODO(ndeloof) shall we implement this as a Validate func on types ?
if err := Validate(dict); err != nil {
return nil, err
}

if opts.ResolvePaths {
paths.ResolveRelativePaths(dict, config.WorkingDir)
}

return dict, nil
}

Expand All @@ -350,7 +372,7 @@ func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options,

includeRefs := make(map[string][]types.IncludeConfig)

dict, err := loadYamlModel(ctx, configDetails.ConfigFiles, opts)
dict, err := loadYamlModel(ctx, configDetails, opts)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -523,8 +545,15 @@ func groupXFieldsIntoExtensions(dict map[string]interface{}) map[string]interfac
extras[key] = value
delete(dict, key)
}
if d, ok := value.(map[string]interface{}); ok {
dict[key] = groupXFieldsIntoExtensions(d)
switch v := value.(type) {
case map[string]interface{}:
dict[key] = groupXFieldsIntoExtensions(v)
case []interface{}:
for i, e := range v {
if m, ok := e.(map[string]interface{}); ok {
v[i] = groupXFieldsIntoExtensions(m)
}
}
}
}
if len(extras) > 0 {
Expand Down Expand Up @@ -816,9 +845,9 @@ func loadServiceWithExtends(ctx context.Context, filename, name string, services
return nil, err
}

// Make paths relative to the importing Compose file. Note that we
// make the paths relative to `file` rather than `baseFilePath` so
// that the resulting paths won't be absolute if `file` isn't an
// Make path relative to the importing Compose file. Note that we
// make the path relative to `file` rather than `baseFilePath` so
// that the resulting path won't be absolute if `file` isn't an
// absolute path.

baseFileParent := filepath.Dir(file)
Expand Down Expand Up @@ -860,8 +889,8 @@ func LoadService(name string, serviceDict map[string]interface{}) (*types.Servic
return serviceConfig, nil
}

// Windows paths, c:\\my\\path\\shiny, need to be changed to be compatible with
// the Engine. Volume paths are expected to be linux style /c/my/path/shiny/
// Windows path, c:\\my\\path\\shiny, need to be changed to be compatible with
// the Engine. Volume path are expected to be linux style /c/my/path/shiny/
func convertVolumePath(volume types.ServiceVolumeConfig) types.ServiceVolumeConfig {
volumeName := strings.ToLower(filepath.VolumeName(volume.Source))
if len(volumeName) != 2 {
Expand All @@ -875,15 +904,15 @@ func convertVolumePath(volume types.ServiceVolumeConfig) types.ServiceVolumeConf
return volume
}

func resolveMaybeUnixPath(workingDir string, path string) string {
filePath := expandUser(path)
func resolveMaybeUnixPath(workingDir string, p string) string {
filePath := expandUser(p)
// Check if source is an absolute path (either Unix or Windows), to
// handle a Windows client with a Unix daemon or vice-versa.
//
// Note that this is not required for Docker for Windows when specifying
// a local Windows path, because Docker for Windows translates the Windows
// path into a valid path within the VM.
if !paths.IsAbs(filePath) && !isAbs(filePath) {
if !path.IsAbs(filePath) && !isAbs(filePath) {
filePath = absPath(workingDir, filePath)
}
return filePath
Expand Down Expand Up @@ -1128,7 +1157,7 @@ func cleanTarget(target string) string {
if target == "" {
return ""
}
return paths.Clean(target)
return path.Clean(target)
}

var transformBuildConfig TransformerFunc = func(data interface{}) (interface{}, error) {
Expand Down
2 changes: 2 additions & 0 deletions loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,7 @@ services:
volumes:
- source: data
type: volume
target: /data
read_only: $thebool
volume:
nocopy: $thebool
Expand Down Expand Up @@ -852,6 +853,7 @@ networks:
{
Source: "data",
Type: "volume",
Target: "/data",
ReadOnly: true,
Volume: &types.ServiceVolumeVolume{NoCopy: true},
},
Expand Down
6 changes: 3 additions & 3 deletions loader/loader_yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import (
)

func TestParseYAMLFiles(t *testing.T) {
model, err := loadYamlModel(context.TODO(),
[]types.ConfigFile{
model, err := loadYamlModel(context.TODO(), types.ConfigDetails{
ConfigFiles: []types.ConfigFile{
{Filename: "test.yaml",
Content: []byte(`
services:
Expand All @@ -43,7 +43,7 @@ services:
image: bar
command: echo world
init: false
`)}}, &Options{})
`)}}}, &Options{})
assert.NilError(t, err)
assert.DeepEqual(t, model, map[string]interface{}{
"services": map[string]interface{}{
Expand Down
Loading

0 comments on commit 9b8be2a

Please sign in to comment.