mirror of
https://github.com/containers/podman.git
synced 2026-07-14 09:11:49 -04:00
Add podman farm build command that sends out builds to nodes defined in the farm, builds the images on the farm nodes, and pulls them back to the local machine to create a manifest list. Signed-off-by: Urvashi Mohnani <umohnani@redhat.com>
298 lines
8.6 KiB
Go
298 lines
8.6 KiB
Go
package farm
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
lmanifests "github.com/containers/common/libimage/manifests"
|
|
"github.com/containers/common/pkg/supplemented"
|
|
cp "github.com/containers/image/v5/copy"
|
|
"github.com/containers/image/v5/manifest"
|
|
"github.com/containers/image/v5/signature"
|
|
"github.com/containers/image/v5/transports/alltransports"
|
|
"github.com/containers/image/v5/types"
|
|
"github.com/containers/podman/v4/pkg/domain/entities"
|
|
"github.com/hashicorp/go-multierror"
|
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type listBuilder interface {
|
|
build(ctx context.Context, images map[entities.BuildReport]entities.ImageEngine) (string, error)
|
|
}
|
|
|
|
type listBuilderOptions struct {
|
|
cleanup bool
|
|
iidFile string
|
|
}
|
|
|
|
type listLocal struct {
|
|
listName string
|
|
localEngine entities.ImageEngine
|
|
options listBuilderOptions
|
|
}
|
|
|
|
// newLocalManifestListBuilder returns a manifest list builder which saves a
|
|
// manifest list and images to local storage.
|
|
func newLocalManifestListBuilder(listName string, localEngine entities.ImageEngine, options listBuilderOptions) listBuilder {
|
|
return &listLocal{
|
|
listName: listName,
|
|
options: options,
|
|
localEngine: localEngine,
|
|
}
|
|
}
|
|
|
|
// Build retrieves images from the build reports and assembles them into a
|
|
// manifest list in local container storage.
|
|
func (l *listLocal) build(ctx context.Context, images map[entities.BuildReport]entities.ImageEngine) (string, error) {
|
|
manifest := l.listName
|
|
exists, err := l.localEngine.ManifestExists(ctx, l.listName)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// Create list if it doesn't exist
|
|
if !exists.Value {
|
|
manifest, err = l.localEngine.ManifestCreate(ctx, l.listName, []string{}, entities.ManifestCreateOptions{})
|
|
if err != nil {
|
|
return "", fmt.Errorf("creating manifest list %q: %w", l.listName, err)
|
|
}
|
|
}
|
|
|
|
// Pull the images into local storage
|
|
var (
|
|
pullGroup multierror.Group
|
|
refsMutex sync.Mutex
|
|
)
|
|
refs := []string{}
|
|
for image, engine := range images {
|
|
image, engine := image, engine
|
|
pullOptions := entities.PullToLocalOptions{
|
|
ImageID: image.ID,
|
|
SaveFormat: image.SaveFormat,
|
|
Destination: l.localEngine,
|
|
}
|
|
pullGroup.Go(func() error {
|
|
logrus.Infof("copying image %s", image.ID)
|
|
defer logrus.Infof("copied image %s", image.ID)
|
|
ref, err := engine.PullToLocal(ctx, pullOptions)
|
|
if err != nil {
|
|
return fmt.Errorf("pulling image %q to local storage: %w", image, err)
|
|
}
|
|
refsMutex.Lock()
|
|
defer refsMutex.Unlock()
|
|
refs = append(refs, ref)
|
|
return nil
|
|
})
|
|
}
|
|
pullErrors := pullGroup.Wait()
|
|
err = pullErrors.ErrorOrNil()
|
|
if err != nil {
|
|
return "", fmt.Errorf("building: %w", err)
|
|
}
|
|
|
|
if l.options.cleanup {
|
|
var rmGroup multierror.Group
|
|
for image, engine := range images {
|
|
if engine.FarmNodeName(ctx) == entities.LocalFarmImageBuilderName {
|
|
continue
|
|
}
|
|
image, engine := image, engine
|
|
rmGroup.Go(func() error {
|
|
_, err := engine.Remove(ctx, []string{image.ID}, entities.ImageRemoveOptions{})
|
|
if len(err) > 0 {
|
|
return err[0]
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
rmErrors := rmGroup.Wait()
|
|
if rmErrors != nil {
|
|
if err = rmErrors.ErrorOrNil(); err != nil {
|
|
return "", fmt.Errorf("removing intermediate images: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear the list in the event it already existed
|
|
if exists.Value {
|
|
_, err = l.localEngine.ManifestListClear(ctx, manifest)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error clearing list %q", manifest)
|
|
}
|
|
}
|
|
|
|
// Add the images to the list
|
|
listID, err := l.localEngine.ManifestAdd(ctx, manifest, refs, entities.ManifestAddOptions{})
|
|
if err != nil {
|
|
return "", fmt.Errorf("adding images %q to list: %w", refs, err)
|
|
}
|
|
|
|
// Write the manifest list's ID file if we're expected to
|
|
if l.options.iidFile != "" {
|
|
if err := os.WriteFile(l.options.iidFile, []byte("sha256:"+listID), 0644); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
return l.listName, nil
|
|
}
|
|
|
|
type listFiles struct {
|
|
directory string
|
|
options listBuilderOptions
|
|
}
|
|
|
|
// newFileManifestListBuilder returns a manifest list builder which saves a manifest
|
|
// list and images to a specified directory in the non-standard dir: format.
|
|
func newFileManifestListBuilder(directory string, options listBuilderOptions) (listBuilder, error) {
|
|
if options.iidFile != "" {
|
|
return nil, fmt.Errorf("saving to dir: format doesn't use image IDs, --iidfile not supported")
|
|
}
|
|
return &listFiles{directory: directory, options: options}, nil
|
|
}
|
|
|
|
// Build retrieves images from the build reports and assembles them into a
|
|
// manifest list in the configured directory.
|
|
func (m *listFiles) build(ctx context.Context, images map[entities.BuildReport]entities.ImageEngine) (string, error) {
|
|
listFormat := v1.MediaTypeImageIndex
|
|
imageFormat := v1.MediaTypeImageManifest
|
|
|
|
tempDir, err := os.MkdirTemp("", "")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
name := fmt.Sprintf("dir:%s", tempDir)
|
|
tempRef, err := alltransports.ParseImageName(name)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parsing temporary image ref %q: %w", name, err)
|
|
}
|
|
if err := os.MkdirAll(m.directory, 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
output, err := alltransports.ParseImageName("dir:" + m.directory)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parsing output directory ref %q: %w", "dir:"+m.directory, err)
|
|
}
|
|
|
|
// Pull the images into the temporary directory
|
|
var (
|
|
pullGroup multierror.Group
|
|
pullErrors *multierror.Error
|
|
refsMutex sync.Mutex
|
|
)
|
|
refs := make(map[entities.BuildReport]types.ImageReference)
|
|
for image, engine := range images {
|
|
image, engine := image, engine
|
|
tempFile, err := os.CreateTemp(tempDir, "archive-*.tar")
|
|
if err != nil {
|
|
defer func() {
|
|
pullErrors = pullGroup.Wait()
|
|
}()
|
|
perr := pullErrors.ErrorOrNil()
|
|
if perr != nil {
|
|
return "", perr
|
|
}
|
|
return "", err
|
|
}
|
|
defer tempFile.Close()
|
|
|
|
pullGroup.Go(func() error {
|
|
logrus.Infof("copying image %s", image.ID)
|
|
defer logrus.Infof("copied image %s", image.ID)
|
|
pullOptions := entities.PullToFileOptions{
|
|
ImageID: image.ID,
|
|
SaveFormat: image.SaveFormat,
|
|
SaveFile: tempFile.Name(),
|
|
}
|
|
if image.SaveFormat == manifest.DockerV2Schema2MediaType {
|
|
listFormat = manifest.DockerV2ListMediaType
|
|
imageFormat = manifest.DockerV2Schema2MediaType
|
|
}
|
|
reference, err := engine.PullToFile(ctx, pullOptions)
|
|
if err != nil {
|
|
return fmt.Errorf("pulling image %q to temporary directory: %w", image, err)
|
|
}
|
|
ref, err := alltransports.ParseImageName(reference)
|
|
if err != nil {
|
|
return fmt.Errorf("pulling image %q to temporary directory: %w", image, err)
|
|
}
|
|
refsMutex.Lock()
|
|
defer refsMutex.Unlock()
|
|
refs[image] = ref
|
|
return nil
|
|
})
|
|
}
|
|
pullErrors = pullGroup.Wait()
|
|
err = pullErrors.ErrorOrNil()
|
|
if err != nil {
|
|
return "", fmt.Errorf("building: %w", err)
|
|
}
|
|
|
|
if m.options.cleanup {
|
|
var rmGroup multierror.Group
|
|
for image, engine := range images {
|
|
image, engine := image, engine
|
|
rmGroup.Go(func() error {
|
|
_, err := engine.Remove(ctx, []string{image.ID}, entities.ImageRemoveOptions{})
|
|
if len(err) > 0 {
|
|
return err[0]
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
rmErrors := rmGroup.Wait()
|
|
if rmErrors != nil {
|
|
if err = rmErrors.ErrorOrNil(); err != nil {
|
|
return "", fmt.Errorf("removing intermediate images: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
supplemental := []types.ImageReference{}
|
|
var sys types.SystemContext
|
|
// Create a manifest list
|
|
list := lmanifests.Create()
|
|
// Add the images to the list
|
|
for image, ref := range refs {
|
|
if _, err = list.Add(ctx, &sys, ref, true); err != nil {
|
|
return "", fmt.Errorf("adding image %q to list: %w", image.ID, err)
|
|
}
|
|
supplemental = append(supplemental, ref)
|
|
}
|
|
// Save the list to the temporary directory to be the main manifest
|
|
listBytes, err := list.Serialize(listFormat)
|
|
if err != nil {
|
|
return "", fmt.Errorf("serializing manifest list: %w", err)
|
|
}
|
|
if err = os.WriteFile(filepath.Join(tempDir, "manifest.json"), listBytes, fs.FileMode(0o600)); err != nil {
|
|
return "", fmt.Errorf("writing temporary manifest list: %w", err)
|
|
}
|
|
|
|
// Now copy everything to the final dir: location
|
|
defaultPolicy, err := signature.DefaultPolicy(&sys)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
policyContext, err := signature.NewPolicyContext(defaultPolicy)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
input := supplemented.Reference(tempRef, supplemental, cp.CopyAllImages, nil)
|
|
copyOptions := cp.Options{
|
|
ForceManifestMIMEType: imageFormat,
|
|
ImageListSelection: cp.CopyAllImages,
|
|
}
|
|
_, err = cp.Image(ctx, policyContext, output, input, ©Options)
|
|
if err != nil {
|
|
return "", fmt.Errorf("copying images to dir:%q: %w", m.directory, err)
|
|
}
|
|
|
|
return "dir:" + m.directory, nil
|
|
}
|