debian-forge-composer/vendor/github.com/osbuild/images/pkg/container/resolver.go
2025-02-06 13:48:59 +01:00

162 lines
3.4 KiB
Go

package container
import (
"context"
"fmt"
"sort"
"strings"
)
type resolveResult struct {
spec Spec
err error
}
type Resolver interface {
Add(spec SourceSpec)
Finish() ([]Spec, error)
}
type asyncResolver struct {
jobs int
queue chan resolveResult
ctx context.Context
Arch string
AuthFilePath string
newClient func(string) (*Client, error)
}
type SourceSpec struct {
Source string
Name string
Digest *string
TLSVerify *bool
Local bool
}
// XXX: use arch.Arch here?
func NewResolver(arch string) *asyncResolver {
// NOTE: this should return the Resolver interface, but osbuild-composer
// sets the AuthFilePath and for now we don't want to break the API.
return &asyncResolver{
ctx: context.Background(),
queue: make(chan resolveResult, 2),
Arch: arch,
newClient: NewClient,
}
}
func (r *asyncResolver) Add(spec SourceSpec) {
client, err := r.newClient(spec.Source)
r.jobs += 1
if err != nil {
r.queue <- resolveResult{err: err}
return
}
client.SetTLSVerify(spec.TLSVerify)
client.SetArchitectureChoice(r.Arch)
if r.AuthFilePath != "" {
client.SetAuthFilePath(r.AuthFilePath)
}
go func() {
spec, err := client.Resolve(r.ctx, spec.Name, spec.Local)
if err != nil {
err = fmt.Errorf("'%s': %w", spec.Source, err)
}
r.queue <- resolveResult{spec: spec, err: err}
}()
}
func (r *asyncResolver) Finish() ([]Spec, error) {
specs := make([]Spec, 0, r.jobs)
errs := make([]string, 0, r.jobs)
for r.jobs > 0 {
result := <-r.queue
r.jobs -= 1
if result.err == nil {
specs = append(specs, result.spec)
} else {
errs = append(errs, result.err.Error())
}
}
if len(errs) > 0 {
detail := strings.Join(errs, "; ")
return specs, fmt.Errorf("failed to resolve container: %s", detail)
}
// Return a stable result, sorted by Digest
sort.Slice(specs, func(i, j int) bool { return specs[i].Digest < specs[j].Digest })
return specs, nil
}
type blockingResolver struct {
Arch string
AuthFilePath string
newClient func(string) (*Client, error)
results []resolveResult
}
// NewBlockingResolver returns a [asyncResolver] that resolves container refs
// synchronously (blocking).
// TODO: Make this the only resolver after all clients have migrated to this.
func NewBlockingResolver(arch string) Resolver {
return &blockingResolver{
Arch: arch,
newClient: NewClient,
}
}
func (r *blockingResolver) Add(src SourceSpec) {
client, err := r.newClient(src.Source)
if err != nil {
r.results = append(r.results, resolveResult{err: err})
return
}
client.SetTLSVerify(src.TLSVerify)
client.SetArchitectureChoice(r.Arch)
if r.AuthFilePath != "" {
client.SetAuthFilePath(r.AuthFilePath)
}
spec, err := client.Resolve(context.TODO(), src.Name, src.Local)
if err != nil {
err = fmt.Errorf("'%s': %w", src.Source, err)
}
r.results = append(r.results, resolveResult{spec: spec, err: err})
}
func (r *blockingResolver) Finish() ([]Spec, error) {
specs := make([]Spec, 0, len(r.results))
errs := make([]string, 0, len(r.results))
for _, result := range r.results {
if result.err == nil {
specs = append(specs, result.spec)
} else {
errs = append(errs, result.err.Error())
}
}
if len(errs) > 0 {
detail := strings.Join(errs, "; ")
return specs, fmt.Errorf("failed to resolve container: %s", detail)
}
// Return a stable result, sorted by Digest
sort.Slice(specs, func(i, j int) bool { return specs[i].Digest < specs[j].Digest })
return specs, nil
}