blob: a4a4cd9d9185a4e7c8e4850988b593df915b5aee [file] [log] [blame] [edit]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package packages
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/avb"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/ffx"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/util"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/zbi"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
)
type BlobStore interface {
Dir() string
OpenBlob(ctx context.Context, merkle string) (*os.File, error)
}
type DirBlobStore struct {
dir string
}
func NewDirBlobStore(dir string) BlobStore {
return &DirBlobStore{dir}
}
func (fs *DirBlobStore) OpenBlob(ctx context.Context, merkle string) (*os.File, error) {
return os.Open(filepath.Join(fs.dir, merkle))
}
func (fs *DirBlobStore) Dir() string {
return fs.dir
}
type Repository struct {
Dir string
// BlobsDir should be a directory called `blobs` where all the blobs are.
BlobStore BlobStore
ffx *ffx.FFXTool
deliveryBlobType *int
}
type signed struct {
Signed targets `json:"signed"`
}
type targets struct {
Targets map[string]targetFile `json:"targets"`
}
type targetFile struct {
Custom custom `json:"custom"`
}
type custom struct {
Merkle string `json:"merkle"`
}
// NewRepository parses the repository from the specified directory. It returns
// an error if the repository does not exist, or it contains malformed metadata.
func NewRepository(ctx context.Context, dir string, blobStore BlobStore, ffx *ffx.FFXTool, deliveryBlobType *int) (*Repository, error) {
logger.Infof(ctx, "creating a repository for %q and %q", dir, blobStore.Dir())
// The repository may have out of date metadata. This updates the repository to
// the latest version so TUF won't complain about the data being old.
if err := ffx.RepositoryPublish(ctx, dir, []string{}, "--refresh-root"); err != nil {
return nil, err
}
return &Repository{
Dir: filepath.Join(dir, "repository"),
BlobStore: blobStore,
ffx: ffx,
deliveryBlobType: deliveryBlobType,
}, nil
}
// NewRepositoryFromTar extracts a repository from a tar.gz, and returns a
// Repository parsed from it. It returns an error if the repository does not
// exist, or contains malformed metadata.
func NewRepositoryFromTar(ctx context.Context, dst string, src string, ffx *ffx.FFXTool, deliveryBlobType *int) (*Repository, error) {
if err := util.Untar(ctx, dst, src); err != nil {
return nil, fmt.Errorf("failed to extract packages: %w", err)
}
return NewRepository(ctx, filepath.Join(dst, "amber-files"), NewDirBlobStore(filepath.Join(dst, "amber-files", "repository", "blobs")), ffx, deliveryBlobType)
}
// OpenPackage opens a package from the repository.
func (r *Repository) OpenPackage(ctx context.Context, path string) (Package, error) {
// Parse the targets file so we can access packages locally.
f, err := os.Open(filepath.Join(r.Dir, "targets.json"))
if err != nil {
return Package{}, err
}
defer f.Close()
var s signed
if err = json.NewDecoder(f).Decode(&s); err != nil {
return Package{}, err
}
if target, ok := s.Signed.Targets[path]; ok {
return newPackage(ctx, r, target.Custom.Merkle)
}
return Package{}, fmt.Errorf("could not find package: %q", path)
}
func (r *Repository) OpenBlob(ctx context.Context, merkle string) (*os.File, error) {
return r.BlobStore.OpenBlob(ctx, merkle)
}
func (r *Repository) Serve(ctx context.Context, localHostname string, repoName string, repoPort int) (*Server, error) {
return newServer(ctx, r.Dir, r.BlobStore, localHostname, repoName, repoPort)
}
func (r *Repository) LookupUpdateSystemImageMerkle(ctx context.Context) (string, error) {
return r.lookupUpdateContentPackageMerkle(ctx, "update/0", "system_image/0")
}
func (r *Repository) LookupUpdatePrimeSystemImage2Merkle(ctx context.Context) (string, error) {
return r.lookupUpdateContentPackageMerkle(ctx, "update_prime2/0", "system_image/0")
}
func (r *Repository) VerifyMatchesAnyUpdateSystemImageMerkle(ctx context.Context, merkle string) error {
systemImageMerkle, err := r.LookupUpdateSystemImageMerkle(ctx)
if err != nil {
return err
}
if merkle == systemImageMerkle {
return nil
}
systemPrimeImage2Merkle, err := r.LookupUpdatePrimeSystemImage2Merkle(ctx)
if err != nil {
return err
}
if merkle == systemPrimeImage2Merkle {
return nil
}
return fmt.Errorf("expected device to be running a system image of %s or %s, got %s",
systemImageMerkle, systemPrimeImage2Merkle, merkle)
}
func (r *Repository) lookupUpdateContentPackageMerkle(ctx context.Context, updatePackageName string, contentPackageName string) (string, error) {
// Extract the "packages" file from the "update" package.
p, err := r.OpenPackage(ctx, updatePackageName)
if err != nil {
return "", err
}
f, err := p.Open(ctx, "packages.json")
if err != nil {
return "", err
}
packages, err := util.ParsePackagesJSON(f)
if err != nil {
return "", err
}
merkle, ok := packages[contentPackageName]
if !ok {
return "", fmt.Errorf("could not find %s merkle", contentPackageName)
}
return merkle, nil
}
// CreatePackage creates a package in this repository named `packagePath` by:
// * creating a temporary directory
// * passing it to the `createFunc` closure. The closure then adds any necessary files.
// * creating a package from the directory contents.
// * publishing the package to the repository with the `packagePath` path.
func (r *Repository) CreatePackage(
ctx context.Context,
packagePath string,
createFunc func(path string) error,
) (string, error) {
logger.Infof(ctx, "creating package %q", packagePath)
// Extract the package name from the path. The variant currently is optional, but if specified, must be "0".
packageName, packageVariant, found := strings.Cut(packagePath, "/")
if found && packageVariant != "0" {
return "", fmt.Errorf("invalid package path found: %q", packagePath)
}
packageVariant = "0"
// Create temp directory. The content of this directory will be included in the package.
tempDir, err := os.MkdirTemp("", "")
if err != nil {
return "", fmt.Errorf("failed to create a temp directory: %w", err)
}
defer os.RemoveAll(tempDir)
// Package content will be created by the user by leveraging the createFunc closure.
if err := createFunc(tempDir); err != nil {
return "", fmt.Errorf("failed to create content of the package: %w", err)
}
// Create package from the temp directory. The package builder doesn't use
// the repository name, so it can be set as `testrepository.com`.
pkgBuilder, err := NewPackageBuilderFromDir(tempDir, packageName, packageVariant, "testrepository.com")
if err != nil {
return "", fmt.Errorf("failed to parse the package from %q: %w", tempDir, err)
}
// Publish the package and ger the merkle of the package.
_, pkgMerkle, err := pkgBuilder.Publish(ctx, r)
if err != nil {
return "", fmt.Errorf("failed to publish the package %q: %w", packagePath, err)
}
return pkgMerkle, nil
}
// EditPackage takes the content of the source package from srcPackagePath,
// copies the content to destination package at dstPackagePath and edits the
// content at destination with the help of editFunc closure.
func (r *Repository) EditPackage(
ctx context.Context,
srcPackagePath string,
dstPackagePath string,
editFunc func(path string) error,
) (Package, error) {
logger.Infof(ctx, "editing package %q. will create %q", srcPackagePath, dstPackagePath)
// First get the source package located at srcPackagePath
pkg, err := r.OpenPackage(ctx, srcPackagePath)
if err != nil {
return Package{}, fmt.Errorf("failed to open the package %q: %w", srcPackagePath, err)
}
// Next create a destination package based on the content oft the source package.
pkgMerkle, err := r.CreatePackage(ctx, dstPackagePath, func(tempDir string) error {
if err := pkg.Expand(ctx, tempDir); err != nil {
return fmt.Errorf("failed to expand the package to %s: %w", tempDir, err)
}
// User can edit the content and return it.
return editFunc(tempDir)
})
if err != nil {
return Package{}, fmt.Errorf("failed to create the package %q: %w", dstPackagePath, err)
}
// Get the newly edited package located at pkgMerkle and return it.
pkg, err = newPackage(ctx, r, pkgMerkle)
if err != nil {
return Package{}, fmt.Errorf("failed to edit the package %q: %w", pkgMerkle, err)
}
return pkg, nil
}
// Extracts the update package into a temporary directory, and injects the
// specified vbmeta property files into the vbmeta.
func (r *Repository) EditUpdatePackageWithVBMetaProperties(
ctx context.Context,
avbTool *avb.AVBTool,
srcUpdatePackage string,
dstUpdatePackage string,
repoName string,
vbmetaPropertyFiles map[string]string,
editFunc func(path string) error) (Package, error) {
return r.EditPackage(ctx, srcUpdatePackage, dstUpdatePackage, func(tempDir string) error {
if err := editFunc(tempDir); err != nil {
return err
}
packagesJsonPath := filepath.Join(tempDir, "packages.json")
logger.Infof(ctx, "setting host name in %q to %q", packagesJsonPath, repoName)
err := util.AtomicallyWriteFile(packagesJsonPath, 0600, func(f *os.File) error {
src, err := os.Open(packagesJsonPath)
if err != nil {
return fmt.Errorf("failed to open packages.json %q: %w", packagesJsonPath, err)
}
if err := util.RehostPackagesJSON(bufio.NewReader(src), bufio.NewWriter(f), repoName); err != nil {
return fmt.Errorf("failed to rehost package.json: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("failed to atomically overwrite %q: %w", packagesJsonPath, err)
}
srcVbmetaPath := filepath.Join(tempDir, "fuchsia.vbmeta")
if _, err := os.Stat(srcVbmetaPath); err != nil {
return fmt.Errorf("vbmeta %q does not exist in repo: %w", srcVbmetaPath, err)
}
logger.Infof(ctx, "updating vbmeta %q", srcVbmetaPath)
err = util.AtomicallyWriteFile(srcVbmetaPath, 0600, func(f *os.File) error {
if err := avbTool.MakeVBMetaImage(ctx, f.Name(), srcVbmetaPath, vbmetaPropertyFiles); err != nil {
return fmt.Errorf("failed to update vbmeta: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("failed to atomically overwrite %q: %w", srcVbmetaPath, err)
}
return nil
})
}
// Extract the update package `srcUpdatePackage` into a temporary directory,
// then build and publish it to the repository as the `dstUpdatePackage` name.
// It will automatically rewrite the `packages.json` file to use `repoName`
// path, to avoid collisions with the `fuchsia.com` repository name.
func (r *Repository) EditUpdatePackage(
ctx context.Context,
avbTool *avb.AVBTool,
zbiTool *zbi.ZBITool,
srcUpdatePackage string,
dstUpdatePackage string,
repoName string,
editFunc func(path string) error,
) (Package, error) {
vbmetaPropertyFiles := map[string]string{}
return r.EditUpdatePackageWithVBMetaProperties(
ctx,
avbTool,
srcUpdatePackage,
dstUpdatePackage,
repoName,
vbmetaPropertyFiles,
func(path string) error {
return editFunc(path)
})
}
func (r *Repository) EditUpdatePackageWithNewSystemImageMerkle(
ctx context.Context,
avbTool *avb.AVBTool,
zbiTool *zbi.ZBITool,
systemImageMerkle string,
srcUpdatePackagePath string,
dstUpdatePackagePath string,
bootfsCompression string,
editFunc func(path string) error,
) (Package, error) {
repoName := "fuchsia.com"
return r.EditUpdatePackage(ctx,
avbTool, zbiTool,
srcUpdatePackagePath,
dstUpdatePackagePath,
repoName,
func(tempDir string) error {
if err := zbiTool.UpdateZBIWithNewSystemImageMerkle(ctx,
systemImageMerkle,
tempDir,
bootfsCompression,
); err != nil {
return err
}
pathToZbi := filepath.Join(tempDir, "zbi")
vbmetaPath := filepath.Join(tempDir, "fuchsia.vbmeta")
if err := avbTool.MakeVBMetaImageWithZbi(ctx, vbmetaPath, vbmetaPath, pathToZbi); err != nil {
return err
}
packagesJsonPath := filepath.Join(tempDir, "packages.json")
err := util.AtomicallyWriteFile(packagesJsonPath, 0600, func(f *os.File) error {
src, err := os.Open(packagesJsonPath)
if err != nil {
return fmt.Errorf("failed to open packages.json %q: %w", packagesJsonPath, err)
}
if err := util.UpdateHashValuePackagesJSON(
bufio.NewReader(src),
bufio.NewWriter(f),
repoName,
"system_image/0",
systemImageMerkle,
); err != nil {
return fmt.Errorf("failed to update system_image_merkle in package.json: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("failed to atomically overwrite %q: %w", packagesJsonPath, err)
}
return editFunc(tempDir)
})
}
func (r *Repository) Publish(ctx context.Context, packageManifestPath string) error {
repoDir := filepath.Dir(r.Dir)
extraArgs := []string{"--blob-repo-dir", r.BlobStore.Dir()}
if r.deliveryBlobType != nil {
extraArgs = append(extraArgs, "--delivery-blob-type", fmt.Sprint(*r.deliveryBlobType))
}
return r.ffx.RepositoryPublish(ctx, repoDir, []string{packageManifestPath}, extraArgs...)
}