162 lines
3.4 KiB
Go
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
|
|
}
|