-
Notifications
You must be signed in to change notification settings - Fork 394
/
features.go
317 lines (275 loc) · 14.7 KB
/
features.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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
package features
import (
"context"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
goflags "github.com/jessevdk/go-flags"
"github.com/pkg/errors"
"github.com/earthly/earthly/analytics"
"github.com/earthly/earthly/ast/spec"
"github.com/earthly/earthly/util/flagutil"
)
// Features is used to denote which features to flip on or off; this is for use in maintaining
// backwards compatibility
type Features struct {
// Never enabled by default
NoUseRegistryForWithDocker bool `long:"no-use-registry-for-with-docker" description:"disable use-registry-for-with-docker"` // escape hatch for disabling WITH DOCKER registry, e.g. used by eine-based tests
EarthlyCIRunnerArg bool `long:"earthly-ci-runner-arg" description:"includes EARTHLY_CI_RUNNER ARG"` // earthly CI was discontinued, no reason to enable this by default
// VERSION 0.5
ExecAfterParallel bool `long:"exec-after-parallel" enabled_in_version:"0.5" description:"force execution after parallel conversion"`
ParallelLoad bool `long:"parallel-load" enabled_in_version:"0.5" description:"perform parallel loading of images into WITH DOCKER"`
UseRegistryForWithDocker bool `long:"use-registry-for-with-docker" enabled_in_version:"0.5" description:"use embedded Docker registry for WITH DOCKER load operations"`
// VERSION 0.6
ForIn bool `long:"for-in" enabled_in_version:"0.6" description:"allow the use of the FOR command"`
NoImplicitIgnore bool `long:"no-implicit-ignore" enabled_in_version:"0.6" description:"disable implicit ignore rules to exclude .tmp-earthly-out/, build.earth, Earthfile, .earthignore and .earthlyignore when resolving local context"`
ReferencedSaveOnly bool `long:"referenced-save-only" enabled_in_version:"0.6" description:"only save artifacts that are directly referenced"`
RequireForceForUnsafeSaves bool `long:"require-force-for-unsafe-saves" enabled_in_version:"0.6" description:"require the --force flag when saving to path outside of current path"`
UseCopyIncludePatterns bool `long:"use-copy-include-patterns" enabled_in_version:"0.6" description:"specify an include pattern to buildkit when performing copies"`
// VERSION 0.7
CheckDuplicateImages bool `long:"check-duplicate-images" enabled_in_version:"0.7" description:"check for duplicate images during output"`
EarthlyCIArg bool `long:"ci-arg" enabled_in_version:"0.7" description:"include EARTHLY_CI arg"`
EarthlyGitAuthorArgs bool `long:"earthly-git-author-args" enabled_in_version:"0.7" description:"includes EARTHLY_GIT_AUTHOR and EARTHLY_GIT_CO_AUTHORS ARGs"`
EarthlyLocallyArg bool `long:"earthly-locally-arg" enabled_in_version:"0.7" description:"includes EARTHLY_LOCALLY ARG"`
EarthlyVersionArg bool `long:"earthly-version-arg" enabled_in_version:"0.7" description:"includes EARTHLY_VERSION and EARTHLY_BUILD_SHA ARGs"`
ExplicitGlobal bool `long:"explicit-global" enabled_in_version:"0.7" description:"require base target args to have explicit settings to be considered global args"`
GitCommitAuthorTimestamp bool `long:"git-commit-author-timestamp" enabled_in_version:"0.7" description:"include EARTHLY_GIT_COMMIT_AUTHOR_TIMESTAMP arg"`
NewPlatform bool `long:"new-platform" enabled_in_version:"0.7" description:"enable new platform behavior"`
NoTarBuildOutput bool `long:"no-tar-build-output" enabled_in_version:"0.7" description:"do not print output when creating a tarball to load into WITH DOCKER"`
SaveArtifactKeepOwn bool `long:"save-artifact-keep-own" enabled_in_version:"0.7" description:"always apply the --keep-own flag with SAVE ARTIFACT"`
ShellOutAnywhere bool `long:"shell-out-anywhere" enabled_in_version:"0.7" description:"allow shelling-out in the middle of ARGs, or any other command"`
UseCacheCommand bool `long:"use-cache-command" enabled_in_version:"0.7" description:"allow use of CACHE command in Earthfiles"`
UseChmod bool `long:"use-chmod" enabled_in_version:"0.7" description:"enable the COPY --chmod option"`
UseCopyLink bool `long:"use-copy-link" enabled_in_version:"0.7" description:"use the equivalent of COPY --link for all copy-like operations"`
UseHostCommand bool `long:"use-host-command" enabled_in_version:"0.7" description:"allow use of HOST command in Earthfiles"`
UseNoManifestList bool `long:"use-no-manifest-list" enabled_in_version:"0.7" description:"enable the SAVE IMAGE --no-manifest-list option"`
UsePipelines bool `long:"use-pipelines" enabled_in_version:"0.7" description:"enable the PIPELINE and TRIGGER commands"`
UseProjectSecrets bool `long:"use-project-secrets" enabled_in_version:"0.7" description:"enable project-based secret resolution"`
WaitBlock bool `long:"wait-block" enabled_in_version:"0.7" description:"enable WITH/END feature, also allows RUN --push mixed with non-push commands"`
// VERSION 0.8
NoNetwork bool `long:"no-network" enabled_in_version:"0.8" description:"allow the use of RUN --network=none commands"`
ArgScopeSet bool `long:"arg-scope-and-set" enabled_in_version:"0.8" description:"enable SET to reassign ARGs and prevent ARGs from being redeclared in the same scope"`
UseDockerIgnore bool `long:"use-docker-ignore" enabled_in_version:"0.8" description:"fallback to .dockerignore incase .earthlyignore or .earthlyignore do not exist in a local \"FROM DOCKERFILE\" target"`
PassArgs bool `long:"pass-args" enabled_in_version:"0.8" description:"Allow the use of the --pass-arg flag in FROM, BUILD, COPY, WITH DOCKER, and DO commands"`
GlobalCache bool `long:"global-cache" enabled_in_version:"0.8" description:"enable global caches (shared across different Earthfiles), for cache mounts and CACHEs having an ID"`
CachePersistOption bool `long:"cache-persist-option" enabled_in_version:"0.8" description:"Adds option to persist caches, Changes default CACHE behaviour to not persist"`
GitRefs bool `long:"git-refs" enabled_in_version:"0.8" description:"includes EARTHLY_GIT_REFS ARG"`
UseVisitedUpfrontHashCollection bool `long:"use-visited-upfront-hash-collection" enabled_in_version:"0.8" description:"Uses a new target visitor implementation that computes upfront the hash of the visited targets and adds support for running all targets with the same name but different args in parallel"`
UseFunctionKeyword bool `long:"use-function-keyword" enabled_in_version:"0.8" description:"Use the FUNCTION key word instead of COMMAND"`
// unreleased
TryFinally bool `long:"try" description:"allow the use of the TRY/FINALLY commands"`
WildcardBuilds bool `long:"wildcard-builds" description:"allow for the expansion of wildcard (glob) paths for BUILD commands"`
BuildAutoSkip bool `long:"build-auto-skip" description:"allow for --auto-skip to be used on individual BUILD commands"`
AllowPrivilegedFromDockerfile bool `long:"allow-privileged-from-dockerfile" description:"Allow the use of the --allow-privileged flag in the FROM DOCKERFILE command"`
RunWithAWS bool `long:"run-with-aws" description:"make AWS credentials in the environment or ~/.aws available to RUN commands"`
WildcardCopy bool `long:"wildcard-copy" description:"allow for the expansion of wildcard (glob) paths for COPY commands"`
RawOutput bool `long:"raw-output" description:"allow for --raw-output on RUN commands"`
GitAuthorEmailNameArgs bool `long:"git-author-email-name-args" description:"includes EARTHLY_GIT_AUTHOR_EMAIL and EARTHLY_GIT_AUTHOR_NAME builtin ARGs"`
AllowWithoutEarthlyLabels bool `long:"allow-without-earthly-labels" description:"Allow the usage of --without-earthly-labels in SAVE IMAGE"`
DockerCache bool `long:"docker-cache" description:"enable the WITH DOCKER --cache-id option"`
RunWithAWSOIDC bool `long:"run-with-aws-oidc" description:"make AWS credentials via OIDC provider available to RUN commands"`
// version numbers
Major int
Minor int
}
type ctxKey struct{}
// Version returns the current version
func (f *Features) Version() string {
return fmt.Sprintf("%d.%d", f.Major, f.Minor)
}
func parseFlagOverrides(env string) map[string]string {
env = strings.TrimSpace(env)
m := map[string]string{}
if env != "" {
for _, flag := range strings.Split(env, ",") {
flagNameAndValue := strings.SplitN(flag, "=", 2)
var flagValue string
flagName := strings.TrimSpace(flagNameAndValue[0])
flagName = strings.TrimPrefix(flagName, "--")
if len(flagNameAndValue) > 1 {
flagValue = strings.TrimSpace(flagNameAndValue[1])
}
m[flagName] = flagValue
}
}
return m
}
// String returns a string representation of the version and set flags
func (f *Features) String() string {
if f == nil {
return "<nil>"
}
v := reflect.ValueOf(*f)
typeOf := v.Type()
flags := []string{}
for i := 0; i < typeOf.NumField(); i++ {
tag := typeOf.Field(i).Tag
if flagName, ok := tag.Lookup("long"); ok {
ifaceVal := v.Field(i).Interface()
if boolVal, ok := ifaceVal.(bool); ok && boolVal {
flags = append(flags, fmt.Sprintf("--%v", flagName))
}
}
}
sort.Strings(flags)
args := []string{"VERSION"}
if len(flags) > 0 {
args = append(args, strings.Join(flags, " "))
}
args = append(args, fmt.Sprintf("%d.%d", f.Major, f.Minor))
return strings.Join(args, " ")
}
// ApplyFlagOverrides parses a comma separated list of feature flag overrides (without the -- flag name prefix)
// and sets them in the referenced features.
func ApplyFlagOverrides(ftrs *Features, envOverrides string) error {
overrides := parseFlagOverrides(envOverrides)
fieldIndices := map[string]int{}
typeOf := reflect.ValueOf(*ftrs).Type()
for i := 0; i < typeOf.NumField(); i++ {
f := typeOf.Field(i)
tag := f.Tag
if flagName, ok := tag.Lookup("long"); ok {
fieldIndices[flagName] = i
}
}
ftrsStruct := reflect.ValueOf(ftrs).Elem()
for key := range overrides {
analytics.Count("override-feature-flags", key)
i, ok := fieldIndices[key]
if !ok {
return fmt.Errorf("unable to set %s: invalid flag", key)
}
fv := ftrsStruct.Field(i)
if fv.IsValid() && fv.CanSet() {
fv.SetBool(true)
} else {
return fmt.Errorf("unable to set %s: field is invalid or cant be set", key)
}
ifaceVal := fv.Interface()
if _, ok := ifaceVal.(bool); ok {
fv.SetBool(true)
} else {
return fmt.Errorf("unable to set %s: only boolean fields are currently supported", key)
}
}
processNegativeFlags(ftrs)
return nil
}
var errUnexpectedArgs = fmt.Errorf("unexpected VERSION arguments; should be VERSION [flags] <major-version>.<minor-version>")
func instrumentVersion(_ string, opt *goflags.Option, s *string) (*string, error) {
analytics.Count("version-feature-flags", opt.LongName)
return s, nil // don't modify the flag, just pass it back.
}
// Get returns a features struct for a particular version
func Get(version *spec.Version) (*Features, bool, error) {
var ftrs Features
hasVersion := (version != nil)
if !hasVersion {
// If no version is specified, we default to 0.5 (the Earthly version
// before the VERSION command was introduced).
version = &spec.Version{
Args: []string{"0.5"},
}
}
if version.Args == nil {
return nil, false, errUnexpectedArgs
}
parsedArgs, err := flagutil.ParseArgsWithValueModifierAndOptions("VERSION", &ftrs, version.Args, instrumentVersion, goflags.PassDoubleDash|goflags.PassAfterNonOption)
if err != nil {
return nil, false, err
}
if len(parsedArgs) != 1 {
return nil, false, errUnexpectedArgs
}
versionValueStr := parsedArgs[0]
majorAndMinor := strings.Split(versionValueStr, ".")
if len(majorAndMinor) != 2 {
return nil, false, errUnexpectedArgs
}
ftrs.Major, err = strconv.Atoi(majorAndMinor[0])
if err != nil {
return nil, false, errors.Wrapf(err, "failed to parse major version %q", majorAndMinor[0])
}
ftrs.Minor, err = strconv.Atoi(majorAndMinor[1])
if err != nil {
return nil, false, errors.Wrapf(err, "failed to parse minor version %q", majorAndMinor[1])
}
if hasVersion {
analytics.Count("version", versionValueStr)
} else {
analytics.Count("version", "missing")
}
return &ftrs, hasVersion, nil
}
// versionAtLeast returns true if the version configured in `ftrs`
// are greater than or equal to the provided major and minor versions.
func versionAtLeast(ftrs Features, majorVersion, minorVersion int) bool {
return (ftrs.Major > majorVersion) || (ftrs.Major == majorVersion && ftrs.Minor >= minorVersion)
}
func processNegativeFlags(ftrs *Features) {
if ftrs.NoUseRegistryForWithDocker {
ftrs.UseRegistryForWithDocker = false
}
}
// WithContext adds the current *Features into the given context and returns a new context.
// Trying to add the *Features to the context more than once will result in an error.
func (f *Features) WithContext(ctx context.Context) (context.Context, error) {
if ctx.Value(ctxKey{}) != nil {
return ctx, errors.New("features is already set")
}
return context.WithValue(ctx, ctxKey{}, f), nil
}
// FromContext returns the *Features associated with the ctx.
// If no features is found, nil is returned.
func FromContext(ctx context.Context) *Features {
if f, ok := ctx.Value(ctxKey{}).(*Features); ok {
return f
}
return nil
}
func (f *Features) ProcessFlags() ([]string, error) {
warningStrs := make([]string, 0)
v := reflect.ValueOf(f).Elem()
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
version := field.Tag.Get("enabled_in_version")
if len(version) == 0 {
continue
}
majorVersion, minorVersion := mustParseVersion(field.Tag.Get("enabled_in_version"))
if versionAtLeast(*f, majorVersion, minorVersion) && value.Kind() == reflect.Bool {
if value.Bool() {
tagName := field.Tag.Get("long")
warningStrs = append(warningStrs, fmt.Sprintf("--%s", strings.ToLower(tagName)))
}
value.SetBool(true)
}
}
processNegativeFlags(f)
if f.ArgScopeSet && !f.ShellOutAnywhere {
// ArgScopeSet uses new ARG declaration logic that requires
// ShellOutAnywhere. We're erroring here to ensure that users get that
// feedback as early as possible.
return nil, errors.New("--arg-scope-and-set requires --shell-out-anywhere")
}
return warningStrs, nil
}
func mustParseVersion(version string) (int, int) {
parts := strings.Split(version, ".")
if len(parts) != 2 {
panic(fmt.Sprintf("invalid version format: %s", version))
}
major, err := strconv.Atoi(parts[0])
if err != nil {
panic(fmt.Sprintf("invalid major version: %s", parts[0]))
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
panic(fmt.Sprintf("invalid minor version: %s", parts[1]))
}
return major, minor
}