Skip to content
26 changes: 24 additions & 2 deletions docs/spec.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1106,16 +1106,38 @@
"description": "Spec is the specification for a package build."
},
"SymlinkTarget": {
"oneOf": [
{
"required": [
"path"
],
"title": "path"
},
{
"required": [
"paths"
],
"title": "paths"
}
],
"properties": {
"path": {
"type": "string",
"description": "Path is the path where the symlink should be placed"
"description": "Path is the path where the symlink should be placed\n\nDeprecated: This is here for backward compatibility. Use `Paths` instead."
},
"paths": {
"items": {
"type": "string"
},
"type": "array",
"description": "Path is a list of `newpath`s that will all point to the same `oldpath`."
}
},
"additionalProperties": false,
"type": "object",
"required": [
"path"
"path",
"paths"
],
"description": "SymlinkTarget specifies the properties of a symlink"
},
Expand Down
2 changes: 1 addition & 1 deletion frontend/azlinux/handle_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func specToContainerLLB(w worker, spec *dalec.Spec, targetKey string, rpmDir llb
dalec.WithConstraints(opts...),
).AddMount(workPath, rootfs)

if post := spec.GetImagePost(targetKey); post != nil && len(post.Symlinks) > 0 {
if post := spec.GetImagePost(targetKey); post != nil {
rootfs = builderImg.
Run(dalec.WithConstraints(opts...), dalec.InstallPostSymlinks(post, workPath)).
AddMount(workPath, rootfs)
Expand Down
20 changes: 12 additions & 8 deletions frontend/windows/handle_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"path"
"runtime"
"sort"
"sync"

"github.com/Azure/dalec"
Expand Down Expand Up @@ -198,16 +199,19 @@ func copySymlinks(post *dalec.PostInstall) llb.StateOption {
return s
}

lm := post.Symlinks
if len(lm) == 0 {
if len(post.Symlinks) == 0 {
return s
}
keys := dalec.SortMapKeys(lm)
for _, srcPath := range keys {
l := lm[srcPath]
dstPath := l.Path
s = s.File(llb.Mkdir(path.Dir(dstPath), 0755, llb.WithParents(true)))
s = s.File(llb.Copy(s, srcPath, dstPath))

sortedKeys := dalec.SortMapKeys(post.Symlinks)
for _, oldpath := range sortedKeys {
newpaths := post.Symlinks[oldpath].Paths
sort.Strings(newpaths)

for _, newpath := range newpaths {
s = s.File(llb.Mkdir(path.Dir(newpath), 0755, llb.WithParents(true)))
s = s.File(llb.Copy(s, oldpath, newpath))
}
}

return s
Expand Down
12 changes: 9 additions & 3 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,9 +383,15 @@ func InstallPostSymlinks(post *PostInstall, rootfsPath string) llb.RunOption {
buf := bytes.NewBuffer(nil)
buf.WriteString("set -ex\n")

for src, tgt := range post.Symlinks {
fmt.Fprintf(buf, "mkdir -p %q\n", filepath.Join(rootfsPath, filepath.Dir(tgt.Path)))
fmt.Fprintf(buf, "ln -s %q %q\n", src, filepath.Join(rootfsPath, tgt.Path))
sortedKeys := SortMapKeys(post.Symlinks)
for _, oldpath := range sortedKeys {
newpaths := post.Symlinks[oldpath].Paths
sort.Strings(newpaths)

for _, newpath := range newpaths {
fmt.Fprintf(buf, "mkdir -p %q\n", filepath.Join(rootfsPath, filepath.Dir(newpath)))
fmt.Fprintf(buf, "ln -s %q %q\n", oldpath, filepath.Join(rootfsPath, newpath))
}
}

const name = "tmp.dalec.symlink.sh"
Expand Down
63 changes: 52 additions & 11 deletions image.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dalec
import (
"context"
goerrors "errors"
"sort"

"github.com/google/shlex"
"github.com/moby/buildkit/client/llb"
Expand Down Expand Up @@ -145,46 +146,53 @@ func MergeImageConfig(dst *DockerImageConfig, src *ImageConfig) error {
return nil
}

func (s *ImageConfig) validate() error {
if s == nil {
func (i *ImageConfig) validate() error {
if i == nil {
return nil
}

var errs []error

if s.Base != "" && len(s.Bases) > 0 {
if i.Base != "" && len(i.Bases) > 0 {
errs = append(errs, errors.New("cannot specify both image.base and image.bases"))
}

for i, base := range s.Bases {
for i, base := range i.Bases {
if err := base.validate(); err != nil {
errs = append(errs, errors.Wrapf(err, "bases[%d]", i))
}
}

if err := i.Post.validate(); err != nil {
errs = append(errs, errors.Wrap(err, "postinstall"))
}

return goerrors.Join(errs...)
}

func (s *ImageConfig) fillDefaults() {
if s == nil {
func (i *ImageConfig) fillDefaults() {
if i == nil {
return
}

// s.Bases is a superset of s.Base, so migrate s.Base to s.Bases
if s.Base != "" {
s.Bases = append(s.Bases, BaseImage{
if i.Base != "" {
i.Bases = append(i.Bases, BaseImage{
Rootfs: Source{
DockerImage: &SourceDockerImage{
Ref: s.Base,
Ref: i.Base,
},
},
})

s.Base = ""
i.Base = ""
}

for _, bi := range s.Bases {
for _, bi := range i.Bases {
bi.fillDefaults()
}

i.Post.normalizeSymlinks()
}

func (s *BaseImage) validate() error {
Expand All @@ -199,10 +207,43 @@ func (s *BaseImage) validate() error {
return nil
}

func (p *PostInstall) validate() error {
if p == nil {
return nil
}

var errs []error

if err := validateSymlinks(p.Symlinks); err != nil {
errs = append(errs, err)
}

return errors.Wrap(goerrors.Join(errs...), "symlink")
}

func (s *BaseImage) fillDefaults() {
fillDefaults(&s.Rootfs)
}

func (p *PostInstall) normalizeSymlinks() {
if p == nil {
return
}

// validation has already taken place
for oldpath := range p.Symlinks {
cfg := p.Symlinks[oldpath]
if cfg.Path == "" {
continue
}

cfg.Paths = append(cfg.Paths, cfg.Path)
cfg.Path = ""
sort.Strings(cfg.Paths)
p.Symlinks[oldpath] = cfg
}
}

func (bi *BaseImage) ResolveImageConfig(ctx context.Context, sOpt SourceOpts, opt sourceresolver.Opt) ([]byte, error) {
// In the future, *BaseImage may support other source types, but for now it only supports Docker images.
//
Expand Down
84 changes: 84 additions & 0 deletions load.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ func (s *Spec) FillDefaults() {
t.fillDefaults()
s.Targets[k] = t
}

s.Image.fillDefaults()
}

func (s Spec) Validate() error {
Expand Down Expand Up @@ -386,8 +388,13 @@ func (s Spec) Validate() error {
}
}

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
Expand Down Expand Up @@ -452,3 +459,80 @@ func (b *ArtifactBuild) processBuildArgs(lex *shell.Lex, args map[string]string,

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...)
}
Loading