From ad1a199c0f372c275a4ddf561b96a6c7862c9727 Mon Sep 17 00:00:00 2001 From: Ruinan Liu Date: Mon, 6 Apr 2026 09:33:36 -0700 Subject: [PATCH 1/4] Part1, adding composition api changes: --- Makefile | 38 +++++++++- api/v1/composition.go | 46 +++++++++++- .../config/crd/eno.azure.io_compositions.yaml | 21 +++++- .../config/crd/eno.azure.io_symphonies.yaml | 20 ++++- api/v1/symphony.go | 13 ++++ api/v1/zz_generated.deepcopy.go | 75 +++++++++++++++++++ 6 files changed, 203 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index b6821bee..7259ff2f 100644 --- a/Makefile +++ b/Makefile @@ -9,18 +9,32 @@ ENO_RECONCILER_IMAGE_NAME ?= eno-reconciler .PHONY: docker-build-eno-controller docker-build-eno-controller: - docker build \ + DOCKER_BUILDKIT=1 docker build \ --file docker/$(ENO_CONTROLLER_IMAGE_NAME)/Dockerfile \ --tag $(REGISTRY)/$(ENO_CONTROLLER_IMAGE_NAME):$(ENO_CONTROLLER_IMAGE_VERSION) . docker push $(REGISTRY)/$(ENO_CONTROLLER_IMAGE_NAME):$(ENO_CONTROLLER_IMAGE_VERSION) .PHONY: docker-build-eno-reconciler docker-build-eno-reconciler: - docker build \ + DOCKER_BUILDKIT=1 docker build \ --file docker/$(ENO_RECONCILER_IMAGE_NAME)/Dockerfile \ --tag $(REGISTRY)/$(ENO_RECONCILER_IMAGE_NAME):$(ENO_RECONCILER_IMAGE_VERSION) . docker push $(REGISTRY)/$(ENO_RECONCILER_IMAGE_NAME):$(ENO_RECONCILER_IMAGE_VERSION) +.PHONY: podman-build-push-eno-controller +podman-build-push-eno-controller: + podman build \ + --file docker/$(ENO_CONTROLLER_IMAGE_NAME)/Dockerfile \ + --tag $(REGISTRY)/$(ENO_CONTROLLER_IMAGE_NAME):$(ENO_CONTROLLER_IMAGE_VERSION) . + podman push $(REGISTRY)/$(ENO_CONTROLLER_IMAGE_NAME):$(ENO_CONTROLLER_IMAGE_VERSION) + +.PHONY: podman-build-push-eno-reconciler +podman-build-push-eno-reconciler: + podman build \ + --file docker/$(ENO_RECONCILER_IMAGE_NAME)/Dockerfile \ + --tag $(REGISTRY)/$(ENO_RECONCILER_IMAGE_NAME):$(ENO_RECONCILER_IMAGE_VERSION) . + podman push $(REGISTRY)/$(ENO_RECONCILER_IMAGE_NAME):$(ENO_RECONCILER_IMAGE_VERSION) + # Setup controller-runtime test environment binaries .PHONY: setup-testenv setup-testenv: @@ -34,3 +48,23 @@ test: .PHONY: test-e2e test-e2e: go test -v -timeout 10m -count=1 ./e2e + +.PHONY: generate +generate: controller-gen + $(CONTROLLER_GEN) object crd paths="./..." output:crd:artifacts:config=api/v1/config/crd + +# find or download controller-gen +controller-gen: +ifeq (, $(shell which controller-gen)) + @{ \ + set -e ;\ + CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\ + cd $$CONTROLLER_GEN_TMP_DIR ;\ + go mod init tmp ;\ + go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.18.0 ;\ + rm -rf $$CONTROLLER_GEN_TMP_DIR ;\ + } +CONTROLLER_GEN=$(shell go env GOPATH)/bin/controller-gen +else +CONTROLLER_GEN=$(shell which controller-gen) +endif \ No newline at end of file diff --git a/api/v1/composition.go b/api/v1/composition.go index 7175ce64..082df649 100644 --- a/api/v1/composition.go +++ b/api/v1/composition.go @@ -9,10 +9,13 @@ import ( ) const ( - enoAzureOperationIDKey = "eno.azure.io/operationID" - enoAzureOperationOrigin = "eno.azure.io/operationOrigin" - OperationIdKey string = "operationID" - OperationOrigionKey string = "operationOrigin" + enoAzureOperationIDKey = "eno.azure.io/operationID" + enoAzureOperationOrigin = "eno.azure.io/operationOrigin" + OperationIdKey string = "operationID" + OperationOrigionKey string = "operationOrigin" + CircularDependencyReason string = "CircularDependency" + WaitingOnDependentsReason string = "WaitingOnDependents" + WaitingOnDependenciesReason string = "WaitingOnDependencies" ) // +kubebuilder:object:root=true @@ -56,6 +59,21 @@ type CompositionSpec struct { // A set of environment variables that will be made available inside the synthesis Pod. // +kubebuilder:validation:MaxItems:=500 SynthesisEnv []EnvVar `json:"synthesisEnv,omitempty"` + + // Declare depdendencies on other compositiosn by name and namespace. A composition can have at most 50 dependencies + // Compositions will not be scheduled for synthesis until all required + // dependencies have CurrentSynthesis.Ready != nil + // Deletion is blocked until all non-optional dependents are fully removed. + // +kubebuilder:validation:MaxItems:=500 + DependsOn []CompositionDependency `json:"dependsOn,omitempty"` +} + +type CompositionDependency struct { + // Name of the dependency composition + Name string `json:"name,omitempty"` + + //Namespace of the dependency composition + Namespace string `json:"namespace,omitempty"` } type CompositionStatus struct { @@ -64,6 +82,26 @@ type CompositionStatus struct { CurrentSynthesis *Synthesis `json:"currentSynthesis,omitempty"` PreviousSynthesis *Synthesis `json:"previousSynthesis,omitempty"` InputRevisions []InputRevisions `json:"inputRevisions,omitempty"` + + // Set +} + +// DependencyStatus holds the information regarding the composition's dependencies. +type DependencyStatus struct { + // Blocked is true when the composition cannot proceed due to dependencies/dependents + Blocked bool `json:"blocked,omitempty"` + + // Reason "WaitingOnDependencies", "WaitingOnDependents"(Deletion), "CircularDependencies" + Reason string `json:"reason,omitempty"` + + //BlockedBy: References to the compositions causing the block + BlockedBy []BlockedByRef `json:"blockedByRef,omitempty"` +} + +type BlockedByRef struct { + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` + Reason string `json:"reason,omitempty"` // "NotFound", "NotDeleted", "NotReady" } type SimplifiedStatus struct { diff --git a/api/v1/config/crd/eno.azure.io_compositions.yaml b/api/v1/config/crd/eno.azure.io_compositions.yaml index 196db13d..07e11051 100644 --- a/api/v1/config/crd/eno.azure.io_compositions.yaml +++ b/api/v1/config/crd/eno.azure.io_compositions.yaml @@ -87,6 +87,23 @@ spec: - resource type: object type: array + dependsOn: + description: |- + Declare depdendencies on other compositiosn by name and namespace. A composition can have at most 50 dependencies + Compositions will not be scheduled for synthesis until all required + dependencies have CurrentSynthesis.Ready != nil + Deletion is blocked until all non-optional dependents are fully removed. + items: + properties: + name: + description: Name of the dependency composition + type: string + namespace: + description: Namespace of the dependency composition + type: string + type: object + maxItems: 500 + type: array synthesisEnv: description: |- SynthesisEnv @@ -107,8 +124,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 @@ -158,6 +173,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 diff --git a/api/v1/config/crd/eno.azure.io_symphonies.yaml b/api/v1/config/crd/eno.azure.io_symphonies.yaml index 15d49ea8..1effa09d 100644 --- a/api/v1/config/crd/eno.azure.io_symphonies.yaml +++ b/api/v1/config/crd/eno.azure.io_symphonies.yaml @@ -129,6 +129,22 @@ spec: - resource type: object type: array + dependsOn: + description: |- + Dependencies for the composition created from this variation + References use synthesizer name - the symphony controller resolves them + to actual composition name when creating/updating compositions + Max dependencies is 50 + items: + properties: + synthesizer: + description: |- + Synthesizer name of the dependency variation (within the same symphony) + Resolved to the actual composition name by the symphony controller + type: string + type: object + maxItems: 50 + type: array labels: additionalProperties: type: string @@ -159,8 +175,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 @@ -210,6 +224,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 diff --git a/api/v1/symphony.go b/api/v1/symphony.go index 81c1a9e8..8d5c5420 100644 --- a/api/v1/symphony.go +++ b/api/v1/symphony.go @@ -71,6 +71,19 @@ type Variation struct { // Optional indicates that this variation should not block the symphony status // when it fails to synthesize, reconcile, or become ready. Optional bool `json:"optional,omitempty"` + + // Dependencies for the composition created from this variation + // References use synthesizer name - the symphony controller resolves them + // to actual composition name when creating/updating compositions + // Max dependencies is 50 + // +kubebuilder:validation:MaxItems:=50 + DependsOn []VariationDependency `json:"dependsOn,omitempty"` +} + +type VariationDependency struct { + // Synthesizer name of the dependency variation (within the same symphony) + // Resolved to the actual composition name by the symphony controller + Synthesizer string `json:"synthesizer,omitempty"` } func (c *Symphony) GetAzureOperationID() string { diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 277bc1f1..b7633d2d 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -26,6 +26,21 @@ func (in *Binding) DeepCopy() *Binding { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BlockedByRef) DeepCopyInto(out *BlockedByRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BlockedByRef. +func (in *BlockedByRef) DeepCopy() *BlockedByRef { + if in == nil { + return nil + } + out := new(BlockedByRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Composition) DeepCopyInto(out *Composition) { *out = *in @@ -53,6 +68,21 @@ func (in *Composition) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CompositionDependency) DeepCopyInto(out *CompositionDependency) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositionDependency. +func (in *CompositionDependency) DeepCopy() *CompositionDependency { + if in == nil { + return nil + } + out := new(CompositionDependency) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CompositionList) DeepCopyInto(out *CompositionList) { *out = *in @@ -99,6 +129,11 @@ func (in *CompositionSpec) DeepCopyInto(out *CompositionSpec) { *out = make([]EnvVar, len(*in)) copy(*out, *in) } + if in.DependsOn != nil { + in, out := &in.DependsOn, &out.DependsOn + *out = make([]CompositionDependency, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositionSpec. @@ -153,6 +188,26 @@ func (in *CompositionStatus) DeepCopy() *CompositionStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DependencyStatus) DeepCopyInto(out *DependencyStatus) { + *out = *in + if in.BlockedBy != nil { + in, out := &in.BlockedBy, &out.BlockedBy + *out = make([]BlockedByRef, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependencyStatus. +func (in *DependencyStatus) DeepCopy() *DependencyStatus { + if in == nil { + return nil + } + out := new(DependencyStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnvVar) DeepCopyInto(out *EnvVar) { *out = *in @@ -839,6 +894,11 @@ func (in *Variation) DeepCopyInto(out *Variation) { *out = make([]EnvVar, len(*in)) copy(*out, *in) } + if in.DependsOn != nil { + in, out := &in.DependsOn, &out.DependsOn + *out = make([]VariationDependency, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Variation. @@ -850,3 +910,18 @@ func (in *Variation) DeepCopy() *Variation { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VariationDependency) DeepCopyInto(out *VariationDependency) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VariationDependency. +func (in *VariationDependency) DeepCopy() *VariationDependency { + if in == nil { + return nil + } + out := new(VariationDependency) + in.DeepCopyInto(out) + return out +} From 8d14bd1a85f424aed4f483f2bc3ec9a4e5684289 Mon Sep 17 00:00:00 2001 From: Ruinan Liu Date: Mon, 6 Apr 2026 09:42:37 -0700 Subject: [PATCH 2/4] Resolving claw comments --- api/v1/composition.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/api/v1/composition.go b/api/v1/composition.go index 082df649..c0f87d19 100644 --- a/api/v1/composition.go +++ b/api/v1/composition.go @@ -60,11 +60,11 @@ type CompositionSpec struct { // +kubebuilder:validation:MaxItems:=500 SynthesisEnv []EnvVar `json:"synthesisEnv,omitempty"` - // Declare depdendencies on other compositiosn by name and namespace. A composition can have at most 50 dependencies + // Declare dependencies on other compositions by name and namespace. A composition can have at most 50 dependencies // Compositions will not be scheduled for synthesis until all required // dependencies have CurrentSynthesis.Ready != nil // Deletion is blocked until all non-optional dependents are fully removed. - // +kubebuilder:validation:MaxItems:=500 + // +kubebuilder:validation:MaxItems:=50 DependsOn []CompositionDependency `json:"dependsOn,omitempty"` } @@ -83,7 +83,8 @@ type CompositionStatus struct { PreviousSynthesis *Synthesis `json:"previousSynthesis,omitempty"` InputRevisions []InputRevisions `json:"inputRevisions,omitempty"` - // Set + // Set when composition is blocked by dependency constraints. Cleared when unblock + DependencyStatus *DependencyStatus `json:"dependencyStatus,omitempty"` } // DependencyStatus holds the information regarding the composition's dependencies. @@ -95,7 +96,7 @@ type DependencyStatus struct { Reason string `json:"reason,omitempty"` //BlockedBy: References to the compositions causing the block - BlockedBy []BlockedByRef `json:"blockedByRef,omitempty"` + BlockedBy []BlockedByRef `json:"blockedBy,omitempty"` } type BlockedByRef struct { From 3ac71edd588a8a4e0609e57b84891186afd3afb1 Mon Sep 17 00:00:00 2001 From: Ruinan Liu Date: Mon, 6 Apr 2026 09:44:40 -0700 Subject: [PATCH 3/4] make generate --- .../config/crd/eno.azure.io_compositions.yaml | 30 +++++++++++++++++-- api/v1/zz_generated.deepcopy.go | 5 ++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/api/v1/config/crd/eno.azure.io_compositions.yaml b/api/v1/config/crd/eno.azure.io_compositions.yaml index 07e11051..7889c1ad 100644 --- a/api/v1/config/crd/eno.azure.io_compositions.yaml +++ b/api/v1/config/crd/eno.azure.io_compositions.yaml @@ -89,7 +89,7 @@ spec: type: array dependsOn: description: |- - Declare depdendencies on other compositiosn by name and namespace. A composition can have at most 50 dependencies + Declare dependencies on other compositions by name and namespace. A composition can have at most 50 dependencies Compositions will not be scheduled for synthesis until all required dependencies have CurrentSynthesis.Ready != nil Deletion is blocked until all non-optional dependents are fully removed. @@ -102,7 +102,7 @@ spec: description: Namespace of the dependency composition type: string type: object - maxItems: 500 + maxItems: 50 type: array synthesisEnv: description: |- @@ -284,6 +284,32 @@ spec: Used internally for strict ordering semantics. type: string type: object + dependencyStatus: + description: Set when composition is blocked by dependency constraints. + Cleared when unblock + properties: + blocked: + description: Blocked is true when the composition cannot proceed + due to dependencies/dependents + type: boolean + blockedBy: + description: 'BlockedBy: References to the compositions causing + the block' + items: + properties: + name: + type: string + namespace: + type: string + reason: + type: string + type: object + type: array + reason: + description: Reason "WaitingOnDependencies", "WaitingOnDependents"(Deletion), + "CircularDependencies" + type: string + type: object inFlightSynthesis: description: |- A synthesis is the result of synthesizing a composition. diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index b7633d2d..521e05f0 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -176,6 +176,11 @@ func (in *CompositionStatus) DeepCopyInto(out *CompositionStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.DependencyStatus != nil { + in, out := &in.DependencyStatus, &out.DependencyStatus + *out = new(DependencyStatus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositionStatus. From c1e5cf4640a67a5816d5d1f276f61403ad008844 Mon Sep 17 00:00:00 2001 From: Ruinan Liu Date: Mon, 6 Apr 2026 10:08:45 -0700 Subject: [PATCH 4/4] Resolving comments --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7259ff2f..401d2095 100644 --- a/Makefile +++ b/Makefile @@ -9,14 +9,14 @@ ENO_RECONCILER_IMAGE_NAME ?= eno-reconciler .PHONY: docker-build-eno-controller docker-build-eno-controller: - DOCKER_BUILDKIT=1 docker build \ + docker build \ --file docker/$(ENO_CONTROLLER_IMAGE_NAME)/Dockerfile \ --tag $(REGISTRY)/$(ENO_CONTROLLER_IMAGE_NAME):$(ENO_CONTROLLER_IMAGE_VERSION) . docker push $(REGISTRY)/$(ENO_CONTROLLER_IMAGE_NAME):$(ENO_CONTROLLER_IMAGE_VERSION) .PHONY: docker-build-eno-reconciler docker-build-eno-reconciler: - DOCKER_BUILDKIT=1 docker build \ + docker build \ --file docker/$(ENO_RECONCILER_IMAGE_NAME)/Dockerfile \ --tag $(REGISTRY)/$(ENO_RECONCILER_IMAGE_NAME):$(ENO_RECONCILER_IMAGE_VERSION) . docker push $(REGISTRY)/$(ENO_RECONCILER_IMAGE_NAME):$(ENO_RECONCILER_IMAGE_VERSION)