-
Notifications
You must be signed in to change notification settings - Fork 2
/
pipeline.go
137 lines (116 loc) · 4.72 KB
/
pipeline.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package pipeline
import (
"fmt"
"github.com/buildkite/go-pipeline/internal/env"
"github.com/buildkite/go-pipeline/ordered"
"github.com/buildkite/go-pipeline/warning"
"github.com/buildkite/interpolate"
)
// Pipeline models a pipeline.
//
// Standard caveats apply - see the package comment.
type Pipeline struct {
Steps Steps `yaml:"steps"`
Env *ordered.MapSS `yaml:"env,omitempty"`
// RemainingFields stores any other top-level mapping items so they at least
// survive an unmarshal-marshal round-trip.
RemainingFields map[string]any `yaml:",inline"`
}
// MarshalJSON marshals a pipeline to JSON. Special handling is needed because
// yaml.v3 has "inline" but encoding/json has no concept of it.
func (p *Pipeline) MarshalJSON() ([]byte, error) {
return inlineFriendlyMarshalJSON(p)
}
// UnmarshalOrdered unmarshals the pipeline from either []any (a legacy
// sequence of steps) or *ordered.MapSA (a modern pipeline configuration).
func (p *Pipeline) UnmarshalOrdered(o any) error {
var warns []error
switch o := o.(type) {
case *ordered.MapSA:
// A pipeline can be a mapping.
// Wrap in a secret type to avoid infinite recursion between this method
// and ordered.Unmarshal.
type wrappedPipeline Pipeline
err := ordered.Unmarshal(o, (*wrappedPipeline)(p))
if w := warning.As(err); w != nil {
warns = append(warns, w)
} else if err != nil {
return fmt.Errorf("unmarshaling Pipeline: %w", err)
}
case []any:
// A pipeline can be a sequence of steps.
err := ordered.Unmarshal(o, &p.Steps)
if w := warning.As(err); w != nil {
warns = append(warns, w)
} else if err != nil {
return fmt.Errorf("unmarshaling steps: %w", err)
}
default:
return fmt.Errorf("unmarshaling Pipeline: unsupported type %T, want either *ordered.Map[string, any] or []any", o)
}
// Ensure Steps is never nil. Server side expects a sequence.
if p.Steps == nil {
p.Steps = Steps{}
warns = append(warns, warning.New("pipeline contains no steps"))
}
return warning.Wrap(warns...)
}
// InterpolationEnv contains environment variables that may be interpolated into
// a pipeline. Users may define an equivalence between environment variable name, for example
// the environment variable names may case-insensitive.
type InterpolationEnv interface {
Get(name string) (string, bool)
Set(name string, value string)
}
// Interpolate interpolates variables defined in both interpolationEnv and p.Env into the pipeline.
// More specifically, it does these things:
// - Interpolate pipeline.Env and copy the results into interpolationEnv, provided they don't
// conflict, to apply later.
// - Interpolate any string value in the rest of the 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 the preferRuntimeEnv option to true instead prefers the runtime environment to pipeline
// environment variables when both are defined.
func (p *Pipeline) Interpolate(interpolationEnv InterpolationEnv, preferRuntimeEnv bool) error {
if interpolationEnv == nil {
interpolationEnv = env.New()
}
// Preprocess any env that are defined in the top level block and place them
// into env for later interpolation into the rest of the pipeline.
if err := p.interpolateEnvBlock(interpolationEnv, preferRuntimeEnv); err != nil {
return err
}
tf := envInterpolator{env: interpolationEnv}
// Recursively go through the rest of the pipeline and perform environment
// variable interpolation on strings. Interpolation is performed in-place.
if err := interpolateSlice(tf, p.Steps); err != nil {
return err
}
return interpolateMap(tf, p.RemainingFields)
}
// interpolateEnvBlock runs interpolate.Interpolate on each pair in p.Env,
// interpolating with the variables defined in interpolationEnv, and then adding the
// results back into p.Env. Since each environment variable in p.Env can
// be interpolated into later environment variables, we also add the results
// to interpolationEnv, making the input ordering of p.Env potentially important.
func (p *Pipeline) interpolateEnvBlock(interpolationEnv InterpolationEnv, preferRuntimeEnv bool) error {
return p.Env.Range(func(k, v string) error {
// We interpolate both keys and values.
intk, err := interpolate.Interpolate(interpolationEnv, k)
if err != nil {
return err
}
// v is always a string in this case.
intv, err := interpolate.Interpolate(interpolationEnv, v)
if err != nil {
return err
}
p.Env.Replace(k, intk, intv)
// If the variable already existed and we prefer the runtime environment then don't overwrite it
if _, exists := interpolationEnv.Get(intk); !(preferRuntimeEnv && exists) {
interpolationEnv.Set(intk, intv)
}
return nil
})
}