diff --git a/Makefile b/Makefile index b6821bee..401d2095 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,20 @@ docker-build-eno-reconciler: --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..c0f87d19 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 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:=50 + 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,27 @@ type CompositionStatus struct { CurrentSynthesis *Synthesis `json:"currentSynthesis,omitempty"` PreviousSynthesis *Synthesis `json:"previousSynthesis,omitempty"` InputRevisions []InputRevisions `json:"inputRevisions,omitempty"` + + // 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. +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:"blockedBy,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..7889c1ad 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 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. + items: + properties: + name: + description: Name of the dependency composition + type: string + namespace: + description: Namespace of the dependency composition + type: string + type: object + maxItems: 50 + 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 @@ -267,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/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..521e05f0 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. @@ -141,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. @@ -153,6 +193,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 +899,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 +915,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 +}