mirror of
https://github.com/containers/podman.git
synced 2026-07-15 01:31:48 -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>
493 lines
14 KiB
Go
493 lines
14 KiB
Go
package farm
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/containers/buildah/define"
|
|
lplatform "github.com/containers/common/libimage/platform"
|
|
"github.com/containers/common/pkg/config"
|
|
"github.com/containers/podman/v4/pkg/domain/entities"
|
|
"github.com/containers/podman/v4/pkg/domain/infra"
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Farm represents a group of connections to builders.
|
|
type Farm struct {
|
|
name string
|
|
localEngine entities.ImageEngine // not nil -> use local engine, too
|
|
builders map[string]entities.ImageEngine // name -> builder
|
|
}
|
|
|
|
// Schedule is a description of where and how we'll do builds.
|
|
type Schedule struct {
|
|
platformBuilders map[string]string // target->connection
|
|
}
|
|
|
|
func newFarmWithBuilders(_ context.Context, name string, destinations *map[string]config.Destination, localEngine entities.ImageEngine) (*Farm, error) {
|
|
farm := &Farm{
|
|
builders: make(map[string]entities.ImageEngine),
|
|
localEngine: localEngine,
|
|
name: name,
|
|
}
|
|
var (
|
|
builderMutex sync.Mutex
|
|
builderGroup multierror.Group
|
|
)
|
|
// Set up the remote connections to handle the builds
|
|
for name, dest := range *destinations {
|
|
name, dest := name, dest
|
|
builderGroup.Go(func() error {
|
|
fmt.Printf("Connecting to %q\n", name)
|
|
engine, err := infra.NewImageEngine(&entities.PodmanConfig{
|
|
EngineMode: entities.TunnelMode,
|
|
URI: dest.URI,
|
|
Identity: dest.Identity,
|
|
MachineMode: dest.IsMachine,
|
|
FarmNodeName: name,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("initializing image engine at %q: %w", dest.URI, err)
|
|
}
|
|
|
|
defer fmt.Printf("Builder %q ready\n", name)
|
|
builderMutex.Lock()
|
|
defer builderMutex.Unlock()
|
|
farm.builders[name] = engine
|
|
return nil
|
|
})
|
|
}
|
|
// If local=true then use the local machine for builds as well
|
|
if localEngine != nil {
|
|
builderGroup.Go(func() error {
|
|
fmt.Println("Setting up local builder")
|
|
defer fmt.Println("Local builder ready")
|
|
builderMutex.Lock()
|
|
defer builderMutex.Unlock()
|
|
farm.builders[entities.LocalFarmImageBuilderName] = localEngine
|
|
return nil
|
|
})
|
|
}
|
|
if builderError := builderGroup.Wait(); builderError != nil {
|
|
if err := builderError.ErrorOrNil(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if len(farm.builders) > 0 {
|
|
defer fmt.Printf("Farm %q ready\n", farm.name)
|
|
return farm, nil
|
|
}
|
|
return nil, errors.New("no builders configured")
|
|
}
|
|
|
|
func NewFarm(ctx context.Context, name string, localEngine entities.ImageEngine) (*Farm, error) {
|
|
// Get the destinations of the connections specified in the farm
|
|
destinations, err := getFarmDestinations(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newFarmWithBuilders(ctx, name, &destinations, localEngine)
|
|
}
|
|
|
|
// Done performs any necessary end-of-process cleanup for the farm's members.
|
|
func (f *Farm) Done(ctx context.Context) error {
|
|
return f.forEach(ctx, func(ctx context.Context, name string, engine entities.ImageEngine) (bool, error) {
|
|
engine.Shutdown(ctx)
|
|
return false, nil
|
|
})
|
|
}
|
|
|
|
// Status polls the connections in the farm and returns a map of their
|
|
// individual status, along with an error if any are down or otherwise unreachable.
|
|
func (f *Farm) Status(ctx context.Context) (map[string]error, error) {
|
|
status := make(map[string]error)
|
|
var (
|
|
statusMutex sync.Mutex
|
|
statusGroup multierror.Group
|
|
)
|
|
for _, engine := range f.builders {
|
|
engine := engine
|
|
statusGroup.Go(func() error {
|
|
logrus.Debugf("getting status of %q", engine.FarmNodeName(ctx))
|
|
defer logrus.Debugf("got status of %q", engine.FarmNodeName(ctx))
|
|
_, err := engine.Config(ctx)
|
|
statusMutex.Lock()
|
|
defer statusMutex.Unlock()
|
|
status[engine.FarmNodeName(ctx)] = err
|
|
return err
|
|
})
|
|
}
|
|
statusError := statusGroup.Wait()
|
|
|
|
return status, statusError.ErrorOrNil()
|
|
}
|
|
|
|
// forEach runs the called function once for every node in the farm and
|
|
// collects their results, continuing until it finishes visiting every node or
|
|
// a function call returns true as its first return value.
|
|
func (f *Farm) forEach(ctx context.Context, fn func(context.Context, string, entities.ImageEngine) (bool, error)) error {
|
|
var merr *multierror.Error
|
|
for name, engine := range f.builders {
|
|
stop, err := fn(ctx, name, engine)
|
|
if err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("%s: %w", engine.FarmNodeName(ctx), err))
|
|
}
|
|
if stop {
|
|
break
|
|
}
|
|
}
|
|
|
|
return merr.ErrorOrNil()
|
|
}
|
|
|
|
// NativePlatforms returns a list of the set of platforms for which the farm
|
|
// can build images natively.
|
|
func (f *Farm) NativePlatforms(ctx context.Context) ([]string, error) {
|
|
nativeMap := make(map[string]struct{})
|
|
platforms := []string{}
|
|
var (
|
|
nativeMutex sync.Mutex
|
|
nativeGroup multierror.Group
|
|
)
|
|
for _, engine := range f.builders {
|
|
engine := engine
|
|
nativeGroup.Go(func() error {
|
|
logrus.Debugf("getting native platform of %q\n", engine.FarmNodeName(ctx))
|
|
defer logrus.Debugf("got native platform of %q", engine.FarmNodeName(ctx))
|
|
inspect, err := engine.FarmNodeInspect(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nativeMutex.Lock()
|
|
defer nativeMutex.Unlock()
|
|
for _, platform := range inspect.NativePlatforms {
|
|
nativeMap[platform] = struct{}{}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
merr := nativeGroup.Wait()
|
|
if merr != nil {
|
|
if err := merr.ErrorOrNil(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for platform := range nativeMap {
|
|
platforms = append(platforms, platform)
|
|
}
|
|
sort.Strings(platforms)
|
|
return platforms, nil
|
|
}
|
|
|
|
// EmulatedPlatforms returns a list of the set of platforms for which the farm
|
|
// can build images with the help of emulation.
|
|
func (f *Farm) EmulatedPlatforms(ctx context.Context) ([]string, error) {
|
|
emulatedMap := make(map[string]struct{})
|
|
platforms := []string{}
|
|
var (
|
|
emulatedMutex sync.Mutex
|
|
emulatedGroup multierror.Group
|
|
)
|
|
for _, engine := range f.builders {
|
|
engine := engine
|
|
emulatedGroup.Go(func() error {
|
|
logrus.Debugf("getting emulated platforms of %q", engine.FarmNodeName(ctx))
|
|
defer logrus.Debugf("got emulated platforms of %q", engine.FarmNodeName(ctx))
|
|
inspect, err := engine.FarmNodeInspect(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
emulatedMutex.Lock()
|
|
defer emulatedMutex.Unlock()
|
|
for _, platform := range inspect.EmulatedPlatforms {
|
|
emulatedMap[platform] = struct{}{}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
merr := emulatedGroup.Wait()
|
|
if merr != nil {
|
|
if err := merr.ErrorOrNil(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for platform := range emulatedMap {
|
|
platforms = append(platforms, platform)
|
|
}
|
|
sort.Strings(platforms)
|
|
return platforms, nil
|
|
}
|
|
|
|
// Schedule takes a list of platforms and returns a list of connections which
|
|
// can be used to build for those platforms. It always prefers native builders
|
|
// over emulated builders, but will assign a builder which can use emulation
|
|
// for a platform if no suitable native builder is available.
|
|
//
|
|
// If platforms is an empty list, all available native platforms will be
|
|
// scheduled.
|
|
//
|
|
// TODO: add (Priority,Weight *int) a la RFC 2782 to destinations that we know
|
|
// of, and factor those in when assigning builds to nodes in here.
|
|
func (f *Farm) Schedule(ctx context.Context, platforms []string) (Schedule, error) {
|
|
var (
|
|
err error
|
|
infoGroup multierror.Group
|
|
infoMutex sync.Mutex
|
|
)
|
|
// If we weren't given a list of target platforms, generate one.
|
|
if len(platforms) == 0 {
|
|
platforms, err = f.NativePlatforms(ctx)
|
|
if err != nil {
|
|
return Schedule{}, fmt.Errorf("reading list of available native platforms: %w", err)
|
|
}
|
|
}
|
|
|
|
platformBuilders := make(map[string]string)
|
|
native := make(map[string]string)
|
|
emulated := make(map[string]string)
|
|
var localPlatform string
|
|
// Make notes of which platforms we can build for natively, and which
|
|
// ones we can build for using emulation.
|
|
for name, engine := range f.builders {
|
|
name, engine := name, engine
|
|
infoGroup.Go(func() error {
|
|
inspect, err := engine.FarmNodeInspect(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
infoMutex.Lock()
|
|
defer infoMutex.Unlock()
|
|
for _, n := range inspect.NativePlatforms {
|
|
if _, assigned := native[n]; !assigned {
|
|
native[n] = name
|
|
}
|
|
if name == entities.LocalFarmImageBuilderName {
|
|
localPlatform = n
|
|
}
|
|
}
|
|
for _, e := range inspect.EmulatedPlatforms {
|
|
if _, assigned := emulated[e]; !assigned {
|
|
emulated[e] = name
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
merr := infoGroup.Wait()
|
|
if merr != nil {
|
|
if err := merr.ErrorOrNil(); err != nil {
|
|
return Schedule{}, err
|
|
}
|
|
}
|
|
// Assign a build to the first node that could build it natively, and
|
|
// if there isn't one, the first one that can build it with the help of
|
|
// emulation, and if there aren't any, error out.
|
|
for _, platform := range platforms {
|
|
if builder, ok := native[platform]; ok {
|
|
platformBuilders[platform] = builder
|
|
} else if builder, ok := emulated[platform]; ok {
|
|
platformBuilders[platform] = builder
|
|
} else {
|
|
return Schedule{}, fmt.Errorf("no builder capable of building for platform %q available", platform)
|
|
}
|
|
}
|
|
// If local is set, prioritize building on local
|
|
if localPlatform != "" {
|
|
platformBuilders[localPlatform] = entities.LocalFarmImageBuilderName
|
|
}
|
|
schedule := Schedule{
|
|
platformBuilders: platformBuilders,
|
|
}
|
|
return schedule, nil
|
|
}
|
|
|
|
// Build runs a build using the specified targetplatform:service map. If all
|
|
// builds succeed, it copies the resulting images from the remote hosts to the
|
|
// local service and builds a manifest list with the specified reference name.
|
|
func (f *Farm) Build(ctx context.Context, schedule Schedule, options entities.BuildOptions, reference string) error {
|
|
switch options.OutputFormat {
|
|
default:
|
|
return fmt.Errorf("unknown output format %q requested", options.OutputFormat)
|
|
case "", define.OCIv1ImageManifest:
|
|
options.OutputFormat = define.OCIv1ImageManifest
|
|
case define.Dockerv2ImageManifest:
|
|
}
|
|
|
|
// Build the list of jobs.
|
|
var jobs sync.Map
|
|
type job struct {
|
|
platform string
|
|
os string
|
|
arch string
|
|
variant string
|
|
builder entities.ImageEngine
|
|
}
|
|
for platform, builderName := range schedule.platformBuilders { // prepare to build
|
|
builder, ok := f.builders[builderName]
|
|
if !ok {
|
|
return fmt.Errorf("unknown builder %q", builderName)
|
|
}
|
|
var rawOS, rawArch, rawVariant string
|
|
p := strings.Split(platform, "/")
|
|
if len(p) > 0 && p[0] != "" {
|
|
rawOS = p[0]
|
|
}
|
|
if len(p) > 1 {
|
|
rawArch = p[1]
|
|
}
|
|
if len(p) > 2 {
|
|
rawVariant = p[2]
|
|
}
|
|
os, arch, variant := lplatform.Normalize(rawOS, rawArch, rawVariant)
|
|
jobs.Store(builderName, job{
|
|
platform: platform,
|
|
os: os,
|
|
arch: arch,
|
|
variant: variant,
|
|
builder: builder,
|
|
})
|
|
}
|
|
|
|
// Decide where the final result will be stored.
|
|
var (
|
|
manifestListBuilder listBuilder
|
|
err error
|
|
)
|
|
listBuilderOptions := listBuilderOptions{
|
|
cleanup: options.Cleanup,
|
|
iidFile: options.IIDFile,
|
|
}
|
|
if strings.HasPrefix(reference, "dir:") || f.localEngine == nil {
|
|
location := strings.TrimPrefix(reference, "dir:")
|
|
manifestListBuilder, err = newFileManifestListBuilder(location, listBuilderOptions)
|
|
if err != nil {
|
|
return fmt.Errorf("preparing to build list: %w", err)
|
|
}
|
|
} else {
|
|
manifestListBuilder = newLocalManifestListBuilder(reference, f.localEngine, listBuilderOptions)
|
|
}
|
|
|
|
// Start builds in parallel and wait for them all to finish.
|
|
var (
|
|
buildResults sync.Map
|
|
buildGroup multierror.Group
|
|
)
|
|
type buildResult struct {
|
|
report entities.BuildReport
|
|
builder entities.ImageEngine
|
|
}
|
|
for platform, builder := range schedule.platformBuilders {
|
|
platform, builder := platform, builder
|
|
outReader, outWriter := io.Pipe()
|
|
errReader, errWriter := io.Pipe()
|
|
go func() {
|
|
defer outReader.Close()
|
|
reader := bufio.NewReader(outReader)
|
|
writer := options.Out
|
|
if writer == nil {
|
|
writer = os.Stdout
|
|
}
|
|
line, err := reader.ReadString('\n')
|
|
for err == nil {
|
|
line = strings.TrimSuffix(line, "\n")
|
|
fmt.Fprintf(writer, "[%s@%s] %s\n", platform, builder, line)
|
|
line, err = reader.ReadString('\n')
|
|
}
|
|
}()
|
|
go func() {
|
|
defer errReader.Close()
|
|
reader := bufio.NewReader(errReader)
|
|
writer := options.Err
|
|
if writer == nil {
|
|
writer = os.Stderr
|
|
}
|
|
line, err := reader.ReadString('\n')
|
|
for err == nil {
|
|
line = strings.TrimSuffix(line, "\n")
|
|
fmt.Fprintf(writer, "[%s@%s] %s\n", platform, builder, line)
|
|
line, err = reader.ReadString('\n')
|
|
}
|
|
}()
|
|
buildGroup.Go(func() error {
|
|
var j job
|
|
defer outWriter.Close()
|
|
defer errWriter.Close()
|
|
c, ok := jobs.Load(builder)
|
|
if !ok {
|
|
return fmt.Errorf("unknown connection for %q (shouldn't happen)", builder)
|
|
}
|
|
if j, ok = c.(job); !ok {
|
|
return fmt.Errorf("unexpected connection type for %q (shouldn't happen)", builder)
|
|
}
|
|
buildOptions := options
|
|
buildOptions.Platforms = []struct{ OS, Arch, Variant string }{{j.os, j.arch, j.variant}}
|
|
buildOptions.Out = outWriter
|
|
buildOptions.Err = errWriter
|
|
fmt.Printf("Starting build for %v at %q\n", buildOptions.Platforms, builder)
|
|
buildReport, err := j.builder.Build(ctx, options.ContainerFiles, buildOptions)
|
|
if err != nil {
|
|
return fmt.Errorf("building for %q on %q: %w", j.platform, builder, err)
|
|
}
|
|
fmt.Printf("finished build for %v at %q: built %s\n", buildOptions.Platforms, builder, buildReport.ID)
|
|
buildResults.Store(platform, buildResult{
|
|
report: *buildReport,
|
|
builder: j.builder,
|
|
})
|
|
return nil
|
|
})
|
|
}
|
|
buildErrors := buildGroup.Wait()
|
|
if err := buildErrors.ErrorOrNil(); err != nil {
|
|
return fmt.Errorf("building: %w", err)
|
|
}
|
|
|
|
// Assemble the final result.
|
|
perArchBuilds := make(map[entities.BuildReport]entities.ImageEngine)
|
|
buildResults.Range(func(k, v any) bool {
|
|
result, ok := v.(buildResult)
|
|
if !ok {
|
|
fmt.Fprintf(os.Stderr, "report %v not a build result?", v)
|
|
return false
|
|
}
|
|
perArchBuilds[result.report] = result.builder
|
|
return true
|
|
})
|
|
location, err := manifestListBuilder.build(ctx, perArchBuilds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Saved list to %q\n", location)
|
|
return nil
|
|
}
|
|
|
|
func getFarmDestinations(name string) (map[string]config.Destination, error) {
|
|
dest := make(map[string]config.Destination)
|
|
cfg, err := config.ReadCustomConfig()
|
|
if err != nil {
|
|
return dest, err
|
|
}
|
|
|
|
// If no farm name is given, then grab all the service destinations available
|
|
if name == "" {
|
|
return cfg.Engine.ServiceDestinations, nil
|
|
}
|
|
|
|
// Go through the connections in the farm and get their destination
|
|
for _, c := range cfg.Farms.List[name] {
|
|
dest[c] = cfg.Engine.ServiceDestinations[c]
|
|
}
|
|
|
|
return dest, nil
|
|
}
|