Skip to content

Commit

Permalink
Add option for prefering runtime env
Browse files Browse the repository at this point in the history
This preserves the v0.4.0->v0.7.0 behaviour which was reverted in v0.8.0
  • Loading branch information
patrobinson committed Apr 24, 2024
1 parent f886aef commit 34c1a81
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 11 deletions.
19 changes: 18 additions & 1 deletion parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ import (
"gopkg.in/yaml.v3"
)

// Options are functional options for creating a new Env.
type Options func(*Pipeline)

// By default if an environment variable exists in both the runtime and pipeline env
// we will substitute with the pipeline env IF the pipeline env is defined first.
// Setting this option to true instead preferres the runtime environment to pipeline
// environment variables when both are defined.
func RuntimeEnvPropagation(preferRuntimeEnv bool) Options {
return func(e *Pipeline) {
e.preferRuntimeEnv = preferRuntimeEnv
}
}

// Parse parses a pipeline. It does not apply interpolation.
// Warnings are passed through the err return:
//
Expand All @@ -21,7 +34,7 @@ import (
// return err
// }
// // Use p
func Parse(src io.Reader) (*Pipeline, error) {
func Parse(src io.Reader, opts ...Options) (*Pipeline, error) {
// First get yaml.v3 to give us a raw document (*yaml.Node).
n := new(yaml.Node)
if err := yaml.NewDecoder(src).Decode(n); err != nil {
Expand All @@ -35,6 +48,10 @@ func Parse(src io.Reader) (*Pipeline, error) {
// with when handling different structural representations of the same
// configuration. Then decode _that_ into a pipeline.
p := new(Pipeline)

for _, o := range opts {
o(p)
}
return p, ordered.Unmarshal(n, p)
}

Expand Down
70 changes: 61 additions & 9 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import (

func ptr[T any](x T) *T { return &x }

func diffPipeline(got *Pipeline, want *Pipeline) string { return cmp.Diff(got, want, cmpopts.IgnoreUnexported(*got), cmp.Comparer(ordered.EqualSS), cmp.Comparer(ordered.EqualSA)) }
func diffPipeline(got *Pipeline, want *Pipeline) string {
return cmp.Diff(got, want, cmpopts.IgnoreUnexported(*got), cmp.Comparer(ordered.EqualSS), cmp.Comparer(ordered.EqualSA))
}

func TestParserParsesYAML(t *testing.T) {
runtimeEnv := env.New(env.FromMap(map[string]string{"ENV_VAR_FRIEND": "friend"}))
Expand Down Expand Up @@ -71,10 +73,11 @@ func TestParserParsesYAML(t *testing.T) {

func TestParserParsesYAMLWithInterpolation(t *testing.T) {
tests := []struct {
desc string
input io.Reader
runtimeEnv map[string]string
want *Pipeline
desc string
input io.Reader
runtimeEnv map[string]string
want *Pipeline
runtimePreferred bool
}{
{
desc: "InterpolationInName",
Expand All @@ -94,14 +97,14 @@ steps:
},
},
{
desc: "InterpolationInKey",
input: strings.NewReader(`
desc: "InterpolationInKey",
input: strings.NewReader(`
steps:
- key: hello-${ENV_VAR_FRIEND}
command: echo hello world
`),
runtimeEnv: map[string]string{"ENV_VAR_FRIEND": "friend"},
want: &Pipeline{
want: &Pipeline{
Steps: Steps{
&CommandStep{
Key: "hello-friend",
Expand All @@ -110,11 +113,60 @@ steps:
},
},
},
{
desc: "InterpolationWithEnvFromRuntimeAndInput",
runtimeEnv: map[string]string{"ENV_VAR_FRIEND": "friend"},
input: strings.NewReader(`
env:
ENV_VAR_FRIEND: foe
MSG: hello ${ENV_VAR_FRIEND}
steps:
- name: echo message
command: echo ${MSG}
`),
want: &Pipeline{
Env: ordered.MapFromItems(
ordered.TupleSS{Key: "ENV_VAR_FRIEND", Value: "foe"},
ordered.TupleSS{Key: "MSG", Value: "hello foe"},
),
Steps: Steps{
&CommandStep{
Label: "echo message",
Command: "echo hello foe",
},
},
},
},
{
desc: "InterpolationWithEnvFromRuntimeAndInputRuntimePreferred",
runtimeEnv: map[string]string{"ENV_VAR_FRIEND": "friend"},
runtimePreferred: true,
input: strings.NewReader(`
env:
ENV_VAR_FRIEND: foe
MSG: hello ${ENV_VAR_FRIEND}
steps:
- name: echo message
command: echo ${MSG}
`),
want: &Pipeline{
Env: ordered.MapFromItems(
ordered.TupleSS{Key: "ENV_VAR_FRIEND", Value: "foe"},
ordered.TupleSS{Key: "MSG", Value: "hello friend"},
),
Steps: Steps{
&CommandStep{
Label: "echo message",
Command: "echo hello friend",
},
},
},
},
}

for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
got, err := Parse(test.input)
got, err := Parse(test.input, RuntimeEnvPropagation(test.runtimePreferred))
if err != nil {
t.Fatalf("Parse(input) error = %v", err)
}
Expand Down
7 changes: 6 additions & 1 deletion pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type Pipeline struct {
// RemainingFields stores any other top-level mapping items so they at least
// survive an unmarshal-marshal round-trip.
RemainingFields map[string]any `yaml:",inline"`

preferRuntimeEnv bool `yaml:",omitempty"`
}

// MarshalJSON marshals a pipeline to JSON. Special handling is needed because
Expand Down Expand Up @@ -122,7 +124,10 @@ func (p *Pipeline) interpolateEnvBlock(interpolationEnv InterpolationEnv) error

p.Env.Replace(k, intk, intv)

interpolationEnv.Set(intk, intv)
// If the variable already existed and we prefer the runtime environment then don't overwrite it
if _, exists := interpolationEnv.Get(intk); !(p.preferRuntimeEnv && exists) {
interpolationEnv.Set(intk, intv)
}

return nil
})
Expand Down

0 comments on commit 34c1a81

Please sign in to comment.