2019-08-14 18:20:01 +02:00
|
|
|
// Copyright 2019 Google LLC
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
|
|
// use this file except in compliance with the License. You may obtain a copy of
|
|
|
|
// the License at
|
|
|
|
//
|
|
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
|
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
|
|
// License for the specific language governing permissions and limitations under
|
|
|
|
// the License.
|
|
|
|
|
|
|
|
// Package builder implements the code required to build images via Nix. Image
|
|
|
|
// build data is cached for up to 24 hours to avoid duplicated calls to Nix
|
|
|
|
// (which are costly even if no building is performed).
|
|
|
|
package builder
|
|
|
|
|
|
|
|
import (
|
2019-09-21 16:04:13 +02:00
|
|
|
"bufio"
|
2019-08-14 18:20:01 +02:00
|
|
|
"bytes"
|
|
|
|
"context"
|
2019-09-30 18:40:01 +02:00
|
|
|
"crypto/sha256"
|
2019-08-14 18:20:01 +02:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
2019-09-30 18:40:01 +02:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2019-08-14 18:20:01 +02:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
2019-08-17 11:18:15 +02:00
|
|
|
"sort"
|
2019-08-14 18:20:01 +02:00
|
|
|
"strings"
|
|
|
|
|
2019-10-03 13:49:26 +02:00
|
|
|
"cloud.google.com/go/storage"
|
|
|
|
"github.com/google/nixery/config"
|
2019-09-30 18:40:01 +02:00
|
|
|
"github.com/google/nixery/layers"
|
2019-10-02 00:27:26 +02:00
|
|
|
"github.com/google/nixery/manifest"
|
2019-09-30 18:40:01 +02:00
|
|
|
"golang.org/x/oauth2/google"
|
2019-08-14 18:20:01 +02:00
|
|
|
)
|
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
// The maximum number of layers in an image is 125. To allow for
|
|
|
|
// extensibility, the actual number of layers Nixery is "allowed" to
|
|
|
|
// use up is set at a lower point.
|
|
|
|
const LayerBudget int = 94
|
|
|
|
|
2019-10-02 16:19:28 +02:00
|
|
|
// API scope needed for renaming objects in GCS
|
|
|
|
const gcsScope = "https://www.googleapis.com/auth/devstorage.read_write"
|
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
// HTTP client to use for direct calls to APIs that are not part of the SDK
|
|
|
|
var client = &http.Client{}
|
|
|
|
|
2019-10-03 13:49:26 +02:00
|
|
|
// State holds the runtime state that is carried around in Nixery and
|
|
|
|
// passed to builder functions.
|
|
|
|
type State struct {
|
|
|
|
Bucket *storage.BucketHandle
|
|
|
|
Cache *LocalCache
|
|
|
|
Cfg config.Config
|
|
|
|
Pop layers.Popularity
|
|
|
|
}
|
|
|
|
|
2019-08-14 18:20:01 +02:00
|
|
|
// Image represents the information necessary for building a container image.
|
|
|
|
// This can be either a list of package names (corresponding to keys in the
|
|
|
|
// nixpkgs set) or a Nix expression that results in a *list* of derivations.
|
|
|
|
type Image struct {
|
|
|
|
Name string
|
|
|
|
Tag string
|
|
|
|
|
|
|
|
// Names of packages to include in the image. These must correspond
|
|
|
|
// directly to top-level names of Nix packages in the nixpkgs tree.
|
|
|
|
Packages []string
|
|
|
|
}
|
|
|
|
|
2019-10-02 16:31:57 +02:00
|
|
|
// BuildResult represents the data returned from the server to the
|
|
|
|
// HTTP handlers. Error information is propagated straight from Nix
|
|
|
|
// for errors inside of the build that should be fed back to the
|
|
|
|
// client (such as missing packages).
|
2019-09-30 18:40:01 +02:00
|
|
|
type BuildResult struct {
|
2019-10-02 00:27:26 +02:00
|
|
|
Error string `json:"error"`
|
|
|
|
Pkgs []string `json:"pkgs"`
|
|
|
|
Manifest json.RawMessage `json:"manifest"`
|
2019-09-30 18:40:01 +02:00
|
|
|
}
|
|
|
|
|
2019-08-14 18:20:01 +02:00
|
|
|
// ImageFromName parses an image name into the corresponding structure which can
|
|
|
|
// be used to invoke Nix.
|
|
|
|
//
|
|
|
|
// It will expand convenience names under the hood (see the `convenienceNames`
|
|
|
|
// function below).
|
2019-08-17 11:18:15 +02:00
|
|
|
//
|
|
|
|
// Once assembled the image structure uses a sorted representation of
|
|
|
|
// the name. This is to avoid unnecessarily cache-busting images if
|
|
|
|
// only the order of requested packages has changed.
|
2019-08-14 18:20:01 +02:00
|
|
|
func ImageFromName(name string, tag string) Image {
|
2019-08-17 11:18:15 +02:00
|
|
|
pkgs := strings.Split(name, "/")
|
|
|
|
expanded := convenienceNames(pkgs)
|
|
|
|
|
|
|
|
sort.Strings(pkgs)
|
|
|
|
sort.Strings(expanded)
|
|
|
|
|
2019-08-14 18:20:01 +02:00
|
|
|
return Image{
|
2019-08-17 11:18:15 +02:00
|
|
|
Name: strings.Join(pkgs, "/"),
|
2019-08-14 18:20:01 +02:00
|
|
|
Tag: tag,
|
2019-08-17 11:18:15 +02:00
|
|
|
Packages: expanded,
|
2019-08-14 18:20:01 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
// ImageResult represents the output of calling the Nix derivation
|
|
|
|
// responsible for preparing an image.
|
|
|
|
type ImageResult struct {
|
|
|
|
// These fields are populated in case of an error
|
|
|
|
Error string `json:"error"`
|
|
|
|
Pkgs []string `json:"pkgs"`
|
|
|
|
|
|
|
|
// These fields are populated in case of success
|
|
|
|
Graph layers.RuntimeGraph `json:"runtimeGraph"`
|
|
|
|
SymlinkLayer struct {
|
|
|
|
Size int `json:"size"`
|
|
|
|
SHA256 string `json:"sha256"`
|
|
|
|
Path string `json:"path"`
|
|
|
|
} `json:"symlinkLayer"`
|
2019-08-14 18:20:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// convenienceNames expands convenience package names defined by Nixery which
|
|
|
|
// let users include commonly required sets of tools in a container quickly.
|
|
|
|
//
|
|
|
|
// Convenience names must be specified as the first package in an image.
|
|
|
|
//
|
|
|
|
// Currently defined convenience names are:
|
|
|
|
//
|
|
|
|
// * `shell`: Includes bash, coreutils and other common command-line tools
|
|
|
|
func convenienceNames(packages []string) []string {
|
2019-08-16 21:18:43 +02:00
|
|
|
shellPackages := []string{"bashInteractive", "cacert", "coreutils", "iana-etc", "moreutils", "nano"}
|
2019-08-14 18:20:01 +02:00
|
|
|
|
|
|
|
if packages[0] == "shell" {
|
|
|
|
return append(packages[1:], shellPackages...)
|
|
|
|
}
|
|
|
|
|
|
|
|
return packages
|
|
|
|
}
|
|
|
|
|
2019-09-21 16:04:13 +02:00
|
|
|
// logNix logs each output line from Nix. It runs in a goroutine per
|
|
|
|
// output channel that should be live-logged.
|
|
|
|
func logNix(name string, r io.ReadCloser) {
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
|
|
for scanner.Scan() {
|
|
|
|
log.Printf("\x1b[31m[nix - %s]\x1b[39m %s\n", name, scanner.Text())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
func callNix(program string, name string, args []string) ([]byte, error) {
|
|
|
|
cmd := exec.Command(program, args...)
|
2019-09-10 12:13:10 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
outpipe, err := cmd.StdoutPipe()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2019-09-10 12:13:10 +02:00
|
|
|
}
|
2019-08-14 18:20:01 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
errpipe, err := cmd.StderrPipe()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
go logNix(name, errpipe)
|
2019-08-14 18:20:01 +02:00
|
|
|
|
2019-10-02 00:27:26 +02:00
|
|
|
if err = cmd.Start(); err != nil {
|
|
|
|
log.Printf("Error starting %s: %s\n", program, err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
log.Printf("Invoked Nix build (%s) for '%s'\n", program, name)
|
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
stdout, _ := ioutil.ReadAll(outpipe)
|
2019-09-08 22:53:22 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
if err = cmd.Wait(); err != nil {
|
|
|
|
log.Printf("%s execution error: %s\nstdout: %s\n", program, err, stdout)
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-08-14 18:20:01 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
resultFile := strings.TrimSpace(string(stdout))
|
|
|
|
buildOutput, err := ioutil.ReadFile(resultFile)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-09-21 13:15:38 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
return buildOutput, nil
|
|
|
|
}
|
2019-08-14 18:20:01 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
// Call out to Nix and request metadata for the image to be built. All
|
|
|
|
// required store paths for the image will be realised, but layers
|
|
|
|
// will not yet be created from them.
|
|
|
|
//
|
|
|
|
// This function is only invoked if the manifest is not found in any
|
|
|
|
// cache.
|
|
|
|
func prepareImage(s *State, image *Image) (*ImageResult, error) {
|
|
|
|
packages, err := json.Marshal(image.Packages)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-08-14 18:20:01 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
srcType, srcArgs := s.Cfg.Pkgs.Render(image.Tag)
|
2019-08-14 18:20:01 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
args := []string{
|
|
|
|
"--timeout", s.Cfg.Timeout,
|
|
|
|
"--argstr", "packages", string(packages),
|
|
|
|
"--argstr", "srcType", srcType,
|
|
|
|
"--argstr", "srcArgs", srcArgs,
|
|
|
|
}
|
2019-08-14 18:20:01 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
output, err := callNix("nixery-build-image", image.Name, args)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("failed to call nixery-build-image: %s\n", err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
log.Printf("Finished image preparation for '%s' via Nix\n", image.Name)
|
2019-08-14 18:20:01 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
var result ImageResult
|
|
|
|
err = json.Unmarshal(output, &result)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-08-14 21:02:52 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
return &result, nil
|
|
|
|
}
|
2019-08-14 21:02:52 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
// Groups layers and checks whether they are present in the cache
|
|
|
|
// already, otherwise calls out to Nix to assemble layers.
|
|
|
|
//
|
2019-10-03 13:12:06 +02:00
|
|
|
// Newly built layers are uploaded to the bucket. Cache entries are
|
|
|
|
// added only after successful uploads, which guarantees that entries
|
|
|
|
// retrieved from the cache are present in the bucket.
|
|
|
|
func prepareLayers(ctx context.Context, s *State, image *Image, result *ImageResult) ([]manifest.Entry, error) {
|
|
|
|
grouped := layers.Group(&result.Graph, &s.Pop, LayerBudget)
|
|
|
|
|
|
|
|
var entries []manifest.Entry
|
|
|
|
|
|
|
|
// Splits the layers into those which are already present in
|
2019-10-03 19:18:52 +02:00
|
|
|
// the cache, and those that are missing.
|
|
|
|
//
|
|
|
|
// Missing layers are built and uploaded to the storage
|
|
|
|
// bucket.
|
2019-10-03 13:12:06 +02:00
|
|
|
for _, l := range grouped {
|
|
|
|
if entry, cached := layerFromCache(ctx, s, l.Hash()); cached {
|
|
|
|
entries = append(entries, *entry)
|
|
|
|
} else {
|
2019-10-03 19:18:52 +02:00
|
|
|
lw := func(w io.Writer) error {
|
|
|
|
return tarStorePaths(&l, w)
|
|
|
|
}
|
2019-10-03 13:12:06 +02:00
|
|
|
|
2019-10-03 19:18:52 +02:00
|
|
|
entry, err := uploadHashLayer(ctx, s, l.Hash(), lw)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-10-03 13:12:06 +02:00
|
|
|
|
2019-10-03 19:18:52 +02:00
|
|
|
go cacheLayer(ctx, s, l.Hash(), *entry)
|
|
|
|
entries = append(entries, *entry)
|
2019-10-03 13:12:06 +02:00
|
|
|
}
|
2019-09-30 18:40:01 +02:00
|
|
|
}
|
2019-09-10 12:13:10 +02:00
|
|
|
|
2019-10-03 19:18:52 +02:00
|
|
|
// Symlink layer (built in the first Nix build) needs to be
|
|
|
|
// included here manually:
|
|
|
|
slkey := result.SymlinkLayer.SHA256
|
|
|
|
entry, err := uploadHashLayer(ctx, s, slkey, func(w io.Writer) error {
|
|
|
|
f, err := os.Open(result.SymlinkLayer.Path)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("failed to upload symlink layer '%s': %s\n", slkey, err)
|
|
|
|
return err
|
2019-09-10 12:13:10 +02:00
|
|
|
}
|
2019-10-03 19:18:52 +02:00
|
|
|
defer f.Close()
|
2019-10-02 00:27:26 +02:00
|
|
|
|
2019-10-03 19:18:52 +02:00
|
|
|
_, err = io.Copy(w, f)
|
|
|
|
return err
|
|
|
|
})
|
2019-09-30 18:40:01 +02:00
|
|
|
|
2019-08-14 18:20:01 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-10-03 19:18:52 +02:00
|
|
|
go cacheLayer(ctx, s, slkey, *entry)
|
|
|
|
entries = append(entries, *entry)
|
2019-09-30 18:40:01 +02:00
|
|
|
|
2019-10-03 19:18:52 +02:00
|
|
|
return entries, nil
|
2019-09-30 18:40:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// renameObject renames an object in the specified Cloud Storage
|
|
|
|
// bucket.
|
|
|
|
//
|
|
|
|
// The Go API for Cloud Storage does not support renaming objects, but
|
|
|
|
// the HTTP API does. The code below makes the relevant call manually.
|
|
|
|
func renameObject(ctx context.Context, s *State, old, new string) error {
|
|
|
|
bucket := s.Cfg.Bucket
|
|
|
|
|
2019-10-02 16:19:28 +02:00
|
|
|
creds, err := google.FindDefaultCredentials(ctx, gcsScope)
|
2019-09-30 18:40:01 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
token, err := creds.TokenSource.Token()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// as per https://cloud.google.com/storage/docs/renaming-copying-moving-objects#rename
|
|
|
|
url := fmt.Sprintf(
|
|
|
|
"https://www.googleapis.com/storage/v1/b/%s/o/%s/rewriteTo/b/%s/o/%s",
|
|
|
|
url.PathEscape(bucket), url.PathEscape(old),
|
|
|
|
url.PathEscape(bucket), url.PathEscape(new),
|
|
|
|
)
|
2019-08-14 21:02:52 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
req, err := http.NewRequest("POST", url, nil)
|
|
|
|
req.Header.Add("Authorization", "Bearer "+token.AccessToken)
|
|
|
|
_, err = client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// It seems that 'rewriteTo' copies objects instead of
|
|
|
|
// renaming/moving them, hence a deletion call afterwards is
|
|
|
|
// required.
|
|
|
|
if err = s.Bucket.Object(old).Delete(ctx); err != nil {
|
|
|
|
log.Printf("failed to delete renamed object '%s': %s\n", old, err)
|
|
|
|
// this error should not break renaming and is not returned
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-10-03 19:18:52 +02:00
|
|
|
// layerWriter is the type for functions that can write a layer to the
|
|
|
|
// multiwriter used for uploading & hashing.
|
|
|
|
//
|
|
|
|
// This type exists to avoid duplication between the handling of
|
|
|
|
// symlink layers and store path layers.
|
|
|
|
type layerWriter func(w io.Writer) error
|
|
|
|
|
|
|
|
// byteCounter is a special io.Writer that counts all bytes written to
|
|
|
|
// it and does nothing else.
|
|
|
|
//
|
|
|
|
// This is required because the ad-hoc writing of tarballs leaves no
|
|
|
|
// single place to count the final tarball size otherwise.
|
|
|
|
type byteCounter struct {
|
|
|
|
count int64
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *byteCounter) Write(p []byte) (n int, err error) {
|
|
|
|
b.count += int64(len(p))
|
|
|
|
return len(p), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Upload a layer tarball to the storage bucket, while hashing it at
|
|
|
|
// the same time. The supplied function is expected to provide the
|
|
|
|
// layer data to the writer.
|
2019-09-30 18:40:01 +02:00
|
|
|
//
|
|
|
|
// The initial upload is performed in a 'staging' folder, as the
|
|
|
|
// SHA256-hash is not yet available when the upload is initiated.
|
|
|
|
//
|
|
|
|
// After a successful upload, the file is moved to its final location
|
|
|
|
// in the bucket and the build cache is populated.
|
|
|
|
//
|
|
|
|
// The return value is the layer's SHA256 hash, which is used in the
|
|
|
|
// image manifest.
|
2019-10-03 19:18:52 +02:00
|
|
|
func uploadHashLayer(ctx context.Context, s *State, key string, lw layerWriter) (*manifest.Entry, error) {
|
2019-09-30 18:40:01 +02:00
|
|
|
staging := s.Bucket.Object("staging/" + key)
|
|
|
|
|
2019-10-02 00:27:26 +02:00
|
|
|
// Sets up a "multiwriter" that simultaneously runs both hash
|
2019-09-30 18:40:01 +02:00
|
|
|
// algorithms and uploads to the bucket
|
|
|
|
sw := staging.NewWriter(ctx)
|
|
|
|
shasum := sha256.New()
|
2019-10-03 19:18:52 +02:00
|
|
|
counter := &byteCounter{}
|
|
|
|
multi := io.MultiWriter(sw, shasum, counter)
|
2019-09-30 18:40:01 +02:00
|
|
|
|
2019-10-03 19:18:52 +02:00
|
|
|
err := lw(multi)
|
2019-09-30 18:40:01 +02:00
|
|
|
if err != nil {
|
2019-10-03 19:18:52 +02:00
|
|
|
log.Printf("failed to create and upload layer '%s': %s\n", key, err)
|
2019-10-02 00:27:26 +02:00
|
|
|
return nil, err
|
2019-09-30 18:40:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if err = sw.Close(); err != nil {
|
|
|
|
log.Printf("failed to upload layer '%s' to staging: %s\n", key, err)
|
|
|
|
}
|
|
|
|
|
2019-10-03 13:11:46 +02:00
|
|
|
sha256sum := fmt.Sprintf("%x", shasum.Sum([]byte{}))
|
2019-09-30 18:40:01 +02:00
|
|
|
|
|
|
|
// Hashes are now known and the object is in the bucket, what
|
|
|
|
// remains is to move it to the correct location and cache it.
|
2019-10-03 13:11:46 +02:00
|
|
|
err = renameObject(ctx, s, "staging/"+key, "layers/"+sha256sum)
|
2019-09-30 18:40:01 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Printf("failed to move layer '%s' from staging: %s\n", key, err)
|
2019-10-02 00:27:26 +02:00
|
|
|
return nil, err
|
2019-09-30 18:40:01 +02:00
|
|
|
}
|
|
|
|
|
2019-10-03 19:18:52 +02:00
|
|
|
size := counter.count
|
2019-10-03 13:11:46 +02:00
|
|
|
log.Printf("Uploaded layer sha256:%s (%v bytes written)", sha256sum, size)
|
2019-09-30 18:40:01 +02:00
|
|
|
|
2019-10-03 13:11:46 +02:00
|
|
|
entry := manifest.Entry{
|
|
|
|
Digest: "sha256:" + sha256sum,
|
2019-10-02 00:27:26 +02:00
|
|
|
Size: size,
|
2019-10-03 13:11:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return &entry, nil
|
2019-09-30 18:40:01 +02:00
|
|
|
}
|
|
|
|
|
2019-10-02 00:27:26 +02:00
|
|
|
func BuildImage(ctx context.Context, s *State, image *Image) (*BuildResult, error) {
|
2019-10-03 12:23:04 +02:00
|
|
|
key := s.Cfg.Pkgs.CacheKey(image.Packages, image.Tag)
|
|
|
|
if key != "" {
|
|
|
|
if m, c := manifestFromCache(ctx, s, key); c {
|
|
|
|
return &BuildResult{
|
|
|
|
Manifest: m,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
}
|
2019-10-02 00:27:26 +02:00
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
imageResult, err := prepareImage(s, image)
|
2019-08-14 18:20:01 +02:00
|
|
|
if err != nil {
|
2019-10-02 00:27:26 +02:00
|
|
|
return nil, fmt.Errorf("failed to prepare image '%s': %s", image.Name, err)
|
2019-08-14 18:20:01 +02:00
|
|
|
}
|
|
|
|
|
2019-09-30 18:40:01 +02:00
|
|
|
if imageResult.Error != "" {
|
|
|
|
return &BuildResult{
|
|
|
|
Error: imageResult.Error,
|
|
|
|
Pkgs: imageResult.Pkgs,
|
|
|
|
}, nil
|
|
|
|
}
|
2019-08-14 21:02:52 +02:00
|
|
|
|
2019-10-03 13:12:06 +02:00
|
|
|
layers, err := prepareLayers(ctx, s, image, imageResult)
|
2019-09-30 18:40:01 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2019-08-14 18:20:01 +02:00
|
|
|
}
|
|
|
|
|
2019-10-02 00:27:26 +02:00
|
|
|
m, c := manifest.Manifest(layers)
|
2019-10-03 19:18:52 +02:00
|
|
|
|
|
|
|
lw := func(w io.Writer) error {
|
|
|
|
r := bytes.NewReader(c.Config)
|
|
|
|
_, err := io.Copy(w, r)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err = uploadHashLayer(ctx, s, c.SHA256, lw); err != nil {
|
2019-10-02 00:27:26 +02:00
|
|
|
log.Printf("failed to upload config for %s: %s\n", image.Name, err)
|
|
|
|
return nil, err
|
2019-08-14 18:20:01 +02:00
|
|
|
}
|
|
|
|
|
2019-10-03 12:23:04 +02:00
|
|
|
if key != "" {
|
|
|
|
go cacheManifest(ctx, s, key, m)
|
|
|
|
}
|
|
|
|
|
2019-10-02 00:27:26 +02:00
|
|
|
result := BuildResult{
|
|
|
|
Manifest: m,
|
|
|
|
}
|
|
|
|
return &result, nil
|
2019-08-14 18:20:01 +02:00
|
|
|
}
|