Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,33 +82,33 @@ func Setup(name string, define func(*cobra.Command), initialize func(context.Con
return nil
}

err := cfg.BindPFlags(cmd.Flags())
if err != nil {
return fmt.Errorf("cannot bind flags: %w", err)
inErr := cfg.BindPFlags(cmd.Flags())
if inErr != nil {
return fmt.Errorf("cannot bind flags: %w", inErr)
}
err = cfg.BindPFlags(cmd.InheritedFlags())
if err != nil {
return fmt.Errorf("cannot bind inherited flags: %w", err)
inErr = cfg.BindPFlags(cmd.InheritedFlags())
if inErr != nil {
return fmt.Errorf("cannot bind inherited flags: %w", inErr)
}

cfgFile := cfg.GetString("config-file")
if cfgFile != "" {
cfg.SetConfigFile(cfgFile)
}

err = cfg.ReadInConfig()
if err != nil {
_, ok := errors.AsType[viper.ConfigFileNotFoundError](err)
inErr = cfg.ReadInConfig()
if inErr != nil {
_, ok := errors.AsType[viper.ConfigFileNotFoundError](inErr)
if cfgFile != "" || !ok {
return fmt.Errorf("cannot read config file: %w", err)
return fmt.Errorf("cannot read config file: %w", inErr)
}
}

if initialize != nil {
var ctx context.Context
ctx, err = initialize(cmd.Context(), cfg)
if err != nil {
return fmt.Errorf("cannot initialize: %w", err)
ctx, inErr = initialize(cmd.Context(), cfg)
if inErr != nil {
return fmt.Errorf("cannot initialize: %w", inErr)
}
//nolint:fatcontext // This is required for ctxFunc closure to return the initialized context.
initializedCtx = ctx
Expand Down
2 changes: 1 addition & 1 deletion internal/cloudprovider/aliyun.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ func (p *aliyun) Publish(ctx context.Context, cname string, manifest *gardenlinu
images := make(map[string]string, len(ecsClients))
publishImages := parallel.NewLimitedActivitySync(ctx, 7)
for toRegion := range ecsClients {
publishImages.Go(func(ctx context.Context) (parallel.ResultFunc, error) {
publishImages.Go(func(ctx context.Context) (parallel.ResultSyncFunc, error) {
ctx = log.WithValues(ctx, "region", toRegion)
localID := imageID
var er error
Expand Down
6 changes: 3 additions & 3 deletions internal/cloudprovider/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ func (p *awsTarget) Publish(ctx context.Context, cname string, manifest *gardenl
outputImages := make([]awsPublishedImage, 0, 4)
publish := parallel.NewActivitySync(ctx)

publish.Go(func(ctx context.Context) (parallel.ResultFunc, error) {
publish.Go(func(ctx context.Context) (parallel.ResultSyncFunc, error) {
ctx = log.WithValues(ctx, "cloud", "public")

images, er := p.publish(ctx, p.source, imagePath.S3Key, image, tags, arch, requireUEFI, uefiData, false)
Expand All @@ -477,7 +477,7 @@ func (p *awsTarget) Publish(ctx context.Context, cname string, manifest *gardenl
})

if p.enableChina && !secureBoot {
publish.Go(func(ctx context.Context) (parallel.ResultFunc, error) {
publish.Go(func(ctx context.Context) (parallel.ResultSyncFunc, error) {
ctx = log.WithValues(ctx, "cloud", "china")

source := p.sourceChina
Expand Down Expand Up @@ -545,7 +545,7 @@ func (p *awsTarget) publish(ctx context.Context, source ArtifactSource, key, ima
images := make(map[string]string, len(regions))
publishImages := parallel.NewLimitedActivitySync(ctx, 12)
for _, toRegion := range regions {
publishImages.Go(func(ctx context.Context) (parallel.ResultFunc, error) {
publishImages.Go(func(ctx context.Context) (parallel.ResultSyncFunc, error) {
ctx = log.WithValues(ctx, "region", toRegion)
localID := imageID
var er error
Expand Down
10 changes: 5 additions & 5 deletions internal/cloudprovider/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ func (p *azure) Publish(ctx context.Context, cname string, manifest *gardenlinux
outputImages := make([]azurePublishedImage, 0, 4)
publish := parallel.NewActivitySync(ctx)

publish.Go(func(ctx context.Context) (parallel.ResultFunc, error) {
publish.Go(func(ctx context.Context) (parallel.ResultSyncFunc, error) {
ctx = log.WithValues(ctx, "cloud", "public")

images, er := p.publish(ctx, cname, p.source, imagePath.S3Key, image, imageVersion, arch, bios, secureBoot, pk, kek, db, false)
Expand All @@ -476,7 +476,7 @@ func (p *azure) Publish(ctx context.Context, cname string, manifest *gardenlinux
})

if p.enableChina {
publish.Go(func(ctx context.Context) (parallel.ResultFunc, error) {
publish.Go(func(ctx context.Context) (parallel.ResultSyncFunc, error) {
ctx = log.WithValues(ctx, "cloud", "china")

source := p.sourceChina
Expand Down Expand Up @@ -577,7 +577,7 @@ func (p *azure) publish(ctx context.Context, cname string, source ArtifactSource

if bios {
blobUsed.Add(1)
createImageVersion.Go(func(_ context.Context) (parallel.ResultFunc, error) {
createImageVersion.Go(func(_ context.Context) (parallel.ResultSyncFunc, error) {
imageID, er := func() (string, error) {
defer blobUsed.Done()
return p.createImage(bctx, blobURL, image, true, china)
Expand Down Expand Up @@ -610,7 +610,7 @@ func (p *azure) publish(ctx context.Context, cname string, source ArtifactSource
})
}

createImageVersion.Go(func(ctx context.Context) (parallel.ResultFunc, error) {
createImageVersion.Go(func(ctx context.Context) (parallel.ResultSyncFunc, error) {
imageID, er := func() (string, error) {
defer blobUsed.Done()
return p.createImage(ctx, blobURL, image, false, china)
Expand Down Expand Up @@ -641,7 +641,7 @@ func (p *azure) publish(ctx context.Context, cname string, source ArtifactSource
}, nil
})

createImageVersion.Go(func(ctx context.Context) (parallel.ResultFunc, error) {
createImageVersion.Go(func(ctx context.Context) (parallel.ResultSyncFunc, error) {
blobUsed.Wait()

er := p.deleteBlob(ctx, blob, false, china)
Expand Down
4 changes: 2 additions & 2 deletions internal/cloudprovider/openstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (p *openstack) createClients(ctx context.Context, config openstackPublishin

initClients := parallel.NewLimitedActivitySync(ctx, 7)
for _, region := range config.Regions {
initClients.Go(func(ctx context.Context) (parallel.ResultFunc, error) {
initClients.Go(func(ctx context.Context) (parallel.ResultSyncFunc, error) {
providerClient, er := openstacksdk.AuthenticatedClient(ctx, gophercloud.AuthOptions{
IdentityEndpoint: strings.Replace(config.Endpoint, "{region}", region, 1),
Username: creds.Username,
Expand Down Expand Up @@ -271,7 +271,7 @@ func (p *openstack) Publish(ctx context.Context, cname string, manifest *gardenl
source = p.sourceChina
}

publishImages.Go(func(ctx context.Context) (parallel.ResultFunc, error) {
publishImages.Go(func(ctx context.Context) (parallel.ResultSyncFunc, error) {
ctx = log.WithValues(ctx, "region", region)

ctx = task.Begin(ctx, "publish/"+image+"/"+region, &openstackTaskState{
Expand Down
26 changes: 21 additions & 5 deletions internal/graph/dfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,28 @@ func (e *CycleError[N]) Error() string {

// ReverseTopologicalSort runs DFS from each root and returns the nodes in reverse topological order or *CycleError if there is a cycle.
func ReverseTopologicalSort[N comparable](roots []N, neighbors func(N) ([]N, error)) ([]N, error) {
return DFS(roots, neighbors, PostOrder)
return DFS(roots, neighbors, PostOrder, CyclesError)
}

// ReachableSet runs DFS from each root and returns every reachable node in pre-order or *CycleError if there is a cycle.
// ReachableSet runs DFS from each root and returns every reachable node in pre-order, allowing cycles.
func ReachableSet[N comparable](roots []N, neighbors func(N) ([]N, error)) ([]N, error) {
return DFS(roots, neighbors, PreOrder)
return DFS(roots, neighbors, PreOrder, CyclesAllow)
}

// DFS runs depth-first search from each root and returns the visited nodes in the requested order or *CycleError if there is a cycle.
func DFS[N comparable](roots []N, neighbors func(N) ([]N, error), order Order) ([]N, error) {
// DFS runs depth-first search from each root and returns the visited nodes in the requested order. Back-edges are handled per cycles.
func DFS[N comparable](roots []N, neighbors func(N) ([]N, error), order Order, cycles Cycles) ([]N, error) {
switch order {
case PreOrder, PostOrder:
default:
return nil, fmt.Errorf("invalid order %d", order)
}

switch cycles {
case CyclesAllow, CyclesError:
default:
return nil, fmt.Errorf("invalid cycles mode %d", cycles)
}

const (
white = 0
gray = 1
Expand Down Expand Up @@ -65,6 +77,10 @@ func DFS[N comparable](roots []N, neighbors func(N) ([]N, error), order Order) (

c := color[f.node]
if c == gray {
if cycles == CyclesAllow {
continue
}

start := slices.Index(path, f.node)
return nil, &CycleError[N]{
Cycle: append([]N(nil), path[start:]...),
Expand Down
10 changes: 10 additions & 0 deletions internal/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,13 @@ const (
// PostOrder visits each node after its children.
PostOrder
)

// Cycles controls how DFS reacts to a back-edge, an edge to a node already on the current path.
type Cycles int

const (
// CyclesAllow makes DFS skip a back-edge and continue the walk.
CyclesAllow Cycles = iota + 1
// CyclesError makes DFS return a *CycleError on any back-edge.
CyclesError
)
14 changes: 8 additions & 6 deletions internal/module/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (r *Root) Init(rawCfg map[string]any) error {
}

var modules []Module
modules, err = r.allModules()
modules, err = r.AllModules()
if err != nil {
return fmt.Errorf("invalid configuration: %w", err)
}
Expand Down Expand Up @@ -103,8 +103,9 @@ func (r *Root) Init(rawCfg map[string]any) error {
return nil
}

func (r *Root) allModules() ([]Module, error) {
configurables, err := graph.ReachableSet([]Configurable{r.self}, func(c Configurable) ([]Configurable, error) {
// AllModules returns every configured module in reverse topological order.
func (r *Root) AllModules() ([]Module, error) {
configurables, err := graph.ReverseTopologicalSort([]Configurable{r.self}, func(c Configurable) ([]Configurable, error) {
return c.Configurables(), nil
})
if err != nil {
Expand Down Expand Up @@ -264,7 +265,7 @@ func RegisterTypeRef[T Module](b *Base, owner Configurable, ptr *T) error {
b.root.refs = append(b.root.refs, refEntry{
owner: owner,
resolve: func(_ *Root, modules []Module) ([]Module, error) {
typeModules := filterByType[T](modules)
typeModules := ModulesOfType[T](modules)
var zero T
if len(typeModules) == 0 {
return nil, fmt.Errorf("no module of type %T", zero)
Expand All @@ -291,7 +292,7 @@ func RegisterSliceTypeRef[T Module](b *Base, owner Configurable, ptr *[]T) error
b.root.refs = append(b.root.refs, refEntry{
owner: owner,
resolve: func(_ *Root, modules []Module) ([]Module, error) {
typeModules := filterByType[T](modules)
typeModules := ModulesOfType[T](modules)

*ptr = typeModules

Expand All @@ -302,7 +303,8 @@ func RegisterSliceTypeRef[T Module](b *Base, owner Configurable, ptr *[]T) error
return nil
}

func filterByType[T Module](modules []Module) []T {
// ModulesOfType returns every configured module satisfying T.
func ModulesOfType[T any](modules []Module) []T {
var typeModules []T
for _, m := range modules {
t, ok := m.(T)
Expand Down
44 changes: 22 additions & 22 deletions internal/module/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

//nolint:gochecknoglobals // Required for automatic registration.
var typeSchemas = make(map[reflect.Type]any)
var configTypes = make(map[reflect.Type]any)

//nolint:gochecknoglobals // Cached reflect.Type for the marker interfaces.
var (
Expand Down Expand Up @@ -125,9 +125,9 @@ type anySliceSlot interface {
sliceSlotType() reflect.Type
}

// RegisterSchema records the config schema for a Configurable, using a nil prototype (e.g. (*T)(nil)) and a zero-value config struct.
func RegisterSchema(prototype Configurable, schema any) {
typeSchemas[reflect.TypeOf(prototype)] = schema
// RegisterConfigType records the config type for a Configurable, called with a nil impl (e.g. (*T)(nil)) and a zero-value config struct.
func RegisterConfigType(impl Configurable, config any) {
configTypes[reflect.TypeOf(impl)] = config
}

// MaxSliceLen returns the longest slice length found anywhere in cfg.
Expand Down Expand Up @@ -160,7 +160,7 @@ func MaxSliceLen(rawCfg any) int {
return maxLen
}

type schemaCollector struct {
type keyCollector struct {
cache map[reflect.Type][]fieldEntry
path map[reflect.Type]struct{}
keys []string
Expand All @@ -174,9 +174,9 @@ type fieldEntry struct {
slice bool
}

// CollectSchemaKeys walks root's tree and returns dotted leaf keys for every reachable schema, with maxSliceLen keys per SliceSlot field.
func CollectSchemaKeys(root Configurable, maxSliceLen int) ([]string, error) {
c := &schemaCollector{
// ConfigKeys walks root's tree and returns dotted leaf keys for every reachable config type, with maxSliceLen keys per SliceSlot field.
func ConfigKeys(root Configurable, maxSliceLen int) ([]string, error) {
c := &keyCollector{
cache: make(map[reflect.Type][]fieldEntry),
path: make(map[reflect.Type]struct{}),
maxSliceLen: maxSliceLen,
Expand All @@ -185,30 +185,30 @@ func CollectSchemaKeys(root Configurable, maxSliceLen int) ([]string, error) {
return c.keys, c.err
}

func (c *schemaCollector) walkType(t reflect.Type, prefix string) {
func (c *keyCollector) walkType(t reflect.Type, prefix string) {
if c.err != nil {
return
}

_, ok := c.path[t]
if ok {
c.err = fmt.Errorf("schema cycle: type %v reachable from itself at %q", t, prefix)
c.err = fmt.Errorf("config type cycle: type %v reachable from itself at %q", t, prefix)
return
}

var schema any
schema, ok = typeSchemas[t]
if !ok || schema == nil {
var configType any
configType, ok = configTypes[t]
if !ok || configType == nil {
return
}

c.path[t] = struct{}{}
c.walkFields(schema, prefix)
c.walkFields(configType, prefix)
delete(c.path, t)
}

func (c *schemaCollector) walkFields(schema any, prefix string) {
for _, f := range c.fieldEntries(schema) {
func (c *keyCollector) walkFields(configType any, prefix string) {
for _, f := range c.fieldEntries(configType) {
field := f.name
if prefix != "" {
field = prefix + "." + f.name
Expand All @@ -227,8 +227,8 @@ func (c *schemaCollector) walkFields(schema any, prefix string) {
}
}

func (c *schemaCollector) fieldEntries(schema any) []fieldEntry {
t := reflect.TypeOf(schema)
func (c *keyCollector) fieldEntries(configType any) []fieldEntry {
t := reflect.TypeOf(configType)
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
Expand Down Expand Up @@ -278,14 +278,14 @@ func (c *schemaCollector) fieldEntries(schema any) []fieldEntry {
return entries
}

func (c *schemaCollector) walkSliceSlot(slotType reflect.Type, prefix string) {
func (c *keyCollector) walkSliceSlot(slotType reflect.Type, prefix string) {
c.keys = append(c.keys, prefix+".id")
for i := range c.maxSliceLen {
c.walkSingleSlot(slotType, prefix+".items."+strconv.Itoa(i))
}
}

func (c *schemaCollector) walkSingleSlot(slotType reflect.Type, prefix string) {
func (c *keyCollector) walkSingleSlot(slotType reflect.Type, prefix string) {
switch slotType.Kind() {
case reflect.Interface:
c.walkCategory(slotType, prefix)
Expand All @@ -294,9 +294,9 @@ func (c *schemaCollector) walkSingleSlot(slotType reflect.Type, prefix string) {
}
}

func (c *schemaCollector) walkCategory(slotType reflect.Type, prefix string) {
func (c *keyCollector) walkCategory(slotType reflect.Type, prefix string) {
c.keys = append(c.keys, prefix+".type", prefix+".id")
for typ := range typeSchemas {
for typ := range configTypes {
if !typ.Implements(slotType) {
continue
}
Expand Down
Loading