load.go (571 lines of code) (raw):

package dalec import ( goerrors "errors" "fmt" "os" "strings" "github.com/goccy/go-yaml" "github.com/goccy/go-yaml/ast" "github.com/goccy/go-yaml/parser" "github.com/goccy/go-yaml/token" "github.com/moby/buildkit/frontend/dockerfile/shell" "github.com/pkg/errors" "golang.org/x/exp/maps" ) const ( // KeyDalecTarget is the key used the build arg name which may be used to read // the target name. KeyDalecTarget = "DALEC_TARGET" parseModeIgnoreComments = 0 ) func knownArg(key string) bool { switch key { case "BUILDKIT_SYNTAX": return true case "DALEC_DISABLE_DIFF_MERGE": return true case "DALEC_SKIP_SIGNING": return true case "DALEC_SIGNING_CONFIG_CONTEXT_NAME": return true case "DALEC_SIGNING_CONFIG_PATH": return true case "SOURCE_DATE_EPOCH": return true case "DALEC_SKIP_TESTS": return true case KeyDalecTarget: return true } return platformArg(key) } func platformArg(key string) bool { switch key { case "TARGETOS", "TARGETARCH", "TARGETPLATFORM", "TARGETVARIANT", "BUILDOS", "BUILDARCH", "BUILDPLATFORM", "BUILDVARIANT": return true default: return false } } const DefaultPatchStrip int = 1 type envGetterMap map[string]string func (m envGetterMap) Get(key string) (string, bool) { v, ok := m[key] return v, ok } func (m envGetterMap) Keys() []string { return maps.Keys(m) } func expandArgs(lex *shell.Lex, s string, args map[string]string, allowArg func(key string) bool) (string, error) { result, err := lex.ProcessWordWithMatches(s, envGetterMap(args)) if err != nil { return "", err } var errs []error for m := range result.Unmatched { if !knownArg(m) && !allowArg(m) { errs = append(errs, fmt.Errorf(`build arg "%s" not declared`, m)) continue } if platformArg(m) { errs = append(errs, fmt.Errorf(`opt-in arg "%s" not present in args`, m)) } } return result.Result, errors.Wrap(goerrors.Join(errs...), "error performing variable expansion") } var errUnknownArg = errors.New("unknown arg") type SubstituteConfig struct { AllowArg func(string) bool } type SubstituteOpt func(*SubstituteConfig) // AllowAnyArg can be used to set [SubstituteConfig.AllowArg] to allow any arg // to be substituted regardless of whether it is declared in the spec. func AllowAnyArg(s string) bool { return true } // WithAllowAnyArg is a [SubstituteOpt] that sets [SubstituteConfig.AllowArg] to // [AllowAnyArg]. func WithAllowAnyArg(cfg *SubstituteConfig) { cfg.AllowArg = AllowAnyArg } // DisallowAllUndeclared can be used to set [SubstituteConfig.AllowArg] to disallow args // unless they are declared in the spec. // This is used by default when substituting args. func DisallowAllUndeclared(s string) bool { return false } func (s *Spec) SubstituteArgs(env map[string]string, opts ...SubstituteOpt) error { var cfg SubstituteConfig cfg.AllowArg = DisallowAllUndeclared for _, o := range opts { o(&cfg) } lex := shell.NewLex('\\') // force the shell lexer to skip unresolved env vars so they aren't // replaced with "" lex.SkipUnsetEnv = true var errs []error appendErr := func(err error) { errs = append(errs, err) } args := make(map[string]string) for k, v := range s.Args { args[k] = v } for k, v := range env { if _, ok := args[k]; !ok { if !knownArg(k) && !cfg.AllowArg(k) { appendErr(fmt.Errorf("%w: %q", errUnknownArg, k)) } // if the build arg isn't present in args by opt-in, skip // and don't automatically inject a value continue } args[k] = v } for name, src := range s.Sources { if err := src.processBuildArgs(lex, args, cfg.AllowArg); err != nil { appendErr(errors.Wrapf(err, "source %q", name)) } s.Sources[name] = src } for src, patchList := range s.Patches { for i, patch := range patchList { updated, err := expandArgs(lex, patch.Path, args, cfg.AllowArg) if err != nil { appendErr(errors.Wrapf(err, "patch %s path %d", src, i)) } s.Patches[src][i].Path = updated } } updated, err := expandArgs(lex, s.Version, args, cfg.AllowArg) if err != nil { appendErr(errors.Wrap(err, "version")) } s.Version = updated updated, err = expandArgs(lex, s.Revision, args, cfg.AllowArg) if err != nil { appendErr(errors.Wrap(err, "revision")) } s.Revision = updated if err := s.Build.processBuildArgs(lex, args, cfg.AllowArg); err != nil { appendErr(errors.Wrap(err, "build")) } if s.Build.NetworkMode != "" { updated, err := expandArgs(lex, s.Build.NetworkMode, args, cfg.AllowArg) if err != nil { appendErr(fmt.Errorf("error performing shell expansion on build network mode: %s: %w", s.Build.NetworkMode, err)) } s.Build.NetworkMode = updated } for i, step := range s.Build.Steps { bs := &step if err := bs.processBuildArgs(lex, args, cfg.AllowArg); err != nil { appendErr(errors.Wrapf(err, "step index %d", i)) } s.Build.Steps[i] = *bs } for _, t := range s.Tests { if err := t.processBuildArgs(lex, args, cfg.AllowArg); err != nil { appendErr(err) } } for name, t := range s.Targets { if err := t.processBuildArgs(lex, args, cfg.AllowArg); err != nil { appendErr(errors.Wrapf(err, "target %s", name)) } s.Targets[name] = t } if s.PackageConfig != nil { if err := s.PackageConfig.processBuildArgs(lex, args, cfg.AllowArg); err != nil { appendErr(errors.Wrap(err, "package config")) } } if err := s.Image.processBuildArgs(lex, args, cfg.AllowArg); err != nil { appendErr(errors.Wrap(err, "package config")) } if err := s.Dependencies.processBuildArgs(lex, args, cfg.AllowArg); err != nil { appendErr(errors.Wrap(err, "dependencies")) } for k, v := range s.Provides { for i, ver := range v.Version { updated, err := expandArgs(lex, ver, args, cfg.AllowArg) if err != nil { appendErr(errors.Wrapf(err, "provides %s version %d", k, i)) } s.Provides[k].Version[i] = updated } } for k, v := range s.Replaces { for i, ver := range v.Version { updated, err := expandArgs(lex, ver, args, cfg.AllowArg) if err != nil { appendErr(errors.Wrapf(err, "replaces %s version %d", k, i)) } s.Replaces[k].Version[i] = updated } } return goerrors.Join(errs...) } // LoadSpec loads a spec from the given data. func LoadSpec(dt []byte) (*Spec, error) { var spec Spec if err := yaml.UnmarshalWithOptions(dt, &spec, yaml.Strict()); err != nil { return nil, errors.Wrap(err, "error unmarshalling spec") } if err := spec.Validate(); err != nil { return nil, err } spec.FillDefaults() return &spec, nil } // rawYAML is similar to json.RawMessage // We use this to store the raw yaml data for extension fields type rawYAML []byte func (y rawYAML) MarshalYAML() ([]byte, error) { return y, nil } func (y *rawYAML) UnmarshalYAML(dt []byte) error { *y = dt return nil } func (f *extensionFields) UnmarshalYAML(dt []byte) error { // We need to store the raw yaml data for each extension key. // Parse the yaml, grab all the extension keys and store the raw values in f. parsed, err := parser.ParseBytes(dt, parser.ParseComments) if err != nil { return errors.Wrapf(err, "error parsing yaml: \n%s", string(dt)) } if len(parsed.Docs) != 1 { return errors.New("expected exactly one yaml document") } doc := parsed.Docs[0] if doc.Body == nil { return nil } body, ok := doc.Body.(*ast.MappingNode) if !ok { return errors.Errorf("expected a mapping node, got %T", body) } var ext extensionFields for _, v := range body.Values { key := v.Key.String() if !strings.HasPrefix(key, "x-") && !strings.HasPrefix(key, "X-") { return errors.Errorf("extension mapping key %q must not start with x-", key) } if ext == nil { ext = make(extensionFields) } ext[key] = rawYAML(v.Value.String()) } if ext != nil { *f = ext } return nil } func (s *Spec) UnmarshalYAML(dt []byte) error { parsed, err := parser.ParseBytes(dt, parser.ParseComments) if err != nil { return errors.Wrapf(err, "error parsing yaml: \n%s", string(dt)) } if len(parsed.Docs) != 1 { return errors.New("expected exactly one yaml document") } // Remove extension nodes from the main AST and store them so we can unmarshal // them separately. body := parsed.Docs[0].Body.(*ast.MappingNode) var extNodes []*ast.MappingValueNode for i := 0; i < len(body.Values); i++ { node := body.Values[i] p := node.GetPath() if !strings.HasPrefix(p, "$.x-") && !strings.HasPrefix(p, "$.X-") { continue } // Delete the extension node from the AST. body.Values = append(body.Values[:i], body.Values[i+1:]...) i-- extNodes = append(extNodes, node) } parsed, err = parser.ParseBytes([]byte(parsed.String()), parser.ParseComments) if err != nil { return errors.Wrapf(err, "error parsing yaml: \n%s", parsed.String()) } // Use an internal type to avoid infinite recursion of UnmarshalYAML. type internalSpec Spec var s2 internalSpec dec := yaml.NewDecoder(parsed, yaml.Strict()) if err := dec.Decode(&s2); err != nil { return fmt.Errorf("%w:\n\n%s", errors.Wrap(err, "error unmarshalling parsed document"), parsed.String()) } *s = Spec(s2) if len(extNodes) > 0 { // Unmarshal all the extension nodes. node := ast.Mapping(token.MappingStart("", &token.Position{}), false, extNodes...) doc := ast.Document(&token.Token{Position: &token.Position{}}, node) var ext extensionFields if err := yaml.NewDecoder(doc).Decode(&ext); err != nil { return errors.Wrap(err, "error unmarshalling extension nodes") } s.extensions = ext if len(ext) == 0 { panic("ext should not be empty") } } return nil } func (s Spec) MarshalYAML() ([]byte, error) { dtExt, err := yaml.Marshal(s.extensions) if err != nil { return nil, errors.Wrap(err, "error marshaling extensions") } parsedExt, err := parser.ParseBytes(dtExt, 0) if err != nil { return nil, errors.Wrapf(err, "error parsing yaml: \n%s", string(dtExt)) } type internalSpec Spec ss := internalSpec(s) dt, err := yaml.Marshal(ss) if err != nil { return nil, errors.Wrap(err, "error marshaling spec") } parsed, err := parser.ParseBytes(dt, parser.ParseComments) if err != nil { return nil, errors.Wrap(err, "error re-parsing spec yaml") } p, err := yaml.PathString("$") if err != nil { return nil, errors.Wrap(err, "error creating path selector") } if err := p.MergeFromFile(parsed, parsedExt); err != nil { return nil, errors.Wrap(err, "error merging extension nodes") } return []byte(parsed.String()), nil } func (s *BuildStep) processBuildArgs(lex *shell.Lex, args map[string]string, allowArg func(string) bool) error { var errs []error for k, v := range s.Env { updated, err := expandArgs(lex, v, args, allowArg) if err != nil { errs = append(errs, errors.Wrapf(err, "env %s=%s", k, v)) } s.Env[k] = updated } return goerrors.Join(errs...) } func (c *Command) processBuildArgs(lex *shell.Lex, args map[string]string, allowArg func(string) bool) error { if c == nil { return nil } var errs []error appendErr := func(err error) { errs = append(errs, err) } for i, s := range c.Mounts { if err := s.processBuildArgs(lex, args, allowArg); err != nil { appendErr(err) continue } c.Mounts[i] = s } for k, v := range c.Env { updated, err := expandArgs(lex, v, args, allowArg) if err != nil { appendErr(errors.Wrapf(err, "env %s=%v", k, v)) continue } c.Env[k] = updated } for i, step := range c.Steps { if err := step.processBuildArgs(lex, args, allowArg); err != nil { appendErr(errors.Wrapf(err, "step index %d", i)) } for k, v := range step.Env { updated, err := expandArgs(lex, v, args, allowArg) if err != nil { appendErr(errors.Wrapf(err, "step env %s=%s", k, v)) continue } step.Env[k] = updated c.Steps[i] = step } } return goerrors.Join(errs...) } func (s *Spec) FillDefaults() { for name, src := range s.Sources { fillDefaults(&src) s.Sources[name] = src } for k, patches := range s.Patches { for i, ps := range patches { if ps.Strip != nil { continue } strip := DefaultPatchStrip s.Patches[k][i].Strip = &strip } } s.Dependencies.fillDefaults() s.Image.fillDefaults() for k := range s.Targets { t := s.Targets[k] t.fillDefaults() s.Targets[k] = t } s.Image.fillDefaults() } func (s Spec) Validate() error { var errs []error for name, src := range s.Sources { if strings.ContainsRune(name, os.PathSeparator) { errs = append(errs, &InvalidSourceError{Name: name, Err: sourceNamePathSeparatorError}) } if err := src.validate(); err != nil { errs = append(errs, &InvalidSourceError{Name: name, Err: fmt.Errorf("error validating source ref %q: %w", name, err)}) } if src.DockerImage != nil && src.DockerImage.Cmd != nil { for p, cfg := range src.DockerImage.Cmd.CacheDirs { if _, err := sharingMode(cfg.Mode); err != nil { errs = append(errs, &InvalidSourceError{Name: name, Err: errors.Wrapf(err, "invalid sharing mode for source %q with cache mount at path %q", name, p)}) } } } } for _, t := range s.Tests { if err := t.validate(); err != nil { errs = append(errs, errors.Wrap(err, t.Name)) } } for src, patches := range s.Patches { for _, patch := range patches { patchSrc, ok := s.Sources[patch.Source] if !ok { errs = append(errs, &InvalidPatchError{Source: src, PatchSpec: &patch, Err: errMissingSource}) continue } if err := validatePatch(patch, patchSrc); err != nil { errs = append(errs, &InvalidPatchError{Source: src, PatchSpec: &patch, Err: err}) } } } switch s.Build.NetworkMode { case "", netModeNone, netModeSandbox: default: errs = append(errs, fmt.Errorf("invalid network mode: %q: valid values %s", s.Build.NetworkMode, []string{netModeNone, netModeSandbox})) } if err := s.Dependencies.validate(); err != nil { errs = append(errs, errors.Wrap(err, "dependencies")) } if err := s.Image.validate(); err != nil { errs = append(errs, errors.Wrap(err, "image")) } for k, t := range s.Targets { if err := t.validate(); err != nil { errs = append(errs, errors.Wrapf(err, "target %s", k)) } } if err := s.Image.validate(); err != nil { errs = append(errs, errors.Wrap(err, "image")) } return goerrors.Join(errs...) } func validatePatch(patch PatchSpec, patchSrc Source) error { if SourceIsDir(patchSrc) { // Patch sources that use directory-backed sources require a subpath in the // patch spec. if isRoot(patch.Path) { return errPatchRequiresSubpath } return nil } // File backed sources with a subpath in the patch spec is invalid since it is // already a file, not a directory. if !isRoot(patch.Path) { return errPatchFileNoSubpath } return nil } func (g *SourceGenerator) Validate() error { if g.Gomod == nil && g.Cargohome == nil { // Gomod and Cargohome are the only valid generator types // An empty generator is invalid return fmt.Errorf("no generator type specified") } return nil } func (s *PackageSigner) processBuildArgs(lex *shell.Lex, args map[string]string, allowArg func(string) bool) error { var errs []error for k, v := range s.Args { updated, err := expandArgs(lex, v, args, allowArg) if err != nil { errs = append(errs, errors.Wrapf(err, "arg %s=%s", k, v)) continue } s.Args[k] = updated } return goerrors.Join(errs...) } func (cfg *PackageConfig) processBuildArgs(lex *shell.Lex, args map[string]string, allowArg func(string) bool) error { if cfg.Signer != nil { if err := cfg.Signer.processBuildArgs(lex, args, allowArg); err != nil { return errors.Wrap(err, "signer") } } return nil } func (b *ArtifactBuild) processBuildArgs(lex *shell.Lex, args map[string]string, allowArg func(string) bool) error { var errs []error for k, v := range b.Env { updated, err := expandArgs(lex, v, args, allowArg) if err != nil { errs = append(errs, errors.Wrapf(err, "env %s=%s", k, v)) continue } b.Env[k] = updated } return goerrors.Join(errs...) } func validateSymlinks(symlinks map[string]SymlinkTarget) error { var ( errs []error numPairs int ) for oldpath, cfg := range symlinks { var err error if oldpath == "" { err = fmt.Errorf("symlink source is empty") errs = append(errs, err) } if (cfg.Path != "" && len(cfg.Paths) > 0) || (cfg.Path == "" && len(cfg.Paths) == 0) { err = fmt.Errorf("'path' and 'paths' fields are mutually exclusive, and at least one is required: "+ "symlink to %s", oldpath) errs = append(errs, err) } if err != nil { continue } if cfg.Path != "" { // this means .Paths is empty numPairs++ continue } for _, newpath := range cfg.Paths { // this means .Path is empty numPairs++ if newpath == "" { errs = append(errs, fmt.Errorf("symlink newpath should not be empty")) continue } } } // The remainder of this function checks for duplicate `newpath`s in the // symlink pairs. This is not allowed: neither the ordering of the // `oldpath` map keys, nor that of the `.Paths` values can be trusted. We // also sort both to avoid cache misses, so we would end up with // inconsistent behavior -- regardless of whether the inputs are the same. if numPairs < 2 { return goerrors.Join(errs...) } var ( oldpath string cfg SymlinkTarget ) seen := make(map[string]string, numPairs) checkDuplicateNewpath := func(newpath string) { if newpath == "" { return } if seenPath, found := seen[newpath]; found { errs = append(errs, fmt.Errorf("symlink 'newpaths' must be unique: %q points to both %q and %q", newpath, oldpath, seenPath)) } seen[newpath] = oldpath } for oldpath, cfg = range symlinks { checkDuplicateNewpath(cfg.Path) for _, newpath := range cfg.Paths { checkDuplicateNewpath(newpath) } } return goerrors.Join(errs...) } func (img *ImageConfig) processBuildArgs(lex *shell.Lex, args map[string]string, allowArg func(string) bool) error { if img == nil { return nil } var errs error for k, v := range img.Labels { updated, err := expandArgs(lex, v, args, allowArg) if err != nil { errs = goerrors.Join(errs, errors.Wrapf(err, "env %s=%s", k, v)) continue } img.Labels[k] = updated } return errs }