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
73 changes: 73 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: E2E Tests
on:
push:
pull_request:
branches:
- main
workflow_dispatch:
schedule:
- cron: "0 0 * * *"

jobs:
e2e:
name: E2E (${{ matrix.config.name }})
runs-on: ubuntu-latest
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
config:
- name: Default
disable_ssa: "false"
- name: Disable SSA
disable_ssa: "true"

steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Create Kind cluster
uses: helm/kind-action@v1

- name: Wait for apiserver
run: |
kind export kubeconfig --name chart-testing
while ! kubectl api-resources; do sleep 1; done

- name: Build and deploy Eno
env:
REGISTRY: localhost
SKIP_PUSH: "yes"
DISABLE_SSA: "${{ matrix.config.disable_ssa }}"
run: ./dev/build.sh

- name: Load images into Kind
run: |
for image in $(docker images --format "{{.Repository}}:{{.Tag}}" | grep localhost); do
kind load docker-image --name chart-testing $image
done

- name: Wait for Eno rollout
timeout-minutes: 3
run: |
kubectl rollout status deployment/eno-controller --timeout=120s
kubectl rollout status deployment/eno-reconciler --timeout=120s

- name: Run E2E tests
timeout-minutes: 10
run: make test-e2e

- name: Dump diagnostics
if: failure()
run: |
echo "=== Controller Logs ===" && kubectl logs -l app=eno-controller --tail=200 || true
echo "=== Reconciler Logs ===" && kubectl logs -l app=eno-reconciler --tail=200 || true
echo "=== Events ===" && kubectl get events --sort-by=.lastTimestamp || true
echo "=== Compositions ===" && kubectl get compositions -A -o yaml || true
echo "=== Synthesizers ===" && kubectl get synthesizers -o yaml || true
echo "=== ResourceSlices ===" && kubectl get resourceslices -A -o yaml || true
echo "=== Pods ===" && kubectl get pods -A -o wide || true
2 changes: 1 addition & 1 deletion .github/workflows/unit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ jobs:
run: echo "KUBEBUILDER_ASSETS=$(./hack/download-k8s.sh)" >> $GITHUB_ENV

- name: Run tests
run: go test -v ./...
run: make test

8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ docker-build-eno-reconciler:
setup-testenv:
@echo "Installing controller-runtime testenv binaries..."
@go run sigs.k8s.io/controller-runtime/tools/setup-envtest@latest use -p path

.PHONY: test
test:
go test -v $$(go list ./... | grep -v '/e2e')

.PHONY: test-e2e
test-e2e:
go test -v -timeout 10m -count=1 ./e2e
83 changes: 81 additions & 2 deletions api/v1/composition.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package v1

import (
"context"
"errors"
"fmt"
"strconv"

Expand Down Expand Up @@ -58,6 +60,82 @@ type CompositionSpec struct {
SynthesisEnv []EnvVar `json:"synthesisEnv,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")
)

// ResolveSynthesizer resolves the Composition's 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 (c *Composition) ResolveSynthesizer(ctx context.Context, cl client.Reader) (*Synthesizer, error) {
ref := &c.Spec.Synthesizer
// LabelSelector takes precedence over name
if ref.LabelSelector != nil {
return c.resolveSynthesizerByLabel(ctx, cl)
}

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

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

// resolveSynthesizerByLabel 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 (c *Composition) resolveSynthesizerByLabel(ctx context.Context, cl client.Reader) (*Synthesizer, error) {
ref := &c.Spec.Synthesizer
// Convert metav1.LabelSelector to labels.Selector
selector, err := metav1.LabelSelectorAsSelector(ref.LabelSelector)
if err != nil {
return nil, fmt.Errorf("converting label selector: %w", err)
}

// List all synthesizers matching the selector
synthList := &SynthesizerList{}
err = cl.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
}
}

type CompositionStatus struct {
Simplified *SimplifiedStatus `json:"simplified,omitempty"`
InFlightSynthesis *Synthesis `json:"inFlightSynthesis,omitempty"`
Expand All @@ -67,8 +145,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
Loading