refactor(nixery): Extract layering logic into separate package
This will be required for making a standalone, Nixery-style image builder function usable from Nix. Change-Id: I5e36348bd4c32d249d56f6628cd046916691319f Reviewed-on: https://cl.tvl.fyi/c/depot/+/5601 Tested-by: BuildkiteCI Reviewed-by: sterni <sternenseemann@systemli.org>
This commit is contained in:
parent
d60feb21e8
commit
796ff086be
4 changed files with 25 additions and 21 deletions
|
@ -16,6 +16,8 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/google/nixery/layers"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create a new compressed tarball from each of the paths in the list
|
// Create a new compressed tarball from each of the paths in the list
|
||||||
|
@ -23,7 +25,7 @@ import (
|
||||||
//
|
//
|
||||||
// The uncompressed tarball is hashed because image manifests must
|
// The uncompressed tarball is hashed because image manifests must
|
||||||
// contain both the hashes of compressed and uncompressed layers.
|
// contain both the hashes of compressed and uncompressed layers.
|
||||||
func packStorePaths(l *layer, w io.Writer) (string, error) {
|
func packStorePaths(l *layers.Layer, w io.Writer) (string, error) {
|
||||||
shasum := sha256.New()
|
shasum := sha256.New()
|
||||||
gz := gzip.NewWriter(w)
|
gz := gzip.NewWriter(w)
|
||||||
multi := io.MultiWriter(shasum, gz)
|
multi := io.MultiWriter(shasum, gz)
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/nixery/config"
|
"github.com/google/nixery/config"
|
||||||
|
"github.com/google/nixery/layers"
|
||||||
"github.com/google/nixery/manifest"
|
"github.com/google/nixery/manifest"
|
||||||
"github.com/google/nixery/storage"
|
"github.com/google/nixery/storage"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -39,7 +40,7 @@ type State struct {
|
||||||
Storage storage.Backend
|
Storage storage.Backend
|
||||||
Cache *LocalCache
|
Cache *LocalCache
|
||||||
Cfg config.Config
|
Cfg config.Config
|
||||||
Pop Popularity
|
Pop layers.Popularity
|
||||||
}
|
}
|
||||||
|
|
||||||
// Architecture represents the possible CPU architectures for which
|
// Architecture represents the possible CPU architectures for which
|
||||||
|
@ -117,7 +118,7 @@ type ImageResult struct {
|
||||||
Pkgs []string `json:"pkgs"`
|
Pkgs []string `json:"pkgs"`
|
||||||
|
|
||||||
// These fields are populated in case of success
|
// These fields are populated in case of success
|
||||||
Graph runtimeGraph `json:"runtimeGraph"`
|
Graph layers.RuntimeGraph `json:"runtimeGraph"`
|
||||||
SymlinkLayer struct {
|
SymlinkLayer struct {
|
||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
TarHash string `json:"tarHash"`
|
TarHash string `json:"tarHash"`
|
||||||
|
@ -281,7 +282,7 @@ func prepareImage(s *State, image *Image) (*ImageResult, error) {
|
||||||
// added only after successful uploads, which guarantees that entries
|
// added only after successful uploads, which guarantees that entries
|
||||||
// retrieved from the cache are present in the bucket.
|
// retrieved from the cache are present in the bucket.
|
||||||
func prepareLayers(ctx context.Context, s *State, image *Image, result *ImageResult) ([]manifest.Entry, error) {
|
func prepareLayers(ctx context.Context, s *State, image *Image, result *ImageResult) ([]manifest.Entry, error) {
|
||||||
grouped := groupLayers(&result.Graph, &s.Pop, LayerBudget)
|
grouped := layers.GroupLayers(&result.Graph, &s.Pop, LayerBudget)
|
||||||
|
|
||||||
var entries []manifest.Entry
|
var entries []manifest.Entry
|
||||||
|
|
||||||
|
@ -318,7 +319,7 @@ func prepareLayers(ctx context.Context, s *State, image *Image, result *ImageRes
|
||||||
|
|
||||||
var pkgs []string
|
var pkgs []string
|
||||||
for _, p := range l.Contents {
|
for _, p := range l.Contents {
|
||||||
pkgs = append(pkgs, packageFromPath(p))
|
pkgs = append(pkgs, layers.PackageFromPath(p))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
|
|
|
@ -103,7 +103,7 @@
|
||||||
//
|
//
|
||||||
// Layer budget: 10
|
// Layer budget: 10
|
||||||
// Layers: { E }, { D, F }, { A }, { B }, { C }
|
// Layers: { E }, { D, F }, { A }, { B }, { C }
|
||||||
package builder
|
package layers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
@ -121,7 +121,7 @@ import (
|
||||||
// dependencies of a derivation.
|
// dependencies of a derivation.
|
||||||
//
|
//
|
||||||
// This is generated in Nix by using the exportReferencesGraph feature.
|
// This is generated in Nix by using the exportReferencesGraph feature.
|
||||||
type runtimeGraph struct {
|
type RuntimeGraph struct {
|
||||||
References struct {
|
References struct {
|
||||||
Graph []string `json:"graph"`
|
Graph []string `json:"graph"`
|
||||||
} `json:"exportReferencesGraph"`
|
} `json:"exportReferencesGraph"`
|
||||||
|
@ -142,19 +142,19 @@ type Popularity = map[string]int
|
||||||
|
|
||||||
// Layer represents the data returned for each layer that Nix should
|
// Layer represents the data returned for each layer that Nix should
|
||||||
// build for the container image.
|
// build for the container image.
|
||||||
type layer struct {
|
type Layer struct {
|
||||||
Contents []string `json:"contents"`
|
Contents []string `json:"contents"`
|
||||||
MergeRating uint64
|
MergeRating uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash the contents of a layer to create a deterministic identifier that can be
|
// Hash the contents of a layer to create a deterministic identifier that can be
|
||||||
// used for caching.
|
// used for caching.
|
||||||
func (l *layer) Hash() string {
|
func (l *Layer) Hash() string {
|
||||||
sum := sha1.Sum([]byte(strings.Join(l.Contents, ":")))
|
sum := sha1.Sum([]byte(strings.Join(l.Contents, ":")))
|
||||||
return fmt.Sprintf("%x", sum)
|
return fmt.Sprintf("%x", sum)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a layer) merge(b layer) layer {
|
func (a Layer) merge(b Layer) Layer {
|
||||||
a.Contents = append(a.Contents, b.Contents...)
|
a.Contents = append(a.Contents, b.Contents...)
|
||||||
a.MergeRating += b.MergeRating
|
a.MergeRating += b.MergeRating
|
||||||
return a
|
return a
|
||||||
|
@ -177,7 +177,7 @@ var nixRegexp = regexp.MustCompile(`^/nix/store/[a-z0-9]+-`)
|
||||||
|
|
||||||
// PackageFromPath returns the name of a Nix package based on its
|
// PackageFromPath returns the name of a Nix package based on its
|
||||||
// output store path.
|
// output store path.
|
||||||
func packageFromPath(path string) string {
|
func PackageFromPath(path string) string {
|
||||||
return nixRegexp.ReplaceAllString(path, "")
|
return nixRegexp.ReplaceAllString(path, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +185,7 @@ func packageFromPath(path string) string {
|
||||||
// the dot format used by GraphViz, into which the dependency graph
|
// the dot format used by GraphViz, into which the dependency graph
|
||||||
// can be rendered.
|
// can be rendered.
|
||||||
func (c *closure) DOTID() string {
|
func (c *closure) DOTID() string {
|
||||||
return packageFromPath(c.Path)
|
return PackageFromPath(c.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// bigOrPopular checks whether this closure should be considered for
|
// bigOrPopular checks whether this closure should be considered for
|
||||||
|
@ -228,7 +228,7 @@ func insertEdges(graph *simple.DirectedGraph, cmap *map[string]*closure, node *c
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a graph structure from the references supplied by Nix.
|
// Create a graph structure from the references supplied by Nix.
|
||||||
func buildGraph(refs *runtimeGraph, pop *Popularity) *simple.DirectedGraph {
|
func buildGraph(refs *RuntimeGraph, pop *Popularity) *simple.DirectedGraph {
|
||||||
cmap := make(map[string]*closure)
|
cmap := make(map[string]*closure)
|
||||||
graph := simple.NewDirectedGraph()
|
graph := simple.NewDirectedGraph()
|
||||||
|
|
||||||
|
@ -288,7 +288,7 @@ func buildGraph(refs *runtimeGraph, pop *Popularity) *simple.DirectedGraph {
|
||||||
// Extracts a subgraph starting at the specified root from the
|
// Extracts a subgraph starting at the specified root from the
|
||||||
// dominator tree. The subgraph is converted into a flat list of
|
// dominator tree. The subgraph is converted into a flat list of
|
||||||
// layers, each containing the store paths and merge rating.
|
// layers, each containing the store paths and merge rating.
|
||||||
func groupLayer(dt *flow.DominatorTree, root *closure) layer {
|
func groupLayer(dt *flow.DominatorTree, root *closure) Layer {
|
||||||
size := root.Size
|
size := root.Size
|
||||||
contents := []string{root.Path}
|
contents := []string{root.Path}
|
||||||
children := dt.DominatedBy(root.ID())
|
children := dt.DominatedBy(root.ID())
|
||||||
|
@ -305,7 +305,7 @@ func groupLayer(dt *flow.DominatorTree, root *closure) layer {
|
||||||
// Contents are sorted to ensure that hashing is consistent
|
// Contents are sorted to ensure that hashing is consistent
|
||||||
sort.Strings(contents)
|
sort.Strings(contents)
|
||||||
|
|
||||||
return layer{
|
return Layer{
|
||||||
Contents: contents,
|
Contents: contents,
|
||||||
MergeRating: uint64(root.Popularity) * size,
|
MergeRating: uint64(root.Popularity) * size,
|
||||||
}
|
}
|
||||||
|
@ -316,10 +316,10 @@ func groupLayer(dt *flow.DominatorTree, root *closure) layer {
|
||||||
//
|
//
|
||||||
// Layers are merged together until they fit into the layer budget,
|
// Layers are merged together until they fit into the layer budget,
|
||||||
// based on their merge rating.
|
// based on their merge rating.
|
||||||
func dominate(budget int, graph *simple.DirectedGraph) []layer {
|
func dominate(budget int, graph *simple.DirectedGraph) []Layer {
|
||||||
dt := flow.Dominators(graph.Node(0), graph)
|
dt := flow.Dominators(graph.Node(0), graph)
|
||||||
|
|
||||||
var layers []layer
|
var layers []Layer
|
||||||
for _, n := range dt.DominatedBy(dt.Root().ID()) {
|
for _, n := range dt.DominatedBy(dt.Root().ID()) {
|
||||||
layers = append(layers, groupLayer(&dt, n.(*closure)))
|
layers = append(layers, groupLayer(&dt, n.(*closure)))
|
||||||
}
|
}
|
||||||
|
@ -347,7 +347,7 @@ func dominate(budget int, graph *simple.DirectedGraph) []layer {
|
||||||
// groupLayers applies the algorithm described above the its input and returns a
|
// groupLayers applies the algorithm described above the its input and returns a
|
||||||
// list of layers, each consisting of a list of Nix store paths that it should
|
// list of layers, each consisting of a list of Nix store paths that it should
|
||||||
// contain.
|
// contain.
|
||||||
func groupLayers(refs *runtimeGraph, pop *Popularity, budget int) []layer {
|
func GroupLayers(refs *RuntimeGraph, pop *Popularity, budget int) []Layer {
|
||||||
graph := buildGraph(refs, pop)
|
graph := buildGraph(refs, pop)
|
||||||
return dominate(budget, graph)
|
return dominate(budget, graph)
|
||||||
}
|
}
|
|
@ -26,6 +26,7 @@ import (
|
||||||
|
|
||||||
"github.com/google/nixery/builder"
|
"github.com/google/nixery/builder"
|
||||||
"github.com/google/nixery/config"
|
"github.com/google/nixery/config"
|
||||||
|
"github.com/google/nixery/layers"
|
||||||
"github.com/google/nixery/logs"
|
"github.com/google/nixery/logs"
|
||||||
mf "github.com/google/nixery/manifest"
|
mf "github.com/google/nixery/manifest"
|
||||||
"github.com/google/nixery/storage"
|
"github.com/google/nixery/storage"
|
||||||
|
@ -52,7 +53,7 @@ var (
|
||||||
|
|
||||||
// Downloads the popularity information for the package set from the
|
// Downloads the popularity information for the package set from the
|
||||||
// URL specified in Nixery's configuration.
|
// URL specified in Nixery's configuration.
|
||||||
func downloadPopularity(url string) (builder.Popularity, error) {
|
func downloadPopularity(url string) (layers.Popularity, error) {
|
||||||
resp, err := http.Get(url)
|
resp, err := http.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -67,7 +68,7 @@ func downloadPopularity(url string) (builder.Popularity, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var pop builder.Popularity
|
var pop layers.Popularity
|
||||||
err = json.Unmarshal(j, &pop)
|
err = json.Unmarshal(j, &pop)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -246,7 +247,7 @@ func main() {
|
||||||
log.WithError(err).Fatal("failed to instantiate build cache")
|
log.WithError(err).Fatal("failed to instantiate build cache")
|
||||||
}
|
}
|
||||||
|
|
||||||
var pop builder.Popularity
|
var pop layers.Popularity
|
||||||
if cfg.PopUrl != "" {
|
if cfg.PopUrl != "" {
|
||||||
pop, err = downloadPopularity(cfg.PopUrl)
|
pop, err = downloadPopularity(cfg.PopUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in a new issue