Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Multiple postinstall symlinks pointing to the same thing #495

26 changes: 24 additions & 2 deletions docs/spec.schema.json
Original file line number Diff line number Diff line change
@@ -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"
},
2 changes: 1 addition & 1 deletion frontend/azlinux/handle_container.go
Original file line number Diff line number Diff line change
@@ -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)
20 changes: 12 additions & 8 deletions frontend/windows/handle_container.go
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import (
"fmt"
"path"
"runtime"
"sort"
"sync"

"github.com/Azure/dalec"
@@ -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
12 changes: 9 additions & 3 deletions helpers.go
Original file line number Diff line number Diff line change
@@ -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"
61 changes: 50 additions & 11 deletions image.go
Original file line number Diff line number Diff line change
@@ -145,46 +145,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 {
@@ -199,10 +206,42 @@ 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 = ""
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.
//
84 changes: 84 additions & 0 deletions load.go
Original file line number Diff line number Diff line change
@@ -324,6 +324,8 @@ func (s *Spec) FillDefaults() {
t.fillDefaults()
s.Targets[k] = t
}

s.Image.fillDefaults()
}

func (s Spec) Validate() error {
@@ -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
@@ -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