Skip to content
Open
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
5 changes: 3 additions & 2 deletions api/v1/composition.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ type CompositionStatus struct {
}

type SimplifiedStatus struct {
Status string `json:"status,omitempty"`
Error string `json:"error,omitempty"`
Status string `json:"status,omitempty"`
Error string `json:"error,omitempty"`
ResolvedSynthName string `json:"resolvedSynthName,omitempty"`
}

func (s *SimplifiedStatus) String() string {
Expand Down
6 changes: 4 additions & 2 deletions api/v1/config/crd/eno.azure.io_compositions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,6 @@ spec:
description: Compositions are synthesized by a Synthesizer, referenced
by name.
properties:
name:
type: string
labelSelector:
description: |-
A label selector is a label query over a set of resources. The result of matchLabels and
Expand Down Expand Up @@ -158,6 +156,8 @@ spec:
type: object
type: object
x-kubernetes-map-type: atomic
name:
type: string
type: object
x-kubernetes-validations:
- message: at least one of name or labelSelector must be set
Expand Down Expand Up @@ -492,6 +492,8 @@ spec:
properties:
error:
type: string
resolvedSynthName:
type: string
status:
type: string
type: object
Expand Down
4 changes: 2 additions & 2 deletions api/v1/config/crd/eno.azure.io_symphonies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,6 @@ spec:
synthesizer:
description: Used to populate the composition's spec.synthesizer.
properties:
name:
type: string
labelSelector:
description: |-
A label selector is a label query over a set of resources. The result of matchLabels and
Expand Down Expand Up @@ -210,6 +208,8 @@ spec:
type: object
type: object
x-kubernetes-map-type: atomic
name:
type: string
type: object
x-kubernetes-validations:
- message: at least one of name or labelSelector must be set
Expand Down
79 changes: 79 additions & 0 deletions api/v1/synthesizer.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package v1

import (
"context"
"errors"
"fmt"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// +kubebuilder:object:root=true
Expand Down Expand Up @@ -75,3 +80,77 @@ type SynthesizerRef struct {
Name string `json:"name,omitempty"`
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
}

// Sentinel errors for synthesizer resolution.
var (
// ErrNoMatchingSelector is returned when no synthesizers match the label selector.
ErrNoMatchingSelector = errors.New("no synthesizers match the label selector")

// ErrMultipleMatches is returned when more than one synthesizer matches the label selector.
ErrMultipleMatches = errors.New("multiple synthesizers match the label selector")
)

// Resolve resolves a SynthesizerRef to a concrete Synthesizer.
//
// Precedence behavior: When both Name and LabelSelector are set in the ref,
// LabelSelector takes precedence and Name is ignored. This allows for more
// flexible matching when needed while maintaining backwards compatibility
// with name-based resolution.
//
// If the ref has a labelSelector, it lists all synthesizers matching the selector.
// Exactly one synthesizer must match; if zero match, ErrNoMatchingSelector is returned,
// and if more than one match, ErrMultipleMatches is returned.
//
// If labelSelector is not set, it uses the name field to get the synthesizer directly.
//
// Returns:
// - The resolved Synthesizer if found
// - nil, ErrNoMatchingSelector if no synthesizers match the label selector
// - nil, ErrMultipleMatches if more than one synthesizer matches the label selector
// - nil, error if there was an error during resolution
func (r *SynthesizerRef) Resolve(ctx context.Context, c client.Reader) (*Synthesizer, error) {
// LabelSelector takes precedence over name
if r.LabelSelector != nil {
return r.resolveByLabel(ctx, c)
}

// Fallback to name-based resolution
synth := &Synthesizer{}
synth.Name = r.Name

return synth, c.Get(ctx, client.ObjectKeyFromObject(synth), synth)
}

// resolveByLabel resolves a Synthesizer using a label selector.
// It lists all synthesizers matching the selector and returns the matching one.
// Exactly one synthesizer must match the selector.
//
// Returns:
// - The resolved Synthesizer if exactly one matches
// - nil, ErrNoMatchingSelector if no synthesizers match the selector
// - nil, ErrMultipleMatches if more than one synthesizer matches the selector
// - nil, error if there was an error during resolution
func (r *SynthesizerRef) resolveByLabel(ctx context.Context, c client.Reader) (*Synthesizer, error) {
// Convert metav1.LabelSelector to labels.Selector
selector, err := metav1.LabelSelectorAsSelector(r.LabelSelector)
if err != nil {
return nil, fmt.Errorf("converting label selector: %w", err)
}

// List all synthesizers matching the selector
synthList := &SynthesizerList{}
err = c.List(ctx, synthList, client.MatchingLabelsSelector{Selector: selector})
if err != nil {
return nil, fmt.Errorf("listing synthesizers by label selector: %w", err)
}

// Handle results based on match count
switch len(synthList.Items) {
case 0:
return nil, ErrNoMatchingSelector
case 1:
return &synthList.Items[0], nil
default:
return nil, ErrMultipleMatches
}
}
Loading
Loading