Skip to content
Open
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
5 changes: 3 additions & 2 deletions cli/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,8 +517,9 @@ func (o *ProjectOptions) prepare(ctx context.Context) (*types.ConfigDetails, err
return configDetails, nil
}

// ProjectFromOptions load a compose project based on command line options
// Deprecated: use ProjectOptions.LoadProject or ProjectOptions.LoadModel
// ProjectFromOptions load a compose project based on command line options.
//
// Deprecated: use ProjectOptions.LoadProject or ProjectOptions.LoadModel.
func ProjectFromOptions(ctx context.Context, options *ProjectOptions) (*types.Project, error) {
return options.LoadProject(ctx)
}
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ require (
github.com/distribution/reference v0.5.0
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.5.0
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/google/go-cmp v0.5.9
github.com/mattn/go-shellwords v1.0.12
github.com/opencontainers/go-digest v1.0.0
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
Expand Down
112 changes: 112 additions & 0 deletions interpolation/node.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
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 interpolation

import (
"fmt"
"os"
"strings"

"go.yaml.in/yaml/v4"

"github.com/compose-spec/compose-go/v2/template"
"github.com/compose-spec/compose-go/v2/tree"
)

// InterpolateNode replaces variables in yaml.Node scalar values
func InterpolateNode(node *yaml.Node, opts Options) error {
if opts.LookupValue == nil {
opts.LookupValue = os.LookupEnv
}
if opts.TypeCastMapping == nil {
opts.TypeCastMapping = make(map[tree.Path]Cast)
}
if opts.Substitute == nil {
opts.Substitute = template.Substitute
}
return recursiveInterpolateNode(node, tree.NewPath(), opts)
}

func recursiveInterpolateNode(node *yaml.Node, path tree.Path, opts Options) error {
switch node.Kind {
case yaml.DocumentNode:
if len(node.Content) > 0 {
return recursiveInterpolateNode(node.Content[0], path, opts)
}
return nil

case yaml.MappingNode:
for i := 0; i+1 < len(node.Content); i += 2 {
key := node.Content[i]
value := node.Content[i+1]
if err := recursiveInterpolateNode(value, path.Next(key.Value), opts); err != nil {
return err
}
}
return nil

case yaml.SequenceNode:
for _, item := range node.Content {
if err := recursiveInterpolateNode(item, path.Next(tree.PathMatchList), opts); err != nil {
return err
}
}
return nil

case yaml.ScalarNode:
if node.Tag != "!!str" && node.Tag != "" && !strings.Contains(node.Value, "$") {
return nil
}
newValue, err := opts.Substitute(node.Value, template.Mapping(opts.LookupValue))
if err != nil {
return newPathError(path, err)
}
caster, ok := opts.getCasterForPath(path)
if !ok {
if newValue != node.Value {
node.Value = newValue
}
return nil
}
casted, err := caster(newValue)
if err != nil {
return newPathError(path, fmt.Errorf("failed to cast to expected type: %w", err))
}
switch casted.(type) {
case bool:
node.Tag = "!!bool"
node.Value = fmt.Sprint(casted)
case int, int64:
node.Tag = "!!int"
node.Value = fmt.Sprint(casted)
case float64:
node.Tag = "!!float"
node.Value = fmt.Sprint(casted)
case nil:
node.Tag = "!!null"
node.Value = "null"
case string:
node.Value = fmt.Sprint(casted)
default:
node.Value = fmt.Sprint(casted)
}
return nil

default:
return nil
}
}
209 changes: 209 additions & 0 deletions interpolation/node_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
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 interpolation

import (
"encoding/json"
"strconv"
"testing"

"github.com/compose-spec/compose-go/v2/tree"
"go.yaml.in/yaml/v4"
"gotest.tools/v3/assert"
)

func TestInterpolateNode_Simple(t *testing.T) {
input := `
services:
web:
image: ${IMAGE}
`
lookup := func(key string) (string, bool) {
if key == "IMAGE" {
return "nginx", true
}
return "", false
}

var node yaml.Node
assert.NilError(t, yaml.Unmarshal([]byte(input), &node))
err := InterpolateNode(&node, Options{LookupValue: lookup})
assert.NilError(t, err)

var result map[string]interface{}
assert.NilError(t, node.Decode(&result))

services := result["services"].(map[string]interface{})
web := services["web"].(map[string]interface{})
assert.Equal(t, "nginx", web["image"])
}

func TestInterpolateNode_Default(t *testing.T) {
input := `
services:
web:
image: ${IMAGE:-default}
`
lookup := func(_ string) (string, bool) {
return "", false
}

var node yaml.Node
assert.NilError(t, yaml.Unmarshal([]byte(input), &node))
err := InterpolateNode(&node, Options{LookupValue: lookup})
assert.NilError(t, err)

var result map[string]interface{}
assert.NilError(t, node.Decode(&result))

services := result["services"].(map[string]interface{})
web := services["web"].(map[string]interface{})
assert.Equal(t, "default", web["image"])
}

func TestInterpolateNode_NoSubstitution(t *testing.T) {
input := `
services:
web:
image: nginx
ports:
- "8080"
`
lookup := func(_ string) (string, bool) {
return "", false
}

var node yaml.Node
assert.NilError(t, yaml.Unmarshal([]byte(input), &node))

// Take a snapshot before interpolation
var before map[string]interface{}
assert.NilError(t, node.Decode(&before))

err := InterpolateNode(&node, Options{LookupValue: lookup})
assert.NilError(t, err)

var after map[string]interface{}
assert.NilError(t, node.Decode(&after))

beforeJSON, _ := json.Marshal(before)
afterJSON, _ := json.Marshal(after)
assert.Equal(t, string(beforeJSON), string(afterJSON))
}

func TestInterpolateNode_TypeCast(t *testing.T) {
input := `
services:
web:
ports:
- ${PORT}
`
lookup := func(key string) (string, bool) {
if key == "PORT" {
return "8080", true
}
return "", false
}

toInt := func(value string) (interface{}, error) {
return strconv.Atoi(value)
}

var node yaml.Node
assert.NilError(t, yaml.Unmarshal([]byte(input), &node))
err := InterpolateNode(&node, Options{
LookupValue: lookup,
TypeCastMapping: map[tree.Path]Cast{
tree.NewPath("services", tree.PathMatchAll, "ports", tree.PathMatchList): toInt,
},
})
assert.NilError(t, err)

var result map[string]interface{}
assert.NilError(t, node.Decode(&result))

services := result["services"].(map[string]interface{})
web := services["web"].(map[string]interface{})
ports := web["ports"].([]interface{})
assert.Equal(t, 8080, ports[0])
}

func TestInterpolateNode_Parity(t *testing.T) {
input := `
services:
web:
image: ${IMAGE}
environment:
FOO: ${FOO_VAL}
BAR: ${BAR_VAL:-default_bar}
labels:
version: ${VERSION}
`
env := map[string]string{
"IMAGE": "nginx",
"FOO_VAL": "hello",
"VERSION": "1.0",
}
testInterpolateParity(t, input, env)
}

func testInterpolateParity(t *testing.T, input string, env map[string]string) {
t.Helper()
lookup := func(key string) (string, bool) {
v, ok := env[key]
return v, ok
}
opts := Options{
LookupValue: lookup,
}

// Map-based
var mapData map[string]interface{}
assert.NilError(t, yaml.Unmarshal([]byte(input), &mapData))
mapResult, err := Interpolate(mapData, opts)
assert.NilError(t, err)

// Node-based
var node yaml.Node
assert.NilError(t, yaml.Unmarshal([]byte(input), &node))
err = InterpolateNode(&node, opts)
assert.NilError(t, err)

var nodeMap map[string]interface{}
assert.NilError(t, node.Decode(&nodeMap))

// Compare via JSON
mapJSON, _ := json.Marshal(mapResult)
nodeJSON, _ := json.Marshal(nodeMap)
assert.Equal(t, string(mapJSON), string(nodeJSON))
}

func TestInterpolateNode_Error(t *testing.T) {
input := `
services:
web:
image: ${IMAGE:?}
`
lookup := func(_ string) (string, bool) {
return "", false
}

var node yaml.Node
assert.NilError(t, yaml.Unmarshal([]byte(input), &node))
err := InterpolateNode(&node, Options{LookupValue: lookup})
assert.Assert(t, err != nil, "expected an error for missing required variable")
}
Loading