14
"github.com/containers/buildah/define"
15
lplatform "github.com/containers/common/libimage/platform"
16
"github.com/containers/common/pkg/config"
17
"github.com/containers/podman/v5/pkg/domain/entities"
18
"github.com/containers/podman/v5/pkg/domain/infra"
19
"github.com/hashicorp/go-multierror"
20
"github.com/sirupsen/logrus"
23
// Farm represents a group of connections to builders.
26
localEngine entities.ImageEngine // not nil -> use local engine, too
27
builders map[string]entities.ImageEngine // name -> builder
30
// Schedule is a description of where and how we'll do builds.
32
platformBuilders map[string]string // target->connection
35
func newFarmWithBuilders(_ context.Context, name string, cons []config.Connection, localEngine entities.ImageEngine, buildLocal bool) (*Farm, error) {
37
builders: make(map[string]entities.ImageEngine),
38
localEngine: localEngine,
42
builderMutex sync.Mutex
43
builderGroup multierror.Group
45
// Set up the remote connections to handle the builds
46
for _, con := range cons {
48
builderGroup.Go(func() error {
49
fmt.Printf("Connecting to %q\n", con.Name)
50
engine, err := infra.NewImageEngine(&entities.PodmanConfig{
51
EngineMode: entities.TunnelMode,
53
Identity: con.Identity,
54
MachineMode: con.IsMachine,
55
FarmNodeName: con.Name,
58
return fmt.Errorf("initializing image engine at %q: %w", con.URI, err)
61
defer fmt.Printf("Builder %q ready\n", con.Name)
63
defer builderMutex.Unlock()
64
farm.builders[name] = engine
68
// If local=true then use the local machine for builds as well
70
builderGroup.Go(func() error {
71
fmt.Println("Setting up local builder")
72
defer fmt.Println("Local builder ready")
74
defer builderMutex.Unlock()
75
farm.builders[entities.LocalFarmImageBuilderName] = localEngine
79
if builderError := builderGroup.Wait(); builderError != nil {
80
if err := builderError.ErrorOrNil(); err != nil {
84
if len(farm.builders) > 0 {
85
defer fmt.Printf("Farm %q ready\n", farm.name)
88
return nil, errors.New("no builders configured")
91
func NewFarm(ctx context.Context, name string, localEngine entities.ImageEngine, buildLocal bool) (*Farm, error) {
92
// Get the destinations of the connections specified in the farm
93
name, destinations, err := getFarmDestinations(name)
98
return newFarmWithBuilders(ctx, name, destinations, localEngine, buildLocal)
101
// Done performs any necessary end-of-process cleanup for the farm's members.
102
func (f *Farm) Done(ctx context.Context) error {
103
return f.forEach(ctx, func(ctx context.Context, name string, engine entities.ImageEngine) (bool, error) {
109
// Status polls the connections in the farm and returns a map of their
110
// individual status, along with an error if any are down or otherwise unreachable.
111
func (f *Farm) Status(ctx context.Context) (map[string]error, error) {
112
status := make(map[string]error)
114
statusMutex sync.Mutex
115
statusGroup multierror.Group
117
for _, engine := range f.builders {
119
statusGroup.Go(func() error {
120
logrus.Debugf("getting status of %q", engine.FarmNodeName(ctx))
121
defer logrus.Debugf("got status of %q", engine.FarmNodeName(ctx))
122
_, err := engine.Config(ctx)
124
defer statusMutex.Unlock()
125
status[engine.FarmNodeName(ctx)] = err
129
statusError := statusGroup.Wait()
131
return status, statusError.ErrorOrNil()
134
// forEach runs the called function once for every node in the farm and
135
// collects their results, continuing until it finishes visiting every node or
136
// a function call returns true as its first return value.
137
func (f *Farm) forEach(ctx context.Context, fn func(context.Context, string, entities.ImageEngine) (bool, error)) error {
138
var merr *multierror.Error
139
for name, engine := range f.builders {
140
stop, err := fn(ctx, name, engine)
142
merr = multierror.Append(merr, fmt.Errorf("%s: %w", engine.FarmNodeName(ctx), err))
149
return merr.ErrorOrNil()
152
// NativePlatforms returns a list of the set of platforms for which the farm
153
// can build images natively.
154
func (f *Farm) NativePlatforms(ctx context.Context) ([]string, error) {
155
nativeMap := make(map[string]struct{})
156
platforms := []string{}
158
nativeMutex sync.Mutex
159
nativeGroup multierror.Group
161
for _, engine := range f.builders {
163
nativeGroup.Go(func() error {
164
logrus.Debugf("getting native platform of %q\n", engine.FarmNodeName(ctx))
165
defer logrus.Debugf("got native platform of %q", engine.FarmNodeName(ctx))
166
inspect, err := engine.FarmNodeInspect(ctx)
171
defer nativeMutex.Unlock()
172
for _, platform := range inspect.NativePlatforms {
173
nativeMap[platform] = struct{}{}
178
merr := nativeGroup.Wait()
180
if err := merr.ErrorOrNil(); err != nil {
185
for platform := range nativeMap {
186
platforms = append(platforms, platform)
188
sort.Strings(platforms)
189
return platforms, nil
192
// EmulatedPlatforms returns a list of the set of platforms for which the farm
193
// can build images with the help of emulation.
194
func (f *Farm) EmulatedPlatforms(ctx context.Context) ([]string, error) {
195
emulatedMap := make(map[string]struct{})
196
platforms := []string{}
198
emulatedMutex sync.Mutex
199
emulatedGroup multierror.Group
201
for _, engine := range f.builders {
203
emulatedGroup.Go(func() error {
204
logrus.Debugf("getting emulated platforms of %q", engine.FarmNodeName(ctx))
205
defer logrus.Debugf("got emulated platforms of %q", engine.FarmNodeName(ctx))
206
inspect, err := engine.FarmNodeInspect(ctx)
211
defer emulatedMutex.Unlock()
212
for _, platform := range inspect.EmulatedPlatforms {
213
emulatedMap[platform] = struct{}{}
218
merr := emulatedGroup.Wait()
220
if err := merr.ErrorOrNil(); err != nil {
225
for platform := range emulatedMap {
226
platforms = append(platforms, platform)
228
sort.Strings(platforms)
229
return platforms, nil
232
// Schedule takes a list of platforms and returns a list of connections which
233
// can be used to build for those platforms. It always prefers native builders
234
// over emulated builders, but will assign a builder which can use emulation
235
// for a platform if no suitable native builder is available.
237
// If platforms is an empty list, all available native platforms will be
240
// TODO: add (Priority,Weight *int) a la RFC 2782 to destinations that we know
241
// of, and factor those in when assigning builds to nodes in here.
242
func (f *Farm) Schedule(ctx context.Context, platforms []string) (Schedule, error) {
245
infoGroup multierror.Group
248
// If we weren't given a list of target platforms, generate one.
249
if len(platforms) == 0 {
250
platforms, err = f.NativePlatforms(ctx)
252
return Schedule{}, fmt.Errorf("reading list of available native platforms: %w", err)
256
platformBuilders := make(map[string]string)
257
native := make(map[string]string)
258
emulated := make(map[string]string)
259
var localPlatform string
260
// Make notes of which platforms we can build for natively, and which
261
// ones we can build for using emulation.
262
for name, engine := range f.builders {
263
name, engine := name, engine
264
infoGroup.Go(func() error {
265
inspect, err := engine.FarmNodeInspect(ctx)
270
defer infoMutex.Unlock()
271
for _, n := range inspect.NativePlatforms {
272
if _, assigned := native[n]; !assigned {
275
if name == entities.LocalFarmImageBuilderName {
279
for _, e := range inspect.EmulatedPlatforms {
280
if _, assigned := emulated[e]; !assigned {
287
merr := infoGroup.Wait()
289
if err := merr.ErrorOrNil(); err != nil {
290
return Schedule{}, err
293
// Assign a build to the first node that could build it natively, and
294
// if there isn't one, the first one that can build it with the help of
295
// emulation, and if there aren't any, error out.
296
for _, platform := range platforms {
297
if builder, ok := native[platform]; ok {
298
platformBuilders[platform] = builder
299
} else if builder, ok := emulated[platform]; ok {
300
platformBuilders[platform] = builder
302
return Schedule{}, fmt.Errorf("no builder capable of building for platform %q available", platform)
305
// If local is set, prioritize building on local
306
if localPlatform != "" {
307
platformBuilders[localPlatform] = entities.LocalFarmImageBuilderName
309
schedule := Schedule{
310
platformBuilders: platformBuilders,
315
// Build runs a build using the specified targetplatform:service map. If all
316
// builds succeed, it copies the resulting images from the remote hosts to the
317
// local service and builds a manifest list with the specified reference name.
318
func (f *Farm) Build(ctx context.Context, schedule Schedule, options entities.BuildOptions, reference string, localEngine entities.ImageEngine) error {
319
switch options.OutputFormat {
321
return fmt.Errorf("unknown output format %q requested", options.OutputFormat)
322
case "", define.OCIv1ImageManifest:
323
options.OutputFormat = define.OCIv1ImageManifest
324
case define.Dockerv2ImageManifest:
327
// Build the list of jobs.
334
builder entities.ImageEngine
336
for platform, builderName := range schedule.platformBuilders { // prepare to build
337
builder, ok := f.builders[builderName]
339
return fmt.Errorf("unknown builder %q", builderName)
341
var rawOS, rawArch, rawVariant string
342
p := strings.Split(platform, "/")
343
if len(p) > 0 && p[0] != "" {
352
os, arch, variant := lplatform.Normalize(rawOS, rawArch, rawVariant)
353
jobs.Store(builderName, job{
362
listBuilderOptions := listBuilderOptions{
363
cleanup: options.Cleanup,
364
iidFile: options.IIDFile,
365
authfile: options.Authfile,
366
skipTLSVerify: options.SkipTLSVerify,
368
manifestListBuilder := newManifestListBuilder(reference, f.localEngine, listBuilderOptions)
370
// Start builds in parallel and wait for them all to finish.
372
buildResults sync.Map
373
buildGroup multierror.Group
375
type buildResult struct {
376
report entities.BuildReport
377
builder entities.ImageEngine
379
for platform, builder := range schedule.platformBuilders {
380
platform, builder := platform, builder
381
outReader, outWriter := io.Pipe()
382
errReader, errWriter := io.Pipe()
384
defer outReader.Close()
385
reader := bufio.NewReader(outReader)
386
writer := options.Out
390
line, err := reader.ReadString('\n')
392
line = strings.TrimSuffix(line, "\n")
393
fmt.Fprintf(writer, "[%s@%s] %s\n", platform, builder, line)
394
line, err = reader.ReadString('\n')
398
defer errReader.Close()
399
reader := bufio.NewReader(errReader)
400
writer := options.Err
404
line, err := reader.ReadString('\n')
406
line = strings.TrimSuffix(line, "\n")
407
fmt.Fprintf(writer, "[%s@%s] %s\n", platform, builder, line)
408
line, err = reader.ReadString('\n')
411
buildGroup.Go(func() error {
413
defer outWriter.Close()
414
defer errWriter.Close()
415
c, ok := jobs.Load(builder)
417
return fmt.Errorf("unknown connection for %q (shouldn't happen)", builder)
419
if j, ok = c.(job); !ok {
420
return fmt.Errorf("unexpected connection type for %q (shouldn't happen)", builder)
422
buildOptions := options
423
buildOptions.Platforms = []struct{ OS, Arch, Variant string }{{j.os, j.arch, j.variant}}
424
buildOptions.Out = outWriter
425
buildOptions.Err = errWriter
426
fmt.Printf("Starting build for %v at %q\n", buildOptions.Platforms, builder)
427
buildReport, err := j.builder.Build(ctx, options.ContainerFiles, buildOptions)
429
return fmt.Errorf("building for %q on %q: %w", j.platform, builder, err)
431
fmt.Printf("finished build for %v at %q: built %s\n", buildOptions.Platforms, builder, buildReport.ID)
432
buildResults.Store(platform, buildResult{
433
report: *buildReport,
439
buildErrors := buildGroup.Wait()
440
if err := buildErrors.ErrorOrNil(); err != nil {
441
return fmt.Errorf("building: %w", err)
444
// Assemble the final result.
445
perArchBuilds := make(map[entities.BuildReport]entities.ImageEngine)
446
buildResults.Range(func(k, v any) bool {
447
result, ok := v.(buildResult)
449
fmt.Fprintf(os.Stderr, "report %v not a build result?", v)
452
perArchBuilds[result.report] = result.builder
455
location, err := manifestListBuilder.build(ctx, perArchBuilds)
459
fmt.Printf("Saved list to %q\n", location)
463
func getFarmDestinations(name string) (string, []config.Connection, error) {
464
cfg, err := config.Default()
470
if name, cons, err := cfg.GetDefaultFarmConnections(); err == nil {
471
// Use default farm if is there is one
472
return name, cons, nil
474
// If no farm name is given, then grab all the service destinations available
475
cons, err := cfg.GetAllConnections()
476
return name, cons, err
478
cons, err := cfg.GetFarmConnections(name)
479
return name, cons, err