github.com/wolfi-dev/wolfictl@v0.16.11/pkg/dag/packages.go (about)

     1  package dag
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/fs"
     7  	"path/filepath"
     8  	"sort"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"chainguard.dev/melange/pkg/build"
    13  	"chainguard.dev/melange/pkg/config"
    14  	"github.com/chainguard-dev/clog"
    15  	apk "github.com/chainguard-dev/go-apk/pkg/apk"
    16  )
    17  
    18  const (
    19  	Local = "local"
    20  )
    21  
    22  // Configuration represents a configuration along with the file that sourced it.
    23  // It can be for an origin package, a subpackage, or something that is provided by a package.
    24  // The Configuration field is a pointer to the actual configuration as parsed from a file. The Path field is the
    25  // path to the file from which the configuration was parsed. The Name and Version fields are the name and version
    26  // of the package, subpackage, or provided item. In the case of an origin package, the Name field
    27  // is the same as the Configuration.Package.Name field, and the Version field is the same as
    28  // the Configuration.Package.Version field with the epoch added as `-r<epoch>`. In the case of a
    29  // subpackage or provided item, the Name and Version fields may be different.
    30  type Configuration struct {
    31  	*config.Configuration
    32  	Path    string
    33  	name    string
    34  	version string
    35  
    36  	// the actual package or subpackage name providing this configuration
    37  	// this allows us to distinguish between a subpackge that is providing a virtual and providing itself
    38  	pkg string
    39  }
    40  
    41  func (c Configuration) String() string {
    42  	return fmt.Sprintf("%s-%s", c.name, c.version)
    43  }
    44  
    45  func (c Configuration) Name() string {
    46  	return c.name
    47  }
    48  
    49  func (c Configuration) Version() string {
    50  	return c.version
    51  }
    52  
    53  func (c Configuration) Source() string {
    54  	return Local
    55  }
    56  
    57  func (c Configuration) FullName() string {
    58  	return fmt.Sprintf("%s-%s-r%d", c.name, c.version, c.Package.Epoch)
    59  }
    60  
    61  func (c Configuration) Resolved() bool {
    62  	return true
    63  }
    64  
    65  // Packages represents a set of package configurations, including
    66  // the parent, or origin, package, its subpackages, and whatever else it 'provides'.
    67  // It contains references from each such origin package, subpackage and provides
    68  // to the origin config.
    69  //
    70  // It also maintains a list of the origin packages.
    71  //
    72  // It does not try to determine relationships and dependencies between packages. For that,
    73  // pass a Packages to NewGraph.
    74  type Packages struct {
    75  	configs  map[string][]*Configuration
    76  	packages map[string][]*Configuration
    77  	index    map[string]*Configuration
    78  }
    79  
    80  var ErrMultipleConfigurations = fmt.Errorf("multiple configurations using the same package name")
    81  
    82  func (p *Packages) addPackage(name string, configuration *Configuration) error {
    83  	if _, exists := p.packages[name]; exists {
    84  		return fmt.Errorf("%s: %w", name, ErrMultipleConfigurations)
    85  	}
    86  
    87  	p.packages[name] = append(p.packages[name], configuration)
    88  
    89  	return nil
    90  }
    91  
    92  func (p *Packages) addConfiguration(name string, configuration *Configuration) error {
    93  	p.configs[name] = append(p.configs[name], configuration)
    94  	p.index[configuration.String()] = configuration
    95  
    96  	return nil
    97  }
    98  
    99  func (p *Packages) addProvides(c *Configuration, provides []string) error {
   100  	for _, prov := range provides {
   101  		pctx := &build.PipelineBuild{
   102  			Build: &build.Build{
   103  				Configuration: *c.Configuration,
   104  			},
   105  			Package: &c.Package,
   106  		}
   107  		template, err := build.MutateWith(pctx, nil)
   108  		if err != nil {
   109  			return err
   110  		}
   111  		for tmpl, val := range template {
   112  			prov = strings.ReplaceAll(prov, tmpl, val)
   113  		}
   114  		name, version := packageNameFromProvides(prov)
   115  		if version == "" {
   116  			version = c.version
   117  		}
   118  		providesc := &Configuration{
   119  			Configuration: c.Configuration,
   120  			Path:          c.Path,
   121  			name:          name,
   122  			version:       version, // provides can have own version or inherit package's version
   123  			pkg:           c.pkg,
   124  		}
   125  		if err := p.addConfiguration(name, providesc); err != nil {
   126  			return err
   127  		}
   128  	}
   129  	return nil
   130  }
   131  
   132  // NewPackages reads an fs.FS to get all of the Melange configuration yamls in
   133  // the given directory, and then parses them, including their subpackages and
   134  // 'provides' parameters, to create a Packages struct with all of the
   135  // information, as well as the list of original packages, and, for each such
   136  // package, the source path (yaml) from which it came. The result is a Packages
   137  // struct.
   138  //
   139  // The input is any fs.FS filesystem implementation. Given a directory path, you
   140  // can call NewPackages like this:
   141  //
   142  // NewPackages(ctx, os.DirFS("/path/to/dir"), "/path/to/dir", "./pipelines")
   143  //
   144  // The repetition of the path is necessary because of how the upstream parser in
   145  // melange requires the full path to the directory to be passed in.
   146  func NewPackages(ctx context.Context, fsys fs.FS, dirPath, pipelineDir string) (*Packages, error) {
   147  	log := clog.FromContext(ctx)
   148  
   149  	pkgs := &Packages{
   150  		configs:  make(map[string][]*Configuration),
   151  		packages: make(map[string][]*Configuration),
   152  		index:    make(map[string]*Configuration),
   153  	}
   154  	err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
   155  		if err != nil {
   156  			return err
   157  		}
   158  
   159  		// Skip anything in .github/ and .git/
   160  		if path == ".github" {
   161  			return fs.SkipDir
   162  		}
   163  		if path == ".git" {
   164  			return fs.SkipDir
   165  		}
   166  
   167  		// Skip .yam.yaml and .melange.k8s.yaml
   168  		if d.Type().IsRegular() && path == ".yam.yaml" {
   169  			return nil
   170  		}
   171  		if d.Type().IsRegular() && path == ".melange.k8s.yaml" {
   172  			return nil
   173  		}
   174  
   175  		// Skip any file that isn't a yaml file
   176  		if !d.Type().IsRegular() || !strings.HasSuffix(path, ".yaml") {
   177  			return nil
   178  		}
   179  
   180  		if filepath.Dir(path) != "." && !strings.HasSuffix(path, ".melange.yaml") {
   181  			log.With("path", path).Debug("skipping non-melange YAML file")
   182  			return nil
   183  		}
   184  
   185  		p := filepath.Join(dirPath, path)
   186  		buildc, err := config.ParseConfiguration(ctx, p)
   187  		if err != nil {
   188  			return err
   189  		}
   190  		c := &Configuration{
   191  			Configuration: buildc,
   192  			Path:          p,
   193  			name:          buildc.Package.Name,
   194  			version:       fullVersion(&buildc.Package),
   195  			pkg:           buildc.Package.Name,
   196  		}
   197  
   198  		name := c.name
   199  		if name == "" {
   200  			return fmt.Errorf("no package name in %q", path)
   201  		}
   202  		if err := pkgs.addConfiguration(name, c); err != nil {
   203  			return err
   204  		}
   205  		if err := pkgs.addPackage(name, c); err != nil {
   206  			return err
   207  		}
   208  		if err := pkgs.addProvides(c, c.Package.Dependencies.Provides); err != nil {
   209  			return err
   210  		}
   211  
   212  		for i := range c.Subpackages {
   213  			subpkg := c.Subpackages[i]
   214  			name := subpkg.Name
   215  			if name == "" {
   216  				return fmt.Errorf("empty subpackage name at index %d for package %q", i, c.Package.Name)
   217  			}
   218  			c := &Configuration{
   219  				Configuration: buildc,
   220  				Path:          p,
   221  				name:          name,
   222  				version:       fullVersion(&buildc.Package), // subpackages have same version as origin
   223  				pkg:           name,
   224  			}
   225  			if err := pkgs.addConfiguration(name, c); err != nil {
   226  				return err
   227  			}
   228  			if err := pkgs.addProvides(c, subpkg.Dependencies.Provides); err != nil {
   229  				return err
   230  			}
   231  
   232  			// TODO: resolve deps via `uses` for subpackage pipelines.
   233  		}
   234  		// Resolve all `uses` used by the pipeline. This updates the set of
   235  		// .environment.contents.packages so the next block can include those as build deps.
   236  		pctx := &build.PipelineBuild{
   237  			Build: &build.Build{
   238  				PipelineDirs:  []string{pipelineDir},
   239  				Configuration: *c.Configuration,
   240  			},
   241  			Package: &c.Package,
   242  		}
   243  		for i := range c.Pipeline {
   244  			s := &build.PipelineContext{Environment: &pctx.Build.Configuration.Environment, PipelineDirs: []string{pipelineDir}, Pipeline: &c.Pipeline[i]}
   245  			if err := s.ApplyNeeds(ctx, pctx); err != nil {
   246  				return fmt.Errorf("unable to resolve needs for package %s: %w", name, err)
   247  			}
   248  			c.Environment.Contents.Packages = pctx.Build.Configuration.Environment.Contents.Packages
   249  		}
   250  
   251  		return nil
   252  	})
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  
   257  	return pkgs, nil
   258  }
   259  
   260  // Config returns the Melange configuration for the package, provides or
   261  // subpackage with the given name, if the package is present in the Graph. If
   262  // it's not present, Config returns an empty list.
   263  //
   264  // Pass packageOnly=true to restruct it just to origin package names.
   265  func (p Packages) Config(name string, packageOnly bool) []*Configuration {
   266  	if p.configs == nil {
   267  		// this would be unexpected
   268  		return nil
   269  	}
   270  	var (
   271  		c  []*Configuration
   272  		ok bool
   273  	)
   274  	if packageOnly {
   275  		c, ok = p.packages[name]
   276  	} else {
   277  		c, ok = p.configs[name]
   278  	}
   279  	if !ok {
   280  		return nil
   281  	}
   282  	list := make([]*Configuration, 0, len(c))
   283  	list = append(list, c...)
   284  
   285  	// sort the list by increasing version
   286  	// this should be better about this, perhaps we will use the apko version sorting library in a future revision
   287  	sort.Slice(list, func(i, j int) bool {
   288  		return fullVersion(&list[i].Package) < fullVersion(&list[j].Package)
   289  	})
   290  	return list
   291  }
   292  
   293  func (p Packages) ConfigByKey(key string) *Configuration {
   294  	if len(p.index) == 0 {
   295  		return nil
   296  	}
   297  	c, ok := p.index[key]
   298  	if !ok {
   299  		return nil
   300  	}
   301  	return c
   302  }
   303  
   304  // PkgConfig returns the melange Configuration for a given package name.
   305  func (p Packages) PkgConfig(pkgName string) *Configuration {
   306  	for _, cfg := range p.packages[pkgName] {
   307  		if pkgName == cfg.Package.Name {
   308  			return cfg
   309  		}
   310  	}
   311  	return nil
   312  }
   313  
   314  // PkgInfo returns the build.Package struct for a given package name.
   315  // If no such package name is found in the packages, return nil package and nil error.
   316  func (p Packages) PkgInfo(pkgName string) *config.Package {
   317  	if cfg := p.PkgConfig(pkgName); cfg != nil {
   318  		return &cfg.Package
   319  	}
   320  	return nil
   321  }
   322  
   323  // Packages returns a slice of every package and subpackage available in the Packages struct,
   324  // sorted alphabetically and then by version, with each package converted to a *apk.RepositoryPackage.
   325  func (p Packages) Packages() []*Configuration {
   326  	allPackages := make([]*Configuration, 0, len(p.packages))
   327  	for _, byVersion := range p.packages {
   328  		allPackages = append(allPackages, byVersion...)
   329  	}
   330  
   331  	// sort for deterministic output
   332  	sort.Slice(allPackages, func(i, j int) bool {
   333  		if allPackages[i].name == allPackages[j].name {
   334  			return allPackages[i].version < allPackages[j].version
   335  		}
   336  		return allPackages[i].name < allPackages[j].name
   337  	})
   338  	return allPackages
   339  }
   340  
   341  // PackageNames returns a slice of the names of all packages, sorted alphabetically.
   342  func (p Packages) PackageNames() []string {
   343  	allPackages := make([]string, 0, len(p.packages))
   344  	for name := range p.packages {
   345  		allPackages = append(allPackages, name)
   346  	}
   347  
   348  	// sort for deterministic output
   349  	sort.Strings(allPackages)
   350  	return allPackages
   351  }
   352  
   353  // Sub returns a new Packages whose members are the named packages or provides that are listed.
   354  // If a listed element is a provides, automatically includes the origin package that provides it.
   355  // If a listed element is a subpackage, automatically includes the origin package that contains it.
   356  // If a listed element does not exist, returns an error.
   357  func (p Packages) Sub(names ...string) (*Packages, error) {
   358  	pkgs := &Packages{
   359  		configs:  make(map[string][]*Configuration),
   360  		index:    make(map[string]*Configuration),
   361  		packages: make(map[string][]*Configuration),
   362  	}
   363  	for _, name := range names {
   364  		if c, ok := p.configs[name]; ok {
   365  			for _, config := range c {
   366  				if err := pkgs.addConfiguration(name, config); err != nil {
   367  					return nil, err
   368  				}
   369  				if err := pkgs.addPackage(name, config); err != nil {
   370  					return nil, err
   371  				}
   372  			}
   373  		} else {
   374  			return nil, fmt.Errorf("package %q not found", name)
   375  		}
   376  	}
   377  	return pkgs, nil
   378  }
   379  
   380  func wantArch(have string, want []string) bool {
   381  	if len(want) == 0 {
   382  		return true
   383  	}
   384  
   385  	for _, a := range want {
   386  		if a == have {
   387  			return true
   388  		}
   389  	}
   390  
   391  	return false
   392  }
   393  
   394  // WithArch returns a new Packages whose members are valid for the given arch.
   395  func (p Packages) WithArch(arch string) (*Packages, error) {
   396  	pkgs := &Packages{
   397  		configs:  make(map[string][]*Configuration),
   398  		index:    p.index,
   399  		packages: make(map[string][]*Configuration),
   400  	}
   401  
   402  	for name, c := range p.configs {
   403  		for _, config := range c {
   404  			if !wantArch(arch, config.Package.TargetArchitecture) {
   405  				continue
   406  			}
   407  			if err := pkgs.addConfiguration(name, config); err != nil {
   408  				return nil, err
   409  			}
   410  		}
   411  	}
   412  
   413  	for name, c := range p.packages {
   414  		for _, config := range c {
   415  			if !wantArch(arch, config.Package.TargetArchitecture) {
   416  				continue
   417  			}
   418  			if err := pkgs.addPackage(name, config); err != nil {
   419  				return nil, err
   420  			}
   421  		}
   422  	}
   423  	return pkgs, nil
   424  }
   425  
   426  // Repository provide the Packages as a apk.RepositoryWithIndex. To be used in other places that require
   427  // using alpine/go structs instead of ours.
   428  func (p Packages) Repository(arch string) apk.NamedIndex {
   429  	repo := apk.NewRepositoryFromComponents(Local, "latest", "", arch)
   430  
   431  	// Precompute the number of packages to avoid growslice.
   432  	size := 0
   433  	for _, byVersion := range p.packages {
   434  		for _, config := range byVersion {
   435  			size++ // top-level package
   436  			size += len(config.Subpackages)
   437  		}
   438  	}
   439  
   440  	packages := make([]*apk.Package, 0, size)
   441  	for _, byVersion := range p.packages {
   442  		for _, cfg := range byVersion {
   443  			cfg := cfg
   444  			packages = append(packages, &apk.Package{
   445  				Arch:         arch,
   446  				Name:         cfg.Package.Name,
   447  				Version:      fullVersion(&cfg.Package),
   448  				Description:  cfg.Package.Description,
   449  				License:      cfg.Package.LicenseExpression(),
   450  				Origin:       cfg.Package.Name,
   451  				URL:          cfg.Package.URL,
   452  				Dependencies: cfg.Environment.Contents.Packages,
   453  				Provides:     cfg.Package.Dependencies.Provides,
   454  				RepoCommit:   cfg.Package.Commit,
   455  			})
   456  			for i := range cfg.Subpackages {
   457  				sub := cfg.Subpackages[i]
   458  				packages = append(packages, &apk.Package{
   459  					Arch:         arch,
   460  					Name:         sub.Name,
   461  					Version:      fullVersion(&cfg.Package),
   462  					Description:  sub.Description,
   463  					License:      cfg.Package.LicenseExpression(),
   464  					Origin:       cfg.Package.Name,
   465  					URL:          cfg.Package.URL,
   466  					Dependencies: cfg.Environment.Contents.Packages,
   467  					Provides:     sub.Dependencies.Provides,
   468  					RepoCommit:   sub.Commit,
   469  				})
   470  			}
   471  		}
   472  	}
   473  	index := &apk.APKIndex{
   474  		Description: "local repository",
   475  		Packages:    packages,
   476  	}
   477  
   478  	return apk.NewNamedRepositoryWithIndex("", repo.WithIndex(index))
   479  }
   480  
   481  func packageNameFromProvides(prov string) (name, version string) {
   482  	var ok bool
   483  	if name, version, ok = strings.Cut(prov, "~="); ok {
   484  		return
   485  	}
   486  	if name, version, ok = strings.Cut(prov, "="); ok {
   487  		return
   488  	}
   489  	name = prov
   490  	return
   491  }
   492  
   493  func fullVersion(pkg *config.Package) string {
   494  	return pkg.Version + "-r" + strconv.FormatUint(pkg.Epoch, 10)
   495  }