From 27bf71a37c36fd46e2fd61dd93abe6c31ae9e66a Mon Sep 17 00:00:00 2001 From: ontave Date: Tue, 12 May 2026 11:42:15 +0200 Subject: [PATCH 01/19] feat(phase-2.1): remove copied InfrastructurePackBuild CRD YAML Non-canonical copy deleted. seam-core is the authority and the type has been removed from seam-core in this migration phase. --- ...re.ontai.dev_infrastructurepackbuilds.yaml | 196 ------------------ 1 file changed, 196 deletions(-) delete mode 100644 config/crd/infrastructure.ontai.dev_infrastructurepackbuilds.yaml diff --git a/config/crd/infrastructure.ontai.dev_infrastructurepackbuilds.yaml b/config/crd/infrastructure.ontai.dev_infrastructurepackbuilds.yaml deleted file mode 100644 index 94281d1..0000000 --- a/config/crd/infrastructure.ontai.dev_infrastructurepackbuilds.yaml +++ /dev/null @@ -1,196 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.16.1 - name: infrastructurepackbuilds.infrastructure.ontai.dev -spec: - group: infrastructure.ontai.dev - names: - kind: InfrastructurePackBuild - listKind: InfrastructurePackBuildList - plural: infrastructurepackbuilds - shortNames: - - ipb - singular: infrastructurepackbuild - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.category - name: Category - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: |- - InfrastructurePackBuild is the seam-core CRD for compiler input specification. - Compiler reads this at compile time; never applied to a cluster as a live CR. - conductor-schema.md §7. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: |- - InfrastructurePackBuildSpec defines the desired state of an InfrastructurePackBuild. - Compiler input specification. Read by the Compiler at compile time; never applied to a cluster as a CR. - conductor-schema.md §7. - properties: - category: - allOf: - - enum: - - helm - - kustomize - - raw - - enum: - - helm - - kustomize - - raw - description: 'Category declares the compilation category. Must be - one of: helm, kustomize, raw.' - type: string - componentName: - description: ComponentName is the name of the component being compiled. - type: string - helmSource: - description: HelmSource describes the Helm chart source. Required - when category=helm. - properties: - chart: - description: Chart is the chart name used for rendering context. - type: string - url: - description: URL is the full URL to the Helm chart tarball (.tgz). - type: string - valuesFile: - description: ValuesFile is the path to a YAML values file. Optional. - type: string - version: - description: Version is the chart version string. - type: string - required: - - chart - - url - - version - type: object - kustomizeSource: - description: KustomizeSource describes the Kustomize source. Required - when category=kustomize. - properties: - path: - description: Path is the path to the kustomization root directory. - type: string - required: - - path - type: object - rawSource: - description: RawSource describes the raw manifest source. Required - when category=raw. - properties: - path: - description: Path is the path to the directory containing raw - YAML manifests. - type: string - required: - - path - type: object - targetClusters: - description: TargetClusters is the list of cluster names to which - the compiled pack should be delivered. - items: - type: string - type: array - required: - - category - - componentName - type: object - status: - description: InfrastructurePackBuildStatus is the observed state of an - InfrastructurePackBuild. - properties: - conditions: - description: Conditions is the list of status conditions for this - PackBuild. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - observedGeneration: - description: ObservedGeneration is the generation most recently reconciled. - format: int64 - type: integer - type: object - type: object - served: true - storage: true - subresources: - status: {} From 5a8cfb9d0e753e80302bbefae8fba96e6f033abd Mon Sep 17 00:00:00 2001 From: ontave Date: Tue, 12 May 2026 12:35:41 +0200 Subject: [PATCH 02/19] feat(phase-2.3+3.3-3.7): migrate dispatcher types from seam-core to wrapper Phase 2 step 2.3: delete all infrastructure.ontai.dev CRD YAML files copied into wrapper/config/crd/ -- these were stale copies that would conflict with the new seam.ontai.dev definitions after the move. Phase 3 steps 3.3-3.7: define PackDelivery, PackExecution, PackInstalled, PackReceipt, and PackLog as first-class types under seam.ontai.dev/v1alpha1 in wrapper/api/seam/v1alpha1/. Generate CRD YAML into config/crd/bases/ and wire embed.go to expose them via compileLaunchBundle. WrapperRunnerRBAC gate renamed to DispatcherRunnerRBAC throughout the PackExecution reconciler. clusterPackRef field references updated to packDeliveryRef in all tests and production code. --- api/seam/v1alpha1/groupversion_info.go | 25 + api/seam/v1alpha1/packdelivery_types.go | 191 +++++ api/seam/v1alpha1/packexecution_types.go | 103 +++ api/seam/v1alpha1/packinstalled_types.go | 148 ++++ api/seam/v1alpha1/packlog_types.go | 223 +++++ api/seam/v1alpha1/packreceipt_types.go | 132 +++ api/seam/v1alpha1/zz_generated.deepcopy.go | 769 ++++++++++++++++++ cmd/wrapper/main.go | 2 + .../seam.ontai.dev_packdeliveries.yaml} | 53 +- .../seam.ontai.dev_packexecutions.yaml} | 67 +- .../seam.ontai.dev_packinstalleds.yaml} | 55 +- .../seam.ontai.dev_packlogs.yaml} | 68 +- .../seam.ontai.dev_packreceipts.yaml} | 117 ++- config/crd/embed.go | 29 +- ...infrastructure.ontai.dev_driftsignals.yaml | 202 ----- ...ontai.dev_infrastructurerunnerconfigs.yaml | 323 -------- internal/controller/clusterpack_reconciler.go | 61 +- .../controller/packexecution_reconciler.go | 122 +-- .../controller/packinstance_reconciler.go | 44 +- test/integration/clusterpack_test.go | 40 +- test/unit/clusterpack_reconciler_test.go | 50 +- test/unit/controller/helpers_test.go | 40 +- .../controller/kueue_job_scheduling_test.go | 18 +- .../controller/packexecution_gates_test.go | 20 +- .../controller/packinstance_lifecycle_test.go | 70 +- test/unit/controller/packversion_test.go | 20 +- test/unit/controller/por_label_lookup_test.go | 22 +- test/unit/controller/rollback_test.go | 24 +- test/unit/dispatcher_types_test.go | 275 +++++++ test/unit/helm_metadata_types_test.go | 32 +- test/unit/packexecution_reconciler_test.go | 68 +- test/unit/packinstance_reconciler_test.go | 76 +- test/unit/packinstance_watch_test.go | 6 +- test/unit/role_independence_test.go | 34 +- 34 files changed, 2482 insertions(+), 1047 deletions(-) create mode 100644 api/seam/v1alpha1/groupversion_info.go create mode 100644 api/seam/v1alpha1/packdelivery_types.go create mode 100644 api/seam/v1alpha1/packexecution_types.go create mode 100644 api/seam/v1alpha1/packinstalled_types.go create mode 100644 api/seam/v1alpha1/packlog_types.go create mode 100644 api/seam/v1alpha1/packreceipt_types.go create mode 100644 api/seam/v1alpha1/zz_generated.deepcopy.go rename config/crd/{infrastructure.ontai.dev_infrastructureclusterpacks.yaml => bases/seam.ontai.dev_packdeliveries.yaml} (89%) rename config/crd/{infrastructure.ontai.dev_infrastructurepackexecutions.yaml => bases/seam.ontai.dev_packexecutions.yaml} (90%) rename config/crd/{infrastructure.ontai.dev_infrastructurepackinstances.yaml => bases/seam.ontai.dev_packinstalleds.yaml} (85%) rename config/crd/{infrastructure.ontai.dev_packoperationresults.yaml => bases/seam.ontai.dev_packlogs.yaml} (83%) rename config/crd/{infrastructure.ontai.dev_infrastructurepackreceipts.yaml => bases/seam.ontai.dev_packreceipts.yaml} (60%) delete mode 100644 config/crd/infrastructure.ontai.dev_driftsignals.yaml delete mode 100644 config/crd/infrastructure.ontai.dev_infrastructurerunnerconfigs.yaml create mode 100644 test/unit/dispatcher_types_test.go diff --git a/api/seam/v1alpha1/groupversion_info.go b/api/seam/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..24652ad --- /dev/null +++ b/api/seam/v1alpha1/groupversion_info.go @@ -0,0 +1,25 @@ +// Package v1alpha1 contains API types for the seam.ontai.dev/v1alpha1 API group. +// +// This package owns all dispatcher (wrapper) CRD types: PackDelivery, PackExecution, +// PackInstalled, PackReceipt, and PackLog. All types live under the seam.ontai.dev +// group and are registered here. INV-008 -- this value is ground truth. +// +// +groupName=seam.ontai.dev +// +kubebuilder:object:generate=true +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is the group and version for all dispatcher types in this package. + GroupVersion = schema.GroupVersion{Group: "seam.ontai.dev", Version: "v1alpha1"} + + // SchemeBuilder is used to add Go types to the Kubernetes runtime scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds all dispatcher types in this package to the provided scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/seam/v1alpha1/packdelivery_types.go b/api/seam/v1alpha1/packdelivery_types.go new file mode 100644 index 0000000..542a8b9 --- /dev/null +++ b/api/seam/v1alpha1/packdelivery_types.go @@ -0,0 +1,191 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/ontai-dev/seam-core/pkg/lineage" +) + +// LifecyclePolicy controls artifact retention behavior. +// wrapper-schema.md §3 PackDelivery spec.lifecyclePolicies. +type LifecyclePolicy struct { + // RetainOnDeletion controls whether the OCI artifact is retained when the + // PackDelivery CR is deleted. Default: true (artifact retained). + // +optional + // +kubebuilder:default=true + RetainOnDeletion bool `json:"retainOnDeletion,omitempty"` +} + +// PackRegistryRef identifies the OCI artifact for a PackDelivery. +type PackRegistryRef struct { + // URL is the OCI registry URL including image name. + URL string `json:"url"` + + // Digest is the OCI image digest (e.g., sha256:abc123...). Immutable after creation. + Digest string `json:"digest"` +} + +// PackDeliveryStage is a single stage in the pack execution order. +type PackDeliveryStage struct { + // Name is the stage name. Must be one of: rbac, storage, stateful, stateless. + // +kubebuilder:validation:Enum=rbac;storage;stateful;stateless + Name string `json:"name"` + + // Manifests is the list of manifest names to apply in this stage. + // +optional + Manifests []string `json:"manifests,omitempty"` +} + +// PackProvenance records build-time metadata for audit and traceability. +type PackProvenance struct { + // BuildID is the CI/CD build identifier that produced this pack. + // +optional + BuildID string `json:"buildID,omitempty"` + + // BuildTimestamp is when the pack artifact was produced. + // +optional + BuildTimestamp *metav1.Time `json:"buildTimestamp,omitempty"` + + // SourceRef is the git reference (commit SHA or tag) from which the pack was built. + // +optional + SourceRef string `json:"sourceRef,omitempty"` +} + +// PackDeliverySpec defines the desired state of a PackDelivery. +// All fields are immutable after creation. wrapper-schema.md §3. +type PackDeliverySpec struct { + // Version is the semantic version of this pack. Immutable after creation. + Version string `json:"version"` + + // RegistryRef identifies the OCI artifact for this pack. Immutable after creation. + RegistryRef PackRegistryRef `json:"registryRef"` + + // Checksum is the content-addressed checksum of the full artifact manifest set. + // +optional + Checksum string `json:"checksum,omitempty"` + + // RBACDigest is the OCI digest of the RBAC layer of this PackDelivery artifact. + // Contains ServiceAccount, Role, ClusterRole, RoleBinding, ClusterRoleBinding manifests. + // +optional + RBACDigest string `json:"rbacDigest,omitempty"` + + // WorkloadDigest is the OCI digest of the workload layer of this PackDelivery artifact. + // Applied after guardian RBACProfile reaches provisioned=true. wrapper-schema.md §4. + // +optional + WorkloadDigest string `json:"workloadDigest,omitempty"` + + // ClusterScopedDigest is the OCI digest of the cluster-scoped non-RBAC layer. + // Applied after guardian RBAC intake and before workload manifests. wrapper-schema.md §4. + // +optional + ClusterScopedDigest string `json:"clusterScopedDigest,omitempty"` + + // SourceBuildRef is an opaque reference to the build that produced this pack. Informational. + // +optional + SourceBuildRef string `json:"sourceBuildRef,omitempty"` + + // ExecutionOrder defines the ordered stages in which pack manifests are applied. + // +optional + ExecutionOrder []PackDeliveryStage `json:"executionOrder,omitempty"` + + // Provenance records build-time metadata for audit and traceability. + // +optional + Provenance *PackProvenance `json:"provenance,omitempty"` + + // BasePackName is the logical pack name shared across versions (e.g., "nginx-ingress"). + // +optional + BasePackName string `json:"basePackName,omitempty"` + + // TargetClusters is the list of cluster names to which this PackDelivery should be delivered. + // +optional + TargetClusters []string `json:"targetClusters,omitempty"` + + // ChartVersion is the version of the Helm chart used to compile this pack. + // +optional + ChartVersion string `json:"chartVersion,omitempty"` + + // ChartURL is the URL of the Helm chart repository used to compile this pack. + // +optional + ChartURL string `json:"chartURL,omitempty"` + + // ChartName is the name of the Helm chart used to compile this pack. + // +optional + ChartName string `json:"chartName,omitempty"` + + // HelmVersion is the version of the Helm SDK used to render this pack. + // +optional + HelmVersion string `json:"helmVersion,omitempty"` + + // ValuesFile is the path to the values file used during pack compilation. + // +optional + ValuesFile string `json:"valuesFile,omitempty"` + + // LifecyclePolicies controls artifact retention behavior. + // +optional + LifecyclePolicies *LifecyclePolicy `json:"lifecyclePolicies,omitempty"` + + // Lineage is the sealed causal chain record for this root declaration. + // Authored once at object creation time and immutable thereafter. + // seam-core-schema.md §5, CLAUDE.md §14 Decision 1. + // +optional + Lineage *lineage.SealedCausalChain `json:"lineage,omitempty"` + + // RollbackToRevision instructs the dispatcher to restore this PackDelivery to the + // artifact version deployed at PackLog revision N-1. When set, the PackDeliveryReconciler + // reads PreviousClusterPackVersion/PreviousRBACDigest/PreviousWorkloadDigest from + // the current PackLog, patches spec.version and spec.*Digest to those values, removes + // the spec-checksum-snapshot annotation, and clears this field. Only one-step + // rollback (to revision N-1) is supported per invocation. Set to 0 to disable. + // Governor-controlled only. wrapper-schema.md §6.2. seam-core-schema.md §7.8. + // +optional + RollbackToRevision int64 `json:"rollbackToRevision,omitempty"` +} + +// PackDeliveryStatus is the observed state of a PackDelivery. +type PackDeliveryStatus struct { + // Signed indicates whether the conductor signing loop has signed this pack. + // +optional + Signed bool `json:"signed,omitempty"` + + // PackSignature is the base64-encoded Ed25519 signature produced by the management cluster conductor. + // +optional + PackSignature string `json:"packSignature,omitempty"` + + // ObservedGeneration is the generation most recently reconciled. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Conditions is the list of status conditions for this PackDelivery. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=pd +// +kubebuilder:printcolumn:name="Version",type=string,JSONPath=".spec.version" +// +kubebuilder:printcolumn:name="Signed",type=boolean,JSONPath=".status.signed" +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" + +// PackDelivery is the dispatcher CRD for pack registration. +// Records an OCI artifact that has been compiled and is ready for runtime delivery. +// Spec is immutable after creation. wrapper-schema.md §3. +type PackDelivery struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PackDeliverySpec `json:"spec,omitempty"` + Status PackDeliveryStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// PackDeliveryList contains a list of PackDelivery. +type PackDeliveryList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PackDelivery `json:"items"` +} + +func init() { + SchemeBuilder.Register(&PackDelivery{}, &PackDeliveryList{}) +} diff --git a/api/seam/v1alpha1/packexecution_types.go b/api/seam/v1alpha1/packexecution_types.go new file mode 100644 index 0000000..f2a822c --- /dev/null +++ b/api/seam/v1alpha1/packexecution_types.go @@ -0,0 +1,103 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/ontai-dev/seam-core/pkg/lineage" +) + +// PackDeliveryRef identifies a specific PackDelivery by name and version. +// wrapper-schema.md §3 PackExecution spec.packDeliveryRef. +type PackDeliveryRef struct { + // Name is the name of the PackDelivery CR. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // Version is the expected version of the PackDelivery. Guards against name reuse. + // +kubebuilder:validation:MinLength=1 + Version string `json:"version"` +} + +// PackExecutionSpec defines the desired state of a PackExecution. +// wrapper-schema.md §3. +type PackExecutionSpec struct { + // PackDeliveryRef identifies the PackDelivery to deploy. + PackDeliveryRef PackDeliveryRef `json:"packDeliveryRef"` + + // TargetClusterRef is the name of the target cluster to deliver the pack to. + TargetClusterRef string `json:"targetClusterRef"` + + // AdmissionProfileRef is the name of the RBACProfile governing this execution. + // +optional + AdmissionProfileRef string `json:"admissionProfileRef,omitempty"` + + // ChartVersion is the Helm chart version for this execution. Carried from PackDelivery. + // +optional + ChartVersion string `json:"chartVersion,omitempty"` + + // ChartURL is the Helm chart repository URL. Carried from PackDelivery. + // +optional + ChartURL string `json:"chartURL,omitempty"` + + // ChartName is the Helm chart name. Carried from PackDelivery. + // +optional + ChartName string `json:"chartName,omitempty"` + + // HelmVersion is the Helm SDK version used to render the pack. Carried from PackDelivery. + // +optional + HelmVersion string `json:"helmVersion,omitempty"` + + // Lineage is the sealed causal chain record for this root declaration. Immutable after creation. + // +optional + Lineage *lineage.SealedCausalChain `json:"lineage,omitempty"` +} + +// PackExecutionStatus is the observed state of a PackExecution. +type PackExecutionStatus struct { + // ObservedGeneration is the generation most recently reconciled. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // JobName is the name of the pack-deploy Kueue Job submitted for this execution. + // +optional + JobName string `json:"jobName,omitempty"` + + // OperationResultRef is the name of the PackLog CR written after + // successful pack-deploy Job completion. + // +optional + OperationResultRef string `json:"operationResultRef,omitempty"` + + // Conditions is the list of status conditions for this PackExecution. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=pe +// +kubebuilder:printcolumn:name="Pack",type=string,JSONPath=".spec.packDeliveryRef.name" +// +kubebuilder:printcolumn:name="Target",type=string,JSONPath=".spec.targetClusterRef" +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" + +// PackExecution is the dispatcher CRD for a runtime pack delivery request. +// wrapper-schema.md §3. +type PackExecution struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PackExecutionSpec `json:"spec,omitempty"` + Status PackExecutionStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// PackExecutionList contains a list of PackExecution. +type PackExecutionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PackExecution `json:"items"` +} + +func init() { + SchemeBuilder.Register(&PackExecution{}, &PackExecutionList{}) +} diff --git a/api/seam/v1alpha1/packinstalled_types.go b/api/seam/v1alpha1/packinstalled_types.go new file mode 100644 index 0000000..ce0060f --- /dev/null +++ b/api/seam/v1alpha1/packinstalled_types.go @@ -0,0 +1,148 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DriftPolicy controls how dependency drift is handled. +// wrapper-schema.md §3 PackInstalled spec.dependencyPolicy.onDrift. +type DriftPolicy string + +const ( + // DriftPolicyBlock stops all further pack ops on this cluster + // when a dependency PackInstalled reports Drifted=True. + DriftPolicyBlock DriftPolicy = "Block" + + // DriftPolicyWarn emits a warning event when a dependency + // PackInstalled reports Drifted=True but does not block further pack ops. + DriftPolicyWarn DriftPolicy = "Warn" + + // DriftPolicyIgnore takes no action when a dependency + // PackInstalled reports Drifted=True. + DriftPolicyIgnore DriftPolicy = "Ignore" +) + +// DependencyPolicy defines behavior when a dependency PackInstalled reports drift. +// wrapper-schema.md §3 PackInstalled spec.dependencyPolicy. +type DependencyPolicy struct { + // OnDrift controls how this PackInstalled responds when a declared dependency + // PackInstalled reports Drifted=True. + // +kubebuilder:validation:Enum=Block;Warn;Ignore + // +kubebuilder:default=Warn + OnDrift DriftPolicy `json:"onDrift,omitempty"` +} + +// DeployedResourceRef records a single Kubernetes resource applied +// by the pack-deploy job. Used by the PackInstalled deletion handler to clean up +// deployed workload when the PackDelivery is deleted. wrapper-schema.md §3. +type DeployedResourceRef struct { + // APIVersion is the Kubernetes apiVersion (e.g., apps/v1, v1). + APIVersion string `json:"apiVersion"` + + // Kind is the Kubernetes resource Kind (e.g., Deployment, Namespace). + Kind string `json:"kind"` + + // Namespace is the resource namespace. Empty for cluster-scoped resources. + // +optional + Namespace string `json:"namespace,omitempty"` + + // Name is the resource name. + Name string `json:"name"` +} + +// PackInstalledSpec defines the desired state of a PackInstalled. +// wrapper-schema.md §3. +type PackInstalledSpec struct { + // PackDeliveryRef is the name of the PackDelivery CR this instance tracks. + PackDeliveryRef string `json:"packDeliveryRef"` + + // Version is the pack version delivered to the target cluster. + Version string `json:"version"` + + // TargetClusterRef is the name of the target cluster this instance is installed on. + TargetClusterRef string `json:"targetClusterRef"` + + // DependsOn is the list of pack base names that must be Delivered before this instance. + // +optional + DependsOn []string `json:"dependsOn,omitempty"` + + // DependencyPolicy defines behavior when a dependency reports drift. + // +optional + DependencyPolicy *DependencyPolicy `json:"dependencyPolicy,omitempty"` + + // ChartVersion is the Helm chart version for this instance. Carried from PackDelivery. + // +optional + ChartVersion string `json:"chartVersion,omitempty"` + + // ChartURL is the Helm chart repository URL. Carried from PackDelivery. + // +optional + ChartURL string `json:"chartURL,omitempty"` + + // ChartName is the Helm chart name. Carried from PackDelivery. + // +optional + ChartName string `json:"chartName,omitempty"` + + // HelmVersion is the Helm SDK version used to render the pack. Carried from PackDelivery. + // +optional + HelmVersion string `json:"helmVersion,omitempty"` +} + +// PackInstalledStatus is the observed state of a PackInstalled. +type PackInstalledStatus struct { + // ObservedGeneration is the generation most recently reconciled. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // DeliveredAt records when the pack was most recently confirmed delivered. + // +optional + DeliveredAt *metav1.Time `json:"deliveredAt,omitempty"` + + // DriftSummary is a human-readable summary of the current drift state. + // +optional + DriftSummary string `json:"driftSummary,omitempty"` + + // UpgradeDirection records the version transition direction for the last deployment. + // Initial: first deployment. Upgrade: newer version. Rollback: older version. Redeploy: same version. + // +optional + // +kubebuilder:validation:Enum=Initial;Upgrade;Rollback;Redeploy + UpgradeDirection string `json:"upgradeDirection,omitempty"` + + // DeployedResources is the list of Kubernetes resources applied by the pack-deploy job. + // Used by the PackInstalled deletion handler for cleanup. + // +optional + DeployedResources []DeployedResourceRef `json:"deployedResources,omitempty"` + + // Conditions is the list of status conditions for this PackInstalled. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=pi +// +kubebuilder:printcolumn:name="Pack",type=string,JSONPath=".spec.packDeliveryRef" +// +kubebuilder:printcolumn:name="Target",type=string,JSONPath=".spec.targetClusterRef" +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" + +// PackInstalled is the dispatcher CRD recording the delivered state of a pack on a cluster. +// wrapper-schema.md §3. +type PackInstalled struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PackInstalledSpec `json:"spec,omitempty"` + Status PackInstalledStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// PackInstalledList contains a list of PackInstalled. +type PackInstalledList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PackInstalled `json:"items"` +} + +func init() { + SchemeBuilder.Register(&PackInstalled{}, &PackInstalledList{}) +} diff --git a/api/seam/v1alpha1/packlog_types.go b/api/seam/v1alpha1/packlog_types.go new file mode 100644 index 0000000..b6799a1 --- /dev/null +++ b/api/seam/v1alpha1/packlog_types.go @@ -0,0 +1,223 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// PackLogResultStatus is the terminal status of a pack-deploy capability execution. +// +kubebuilder:validation:Enum=Succeeded;Failed +type PackLogResultStatus string + +const ( + PackLogResultSucceeded PackLogResultStatus = "Succeeded" + PackLogResultFailed PackLogResultStatus = "Failed" +) + +// PackLogUpgradeDirection records the version transition direction for a PackInstalled. +// +kubebuilder:validation:Enum=Initial;Upgrade;Rollback;Redeploy +type PackLogUpgradeDirection string + +const ( + PackLogUpgradeDirectionInitial PackLogUpgradeDirection = "Initial" + PackLogUpgradeDirectionUpgrade PackLogUpgradeDirection = "Upgrade" + PackLogUpgradeDirectionRollback PackLogUpgradeDirection = "Rollback" + PackLogUpgradeDirectionRedeploy PackLogUpgradeDirection = "Redeploy" +) + +// PackLogDeployedResource records a single Kubernetes resource applied +// during a pack-deploy execution. +type PackLogDeployedResource struct { + // APIVersion is the Kubernetes apiVersion (e.g., apps/v1, v1). + APIVersion string `json:"apiVersion"` + + // Kind is the Kubernetes resource Kind (e.g., Deployment, Namespace). + Kind string `json:"kind"` + + // Namespace is the resource namespace. Empty for cluster-scoped resources. + // +optional + Namespace string `json:"namespace,omitempty"` + + // Name is the resource name. + Name string `json:"name"` +} + +// PackLogArtifact is a structured reference to an artifact produced +// by a pack-deploy execution. Never contains raw artifact content. +type PackLogArtifact struct { + // Name is a logical identifier for this artifact. + Name string `json:"name"` + + // Kind declares the artifact type. One of: ConfigMap, Secret, OCIImage, S3Object. + // +kubebuilder:validation:Enum=ConfigMap;Secret;OCIImage;S3Object + Kind string `json:"kind"` + + // Reference is the fully qualified reference for the artifact kind. + Reference string `json:"reference"` + + // Checksum is the content-addressed checksum. Format: sha256:. + // +optional + Checksum string `json:"checksum,omitempty"` +} + +// PackLogStepResult is the execution result for one step within a +// multi-step capability. +type PackLogStepResult struct { + // Name is the step identifier within the capability. + Name string `json:"name"` + + // Status is the terminal status of this step. + // +kubebuilder:validation:Enum=Succeeded;Failed + Status PackLogResultStatus `json:"status"` + + // StartedAt is the time this step began execution. + // +optional + StartedAt *metav1.Time `json:"startedAt,omitempty"` + + // CompletedAt is the time this step finished execution. + // +optional + CompletedAt *metav1.Time `json:"completedAt,omitempty"` + + // Message provides additional context about the step outcome. + // +optional + Message string `json:"message,omitempty"` +} + +// PackLogFailureReason is a structured failure description. +type PackLogFailureReason struct { + // Category classifies the failure domain. + // +kubebuilder:validation:Enum=ValidationFailure;CapabilityUnavailable;ExecutionFailure;ExternalDependencyFailure;InvariantViolation;LicenseViolation;StorageUnavailable + Category string `json:"category"` + + // Reason is a human-readable description of the specific failure. + Reason string `json:"reason"` + + // FailedStep is the name of the step that failed. Empty for single-step capabilities. + // +optional + FailedStep string `json:"failedStep,omitempty"` +} + +// PackLogSpec is the complete result document written by the +// Conductor execute-mode Job before exit. Written by conductor; read by dispatcher. +// seam-core-schema.md §8, Decision 11. +type PackLogSpec struct { + // Revision is the monotonically increasing revision counter for this pack operation + // sequence. Incremented each time a new result supersedes the previous one. + Revision int64 `json:"revision"` + + // PreviousRevisionRef is the name of the PackLog CR that was superseded when + // this revision was written. Absent for revision 1 (no predecessor). + // +optional + PreviousRevisionRef string `json:"previousRevisionRef,omitempty"` + + // TalosClusterOperationResultRef is reserved for future cross-reference to a + // TalosCluster-scoped OperationResult. Stub field; not populated by any current + // controller. + // +optional + TalosClusterOperationResultRef string `json:"talosClusterOperationResultRef,omitempty"` + + // PackExecutionRef is the name of the PackExecution CR that triggered this operation. + // +optional + PackExecutionRef string `json:"packExecutionRef,omitempty"` + + // PackDeliveryRef is the name of the PackDelivery CR that was deployed. + // +optional + PackDeliveryRef string `json:"packDeliveryRef,omitempty"` + + // TargetClusterRef is the name of the target cluster this operation ran against. + // +optional + TargetClusterRef string `json:"targetClusterRef,omitempty"` + + // Capability is the name of the Conductor capability that produced this result. + Capability string `json:"capability"` + + // Phase identifies the RunnerConfig phase this result belongs to. + // +optional + Phase string `json:"phase,omitempty"` + + // Status is the terminal status of the capability execution. + // +kubebuilder:validation:Enum=Succeeded;Failed + Status PackLogResultStatus `json:"status"` + + // StartedAt is the time the capability execution began. + // +optional + StartedAt *metav1.Time `json:"startedAt,omitempty"` + + // CompletedAt is the time the capability execution finished. + // +optional + CompletedAt *metav1.Time `json:"completedAt,omitempty"` + + // FailureReason is populated when Status is Failed. Nil on success. + // +optional + FailureReason *PackLogFailureReason `json:"failureReason,omitempty"` + + // DeployedResources is the list of Kubernetes resources applied during this + // execution. Populated by pack-deploy on success. Used by PackInstalledReconciler + // for deletion cleanup. + // +optional + DeployedResources []PackLogDeployedResource `json:"deployedResources,omitempty"` + + // Artifacts is the list of artifacts produced by this execution. + // +optional + Artifacts []PackLogArtifact `json:"artifacts,omitempty"` + + // Steps contains individual step results for multi-step capabilities. + // +optional + Steps []PackLogStepResult `json:"steps,omitempty"` + + // PackDeliveryVersion is the PackDelivery spec.version deployed in this operation. + // Populated by the Conductor executor at write time. Rollback anchor. + // seam-core-schema.md §7.8. + // +optional + PackDeliveryVersion string `json:"packDeliveryVersion,omitempty"` + + // RBACDigest is the OCI digest of the RBAC layer deployed in this operation. + // Copied from PackDelivery.spec.rbacDigest at deploy time. Rollback anchor. + // +optional + RBACDigest string `json:"rbacDigest,omitempty"` + + // WorkloadDigest is the OCI digest of the workload layer deployed in this operation. + // Copied from PackDelivery.spec.workloadDigest at deploy time. Rollback anchor. + // +optional + WorkloadDigest string `json:"workloadDigest,omitempty"` +} + +// PackLogStatus is the observed state of a PackLog. +// Currently empty; reserved for future controller-set conditions. +type PackLogStatus struct { + // ObservedGeneration is the last generation processed by any consumer. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=pl +// +kubebuilder:printcolumn:name="Capability",type=string,JSONPath=`.spec.capability` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.spec.status` +// +kubebuilder:printcolumn:name="Cluster",type=string,JSONPath=`.spec.targetClusterRef` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` + +// PackLog is the immutable result record written by the Conductor +// execute-mode Job after a pack-deploy capability completes. One PackLog +// per PackExecution, created in namespace seam-tenant-{clusterName}. +// seam-core-schema.md §8, Decision 11. +type PackLog struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PackLogSpec `json:"spec,omitempty"` + Status PackLogStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// PackLogList contains a list of PackLog. +type PackLogList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PackLog `json:"items"` +} + +func init() { + SchemeBuilder.Register(&PackLog{}, &PackLogList{}) +} diff --git a/api/seam/v1alpha1/packreceipt_types.go b/api/seam/v1alpha1/packreceipt_types.go new file mode 100644 index 0000000..ab13066 --- /dev/null +++ b/api/seam/v1alpha1/packreceipt_types.go @@ -0,0 +1,132 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// PackReceiptDeployedResource records a single Kubernetes resource that was applied +// to the tenant cluster as part of a pack-deploy Job. Used by conductor role=tenant +// to detect drift between declared and actual cluster state. +// CLUSTERPACK-BL-VERSION-CLEANUP. conductor-schema.md. +type PackReceiptDeployedResource struct { + // APIVersion is the full API version string (e.g., "apps/v1"). + APIVersion string `json:"apiVersion"` + + // Kind is the resource kind (e.g., "Deployment"). + Kind string `json:"kind"` + + // Namespace is the namespace the resource was applied to. Empty for cluster-scoped resources. + // +optional + Namespace string `json:"namespace,omitempty"` + + // Name is the resource name. + Name string `json:"name"` +} + +// PackReceiptSpec defines the desired state of a PackReceipt. +// Written by the packinstalled pull loop on the tenant cluster conductor after +// Ed25519 signature verification. INV-026. conductor-schema.md. +type PackReceiptSpec struct { + // PackInstalledRef is the name of the PackInstalled CR this receipt acknowledges. + // +optional + PackInstalledRef string `json:"packInstalledRef,omitempty"` + + // SignatureRef is the name of the signed artifact Secret on the management cluster + // (seam-pack-signed-{cluster}-{packInstalled}) from which this receipt was derived. + // +optional + SignatureRef string `json:"signatureRef,omitempty"` + + // PackDeliveryRef is the name of the PackDelivery CR this receipt acknowledges. + PackDeliveryRef string `json:"packDeliveryRef"` + + // TargetClusterRef is the name of the cluster this receipt was generated on. + TargetClusterRef string `json:"targetClusterRef"` + + // RBACDigest is the OCI digest of the RBAC layer. Carried from PackDelivery for audit. + // +optional + RBACDigest string `json:"rbacDigest,omitempty"` + + // WorkloadDigest is the OCI digest of the workload layer. Carried from PackDelivery. + // +optional + WorkloadDigest string `json:"workloadDigest,omitempty"` + + // ChartVersion is the Helm chart version. Carried from PackDelivery. + // +optional + ChartVersion string `json:"chartVersion,omitempty"` + + // ChartURL is the Helm chart repository URL. Carried from PackDelivery. + // +optional + ChartURL string `json:"chartURL,omitempty"` + + // ChartName is the Helm chart name. Carried from PackDelivery. + // +optional + ChartName string `json:"chartName,omitempty"` + + // HelmVersion is the Helm SDK version. Carried from PackDelivery. + // +optional + HelmVersion string `json:"helmVersion,omitempty"` + + // DeployedResources is the inventory of Kubernetes resources applied to the tenant cluster + // during the pack-deploy Job. Conductor role=tenant uses this list to detect drift by + // verifying each resource still exists with the expected state. + // CLUSTERPACK-BL-VERSION-CLEANUP, conductor-schema.md. + // +optional + DeployedResources []PackReceiptDeployedResource `json:"deployedResources,omitempty"` +} + +// PackReceiptStatus is the observed state of a PackReceipt. +// Written by the packinstalled pull loop after signature verification. INV-026. +type PackReceiptStatus struct { + // Verified indicates whether the Ed25519 signature on the PackInstalled artifact + // was successfully verified against the management cluster's public key. INV-026. + // +optional + Verified bool `json:"verified,omitempty"` + + // Signature is the base64-encoded Ed25519 signature from the signed artifact Secret. + // Stored for auditability and idempotency checking. INV-026. + // +optional + Signature string `json:"signature,omitempty"` + + // VerificationFailedReason is set when Verified=false and describes the + // specific verification failure (e.g., "Ed25519 signature verification failed (INV-026)"). + // +optional + VerificationFailedReason string `json:"verificationFailedReason,omitempty"` + + // ObservedGeneration is the generation most recently reconciled. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Conditions is the list of status conditions for this PackReceipt. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,shortName=pr +// +kubebuilder:printcolumn:name="Pack",type=string,JSONPath=".spec.packDeliveryRef" +// +kubebuilder:printcolumn:name="Verified",type=boolean,JSONPath=".status.verified" +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" + +// PackReceipt is the dispatcher CRD for pack delivery acknowledgement on a tenant cluster. +// Written by conductor agent after signature verification. INV-026. +type PackReceipt struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PackReceiptSpec `json:"spec,omitempty"` + Status PackReceiptStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// PackReceiptList contains a list of PackReceipt. +type PackReceiptList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PackReceipt `json:"items"` +} + +func init() { + SchemeBuilder.Register(&PackReceipt{}, &PackReceiptList{}) +} diff --git a/api/seam/v1alpha1/zz_generated.deepcopy.go b/api/seam/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..25534f1 --- /dev/null +++ b/api/seam/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,769 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "github.com/ontai-dev/seam-core/pkg/lineage" + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DependencyPolicy) DeepCopyInto(out *DependencyPolicy) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependencyPolicy. +func (in *DependencyPolicy) DeepCopy() *DependencyPolicy { + if in == nil { + return nil + } + out := new(DependencyPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeployedResourceRef) DeepCopyInto(out *DeployedResourceRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeployedResourceRef. +func (in *DeployedResourceRef) DeepCopy() *DeployedResourceRef { + if in == nil { + return nil + } + out := new(DeployedResourceRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LifecyclePolicy) DeepCopyInto(out *LifecyclePolicy) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LifecyclePolicy. +func (in *LifecyclePolicy) DeepCopy() *LifecyclePolicy { + if in == nil { + return nil + } + out := new(LifecyclePolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackDelivery) DeepCopyInto(out *PackDelivery) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackDelivery. +func (in *PackDelivery) DeepCopy() *PackDelivery { + if in == nil { + return nil + } + out := new(PackDelivery) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PackDelivery) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackDeliveryList) DeepCopyInto(out *PackDeliveryList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PackDelivery, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackDeliveryList. +func (in *PackDeliveryList) DeepCopy() *PackDeliveryList { + if in == nil { + return nil + } + out := new(PackDeliveryList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PackDeliveryList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackDeliveryRef) DeepCopyInto(out *PackDeliveryRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackDeliveryRef. +func (in *PackDeliveryRef) DeepCopy() *PackDeliveryRef { + if in == nil { + return nil + } + out := new(PackDeliveryRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackDeliverySpec) DeepCopyInto(out *PackDeliverySpec) { + *out = *in + out.RegistryRef = in.RegistryRef + if in.ExecutionOrder != nil { + in, out := &in.ExecutionOrder, &out.ExecutionOrder + *out = make([]PackDeliveryStage, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Provenance != nil { + in, out := &in.Provenance, &out.Provenance + *out = new(PackProvenance) + (*in).DeepCopyInto(*out) + } + if in.TargetClusters != nil { + in, out := &in.TargetClusters, &out.TargetClusters + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.LifecyclePolicies != nil { + in, out := &in.LifecyclePolicies, &out.LifecyclePolicies + *out = new(LifecyclePolicy) + **out = **in + } + if in.Lineage != nil { + in, out := &in.Lineage, &out.Lineage + *out = new(lineage.SealedCausalChain) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackDeliverySpec. +func (in *PackDeliverySpec) DeepCopy() *PackDeliverySpec { + if in == nil { + return nil + } + out := new(PackDeliverySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackDeliveryStage) DeepCopyInto(out *PackDeliveryStage) { + *out = *in + if in.Manifests != nil { + in, out := &in.Manifests, &out.Manifests + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackDeliveryStage. +func (in *PackDeliveryStage) DeepCopy() *PackDeliveryStage { + if in == nil { + return nil + } + out := new(PackDeliveryStage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackDeliveryStatus) DeepCopyInto(out *PackDeliveryStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackDeliveryStatus. +func (in *PackDeliveryStatus) DeepCopy() *PackDeliveryStatus { + if in == nil { + return nil + } + out := new(PackDeliveryStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackExecution) DeepCopyInto(out *PackExecution) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackExecution. +func (in *PackExecution) DeepCopy() *PackExecution { + if in == nil { + return nil + } + out := new(PackExecution) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PackExecution) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackExecutionList) DeepCopyInto(out *PackExecutionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PackExecution, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackExecutionList. +func (in *PackExecutionList) DeepCopy() *PackExecutionList { + if in == nil { + return nil + } + out := new(PackExecutionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PackExecutionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackExecutionSpec) DeepCopyInto(out *PackExecutionSpec) { + *out = *in + out.PackDeliveryRef = in.PackDeliveryRef + if in.Lineage != nil { + in, out := &in.Lineage, &out.Lineage + *out = new(lineage.SealedCausalChain) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackExecutionSpec. +func (in *PackExecutionSpec) DeepCopy() *PackExecutionSpec { + if in == nil { + return nil + } + out := new(PackExecutionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackExecutionStatus) DeepCopyInto(out *PackExecutionStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackExecutionStatus. +func (in *PackExecutionStatus) DeepCopy() *PackExecutionStatus { + if in == nil { + return nil + } + out := new(PackExecutionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackInstalled) DeepCopyInto(out *PackInstalled) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackInstalled. +func (in *PackInstalled) DeepCopy() *PackInstalled { + if in == nil { + return nil + } + out := new(PackInstalled) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PackInstalled) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackInstalledList) DeepCopyInto(out *PackInstalledList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PackInstalled, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackInstalledList. +func (in *PackInstalledList) DeepCopy() *PackInstalledList { + if in == nil { + return nil + } + out := new(PackInstalledList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PackInstalledList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackInstalledSpec) DeepCopyInto(out *PackInstalledSpec) { + *out = *in + if in.DependsOn != nil { + in, out := &in.DependsOn, &out.DependsOn + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.DependencyPolicy != nil { + in, out := &in.DependencyPolicy, &out.DependencyPolicy + *out = new(DependencyPolicy) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackInstalledSpec. +func (in *PackInstalledSpec) DeepCopy() *PackInstalledSpec { + if in == nil { + return nil + } + out := new(PackInstalledSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackInstalledStatus) DeepCopyInto(out *PackInstalledStatus) { + *out = *in + if in.DeliveredAt != nil { + in, out := &in.DeliveredAt, &out.DeliveredAt + *out = (*in).DeepCopy() + } + if in.DeployedResources != nil { + in, out := &in.DeployedResources, &out.DeployedResources + *out = make([]DeployedResourceRef, len(*in)) + copy(*out, *in) + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackInstalledStatus. +func (in *PackInstalledStatus) DeepCopy() *PackInstalledStatus { + if in == nil { + return nil + } + out := new(PackInstalledStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackLog) DeepCopyInto(out *PackLog) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackLog. +func (in *PackLog) DeepCopy() *PackLog { + if in == nil { + return nil + } + out := new(PackLog) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PackLog) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackLogArtifact) DeepCopyInto(out *PackLogArtifact) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackLogArtifact. +func (in *PackLogArtifact) DeepCopy() *PackLogArtifact { + if in == nil { + return nil + } + out := new(PackLogArtifact) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackLogDeployedResource) DeepCopyInto(out *PackLogDeployedResource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackLogDeployedResource. +func (in *PackLogDeployedResource) DeepCopy() *PackLogDeployedResource { + if in == nil { + return nil + } + out := new(PackLogDeployedResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackLogFailureReason) DeepCopyInto(out *PackLogFailureReason) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackLogFailureReason. +func (in *PackLogFailureReason) DeepCopy() *PackLogFailureReason { + if in == nil { + return nil + } + out := new(PackLogFailureReason) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackLogList) DeepCopyInto(out *PackLogList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PackLog, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackLogList. +func (in *PackLogList) DeepCopy() *PackLogList { + if in == nil { + return nil + } + out := new(PackLogList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PackLogList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackLogSpec) DeepCopyInto(out *PackLogSpec) { + *out = *in + if in.StartedAt != nil { + in, out := &in.StartedAt, &out.StartedAt + *out = (*in).DeepCopy() + } + if in.CompletedAt != nil { + in, out := &in.CompletedAt, &out.CompletedAt + *out = (*in).DeepCopy() + } + if in.FailureReason != nil { + in, out := &in.FailureReason, &out.FailureReason + *out = new(PackLogFailureReason) + **out = **in + } + if in.DeployedResources != nil { + in, out := &in.DeployedResources, &out.DeployedResources + *out = make([]PackLogDeployedResource, len(*in)) + copy(*out, *in) + } + if in.Artifacts != nil { + in, out := &in.Artifacts, &out.Artifacts + *out = make([]PackLogArtifact, len(*in)) + copy(*out, *in) + } + if in.Steps != nil { + in, out := &in.Steps, &out.Steps + *out = make([]PackLogStepResult, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackLogSpec. +func (in *PackLogSpec) DeepCopy() *PackLogSpec { + if in == nil { + return nil + } + out := new(PackLogSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackLogStatus) DeepCopyInto(out *PackLogStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackLogStatus. +func (in *PackLogStatus) DeepCopy() *PackLogStatus { + if in == nil { + return nil + } + out := new(PackLogStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackLogStepResult) DeepCopyInto(out *PackLogStepResult) { + *out = *in + if in.StartedAt != nil { + in, out := &in.StartedAt, &out.StartedAt + *out = (*in).DeepCopy() + } + if in.CompletedAt != nil { + in, out := &in.CompletedAt, &out.CompletedAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackLogStepResult. +func (in *PackLogStepResult) DeepCopy() *PackLogStepResult { + if in == nil { + return nil + } + out := new(PackLogStepResult) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackProvenance) DeepCopyInto(out *PackProvenance) { + *out = *in + if in.BuildTimestamp != nil { + in, out := &in.BuildTimestamp, &out.BuildTimestamp + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackProvenance. +func (in *PackProvenance) DeepCopy() *PackProvenance { + if in == nil { + return nil + } + out := new(PackProvenance) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackReceipt) DeepCopyInto(out *PackReceipt) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackReceipt. +func (in *PackReceipt) DeepCopy() *PackReceipt { + if in == nil { + return nil + } + out := new(PackReceipt) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PackReceipt) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackReceiptDeployedResource) DeepCopyInto(out *PackReceiptDeployedResource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackReceiptDeployedResource. +func (in *PackReceiptDeployedResource) DeepCopy() *PackReceiptDeployedResource { + if in == nil { + return nil + } + out := new(PackReceiptDeployedResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackReceiptList) DeepCopyInto(out *PackReceiptList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PackReceipt, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackReceiptList. +func (in *PackReceiptList) DeepCopy() *PackReceiptList { + if in == nil { + return nil + } + out := new(PackReceiptList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PackReceiptList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackReceiptSpec) DeepCopyInto(out *PackReceiptSpec) { + *out = *in + if in.DeployedResources != nil { + in, out := &in.DeployedResources, &out.DeployedResources + *out = make([]PackReceiptDeployedResource, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackReceiptSpec. +func (in *PackReceiptSpec) DeepCopy() *PackReceiptSpec { + if in == nil { + return nil + } + out := new(PackReceiptSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackReceiptStatus) DeepCopyInto(out *PackReceiptStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackReceiptStatus. +func (in *PackReceiptStatus) DeepCopy() *PackReceiptStatus { + if in == nil { + return nil + } + out := new(PackReceiptStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackRegistryRef) DeepCopyInto(out *PackRegistryRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackRegistryRef. +func (in *PackRegistryRef) DeepCopy() *PackRegistryRef { + if in == nil { + return nil + } + out := new(PackRegistryRef) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/wrapper/main.go b/cmd/wrapper/main.go index cfb6d74..dc4fd41 100644 --- a/cmd/wrapper/main.go +++ b/cmd/wrapper/main.go @@ -19,6 +19,7 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" seamv1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/wrapper/internal/controller" ) @@ -27,6 +28,7 @@ var scheme = runtime.NewScheme() func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(seamv1alpha1.AddToScheme(scheme)) + utilruntime.Must(dispatcherv1alpha1.AddToScheme(scheme)) } func main() { diff --git a/config/crd/infrastructure.ontai.dev_infrastructureclusterpacks.yaml b/config/crd/bases/seam.ontai.dev_packdeliveries.yaml similarity index 89% rename from config/crd/infrastructure.ontai.dev_infrastructureclusterpacks.yaml rename to config/crd/bases/seam.ontai.dev_packdeliveries.yaml index 3814e3d..ab92c18 100644 --- a/config/crd/infrastructure.ontai.dev_infrastructureclusterpacks.yaml +++ b/config/crd/bases/seam.ontai.dev_packdeliveries.yaml @@ -4,16 +4,16 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.16.1 - name: infrastructureclusterpacks.infrastructure.ontai.dev + name: packdeliveries.seam.ontai.dev spec: - group: infrastructure.ontai.dev + group: seam.ontai.dev names: - kind: InfrastructureClusterPack - listKind: InfrastructureClusterPackList - plural: infrastructureclusterpacks + kind: PackDelivery + listKind: PackDeliveryList + plural: packdeliveries shortNames: - - icp - singular: infrastructureclusterpack + - pd + singular: packdelivery scope: Namespaced versions: - additionalPrinterColumns: @@ -30,7 +30,7 @@ spec: schema: openAPIV3Schema: description: |- - InfrastructureClusterPack is the seam-core CRD for pack registration. + PackDelivery is the dispatcher CRD for pack registration. Records an OCI artifact that has been compiled and is ready for runtime delivery. Spec is immutable after creation. wrapper-schema.md §3. properties: @@ -53,7 +53,7 @@ spec: type: object spec: description: |- - InfrastructureClusterPackSpec defines the desired state of an InfrastructureClusterPack. + PackDeliverySpec defines the desired state of a PackDelivery. All fields are immutable after creation. wrapper-schema.md §3. properties: basePackName: @@ -85,8 +85,8 @@ spec: description: ExecutionOrder defines the ordered stages in which pack manifests are applied. items: - description: InfrastructurePackExecutionStage is a single stage - in the pack execution order. + description: PackDeliveryStage is a single stage in the pack execution + order. properties: manifests: description: Manifests is the list of manifest names to apply @@ -118,7 +118,7 @@ spec: default: true description: |- RetainOnDeletion controls whether the OCI artifact is retained when the - ClusterPack CR is deleted. Default: true (artifact retained). + PackDelivery CR is deleted. Default: true (artifact retained). type: boolean type: object lineage: @@ -213,7 +213,7 @@ spec: type: object rbacDigest: description: |- - RBACDigest is the OCI digest of the RBAC layer of this ClusterPack artifact. + RBACDigest is the OCI digest of the RBAC layer of this PackDelivery artifact. Contains ServiceAccount, Role, ClusterRole, RoleBinding, ClusterRoleBinding manifests. type: string registryRef: @@ -231,22 +231,30 @@ spec: - digest - url type: object + rollbackToRevision: + description: |- + RollbackToRevision instructs the dispatcher to restore this PackDelivery to the + artifact version deployed at PackLog revision N-1. When set, the PackDeliveryReconciler + reads PreviousClusterPackVersion/PreviousRBACDigest/PreviousWorkloadDigest from + the current PackLog, patches spec.version and spec.*Digest to those values, removes + the spec-checksum-snapshot annotation, and clears this field. Only one-step + rollback (to revision N-1) is supported per invocation. Set to 0 to disable. + Governor-controlled only. wrapper-schema.md §6.2. seam-core-schema.md §7.8. + format: int64 + type: integer sourceBuildRef: description: SourceBuildRef is an opaque reference to the build that produced this pack. Informational. type: string targetClusters: description: TargetClusters is the list of cluster names to which - this ClusterPack should be delivered. + this PackDelivery should be delivered. items: type: string type: array valuesFile: - description: |- - ValuesFile is the path to the values file used during pack compilation. - For Helm packs: the user-supplied values file merged with chart defaults at render time. - For kustomize/raw packs: the overlay or patch file applied during the external build. - Informational -- recorded so admins can trace which customization produced this artifact. + description: ValuesFile is the path to the values file used during + pack compilation. type: string version: description: Version is the semantic version of this pack. Immutable @@ -254,7 +262,7 @@ spec: type: string workloadDigest: description: |- - WorkloadDigest is the OCI digest of the workload layer of this ClusterPack artifact. + WorkloadDigest is the OCI digest of the workload layer of this PackDelivery artifact. Applied after guardian RBACProfile reaches provisioned=true. wrapper-schema.md §4. type: string required: @@ -262,12 +270,11 @@ spec: - version type: object status: - description: InfrastructureClusterPackStatus is the observed state of - an InfrastructureClusterPack. + description: PackDeliveryStatus is the observed state of a PackDelivery. properties: conditions: description: Conditions is the list of status conditions for this - ClusterPack. + PackDelivery. items: description: Condition contains details for one aspect of the current state of this API Resource. diff --git a/config/crd/infrastructure.ontai.dev_infrastructurepackexecutions.yaml b/config/crd/bases/seam.ontai.dev_packexecutions.yaml similarity index 90% rename from config/crd/infrastructure.ontai.dev_infrastructurepackexecutions.yaml rename to config/crd/bases/seam.ontai.dev_packexecutions.yaml index d691fbb..4f158e8 100644 --- a/config/crd/infrastructure.ontai.dev_infrastructurepackexecutions.yaml +++ b/config/crd/bases/seam.ontai.dev_packexecutions.yaml @@ -4,20 +4,20 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.16.1 - name: infrastructurepackexecutions.infrastructure.ontai.dev + name: packexecutions.seam.ontai.dev spec: - group: infrastructure.ontai.dev + group: seam.ontai.dev names: - kind: InfrastructurePackExecution - listKind: InfrastructurePackExecutionList - plural: infrastructurepackexecutions + kind: PackExecution + listKind: PackExecutionList + plural: packexecutions shortNames: - - ipe - singular: infrastructurepackexecution + - pe + singular: packexecution scope: Namespaced versions: - additionalPrinterColumns: - - jsonPath: .spec.clusterPackRef.name + - jsonPath: .spec.packDeliveryRef.name name: Pack type: string - jsonPath: .spec.targetClusterRef @@ -30,7 +30,7 @@ spec: schema: openAPIV3Schema: description: |- - InfrastructurePackExecution is the seam-core CRD for a runtime pack delivery request. + PackExecution is the dispatcher CRD for a runtime pack delivery request. wrapper-schema.md §3. properties: apiVersion: @@ -52,7 +52,7 @@ spec: type: object spec: description: |- - InfrastructurePackExecutionSpec defines the desired state of an InfrastructurePackExecution. + PackExecutionSpec defines the desired state of a PackExecution. wrapper-schema.md §3. properties: admissionProfileRef: @@ -60,35 +60,19 @@ spec: this execution. type: string chartName: - description: ChartName is the Helm chart name. Carried from ClusterPack. + description: ChartName is the Helm chart name. Carried from PackDelivery. type: string chartURL: description: ChartURL is the Helm chart repository URL. Carried from - ClusterPack. + PackDelivery. type: string chartVersion: description: ChartVersion is the Helm chart version for this execution. - Carried from ClusterPack. + Carried from PackDelivery. type: string - clusterPackRef: - description: ClusterPackRef identifies the ClusterPack to deploy. - properties: - name: - description: Name is the name of the ClusterPack CR. - minLength: 1 - type: string - version: - description: Version is the expected version of the ClusterPack. - Guards against name reuse. - minLength: 1 - type: string - required: - - name - - version - type: object helmVersion: description: HelmVersion is the Helm SDK version used to render the - pack. Carried from ClusterPack. + pack. Carried from PackDelivery. type: string lineage: description: Lineage is the sealed causal chain record for this root @@ -161,17 +145,32 @@ spec: - rootNamespace - rootUID type: object + packDeliveryRef: + description: PackDeliveryRef identifies the PackDelivery to deploy. + properties: + name: + description: Name is the name of the PackDelivery CR. + minLength: 1 + type: string + version: + description: Version is the expected version of the PackDelivery. + Guards against name reuse. + minLength: 1 + type: string + required: + - name + - version + type: object targetClusterRef: description: TargetClusterRef is the name of the target cluster to deliver the pack to. type: string required: - - clusterPackRef + - packDeliveryRef - targetClusterRef type: object status: - description: InfrastructurePackExecutionStatus is the observed state of - an InfrastructurePackExecution. + description: PackExecutionStatus is the observed state of a PackExecution. properties: conditions: description: Conditions is the list of status conditions for this @@ -241,7 +240,7 @@ spec: type: integer operationResultRef: description: |- - OperationResultRef is the name of the PackOperationResult CR written after + OperationResultRef is the name of the PackLog CR written after successful pack-deploy Job completion. type: string type: object diff --git a/config/crd/infrastructure.ontai.dev_infrastructurepackinstances.yaml b/config/crd/bases/seam.ontai.dev_packinstalleds.yaml similarity index 85% rename from config/crd/infrastructure.ontai.dev_infrastructurepackinstances.yaml rename to config/crd/bases/seam.ontai.dev_packinstalleds.yaml index 13d6d9a..42401c5 100644 --- a/config/crd/infrastructure.ontai.dev_infrastructurepackinstances.yaml +++ b/config/crd/bases/seam.ontai.dev_packinstalleds.yaml @@ -4,20 +4,20 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.16.1 - name: infrastructurepackinstances.infrastructure.ontai.dev + name: packinstalleds.seam.ontai.dev spec: - group: infrastructure.ontai.dev + group: seam.ontai.dev names: - kind: InfrastructurePackInstance - listKind: InfrastructurePackInstanceList - plural: infrastructurepackinstances + kind: PackInstalled + listKind: PackInstalledList + plural: packinstalleds shortNames: - - ipi - singular: infrastructurepackinstance + - pi + singular: packinstalled scope: Namespaced versions: - additionalPrinterColumns: - - jsonPath: .spec.clusterPackRef + - jsonPath: .spec.packDeliveryRef name: Pack type: string - jsonPath: .spec.targetClusterRef @@ -30,7 +30,7 @@ spec: schema: openAPIV3Schema: description: |- - InfrastructurePackInstance is the seam-core CRD recording the delivered state of a pack on a cluster. + PackInstalled is the dispatcher CRD recording the delivered state of a pack on a cluster. wrapper-schema.md §3. properties: apiVersion: @@ -52,23 +52,19 @@ spec: type: object spec: description: |- - InfrastructurePackInstanceSpec defines the desired state of an InfrastructurePackInstance. + PackInstalledSpec defines the desired state of a PackInstalled. wrapper-schema.md §3. properties: chartName: - description: ChartName is the Helm chart name. Carried from ClusterPack. + description: ChartName is the Helm chart name. Carried from PackDelivery. type: string chartURL: description: ChartURL is the Helm chart repository URL. Carried from - ClusterPack. + PackDelivery. type: string chartVersion: description: ChartVersion is the Helm chart version for this instance. - Carried from ClusterPack. - type: string - clusterPackRef: - description: ClusterPackRef is the name of the ClusterPack CR this - instance tracks. + Carried from PackDelivery. type: string dependencyPolicy: description: DependencyPolicy defines behavior when a dependency reports @@ -77,8 +73,8 @@ spec: onDrift: default: Warn description: |- - OnDrift controls how this PackInstance responds when a declared dependency - PackInstance reports Drifted=True. + OnDrift controls how this PackInstalled responds when a declared dependency + PackInstalled reports Drifted=True. enum: - Block - Warn @@ -93,7 +89,11 @@ spec: type: array helmVersion: description: HelmVersion is the Helm SDK version used to render the - pack. Carried from ClusterPack. + pack. Carried from PackDelivery. + type: string + packDeliveryRef: + description: PackDeliveryRef is the name of the PackDelivery CR this + instance tracks. type: string targetClusterRef: description: TargetClusterRef is the name of the target cluster this @@ -103,17 +103,16 @@ spec: description: Version is the pack version delivered to the target cluster. type: string required: - - clusterPackRef + - packDeliveryRef - targetClusterRef - version type: object status: - description: InfrastructurePackInstanceStatus is the observed state of - an InfrastructurePackInstance. + description: PackInstalledStatus is the observed state of a PackInstalled. properties: conditions: description: Conditions is the list of status conditions for this - PackInstance. + PackInstalled. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -177,12 +176,12 @@ spec: deployedResources: description: |- DeployedResources is the list of Kubernetes resources applied by the pack-deploy job. - Used by the PackInstance deletion handler for cleanup. + Used by the PackInstalled deletion handler for cleanup. items: description: |- - InfrastructureDeployedResourceRef records a single Kubernetes resource applied - by the pack-deploy job. Used by the PackInstance deletion handler to clean up - deployed workload when the ClusterPack is deleted. wrapper-schema.md §3. + DeployedResourceRef records a single Kubernetes resource applied + by the pack-deploy job. Used by the PackInstalled deletion handler to clean up + deployed workload when the PackDelivery is deleted. wrapper-schema.md §3. properties: apiVersion: description: APIVersion is the Kubernetes apiVersion (e.g., diff --git a/config/crd/infrastructure.ontai.dev_packoperationresults.yaml b/config/crd/bases/seam.ontai.dev_packlogs.yaml similarity index 83% rename from config/crd/infrastructure.ontai.dev_packoperationresults.yaml rename to config/crd/bases/seam.ontai.dev_packlogs.yaml index a1e8369..bf1ab5c 100644 --- a/config/crd/infrastructure.ontai.dev_packoperationresults.yaml +++ b/config/crd/bases/seam.ontai.dev_packlogs.yaml @@ -4,16 +4,16 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.16.1 - name: packoperationresults.infrastructure.ontai.dev + name: packlogs.seam.ontai.dev spec: - group: infrastructure.ontai.dev + group: seam.ontai.dev names: - kind: PackOperationResult - listKind: PackOperationResultList - plural: packoperationresults + kind: PackLog + listKind: PackLogList + plural: packlogs shortNames: - - por - singular: packoperationresult + - pl + singular: packlog scope: Namespaced versions: - additionalPrinterColumns: @@ -33,11 +33,8 @@ spec: schema: openAPIV3Schema: description: |- - PackOperationResult is the immutable result record written by the Conductor - execute-mode Job after a pack-deploy capability completes. It replaces the - ConfigMap output channel, providing a versioned, richly-typed CR that the - wrapper PackExecutionReconciler reads to advance PackExecution status and - that the lineagesink can consume as additional data. One PackOperationResult + PackLog is the immutable result record written by the Conductor + execute-mode Job after a pack-deploy capability completes. One PackLog per PackExecution, created in namespace seam-tenant-{clusterName}. seam-core-schema.md §8, Decision 11. properties: @@ -60,15 +57,15 @@ spec: type: object spec: description: |- - PackOperationResultSpec is the complete result document written by the - Conductor execute-mode Job before exit. Written by conductor; read by wrapper. + PackLogSpec is the complete result document written by the + Conductor execute-mode Job before exit. Written by conductor; read by dispatcher. seam-core-schema.md §8, Decision 11. properties: artifacts: description: Artifacts is the list of artifacts produced by this execution. items: description: |- - PackOperationArtifact is a structured reference to an artifact produced + PackLogArtifact is a structured reference to an artifact produced by a pack-deploy execution. Never contains raw artifact content. properties: checksum: @@ -101,10 +98,6 @@ spec: description: Capability is the name of the Conductor capability that produced this result. type: string - clusterPackRef: - description: ClusterPackRef is the name of the ClusterPack CR that - was deployed. - type: string completedAt: description: CompletedAt is the time the capability execution finished. format: date-time @@ -112,11 +105,11 @@ spec: deployedResources: description: |- DeployedResources is the list of Kubernetes resources applied during this - execution. Populated by pack-deploy on success. Used by PackInstanceReconciler + execution. Populated by pack-deploy on success. Used by PackInstalledReconciler for deletion cleanup. items: description: |- - PackOperationDeployedResource records a single Kubernetes resource applied + PackLogDeployedResource records a single Kubernetes resource applied during a pack-deploy execution. properties: apiVersion: @@ -167,6 +160,16 @@ spec: - category - reason type: object + packDeliveryRef: + description: PackDeliveryRef is the name of the PackDelivery CR that + was deployed. + type: string + packDeliveryVersion: + description: |- + PackDeliveryVersion is the PackDelivery spec.version deployed in this operation. + Populated by the Conductor executor at write time. Rollback anchor. + seam-core-schema.md §7.8. + type: string packExecutionRef: description: PackExecutionRef is the name of the PackExecution CR that triggered this operation. @@ -177,9 +180,13 @@ spec: type: string previousRevisionRef: description: |- - PreviousRevisionRef is the name of the PackOperationResult CR deleted when - this revision was written. Enables chain reconstruction for the full operation history. - Absent for revision 1 (no predecessor). + PreviousRevisionRef is the name of the PackLog CR that was superseded when + this revision was written. Absent for revision 1 (no predecessor). + type: string + rbacDigest: + description: |- + RBACDigest is the OCI digest of the RBAC layer deployed in this operation. + Copied from PackDelivery.spec.rbacDigest at deploy time. Rollback anchor. type: string revision: description: |- @@ -206,7 +213,7 @@ spec: capabilities. items: description: |- - PackOperationStepResult is the execution result for one step within a + PackLogStepResult is the execution result for one step within a multi-step capability. properties: completedAt: @@ -242,19 +249,26 @@ spec: talosClusterOperationResultRef: description: |- TalosClusterOperationResultRef is reserved for future cross-reference to a - TalosCluster-scoped OperationResult. Stub field; not populated by any current controller. + TalosCluster-scoped OperationResult. Stub field; not populated by any current + controller. type: string targetClusterRef: description: TargetClusterRef is the name of the target cluster this operation ran against. type: string + workloadDigest: + description: |- + WorkloadDigest is the OCI digest of the workload layer deployed in this operation. + Copied from PackDelivery.spec.workloadDigest at deploy time. Rollback anchor. + type: string required: - capability + - revision - status type: object status: description: |- - PackOperationResultStatus is the observed state of a PackOperationResult. + PackLogStatus is the observed state of a PackLog. Currently empty; reserved for future controller-set conditions. properties: observedGeneration: diff --git a/config/crd/infrastructure.ontai.dev_infrastructurepackreceipts.yaml b/config/crd/bases/seam.ontai.dev_packreceipts.yaml similarity index 60% rename from config/crd/infrastructure.ontai.dev_infrastructurepackreceipts.yaml rename to config/crd/bases/seam.ontai.dev_packreceipts.yaml index 516df1c..dc65fb1 100644 --- a/config/crd/infrastructure.ontai.dev_infrastructurepackreceipts.yaml +++ b/config/crd/bases/seam.ontai.dev_packreceipts.yaml @@ -4,23 +4,23 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.16.1 - name: infrastructurepackreceipts.infrastructure.ontai.dev + name: packreceipts.seam.ontai.dev spec: - group: infrastructure.ontai.dev + group: seam.ontai.dev names: - kind: InfrastructurePackReceipt - listKind: InfrastructurePackReceiptList - plural: infrastructurepackreceipts + kind: PackReceipt + listKind: PackReceiptList + plural: packreceipts shortNames: - - ipr - singular: infrastructurepackreceipt + - pr + singular: packreceipt scope: Namespaced versions: - additionalPrinterColumns: - - jsonPath: .spec.clusterPackRef + - jsonPath: .spec.packDeliveryRef name: Pack type: string - - jsonPath: .spec.signatureVerified + - jsonPath: .status.verified name: Verified type: boolean - jsonPath: .metadata.creationTimestamp @@ -30,7 +30,7 @@ spec: schema: openAPIV3Schema: description: |- - InfrastructurePackReceipt is the seam-core CRD for pack delivery acknowledgement on a tenant cluster. + PackReceipt is the dispatcher CRD for pack delivery acknowledgement on a tenant cluster. Written by conductor agent after signature verification. INV-026. properties: apiVersion: @@ -52,56 +52,90 @@ spec: type: object spec: description: |- - InfrastructurePackReceiptSpec defines the desired state of an InfrastructurePackReceipt. - Written by conductor agent on the tenant cluster after verifying the signed PackInstance. - INV-026. conductor-schema.md. + PackReceiptSpec defines the desired state of a PackReceipt. + Written by the packinstalled pull loop on the tenant cluster conductor after + Ed25519 signature verification. INV-026. conductor-schema.md. properties: chartName: - description: ChartName is the Helm chart name. Carried from ClusterPack. + description: ChartName is the Helm chart name. Carried from PackDelivery. type: string chartURL: description: ChartURL is the Helm chart repository URL. Carried from - ClusterPack. + PackDelivery. type: string chartVersion: description: ChartVersion is the Helm chart version. Carried from - ClusterPack. - type: string - clusterPackRef: - description: ClusterPackRef is the name of the ClusterPack CR this - receipt acknowledges. + PackDelivery. type: string + deployedResources: + description: |- + DeployedResources is the inventory of Kubernetes resources applied to the tenant cluster + during the pack-deploy Job. Conductor role=tenant uses this list to detect drift by + verifying each resource still exists with the expected state. + CLUSTERPACK-BL-VERSION-CLEANUP, conductor-schema.md. + items: + description: |- + PackReceiptDeployedResource records a single Kubernetes resource that was applied + to the tenant cluster as part of a pack-deploy Job. Used by conductor role=tenant + to detect drift between declared and actual cluster state. + CLUSTERPACK-BL-VERSION-CLEANUP. conductor-schema.md. + properties: + apiVersion: + description: APIVersion is the full API version string (e.g., + "apps/v1"). + type: string + kind: + description: Kind is the resource kind (e.g., "Deployment"). + type: string + name: + description: Name is the resource name. + type: string + namespace: + description: Namespace is the namespace the resource was applied + to. Empty for cluster-scoped resources. + type: string + required: + - apiVersion + - kind + - name + type: object + type: array helmVersion: - description: HelmVersion is the Helm SDK version. Carried from ClusterPack. + description: HelmVersion is the Helm SDK version. Carried from PackDelivery. type: string - packSignature: - description: |- - PackSignature is the base64-encoded Ed25519 signature from the management cluster conductor. - INV-026. + packDeliveryRef: + description: PackDeliveryRef is the name of the PackDelivery CR this + receipt acknowledges. + type: string + packInstalledRef: + description: PackInstalledRef is the name of the PackInstalled CR + this receipt acknowledges. type: string rbacDigest: description: RBACDigest is the OCI digest of the RBAC layer. Carried - from ClusterPack for audit. + from PackDelivery for audit. + type: string + signatureRef: + description: |- + SignatureRef is the name of the signed artifact Secret on the management cluster + (seam-pack-signed-{cluster}-{packInstalled}) from which this receipt was derived. type: string - signatureVerified: - description: SignatureVerified indicates whether the conductor agent - verified the pack signature. - type: boolean targetClusterRef: description: TargetClusterRef is the name of the cluster this receipt was generated on. type: string workloadDigest: description: WorkloadDigest is the OCI digest of the workload layer. - Carried from ClusterPack. + Carried from PackDelivery. type: string required: - - clusterPackRef + - packDeliveryRef - targetClusterRef type: object status: - description: InfrastructurePackReceiptStatus is the observed state of - an InfrastructurePackReceipt. + description: |- + PackReceiptStatus is the observed state of a PackReceipt. + Written by the packinstalled pull loop after signature verification. INV-026. properties: conditions: description: Conditions is the list of status conditions for this @@ -165,6 +199,21 @@ spec: description: ObservedGeneration is the generation most recently reconciled. format: int64 type: integer + signature: + description: |- + Signature is the base64-encoded Ed25519 signature from the signed artifact Secret. + Stored for auditability and idempotency checking. INV-026. + type: string + verificationFailedReason: + description: |- + VerificationFailedReason is set when Verified=false and describes the + specific verification failure (e.g., "Ed25519 signature verification failed (INV-026)"). + type: string + verified: + description: |- + Verified indicates whether the Ed25519 signature on the PackInstalled artifact + was successfully verified against the management cluster's public key. INV-026. + type: boolean type: object type: object served: true diff --git a/config/crd/embed.go b/config/crd/embed.go index 521cc75..fc04688 100644 --- a/config/crd/embed.go +++ b/config/crd/embed.go @@ -1,12 +1,23 @@ -// Package crd previously embedded wrapper's own CRD YAML files. -// After T-2B-9 migration, all wrapper CRDs (InfrastructureClusterPack, -// InfrastructurePackExecution, InfrastructurePackInstance) are declared in -// seam-core (infrastructure.ontai.dev). The compiler bundles them from -// seam-core/config/crd directly. -// This package is retained for structural consistency only. +// Package crd exposes the wrapper operator's controller-gen CRD YAML files +// as an embedded filesystem. Imported by the Compiler binary to build the +// CRD manifest bundle for compiler launch. conductor-schema.md §9. +// Dispatcher types (PackDelivery, PackExecution, PackInstalled, PackReceipt, +// PackLog) are declared in wrapper under seam.ontai.dev/v1alpha1. package crd -import "embed" +import ( + "embed" + "io/fs" +) -// FS is an empty embedded filesystem. Wrapper's CRDs are now in seam-core. -var FS embed.FS +//go:embed bases +var raw embed.FS + +// FS contains all CRD YAML files from the bases subdirectory, rooted at bases/. +var FS fs.FS = func() fs.FS { + sub, err := fs.Sub(raw, "bases") + if err != nil { + panic("wrapper/config/crd: fs.Sub bases: " + err.Error()) + } + return sub +}() diff --git a/config/crd/infrastructure.ontai.dev_driftsignals.yaml b/config/crd/infrastructure.ontai.dev_driftsignals.yaml deleted file mode 100644 index 51efb3d..0000000 --- a/config/crd/infrastructure.ontai.dev_driftsignals.yaml +++ /dev/null @@ -1,202 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.16.1 - name: driftsignals.infrastructure.ontai.dev -spec: - group: infrastructure.ontai.dev - names: - kind: DriftSignal - listKind: DriftSignalList - plural: driftsignals - shortNames: - - ds - singular: driftsignal - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.state - name: State - type: string - - jsonPath: .spec.correlationID - name: CorrelationID - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: |- - DriftSignal is the seam-core CRD for the three-state drift acknowledgement chain. - Written by conductor role=tenant; acknowledged by conductor role=management. - Decision I. At-least-once delivery. Human-at-Boundary invariant enforced via - configurable escalation threshold and TerminalDrift Condition. conductor-schema.md. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: |- - DriftSignalSpec defines the observed drift event written by conductor role=tenant. - Decision I. conductor-schema.md. - properties: - affectedCRRef: - description: AffectedCRRef is a typed reference to the CR that exhibited - drift. - properties: - group: - description: Group is the API group of the drifted CR. - type: string - kind: - description: Kind is the Kind of the drifted CR. - type: string - name: - description: Name is the name of the drifted CR. - type: string - namespace: - description: Namespace is the namespace of the drifted CR. Empty - for cluster-scoped resources. - type: string - required: - - group - - kind - - name - type: object - correctionJobRef: - description: |- - CorrectionJobRef is the name of the corrective Job created by the management cluster. - Populated when state transitions to queued. - type: string - correlationID: - description: |- - CorrelationID is a unique identifier for this drift event, used to deduplicate - signals across federation retries. Format: UUID v4. - type: string - driftReason: - description: DriftReason is a human-readable description of why drift - was detected. - type: string - escalationCounter: - description: |- - EscalationCounter is the number of times this signal has been re-emitted without - acknowledgement. Incremented by conductor role=tenant on each re-emit cycle. - When this counter reaches the configurable escalation threshold, conductor writes a - type=TerminalDrift Condition on the affected CR and stops re-emitting. Decision I. - format: int32 - type: integer - observedAt: - description: ObservedAt is the time the drift was first observed by - conductor role=tenant. - format: date-time - type: string - state: - allOf: - - enum: - - pending - - delivered - - queued - - confirmed - - enum: - - pending - - delivered - - queued - - confirmed - description: State is the current acknowledgement state of this drift - signal. Decision I. - type: string - required: - - affectedCRRef - - correlationID - - driftReason - - observedAt - - state - type: object - status: - description: DriftSignalStatus is the observed state of a DriftSignal. - Written by conductor role=management. - properties: - conditions: - description: Conditions is the list of status conditions for this - DriftSignal. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - observedGeneration: - description: ObservedGeneration is the generation most recently reconciled. - format: int64 - type: integer - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/crd/infrastructure.ontai.dev_infrastructurerunnerconfigs.yaml b/config/crd/infrastructure.ontai.dev_infrastructurerunnerconfigs.yaml deleted file mode 100644 index 43ea7a5..0000000 --- a/config/crd/infrastructure.ontai.dev_infrastructurerunnerconfigs.yaml +++ /dev/null @@ -1,323 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.16.1 - name: infrastructurerunnerconfigs.infrastructure.ontai.dev -spec: - group: infrastructure.ontai.dev - names: - kind: InfrastructureRunnerConfig - listKind: InfrastructureRunnerConfigList - plural: infrastructurerunnerconfigs - shortNames: - - irc - singular: infrastructurerunnerconfig - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .spec.clusterRef - name: Cluster - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: |- - InfrastructureRunnerConfig is the seam-core CRD for Conductor agent runtime configuration. - Owned by seam-core; authored exclusively by the platform operator. INV-009. - conductor-schema.md. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: |- - InfrastructureRunnerConfigSpec is the operator-generated operational contract for a - specific cluster. Generated at runtime by platform using the runner shared library. - Never human-authored. INV-009, INV-010. conductor-schema.md. - properties: - clusterRef: - description: ClusterRef is the name of the TalosCluster this RunnerConfig - is authoritative for. - type: string - maintenanceTargetNodes: - description: MaintenanceTargetNodes is the list of node names that - are the subject of the operation. - items: - type: string - type: array - operationalHistory: - description: OperationalHistory is an append-only record of completed - RunnerConfig executions. - items: - description: |- - RunnerOperationalHistoryEntry is a single append-only audit record describing one - configuration change applied to this RunnerConfig. Never truncated. - properties: - appliedAt: - description: AppliedAt is the time this change was applied. - format: date-time - type: string - appliedBy: - description: AppliedBy identifies who applied the change. - type: string - concern: - description: Concern identifies what aspect of configuration - changed. - type: string - newValue: - description: NewValue is the value after the change. - type: string - previousValue: - description: PreviousValue is the value before the change. Empty - for initial entries. - type: string - required: - - appliedAt - - appliedBy - - concern - - newValue - type: object - type: array - operatorLeaderNode: - description: OperatorLeaderNode is the node hosting the leader pod - of the initiating operator. - type: string - phases: - description: Phases is the ordered list of operational phases for - this cluster's Conductor lifecycle. - items: - description: RunnerPhaseConfig carries per-phase parameters for - the runner's execution context. - properties: - name: - description: Name identifies the phase. - type: string - parameters: - additionalProperties: - type: string - description: Parameters holds phase-specific key-value configuration. - type: object - required: - - name - type: object - type: array - runnerImage: - description: |- - RunnerImage is the fully qualified container image reference for the Conductor agent. - Tag convention: v{talosVersion}-r{revision} stable, dev/dev-rc{N} development. INV-011. - type: string - selfOperation: - description: SelfOperation is true when the Job's execution cluster - and the target cluster are the same. - type: boolean - steps: - description: Steps is the ordered list of execution steps across all - phases. - items: - description: RunnerConfigStep declares one step in a multi-step - operation intent. - properties: - capability: - description: Capability is the named Conductor capability to - invoke for this step. - type: string - dependsOn: - description: DependsOn is the name of a prior step that must - complete before this step begins. - type: string - haltOnFailure: - description: |- - HaltOnFailure controls sequencer behaviour when this step fails. - When true, failure terminates the RunnerConfig with no further steps executing. - type: boolean - name: - description: Name is the unique identifier for this step within - the RunnerConfig. - type: string - parameters: - additionalProperties: - type: string - description: Parameters is the input parameter map passed to - the capability at Job materialisation time. - type: object - required: - - capability - - name - type: object - type: array - required: - - clusterRef - - runnerImage - type: object - status: - description: |- - InfrastructureRunnerConfigStatus is written exclusively by the Conductor agent leader. - CR-INV-006. - properties: - agentLeader: - description: AgentLeader is the pod name of the current Conductor - agent leader. - type: string - agentVersion: - description: AgentVersion is the version string of the Conductor agent - binary currently running. - type: string - capabilities: - description: |- - Capabilities is the self-declared capability manifest emitted by the Conductor agent on startup. - CR-INV-005. - items: - description: RunnerCapabilityEntry is one capability declared by - the Conductor agent on startup. - properties: - description: - description: Description is a human-readable description of - what this capability does. - type: string - name: - description: Name is the capability name (e.g., pack-deploy, - talos-upgrade). - type: string - version: - description: Version is the capability version declared by the - agent. - type: string - required: - - name - - version - type: object - type: array - conditions: - description: Conditions is the standard Kubernetes condition list - for this RunnerConfig. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - failedStep: - description: |- - FailedStep is the name of the first step that reached the Failed phase. - Present only when Phase="Failed". conductor-schema.md §17. - type: string - phase: - description: |- - Phase is the terminal execution phase written by Conductor execute mode. - "Completed" means all steps succeeded. "Failed" means at least one step failed. - Empty means execution is in progress. Platform operators watch this field to - detect terminal conditions without scanning StepResults. conductor-schema.md §17. - type: string - stepResults: - description: StepResults is the ordered list of step result records - written by Conductor execute mode. - items: - description: RunnerConfigStepResult is the status record for one - step. - properties: - completedAt: - description: CompletedAt is the time this step finished execution. - format: date-time - type: string - message: - description: Message is additional context about the step outcome. - type: string - name: - description: Name matches the Name field of the corresponding - RunnerConfigStep in spec. - type: string - startedAt: - description: StartedAt is the time this step began execution. - format: date-time - type: string - status: - allOf: - - enum: - - Succeeded - - Failed - - Skipped - - enum: - - Succeeded - - Failed - - Skipped - description: Status is the terminal status of this step execution. - type: string - required: - - name - - status - type: object - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/internal/controller/clusterpack_reconciler.go b/internal/controller/clusterpack_reconciler.go index f5ddd0d..d8afab8 100644 --- a/internal/controller/clusterpack_reconciler.go +++ b/internal/controller/clusterpack_reconciler.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/seam-core/pkg/conditions" ) @@ -58,17 +59,17 @@ type ClusterPackReconciler struct { // Reconcile is the main reconciliation loop for ClusterPack. // -// +kubebuilder:rbac:groups=infrastructure.ontai.dev,resources=infrastructureclusterpacks,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=infrastructure.ontai.dev,resources=infrastructureclusterpacks/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=infrastructure.ontai.dev,resources=infrastructureclusterpacks/finalizers,verbs=update -// +kubebuilder:rbac:groups=infrastructure.ontai.dev,resources=infrastructurepackinstances,verbs=get;list;delete -// +kubebuilder:rbac:groups=infrastructure.ontai.dev,resources=infrastructurepackexecutions,verbs=get;list;create;delete +// +kubebuilder:rbac:groups=seam.ontai.dev,resources=packdeliveries,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=seam.ontai.dev,resources=packdeliveries/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=seam.ontai.dev,resources=packdeliveries/finalizers,verbs=update +// +kubebuilder:rbac:groups=seam.ontai.dev,resources=packinstalleds,verbs=get;list;delete +// +kubebuilder:rbac:groups=seam.ontai.dev,resources=packexecutions,verbs=get;list;create;delete // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch func (r *ClusterPackReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) // Step A — Fetch the ClusterPack CR. - cp := &seamcorev1alpha1.InfrastructureClusterPack{} + cp := &dispatcherv1alpha1.PackDelivery{} if err := r.Client.Get(ctx, req.NamespacedName, cp); err != nil { if apierrors.IsNotFound(err) { logger.Info("ClusterPack not found — likely deleted, ignoring", @@ -236,7 +237,7 @@ func (r *ClusterPackReconciler) Reconcile(ctx context.Context, req ctrl.Request) peName := cp.Name + "-" + clusterName // If PackInstance exists with current version, delivery is already complete — skip. - existingPI := &seamcorev1alpha1.InfrastructurePackInstance{} + existingPI := &dispatcherv1alpha1.PackInstalled{} piErr := r.Client.Get(ctx, client.ObjectKey{Name: peName, Namespace: tenantNS}, existingPI) if piErr == nil { if existingPI.Spec.Version == cp.Spec.Version { @@ -255,7 +256,7 @@ func (r *ClusterPackReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // Skip if PackExecution already exists — delivery is in progress. - existingPE := &seamcorev1alpha1.InfrastructurePackExecution{} + existingPE := &dispatcherv1alpha1.PackExecution{} if err := r.Client.Get(ctx, client.ObjectKey{Name: peName, Namespace: tenantNS}, existingPE); err == nil { logger.Info("PackExecution exists — skipping creation", "pack", cp.Name, "cluster", clusterName) @@ -265,7 +266,7 @@ func (r *ClusterPackReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // Create PackExecution directly for pack delivery to this cluster. - newPE := &seamcorev1alpha1.InfrastructurePackExecution{ + newPE := &dispatcherv1alpha1.PackExecution{ ObjectMeta: metav1.ObjectMeta{ Name: peName, Namespace: tenantNS, @@ -275,8 +276,8 @@ func (r *ClusterPackReconciler) Reconcile(ctx context.Context, req ctrl.Request) "infrastructure.ontai.dev/cluster": clusterName, }, }, - Spec: seamcorev1alpha1.InfrastructurePackExecutionSpec{ - ClusterPackRef: seamcorev1alpha1.InfrastructureClusterPackRef{ + Spec: dispatcherv1alpha1.PackExecutionSpec{ + PackDeliveryRef: dispatcherv1alpha1.PackDeliveryRef{ Name: cp.Name, Version: cp.Spec.Version, }, @@ -303,12 +304,12 @@ func (r *ClusterPackReconciler) Reconcile(ctx context.Context, req ctrl.Request) // re-records the rolled-back state on the next reconcile), and clears rollbackToRevision. // N-step rollback: any prior retained revision is reachable in one operation. // wrapper-schema.md §6.2. seam-core-schema.md §7.8. -func (r *ClusterPackReconciler) handleRollback(ctx context.Context, cp *seamcorev1alpha1.InfrastructureClusterPack) (ctrl.Result, error) { +func (r *ClusterPackReconciler) handleRollback(ctx context.Context, cp *dispatcherv1alpha1.PackDelivery) (ctrl.Result, error) { logger := log.FromContext(ctx) targetRevision := cp.Spec.RollbackToRevision // List ALL PORs for this ClusterPack: active and superseded. - porList := &seamcorev1alpha1.PackOperationResultList{} + porList := &dispatcherv1alpha1.PackLogList{} if err := r.Client.List(ctx, porList, client.InNamespace(cp.Namespace), client.MatchingLabels{"ontai.dev/cluster-pack": cp.Name}, @@ -323,7 +324,7 @@ func (r *ClusterPackReconciler) handleRollback(ctx context.Context, cp *seamcore } // Find the POR at exactly the requested revision. - var targetPOR *seamcorev1alpha1.PackOperationResult + var targetPOR *dispatcherv1alpha1.PackLog for i := range porList.Items { if porList.Items[i].Spec.Revision == targetRevision { targetPOR = &porList.Items[i] @@ -337,7 +338,7 @@ func (r *ClusterPackReconciler) handleRollback(ctx context.Context, cp *seamcore return r.clearRollbackField(ctx, cp) } - targetVersion := targetPOR.Spec.ClusterPackVersion + targetVersion := targetPOR.Spec.PackDeliveryVersion targetRBACDigest := targetPOR.Spec.RBACDigest targetWorkloadDigest := targetPOR.Spec.WorkloadDigest @@ -375,7 +376,7 @@ func (r *ClusterPackReconciler) handleRollback(ctx context.Context, cp *seamcore } // clearRollbackField resets spec.rollbackToRevision to 0 when rollback cannot proceed. -func (r *ClusterPackReconciler) clearRollbackField(ctx context.Context, cp *seamcorev1alpha1.InfrastructureClusterPack) (ctrl.Result, error) { +func (r *ClusterPackReconciler) clearRollbackField(ctx context.Context, cp *dispatcherv1alpha1.PackDelivery) (ctrl.Result, error) { patch := client.MergeFrom(cp.DeepCopy()) cp.Spec.RollbackToRevision = 0 if err := r.Client.Patch(ctx, cp, patch); err != nil { @@ -390,19 +391,19 @@ func (r *ClusterPackReconciler) clearRollbackField(ctx context.Context, cp *seam // 2. Delete all PackExecutions whose spec.clusterPackRef.name matches cp.Name // (listed cluster-wide, filtered in memory — no field index registered). // 3. Remove the clusterPackFinalizer so the API server can delete the object. -func (r *ClusterPackReconciler) handleClusterPackDeletion(ctx context.Context, cp *seamcorev1alpha1.InfrastructureClusterPack) (ctrl.Result, error) { +func (r *ClusterPackReconciler) handleClusterPackDeletion(ctx context.Context, cp *dispatcherv1alpha1.PackDelivery) (ctrl.Result, error) { logger := log.FromContext(ctx) // 1. Delete PackInstances matching spec.clusterPackRef == cp.Name. // PackInstances have no registered field index on spec.clusterPackRef, so list // all PackInstances cluster-wide and filter in memory. - piList := &seamcorev1alpha1.InfrastructurePackInstanceList{} + piList := &dispatcherv1alpha1.PackInstalledList{} if err := r.Client.List(ctx, piList); err != nil { return ctrl.Result{}, fmt.Errorf("list PackInstances for ClusterPack %s cleanup: %w", cp.Name, err) } for i := range piList.Items { pi := &piList.Items[i] - if pi.Spec.ClusterPackRef != cp.Name { + if pi.Spec.PackDeliveryRef != cp.Name { continue } if err := r.Client.Delete(ctx, pi); err != nil && !apierrors.IsNotFound(err) { @@ -415,13 +416,13 @@ func (r *ClusterPackReconciler) handleClusterPackDeletion(ctx context.Context, c // 2. Delete PackExecutions whose spec.clusterPackRef.name matches cp.Name. // PackExecution has no registered field index on spec.clusterPackRef.name, so list // all cluster-wide and filter in memory. wrapper-schema.md §3 delivery chain. - peList := &seamcorev1alpha1.InfrastructurePackExecutionList{} + peList := &dispatcherv1alpha1.PackExecutionList{} if err := r.Client.List(ctx, peList); err != nil { return ctrl.Result{}, fmt.Errorf("list PackExecutions for ClusterPack %s cleanup: %w", cp.Name, err) } for i := range peList.Items { pe := &peList.Items[i] - if pe.Spec.ClusterPackRef.Name != cp.Name { + if pe.Spec.PackDeliveryRef.Name != cp.Name { continue } if err := r.Client.Delete(ctx, pe); err != nil && !apierrors.IsNotFound(err) { @@ -465,11 +466,11 @@ func (r *ClusterPackReconciler) MapPackExecutionToClusterPack( ctx context.Context, obj client.Object, ) []reconcile.Request { - pe, ok := obj.(*seamcorev1alpha1.InfrastructurePackExecution) + pe, ok := obj.(*dispatcherv1alpha1.PackExecution) if !ok { return nil } - cpName := pe.Spec.ClusterPackRef.Name + cpName := pe.Spec.PackDeliveryRef.Name if cpName == "" { return nil } @@ -479,7 +480,7 @@ func (r *ClusterPackReconciler) MapPackExecutionToClusterPack( // name share the same convention: {cpName}-{clusterName}. The deleted PE's own name // is therefore exactly the PackInstance name we need to remove. ns := pe.GetNamespace() - pi := &seamcorev1alpha1.InfrastructurePackInstance{} + pi := &dispatcherv1alpha1.PackInstalled{} if getErr := r.Client.Get(ctx, client.ObjectKey{Name: pe.GetName(), Namespace: ns}, pi); getErr == nil { _ = r.Client.Delete(ctx, pi) } @@ -532,16 +533,16 @@ func (r *ClusterPackReconciler) SetupWithManager(mgr ctrl.Manager) error { GenericFunc: func(_ event.GenericEvent) bool { return false }, } return ctrl.NewControllerManagedBy(mgr). - For(&seamcorev1alpha1.InfrastructureClusterPack{}, builder.WithPredicates(predicate.Or( + For(&dispatcherv1alpha1.PackDelivery{}, builder.WithPredicates(predicate.Or( predicate.GenerationChangedPredicate{}, predicate.AnnotationChangedPredicate{}, ))). Owns(&batchv1.Job{}). - Watches(&seamcorev1alpha1.InfrastructurePackInstance{}, + Watches(&dispatcherv1alpha1.PackInstalled{}, handler.EnqueueRequestsFromMapFunc(r.MapPackInstanceToClusterPack), builder.WithPredicates(deleteOnlyPredicate), ). - Watches(&seamcorev1alpha1.InfrastructurePackExecution{}, + Watches(&dispatcherv1alpha1.PackExecution{}, handler.EnqueueRequestsFromMapFunc(r.MapPackExecutionToClusterPack), builder.WithPredicates(deleteOnlyPredicate), ). @@ -558,11 +559,11 @@ func (r *ClusterPackReconciler) MapPackInstanceToClusterPack( ctx context.Context, obj client.Object, ) []reconcile.Request { - pi, ok := obj.(*seamcorev1alpha1.InfrastructurePackInstance) + pi, ok := obj.(*dispatcherv1alpha1.PackInstalled) if !ok { return nil } - cpName := pi.Spec.ClusterPackRef + cpName := pi.Spec.PackDeliveryRef if cpName == "" { return nil } @@ -573,7 +574,7 @@ func (r *ClusterPackReconciler) MapPackInstanceToClusterPack( clusterName := strings.TrimPrefix(ns, "seam-tenant-") if clusterName != ns { peName := cpName + "-" + clusterName - pe := &seamcorev1alpha1.InfrastructurePackExecution{} + pe := &dispatcherv1alpha1.PackExecution{} if getErr := r.Client.Get(ctx, client.ObjectKey{Name: peName, Namespace: ns}, pe); getErr == nil { _ = r.Client.Delete(ctx, pe) } diff --git a/internal/controller/packexecution_reconciler.go b/internal/controller/packexecution_reconciler.go index 29663f4..45c1913 100644 --- a/internal/controller/packexecution_reconciler.go +++ b/internal/controller/packexecution_reconciler.go @@ -25,7 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" - seamv1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/seam-core/pkg/conditions" "github.com/ontai-dev/seam-core/pkg/lineage" ) @@ -96,9 +96,9 @@ const ( // safely receive any pack delivery regardless of pack signature or RBAC state. // platform-schema.md §12, Gap 27. // RBACReadyChecker is the function signature for gate 5 RBAC checks. -// In production, the reconciler uses isWrapperRunnerRBACReady (SubjectAccessReview). +// In production, the reconciler uses isDispatcherRunnerRBACReady (SubjectAccessReview). // In tests, set RBACChecker to a stub that returns (true, "", nil) to bypass the gate. -type RBACReadyChecker func(ctx context.Context, pe *seamv1alpha1.InfrastructurePackExecution) (bool, string, error) +type RBACReadyChecker func(ctx context.Context, pe *dispatcherv1alpha1.PackExecution) (bool, string, error) type PackExecutionReconciler struct { Client client.Client @@ -110,10 +110,10 @@ type PackExecutionReconciler struct { // Reconcile is the main reconciliation loop for PackExecution. // -// +kubebuilder:rbac:groups=infrastructure.ontai.dev,resources=infrastructurepackexecutions,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=infrastructure.ontai.dev,resources=infrastructurepackexecutions/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=infrastructure.ontai.dev,resources=infrastructurepackexecutions/finalizers,verbs=update -// +kubebuilder:rbac:groups=infrastructure.ontai.dev,resources=infrastructureclusterpacks,verbs=get;list;watch +// +kubebuilder:rbac:groups=seam.ontai.dev,resources=packexecutions,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=seam.ontai.dev,resources=packexecutions/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=seam.ontai.dev,resources=packexecutions/finalizers,verbs=update +// +kubebuilder:rbac:groups=seam.ontai.dev,resources=packdeliveries,verbs=get;list;watch // +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch @@ -122,7 +122,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques logger := log.FromContext(ctx) // Step A — Fetch the PackExecution CR. - pe := &seamv1alpha1.InfrastructurePackExecution{} + pe := &dispatcherv1alpha1.PackExecution{} if err := r.Client.Get(ctx, req.NamespacedName, pe); err != nil { if apierrors.IsNotFound(err) { logger.Info("PackExecution not found — likely deleted, ignoring", @@ -219,8 +219,8 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques ) // Gate 1: Signature gate. - cp := &seamv1alpha1.InfrastructureClusterPack{} - cpKey := client.ObjectKey{Name: pe.Spec.ClusterPackRef.Name, Namespace: pe.Namespace} + cp := &dispatcherv1alpha1.PackDelivery{} + cpKey := client.ObjectKey{Name: pe.Spec.PackDeliveryRef.Name, Namespace: pe.Namespace} if err := r.Client.Get(ctx, cpKey, cp); err != nil { if apierrors.IsNotFound(err) { conditions.SetCondition( @@ -228,7 +228,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques conditions.ConditionTypePackExecutionPending, metav1.ConditionTrue, conditions.ReasonGatesClearing, - fmt.Sprintf("ClusterPack %q not found.", pe.Spec.ClusterPackRef.Name), + fmt.Sprintf("ClusterPack %q not found.", pe.Spec.PackDeliveryRef.Name), pe.Generation, ) return ctrl.Result{RequeueAfter: gateRequeueInterval}, nil @@ -241,14 +241,14 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques // the RunnerConfig with the current version and conductor can create a fresh // PackExecution. The deferred status patch will receive a NotFound error on // the deleted object, which is already silently ignored. - if cp.Spec.Version != pe.Spec.ClusterPackRef.Version { + if cp.Spec.Version != pe.Spec.PackDeliveryRef.Version { logger.Info("PackExecution version mismatch — deleting stale PackExecution", "name", pe.Name, - "peVersion", pe.Spec.ClusterPackRef.Version, + "peVersion", pe.Spec.PackDeliveryRef.Version, "cpVersion", cp.Spec.Version) r.Recorder.Eventf(pe, nil, corev1.EventTypeWarning, "StalePackExecutionDeleted", "StalePackExecutionDeleted", "PackExecution %q references ClusterPack version %q but current is %q -- deleting stale object.", - pe.Name, pe.Spec.ClusterPackRef.Version, cp.Spec.Version) + pe.Name, pe.Spec.PackDeliveryRef.Version, cp.Spec.Version) if err := r.Client.Delete(ctx, pe); err != nil && !apierrors.IsNotFound(err) { return ctrl.Result{}, fmt.Errorf("delete stale PackExecution %s: %w", pe.Name, err) } @@ -273,7 +273,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques pe.Generation, ) logger.Info("PackExecution gate 1 (signature) not cleared — requeueing", - "name", pe.Name, "clusterPack", pe.Spec.ClusterPackRef.Name) + "name", pe.Name, "clusterPack", pe.Spec.PackDeliveryRef.Name) return ctrl.Result{RequeueAfter: signatureRequeueInterval}, nil } // Signature cleared — clear the SignaturePending condition if set. @@ -375,10 +375,10 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques pe.Generation, ) - // Gate 5: WrapperRunnerRBAC gate. + // Gate 5: DispatcherRunnerRBAC gate. // SubjectAccessReview verifies wrapper-runner SA has required permissions // before submitting the Job. Catches stale or missing RBAC before runtime failure. - rbacCheckFn := r.isWrapperRunnerRBACReady + rbacCheckFn := r.isDispatcherRunnerRBACReady if r.RBACChecker != nil { rbacCheckFn = r.RBACChecker } @@ -389,9 +389,9 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques if !rbacSARReady { conditions.SetCondition( &pe.Status.Conditions, - conditions.ConditionTypeWrapperRunnerRBACNotReady, + conditions.ConditionTypeDispatcherRunnerRBACNotReady, metav1.ConditionTrue, - conditions.ReasonWrapperRunnerRBACNotReady, + conditions.ReasonDispatcherRunnerRBACNotReady, rbacSARDenyReason, pe.Generation, ) @@ -400,7 +400,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques conditions.ConditionTypePackExecutionPending, metav1.ConditionTrue, conditions.ReasonGatesClearing, - "Waiting for WrapperRunnerRBAC gate (gate 5).", + "Waiting for DispatcherRunnerRBAC gate (gate 5).", pe.Generation, ) logger.Info("PackExecution gate 5 (WrapperRunnerRBAC) not cleared — requeueing", @@ -409,9 +409,9 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques } conditions.SetCondition( &pe.Status.Conditions, - conditions.ConditionTypeWrapperRunnerRBACNotReady, + conditions.ConditionTypeDispatcherRunnerRBACNotReady, metav1.ConditionFalse, - conditions.ReasonWrapperRunnerRBACNotReady, + conditions.ReasonDispatcherRunnerRBACNotReady, "wrapper-runner has required RBAC permissions.", pe.Generation, ) @@ -485,8 +485,8 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques if !alreadyOwned { porPatch := client.MergeFrom(resultPOR.DeepCopy()) resultPOR.OwnerReferences = append(resultPOR.OwnerReferences, metav1.OwnerReference{ - APIVersion: seamv1alpha1.GroupVersion.String(), - Kind: "InfrastructurePackExecution", + APIVersion: dispatcherv1alpha1.GroupVersion.String(), + Kind: "PackExecution", Name: pe.Name, UID: pe.UID, Controller: boolPtr(false), @@ -503,7 +503,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques porFailureReason := resultPOR.Spec.FailureReason porDeployedResources := resultPOR.Spec.DeployedResources - if porStatus == seamv1alpha1.PackResultFailed { + if porStatus == dispatcherv1alpha1.PackLogResultFailed { failMsg := "pack-deploy capability reported failure." if porFailureReason != nil { failMsg = fmt.Sprintf("step %q failed: %s", porFailureReason.FailedStep, porFailureReason.Reason) @@ -559,7 +559,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques // Step I.b — Create PackInstance only on capability success. // A failed capability means the workload was not (fully) delivered; // recording a PackInstance would misrepresent the cluster state. - if porStatus == seamv1alpha1.PackResultFailed { + if porStatus == dispatcherv1alpha1.PackLogResultFailed { return ctrl.Result{}, nil } @@ -573,7 +573,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques // one in-place rather than creating a parallel PackInstance. Decision 11. piBaseName := cp.Spec.BasePackName if piBaseName == "" { - piBaseName = pe.Spec.ClusterPackRef.Name + piBaseName = pe.Spec.PackDeliveryRef.Name } piName := piBaseName + "-" + pe.Spec.TargetClusterRef piNamespace := "seam-tenant-" + pe.Spec.TargetClusterRef @@ -581,25 +581,25 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques // Determine upgradeDirection by comparing existing PackInstance version. upgradeDir := packUpgradeDirection(ctx, r.Client, piName, piNamespace, cp.Spec.Version) - pi := &seamv1alpha1.InfrastructurePackInstance{ + pi := &dispatcherv1alpha1.PackInstalled{ ObjectMeta: metav1.ObjectMeta{ Name: piName, Namespace: piNamespace, Labels: map[string]string{ - "infrastructure.ontai.dev/pack": pe.Spec.ClusterPackRef.Name, + "infrastructure.ontai.dev/pack": pe.Spec.PackDeliveryRef.Name, "infrastructure.ontai.dev/cluster": pe.Spec.TargetClusterRef, }, OwnerReferences: []metav1.OwnerReference{{ - APIVersion: seamv1alpha1.GroupVersion.String(), - Kind: "InfrastructurePackExecution", + APIVersion: dispatcherv1alpha1.GroupVersion.String(), + Kind: "PackExecution", Name: pe.Name, UID: pe.UID, Controller: boolPtr(true), BlockOwnerDeletion: boolPtr(true), }}, }, - Spec: seamv1alpha1.InfrastructurePackInstanceSpec{ - ClusterPackRef: pe.Spec.ClusterPackRef.Name, + Spec: dispatcherv1alpha1.PackInstalledSpec{ + PackDeliveryRef: pe.Spec.PackDeliveryRef.Name, Version: cp.Spec.Version, // WS6: carry version for DSNSReconciler TargetClusterRef: pe.Spec.TargetClusterRef, }, @@ -613,12 +613,12 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques } // PackInstance already exists (basePackName supersession path). // Update spec to reflect the new ClusterPack version. - existing := &seamv1alpha1.InfrastructurePackInstance{} + existing := &dispatcherv1alpha1.PackInstalled{} if getErr := r.Client.Get(ctx, client.ObjectKey{Name: piName, Namespace: piNamespace}, existing); getErr != nil { return ctrl.Result{}, fmt.Errorf("get existing PackInstance %s: %w", piName, getErr) } specPatch := client.MergeFrom(existing.DeepCopy()) - existing.Spec.ClusterPackRef = pe.Spec.ClusterPackRef.Name + existing.Spec.PackDeliveryRef = pe.Spec.PackDeliveryRef.Name existing.Spec.Version = cp.Spec.Version if patchErr := r.Client.Patch(ctx, existing, specPatch); patchErr != nil { return ctrl.Result{}, fmt.Errorf("patch PackInstance spec %s: %w", piName, patchErr) @@ -630,16 +630,16 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques // Write DeployedResources and UpgradeDirection to PackInstance status. // DeployedResources enables deletion cleanup (INV-006: no Jobs on delete path). // UpgradeDirection enables rollback tracking. wrapper-schema.md §3, Decision 11. - piStatus := &seamv1alpha1.InfrastructurePackInstance{} + piStatus := &dispatcherv1alpha1.PackInstalled{} piKey := client.ObjectKey{Name: piName, Namespace: piNamespace} if getErr := r.Client.Get(ctx, piKey, piStatus); getErr == nil { piPatch := client.MergeFrom(piStatus.DeepCopy()) // Replace DeployedResources (not append) to reflect the current deployment. // On version supersession the previous resource list is replaced with // the new one so the deletion handler cleans up the correct resources. - newRefs := make([]seamv1alpha1.InfrastructureDeployedResourceRef, 0, len(porDeployedResources)) + newRefs := make([]dispatcherv1alpha1.DeployedResourceRef, 0, len(porDeployedResources)) for _, dr := range porDeployedResources { - newRefs = append(newRefs, seamv1alpha1.InfrastructureDeployedResourceRef(dr)) + newRefs = append(newRefs, dispatcherv1alpha1.DeployedResourceRef(dr)) } piStatus.Status.DeployedResources = newRefs piStatus.Status.UpgradeDirection = string(upgradeDir) @@ -713,7 +713,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques // has a Fresh=True condition. PermissionSnapshots live exclusively in seam-system // and are named "snapshot-{clusterRef}". guardian-schema.md §PS condition vocabulary. // Returns false (not error) if the snapshot is not found or not fresh. -func (r *PackExecutionReconciler) isPermissionSnapshotCurrent(ctx context.Context, pe *seamv1alpha1.InfrastructurePackExecution) (bool, error) { +func (r *PackExecutionReconciler) isPermissionSnapshotCurrent(ctx context.Context, pe *dispatcherv1alpha1.PackExecution) (bool, error) { ps := &unstructured.Unstructured{} ps.SetGroupVersionKind(schema.GroupVersionKind{ Group: "security.ontai.dev", @@ -752,7 +752,7 @@ func (r *PackExecutionReconciler) isPermissionSnapshotCurrent(ctx context.Contex // Returns false only when the profile EXISTS but provisioned=false, blocking the Job // until guardian reconciles it. Pack RBACProfiles live in seam-tenant-{targetCluster}. // guardian-schema.md §RBACProfile. -func (r *PackExecutionReconciler) isRBACProfileProvisioned(ctx context.Context, pe *seamv1alpha1.InfrastructurePackExecution) (bool, error) { +func (r *PackExecutionReconciler) isRBACProfileProvisioned(ctx context.Context, pe *dispatcherv1alpha1.PackExecution) (bool, error) { rp := &unstructured.Unstructured{} rp.SetGroupVersionKind(schema.GroupVersionKind{ Group: "security.ontai.dev", @@ -796,7 +796,7 @@ func (r *PackExecutionReconciler) isRBACProfileProvisioned(ctx context.Context, // Returns (false, nil) when TalosCluster is absent, RunnerConfig is absent, or // status.capabilities is empty. Returns (false, err) for unexpected API errors. // platform-schema.md §12, Gap 27. -func (r *PackExecutionReconciler) isConductorReadyForCluster(ctx context.Context, pe *seamv1alpha1.InfrastructurePackExecution) (bool, error) { +func (r *PackExecutionReconciler) isConductorReadyForCluster(ctx context.Context, pe *dispatcherv1alpha1.PackExecution) (bool, error) { clusterRef := pe.Spec.TargetClusterRef tcGVK := schema.GroupVersionKind{ Group: "infrastructure.ontai.dev", @@ -846,7 +846,7 @@ func (r *PackExecutionReconciler) isConductorReadyForCluster(ctx context.Context return true, nil } -// isWrapperRunnerRBACReady performs SubjectAccessReview checks to verify that +// isDispatcherRunnerRBACReady performs SubjectAccessReview checks to verify that // the wrapper-runner ServiceAccount in pe.Namespace has the required RBAC // permissions before a Kueue Job is submitted. Returns (true, "", nil) when // all permissions are granted. Returns (false, denyReason, nil) when any @@ -856,7 +856,7 @@ func (r *PackExecutionReconciler) isConductorReadyForCluster(ctx context.Context // RBAC before the Job pod runs and fails with a Forbidden error. The three // checks mirror the permissions declared in the compiler-generated // wrapper-runner Role (05-post-bootstrap/wrapper-runner.yaml). -func (r *PackExecutionReconciler) isWrapperRunnerRBACReady(ctx context.Context, pe *seamv1alpha1.InfrastructurePackExecution) (bool, string, error) { +func (r *PackExecutionReconciler) isDispatcherRunnerRBACReady(ctx context.Context, pe *dispatcherv1alpha1.PackExecution) (bool, string, error) { saUser := "system:serviceaccount:" + pe.Namespace + ":wrapper-runner" checks := []struct { @@ -900,8 +900,8 @@ func (r *PackExecutionReconciler) isWrapperRunnerRBACReady(ctx context.Context, // The Job carries the kueue.x-k8s.io/queue-name label — without this label, // the Job will not be admitted. wrapper-design.md §4. func (r *PackExecutionReconciler) buildPackDeployJob( - pe *seamv1alpha1.InfrastructurePackExecution, - cp *seamv1alpha1.InfrastructureClusterPack, + pe *dispatcherv1alpha1.PackExecution, + cp *dispatcherv1alpha1.PackDelivery, jobName string, ) *batchv1.Job { ttl := packDeployJobTTL @@ -924,8 +924,8 @@ func (r *PackExecutionReconciler) buildPackDeployJob( }, OwnerReferences: []metav1.OwnerReference{ { - APIVersion: seamv1alpha1.GroupVersion.String(), - Kind: "InfrastructurePackExecution", + APIVersion: dispatcherv1alpha1.GroupVersion.String(), + Kind: "PackExecution", Name: pe.Name, UID: pe.UID, Controller: boolPtr(true), @@ -998,7 +998,7 @@ func (r *PackExecutionReconciler) buildPackDeployJob( } // packDeployJobName constructs a deterministic Job name from the PackExecution name. -func packDeployJobName(pe *seamv1alpha1.InfrastructurePackExecution) string { +func packDeployJobName(pe *dispatcherv1alpha1.PackExecution) string { return fmt.Sprintf("pack-deploy-%s", pe.Name) } @@ -1042,7 +1042,7 @@ func (r *PackExecutionReconciler) SetupWithManager(mgr ctrl.Manager) error { Kind: "InfrastructureRunnerConfig", }) return ctrl.NewControllerManagedBy(mgr). - For(&seamv1alpha1.InfrastructurePackExecution{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + For(&dispatcherv1alpha1.PackExecution{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). Owns(&batchv1.Job{}). Watches(psObj, handler.EnqueueRequestsFromMapFunc(r.mapSnapshotToPackExecutions)). Watches(rpObj, handler.EnqueueRequestsFromMapFunc(r.mapRBACProfileToPackExecutions)). @@ -1064,7 +1064,7 @@ func (r *PackExecutionReconciler) mapSnapshotToPackExecutions( return nil } ns := "seam-tenant-" + clusterRef - peList := &seamv1alpha1.InfrastructurePackExecutionList{} + peList := &dispatcherv1alpha1.PackExecutionList{} if err := r.Client.List(ctx, peList, client.InNamespace(ns)); err != nil { return nil } @@ -1090,23 +1090,23 @@ func packUpgradeDirection( ctx context.Context, c client.Client, piName, piNamespace, newVersion string, -) seamv1alpha1.PackUpgradeDirection { - existing := &seamv1alpha1.InfrastructurePackInstance{} +) dispatcherv1alpha1.PackLogUpgradeDirection { + existing := &dispatcherv1alpha1.PackInstalled{} if err := c.Get(ctx, client.ObjectKey{Name: piName, Namespace: piNamespace}, existing); err != nil { - return seamv1alpha1.PackUpgradeDirectionInitial + return dispatcherv1alpha1.PackLogUpgradeDirectionInitial } existingVer := existing.Spec.Version if existingVer == "" { - return seamv1alpha1.PackUpgradeDirectionInitial + return dispatcherv1alpha1.PackLogUpgradeDirectionInitial } cmp := comparePackVersion(existingVer, newVersion) switch { case cmp == 0: - return seamv1alpha1.PackUpgradeDirectionRedeploy + return dispatcherv1alpha1.PackLogUpgradeDirectionRedeploy case cmp < 0: - return seamv1alpha1.PackUpgradeDirectionUpgrade + return dispatcherv1alpha1.PackLogUpgradeDirectionUpgrade default: - return seamv1alpha1.PackUpgradeDirectionRollback + return dispatcherv1alpha1.PackLogUpgradeDirectionRollback } } @@ -1158,7 +1158,7 @@ func (r *PackExecutionReconciler) mapRunnerConfigToPackExecutions( } clusterRef := obj.GetName() ns := "seam-tenant-" + clusterRef - peList := &seamv1alpha1.InfrastructurePackExecutionList{} + peList := &dispatcherv1alpha1.PackExecutionList{} if err := r.Client.List(ctx, peList, client.InNamespace(ns)); err != nil { return nil } @@ -1181,7 +1181,7 @@ func (r *PackExecutionReconciler) mapRBACProfileToPackExecutions( obj client.Object, ) []reconcile.Request { profileName := obj.GetName() - peList := &seamv1alpha1.InfrastructurePackExecutionList{} + peList := &dispatcherv1alpha1.PackExecutionList{} if err := r.Client.List(ctx, peList); err != nil { return nil } @@ -1204,8 +1204,8 @@ func findLatestPOR( ctx context.Context, c client.Client, namespace, packExecutionRef string, -) (*seamv1alpha1.PackOperationResult, error) { - list := &seamv1alpha1.PackOperationResultList{} +) (*dispatcherv1alpha1.PackLog, error) { + list := &dispatcherv1alpha1.PackLogList{} if err := c.List(ctx, list, client.InNamespace(namespace), client.MatchingLabels{labelPackExecution: packExecutionRef}, diff --git a/internal/controller/packinstance_reconciler.go b/internal/controller/packinstance_reconciler.go index e9537b1..3fbd3ee 100644 --- a/internal/controller/packinstance_reconciler.go +++ b/internal/controller/packinstance_reconciler.go @@ -20,7 +20,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" - seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/seam-core/pkg/conditions" ) @@ -62,16 +62,16 @@ type PackInstanceReconciler struct { // Reconcile is the main reconciliation loop for PackInstance. // -// +kubebuilder:rbac:groups=infrastructure.ontai.dev,resources=infrastructurepackinstances,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=infrastructure.ontai.dev,resources=infrastructurepackinstances/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=infrastructure.ontai.dev,resources=infrastructurepackinstances/finalizers,verbs=update -// +kubebuilder:rbac:groups=infrastructure.ontai.dev,resources=infrastructurepackexecutions,verbs=get;list;watch +// +kubebuilder:rbac:groups=seam.ontai.dev,resources=packinstalleds,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=seam.ontai.dev,resources=packinstalleds/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=seam.ontai.dev,resources=packinstalleds/finalizers,verbs=update +// +kubebuilder:rbac:groups=seam.ontai.dev,resources=packexecutions,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch func (r *PackInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) // Step A — Fetch the PackInstance CR. - pi := &seamcorev1alpha1.InfrastructurePackInstance{} + pi := &dispatcherv1alpha1.PackInstalled{} if err := r.Client.Get(ctx, req.NamespacedName, pi); err != nil { if apierrors.IsNotFound(err) { logger.Info("PackInstance not found — likely deleted, ignoring", @@ -141,7 +141,7 @@ func (r *PackInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request // PackInstance has Ready=True. We set Ready=True here as soon as the // pack-deploy Job completes successfully, without waiting for the conductor // agent to write a PackReceipt on the target cluster. - succeededPE, err := r.findSucceededPackExecution(ctx, pi.Namespace, pi.Spec.ClusterPackRef, pi.Spec.TargetClusterRef) + succeededPE, err := r.findSucceededPackExecution(ctx, pi.Namespace, pi.Spec.PackDeliveryRef, pi.Spec.TargetClusterRef) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to find succeeded PackExecution: %w", err) } @@ -152,8 +152,8 @@ func (r *PackInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request metav1.ConditionTrue, conditions.ReasonPackDelivered, fmt.Sprintf("Pack %s %s successfully delivered to %s.", - succeededPE.Spec.ClusterPackRef.Name, - succeededPE.Spec.ClusterPackRef.Version, + succeededPE.Spec.PackDeliveryRef.Name, + succeededPE.Spec.PackDeliveryRef.Version, pi.Spec.TargetClusterRef), pi.Generation, ) @@ -173,7 +173,7 @@ func (r *PackInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request // Step F — Read PackReceipt from the management cluster namespace. // PackReceipt is mirrored into the tenant namespace by the conductor agent. // PackReceipt name convention: {clusterPackRef}-{targetClusterRef} - receiptName := pi.Spec.ClusterPackRef + "-" + pi.Spec.TargetClusterRef + receiptName := pi.Spec.PackDeliveryRef + "-" + pi.Spec.TargetClusterRef receipt, err := r.getPackReceipt(ctx, receiptName, pi.Namespace) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to read PackReceipt: %w", err) @@ -207,7 +207,7 @@ func (r *PackInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request msg := fmt.Sprintf( "PackReceipt for pack %q on cluster %q reports signatureVerified=false. "+ "Security violation — all pack operations on this cluster are blocked.", - pi.Spec.ClusterPackRef, pi.Spec.TargetClusterRef, + pi.Spec.PackDeliveryRef, pi.Spec.TargetClusterRef, ) conditions.SetCondition( &pi.Status.Conditions, @@ -347,14 +347,14 @@ func (r *PackInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request func (r *PackInstanceReconciler) findSucceededPackExecution( ctx context.Context, namespace, clusterPackRef, targetClusterRef string, -) (*seamcorev1alpha1.InfrastructurePackExecution, error) { - list := &seamcorev1alpha1.InfrastructurePackExecutionList{} +) (*dispatcherv1alpha1.PackExecution, error) { + list := &dispatcherv1alpha1.PackExecutionList{} if err := r.Client.List(ctx, list, client.InNamespace(namespace)); err != nil { return nil, fmt.Errorf("list PackExecutions: %w", err) } for i := range list.Items { pe := &list.Items[i] - if pe.Spec.ClusterPackRef.Name != clusterPackRef { + if pe.Spec.PackDeliveryRef.Name != clusterPackRef { continue } if pe.Spec.TargetClusterRef != targetClusterRef { @@ -373,9 +373,9 @@ func (r *PackInstanceReconciler) findSucceededPackExecution( func (r *PackInstanceReconciler) getPackReceipt(ctx context.Context, name, namespace string) (*unstructured.Unstructured, error) { receipt := &unstructured.Unstructured{} receipt.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "infrastructure.ontai.dev", + Group: "seam.ontai.dev", Version: "v1alpha1", - Kind: "InfrastructurePackReceipt", + Kind: "PackReceipt", }) key := types.NamespacedName{Name: name, Namespace: namespace} if err := r.Client.Get(ctx, key, receipt); err != nil { @@ -389,20 +389,20 @@ func (r *PackInstanceReconciler) getPackReceipt(ctx context.Context, name, names // checkDependencyDrift checks the DependsOn list for any PackInstance that is // drifted and the DependencyPolicy is Block. Returns (blocked, blockedByName, err). -func (r *PackInstanceReconciler) checkDependencyDrift(ctx context.Context, pi *seamcorev1alpha1.InfrastructurePackInstance) (bool, string, error) { +func (r *PackInstanceReconciler) checkDependencyDrift(ctx context.Context, pi *dispatcherv1alpha1.PackInstalled) (bool, string, error) { if len(pi.Spec.DependsOn) == 0 { return false, "", nil } - policy := seamcorev1alpha1.InfrastructureDriftPolicyWarn + policy := dispatcherv1alpha1.DriftPolicyWarn if pi.Spec.DependencyPolicy != nil { policy = pi.Spec.DependencyPolicy.OnDrift } - if policy != seamcorev1alpha1.InfrastructureDriftPolicyBlock { + if policy != dispatcherv1alpha1.DriftPolicyBlock { // Warn and Ignore policies do not block. return false, "", nil } for _, depName := range pi.Spec.DependsOn { - dep := &seamcorev1alpha1.InfrastructurePackInstance{} + dep := &dispatcherv1alpha1.PackInstalled{} if err := r.Client.Get(ctx, client.ObjectKey{Name: depName, Namespace: pi.Namespace}, dep); err != nil { if apierrors.IsNotFound(err) { continue @@ -422,7 +422,7 @@ func (r *PackInstanceReconciler) checkDependencyDrift(ctx context.Context, pi *s // Secret in the seam-tenant-{targetCluster} namespace. Errors are logged per resource // but do not abort the cleanup. Returns an error only if the kubeconfig is unreadable. // INV-006: no Jobs on the delete path. wrapper-schema.md §3, Decision 11. -func (r *PackInstanceReconciler) cleanupDeployedResources(ctx context.Context, pi *seamcorev1alpha1.InfrastructurePackInstance) error { +func (r *PackInstanceReconciler) cleanupDeployedResources(ctx context.Context, pi *dispatcherv1alpha1.PackInstalled) error { logger := log.FromContext(ctx) if len(pi.Status.DeployedResources) == 0 { return nil @@ -517,6 +517,6 @@ func piIsVowel(c byte) bool { // SetupWithManager registers PackInstanceReconciler as the controller for PackInstance. func (r *PackInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&seamcorev1alpha1.InfrastructurePackInstance{}). + For(&dispatcherv1alpha1.PackInstalled{}). Complete(r) } diff --git a/test/integration/clusterpack_test.go b/test/integration/clusterpack_test.go index 685c681..dc36b6f 100644 --- a/test/integration/clusterpack_test.go +++ b/test/integration/clusterpack_test.go @@ -14,20 +14,20 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/seam-core/pkg/conditions" ) // newClusterPack constructs a minimal valid ClusterPack for testing. -func newClusterPack(name, namespace string) *seamcorev1alpha1.InfrastructureClusterPack { - return &seamcorev1alpha1.InfrastructureClusterPack{ +func newClusterPack(name, namespace string) *dispatcherv1alpha1.PackDelivery { + return &dispatcherv1alpha1.PackDelivery{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: seamcorev1alpha1.InfrastructureClusterPackSpec{ + Spec: dispatcherv1alpha1.PackDeliverySpec{ Version: "v1.0.0", - RegistryRef: seamcorev1alpha1.InfrastructurePackRegistryRef{ + RegistryRef: dispatcherv1alpha1.PackRegistryRef{ URL: "registry.example.com/packs/" + name, Digest: "sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc123", }, @@ -60,7 +60,7 @@ func TestClusterPack_AcceptedByAPIServer_SetsSignaturePending(t *testing.T) { // Verify the object persists in etcd. nn := types.NamespacedName{Name: cp.Name, Namespace: ns} - got := &seamcorev1alpha1.InfrastructureClusterPack{} + got := &dispatcherv1alpha1.PackDelivery{} if err := k8sClient.Get(ctx, nn, got); err != nil { t.Fatalf("ClusterPack not found in etcd after Create: %v", err) } @@ -69,7 +69,7 @@ func TestClusterPack_AcceptedByAPIServer_SetsSignaturePending(t *testing.T) { // The reconciler sets this condition when no pack signature annotation is // present (the pack has not yet been signed by the management cluster conductor). ok := poll(t, 10*time.Second, func() bool { - got := &seamcorev1alpha1.InfrastructureClusterPack{} + got := &dispatcherv1alpha1.PackDelivery{} if err := k8sClient.Get(ctx, nn, got); err != nil { return false } @@ -77,7 +77,7 @@ func TestClusterPack_AcceptedByAPIServer_SetsSignaturePending(t *testing.T) { return c != nil && c.Status == metav1.ConditionTrue }) if !ok { - got := &seamcorev1alpha1.InfrastructureClusterPack{} + got := &dispatcherv1alpha1.PackDelivery{} _ = k8sClient.Get(ctx, nn, got) t.Errorf("timed out waiting for SignaturePending=True; conditions: %v", got.Status.Conditions) } @@ -99,14 +99,14 @@ func TestClusterPack_ObservedGenerationAdvances(t *testing.T) { nn := types.NamespacedName{Name: cp.Name, Namespace: ns} ok := poll(t, 10*time.Second, func() bool { - got := &seamcorev1alpha1.InfrastructureClusterPack{} + got := &dispatcherv1alpha1.PackDelivery{} if err := k8sClient.Get(ctx, nn, got); err != nil { return false } return got.Status.ObservedGeneration == got.Generation && got.Generation > 0 }) if !ok { - got := &seamcorev1alpha1.InfrastructureClusterPack{} + got := &dispatcherv1alpha1.PackDelivery{} _ = k8sClient.Get(ctx, nn, got) t.Errorf("timed out: ObservedGeneration=%d Generation=%d", got.Status.ObservedGeneration, got.Generation) @@ -134,13 +134,13 @@ func TestPackInstance_OwnerRefCascade_DeletedWhenPackExecutionDeleted(t *testing ns := "seam-system" // Create the PackExecution owner object manually (no reconciler needed). - pe := &seamcorev1alpha1.InfrastructurePackExecution{ + pe := &dispatcherv1alpha1.PackExecution{ ObjectMeta: metav1.ObjectMeta{ Name: "cascade-owner-pe", Namespace: ns, }, - Spec: seamcorev1alpha1.InfrastructurePackExecutionSpec{ - ClusterPackRef: seamcorev1alpha1.InfrastructureClusterPackRef{Name: "some-pack", Version: "v1.0.0"}, + Spec: dispatcherv1alpha1.PackExecutionSpec{ + PackDeliveryRef: dispatcherv1alpha1.PackDeliveryRef{Name: "some-pack", Version: "v1.0.0"}, TargetClusterRef: "ccs-test", AdmissionProfileRef: "some-profile", }, @@ -150,7 +150,7 @@ func TestPackInstance_OwnerRefCascade_DeletedWhenPackExecutionDeleted(t *testing } // Read back to get the UID for the ownerRef. - pePatch := &seamcorev1alpha1.InfrastructurePackExecution{} + pePatch := &dispatcherv1alpha1.PackExecution{} if err := k8sClient.Get(ctx, types.NamespacedName{Name: pe.Name, Namespace: ns}, pePatch); err != nil { t.Fatalf("Get PackExecution UID: %v", err) } @@ -158,12 +158,12 @@ func TestPackInstance_OwnerRefCascade_DeletedWhenPackExecutionDeleted(t *testing // Create the PackInstance with BlockOwnerDeletion ownerRef to PackExecution. // This mirrors the ownerRef set by PackExecutionReconciler in production. truePtr := true - pi := &seamcorev1alpha1.InfrastructurePackInstance{ + pi := &dispatcherv1alpha1.PackInstalled{ ObjectMeta: metav1.ObjectMeta{ Name: "cascade-owned-pi", Namespace: ns, OwnerReferences: []metav1.OwnerReference{{ - APIVersion: seamcorev1alpha1.GroupVersion.String(), + APIVersion: dispatcherv1alpha1.GroupVersion.String(), Kind: "PackExecution", Name: pePatch.Name, UID: pePatch.UID, @@ -171,8 +171,8 @@ func TestPackInstance_OwnerRefCascade_DeletedWhenPackExecutionDeleted(t *testing BlockOwnerDeletion: &truePtr, }}, }, - Spec: seamcorev1alpha1.InfrastructurePackInstanceSpec{ - ClusterPackRef: "some-pack", + Spec: dispatcherv1alpha1.PackInstalledSpec{ + PackDeliveryRef: "some-pack", TargetClusterRef: "ccs-test", }, } @@ -182,7 +182,7 @@ func TestPackInstance_OwnerRefCascade_DeletedWhenPackExecutionDeleted(t *testing // Confirm both objects exist. piNN := types.NamespacedName{Name: pi.Name, Namespace: ns} - if err := k8sClient.Get(ctx, piNN, &seamcorev1alpha1.InfrastructurePackInstance{}); err != nil { + if err := k8sClient.Get(ctx, piNN, &dispatcherv1alpha1.PackInstalled{}); err != nil { t.Fatalf("PackInstance not found before cascade test: %v", err) } @@ -195,7 +195,7 @@ func TestPackInstance_OwnerRefCascade_DeletedWhenPackExecutionDeleted(t *testing // Wait for the PackInstance to be cascade-deleted from etcd. // The Kubernetes GC controller runs in the background and may take a moment. ok := poll(t, 20*time.Second, func() bool { - err := k8sClient.Get(ctx, piNN, &seamcorev1alpha1.InfrastructurePackInstance{}) + err := k8sClient.Get(ctx, piNN, &dispatcherv1alpha1.PackInstalled{}) return apierrors.IsNotFound(err) }) if !ok { diff --git a/test/unit/clusterpack_reconciler_test.go b/test/unit/clusterpack_reconciler_test.go index ab05e6b..f242ab4 100644 --- a/test/unit/clusterpack_reconciler_test.go +++ b/test/unit/clusterpack_reconciler_test.go @@ -16,6 +16,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/seam-core/pkg/conditions" "github.com/ontai-dev/wrapper/internal/controller" ) @@ -29,19 +30,22 @@ func newClusterPackScheme(t *testing.T) *runtime.Scheme { if err := seamcorev1alpha1.AddToScheme(s); err != nil { t.Fatalf("AddToScheme seamcorev1alpha1: %v", err) } + if err := dispatcherv1alpha1.AddToScheme(s); err != nil { + t.Fatalf("AddToScheme dispatcherv1alpha1: %v", err) + } return s } -func newClusterPack(name, namespace, version string) *seamcorev1alpha1.InfrastructureClusterPack { - return &seamcorev1alpha1.InfrastructureClusterPack{ +func newClusterPack(name, namespace, version string) *dispatcherv1alpha1.PackDelivery { + return &dispatcherv1alpha1.PackDelivery{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, Finalizers: []string{"infrastructure.ontai.dev/clusterpack-cleanup"}, }, - Spec: seamcorev1alpha1.InfrastructureClusterPackSpec{ + Spec: dispatcherv1alpha1.PackDeliverySpec{ Version: version, - RegistryRef: seamcorev1alpha1.InfrastructurePackRegistryRef{ + RegistryRef: dispatcherv1alpha1.PackRegistryRef{ URL: "registry.ontai.dev/packs/" + name, Digest: "sha256:abc123", }, @@ -50,7 +54,7 @@ func newClusterPack(name, namespace, version string) *seamcorev1alpha1.Infrastru } } -func reconcileCP(t *testing.T, r *controller.ClusterPackReconciler, cp *seamcorev1alpha1.InfrastructureClusterPack) ctrl.Result { +func reconcileCP(t *testing.T, r *controller.ClusterPackReconciler, cp *dispatcherv1alpha1.PackDelivery) ctrl.Result { t.Helper() result, err := r.Reconcile(context.Background(), ctrl.Request{ NamespacedName: types.NamespacedName{Name: cp.Name, Namespace: cp.Namespace}, @@ -68,7 +72,7 @@ func TestClusterPackReconciler_AwaitingSignature(t *testing.T) { s := newClusterPackScheme(t) cp := newClusterPack("my-pack", "infra-system", "v1.0.0") fakeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(cp). - WithStatusSubresource(&seamcorev1alpha1.InfrastructureClusterPack{}).Build() + WithStatusSubresource(&dispatcherv1alpha1.PackDelivery{}).Build() r := &controller.ClusterPackReconciler{ Client: fakeClient, Scheme: s, @@ -82,7 +86,7 @@ func TestClusterPackReconciler_AwaitingSignature(t *testing.T) { } // Re-fetch to check status. - updated := &seamcorev1alpha1.InfrastructureClusterPack{} + updated := &dispatcherv1alpha1.PackDelivery{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(cp), updated); err != nil { t.Fatalf("get updated ClusterPack: %v", err) } @@ -115,7 +119,7 @@ func TestClusterPackReconciler_SignedTransitionsToAvailable(t *testing.T) { "infrastructure.ontai.dev/spec-checksum-snapshot": cp.Spec.Checksum + "|" + cp.Spec.RegistryRef.URL + "|" + cp.Spec.RegistryRef.Digest + "|" + cp.Spec.Version, } fakeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(cp). - WithStatusSubresource(&seamcorev1alpha1.InfrastructureClusterPack{}).Build() + WithStatusSubresource(&dispatcherv1alpha1.PackDelivery{}).Build() r := &controller.ClusterPackReconciler{ Client: fakeClient, Scheme: s, @@ -128,7 +132,7 @@ func TestClusterPackReconciler_SignedTransitionsToAvailable(t *testing.T) { t.Errorf("expected no requeue on signed pack, got %+v", result) } - updated := &seamcorev1alpha1.InfrastructureClusterPack{} + updated := &dispatcherv1alpha1.PackDelivery{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(cp), updated); err != nil { t.Fatalf("get updated ClusterPack: %v", err) } @@ -155,7 +159,7 @@ func TestClusterPackReconciler_LineageSyncedInitialized(t *testing.T) { s := newClusterPackScheme(t) cp := newClusterPack("lineage-pack", "infra-system", "v1.0.0") fakeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(cp). - WithStatusSubresource(&seamcorev1alpha1.InfrastructureClusterPack{}).Build() + WithStatusSubresource(&dispatcherv1alpha1.PackDelivery{}).Build() r := &controller.ClusterPackReconciler{ Client: fakeClient, Scheme: s, @@ -164,7 +168,7 @@ func TestClusterPackReconciler_LineageSyncedInitialized(t *testing.T) { reconcileCP(t, r, cp) - updated := &seamcorev1alpha1.InfrastructureClusterPack{} + updated := &dispatcherv1alpha1.PackDelivery{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(cp), updated); err != nil { t.Fatalf("get updated ClusterPack: %v", err) } @@ -191,7 +195,7 @@ func TestClusterPackReconciler_ImmutabilityViolation(t *testing.T) { "infrastructure.ontai.dev/spec-checksum-snapshot": "sha256:different|old-url|old-digest|v0.9.0", } fakeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(cp). - WithStatusSubresource(&seamcorev1alpha1.InfrastructureClusterPack{}).Build() + WithStatusSubresource(&dispatcherv1alpha1.PackDelivery{}).Build() r := &controller.ClusterPackReconciler{ Client: fakeClient, Scheme: s, @@ -204,7 +208,7 @@ func TestClusterPackReconciler_ImmutabilityViolation(t *testing.T) { t.Errorf("expected no requeue on immutability violation, got %+v", result) } - updated := &seamcorev1alpha1.InfrastructureClusterPack{} + updated := &dispatcherv1alpha1.PackDelivery{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(cp), updated); err != nil { t.Fatalf("get updated ClusterPack: %v", err) } @@ -245,7 +249,7 @@ func TestClusterPackReconciler_DeletionCascadesDriftSignal(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, signal). - WithStatusSubresource(&seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackDelivery{}). Build() r := &controller.ClusterPackReconciler{ Client: fakeClient, @@ -293,15 +297,15 @@ func TestClusterPackReconciler_PackExecutionDeletedRecreatesPE(t *testing.T) { } // PackInstance exists at current version — without the fix, this would suppress PE recreation. - pi := &seamcorev1alpha1.InfrastructurePackInstance{ + pi := &dispatcherv1alpha1.PackInstalled{ ObjectMeta: metav1.ObjectMeta{Name: peName, Namespace: tenantNS}, - Spec: seamcorev1alpha1.InfrastructurePackInstanceSpec{Version: version, ClusterPackRef: cpName}, + Spec: dispatcherv1alpha1.PackInstalledSpec{Version: version, PackDeliveryRef: cpName}, } // PackExecution is absent — it was externally deleted by DriftSignalHandler. fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pi). - WithStatusSubresource(&seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackDelivery{}). Build() r := &controller.ClusterPackReconciler{ Client: fakeClient, @@ -310,10 +314,10 @@ func TestClusterPackReconciler_PackExecutionDeletedRecreatesPE(t *testing.T) { } // Simulate the PE deletion watch firing: call the mapper with the deleted PE object. - deletedPE := &seamcorev1alpha1.InfrastructurePackExecution{ + deletedPE := &dispatcherv1alpha1.PackExecution{ ObjectMeta: metav1.ObjectMeta{Name: peName, Namespace: tenantNS}, - Spec: seamcorev1alpha1.InfrastructurePackExecutionSpec{ - ClusterPackRef: seamcorev1alpha1.InfrastructureClusterPackRef{Name: cpName, Version: version}, + Spec: dispatcherv1alpha1.PackExecutionSpec{ + PackDeliveryRef: dispatcherv1alpha1.PackDeliveryRef{Name: cpName, Version: version}, TargetClusterRef: clusterName, }, } @@ -327,7 +331,7 @@ func TestClusterPackReconciler_PackExecutionDeletedRecreatesPE(t *testing.T) { } // Mapper must have deleted the PackInstance. - remainingPI := &seamcorev1alpha1.InfrastructurePackInstance{} + remainingPI := &dispatcherv1alpha1.PackInstalled{} if err := fakeClient.Get(context.Background(), client.ObjectKey{Name: peName, Namespace: tenantNS}, remainingPI); !apierrors.IsNotFound(err) { t.Errorf("expected PackInstance to be deleted by mapper; got err=%v", err) } @@ -335,7 +339,7 @@ func TestClusterPackReconciler_PackExecutionDeletedRecreatesPE(t *testing.T) { // Reconcile: PackInstance gone, PE gone → new PE must be created. reconcileCP(t, r, cp) - newPE := &seamcorev1alpha1.InfrastructurePackExecution{} + newPE := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(context.Background(), client.ObjectKey{Name: peName, Namespace: tenantNS}, newPE); err != nil { t.Errorf("expected new PackExecution to be created after PE deletion; got err=%v", err) } @@ -360,7 +364,7 @@ func TestClusterPackReconciler_RevokedNoRequeue(t *testing.T) { }, } fakeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(cp). - WithStatusSubresource(&seamcorev1alpha1.InfrastructureClusterPack{}).Build() + WithStatusSubresource(&dispatcherv1alpha1.PackDelivery{}).Build() r := &controller.ClusterPackReconciler{ Client: fakeClient, Scheme: s, diff --git a/test/unit/controller/helpers_test.go b/test/unit/controller/helpers_test.go index 1d282dc..fd79341 100644 --- a/test/unit/controller/helpers_test.go +++ b/test/unit/controller/helpers_test.go @@ -18,13 +18,14 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" seamv1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/wrapper/internal/controller" ) // rbacAllowedStub is a RBACReadyChecker stub that always grants all permissions. // Use this in tests that need gate 5 (WrapperRunnerRBAC) to pass so they can reach // job-submission or post-job logic. The real gate requires a live API server SAR. -func rbacAllowedStub(_ context.Context, _ *seamv1alpha1.InfrastructurePackExecution) (bool, string, error) { +func rbacAllowedStub(_ context.Context, _ *dispatcherv1alpha1.PackExecution) (bool, string, error) { return true, "", nil } @@ -42,6 +43,9 @@ func buildTestScheme(t *testing.T) *runtime.Scheme { if err := seamv1alpha1.AddToScheme(s); err != nil { t.Fatalf("AddToScheme seamv1alpha1: %v", err) } + if err := dispatcherv1alpha1.AddToScheme(s); err != nil { + t.Fatalf("AddToScheme dispatcherv1alpha1: %v", err) + } return s } @@ -50,8 +54,8 @@ func boolPtr(b bool) *bool { return &b } // newSignedCP returns a signed ClusterPack in the given namespace with a stable UID. // Status.Signed=true and PackSignature are pre-set so the PackExecutionReconciler // signature gate passes without running the ClusterPackReconciler. -func newSignedCP(name, version, namespace string) *seamv1alpha1.InfrastructureClusterPack { - return &seamv1alpha1.InfrastructureClusterPack{ +func newSignedCP(name, version, namespace string) *dispatcherv1alpha1.PackDelivery { + return &dispatcherv1alpha1.PackDelivery{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, @@ -60,15 +64,15 @@ func newSignedCP(name, version, namespace string) *seamv1alpha1.InfrastructureCl "ontai.dev/pack-signature": "valid-sig==", }, }, - Spec: seamv1alpha1.InfrastructureClusterPackSpec{ + Spec: dispatcherv1alpha1.PackDeliverySpec{ Version: version, - RegistryRef: seamv1alpha1.InfrastructurePackRegistryRef{ + RegistryRef: dispatcherv1alpha1.PackRegistryRef{ URL: "registry.ontai.dev/packs/" + name, Digest: "sha256:abc123", }, Checksum: "sha256:def456", }, - Status: seamv1alpha1.InfrastructureClusterPackStatus{ + Status: dispatcherv1alpha1.PackDeliveryStatus{ Signed: true, PackSignature: "valid-sig==", }, @@ -77,23 +81,23 @@ func newSignedCP(name, version, namespace string) *seamv1alpha1.InfrastructureCl // newPE returns a PackExecution in the given namespace with an ownerReference // pointing to the ClusterPack identified by cpName/cpUID. -func newPE(name, cpName, cpVersion string, cpUID types.UID, clusterRef, profileRef, namespace string) *seamv1alpha1.InfrastructurePackExecution { - return &seamv1alpha1.InfrastructurePackExecution{ +func newPE(name, cpName, cpVersion string, cpUID types.UID, clusterRef, profileRef, namespace string) *dispatcherv1alpha1.PackExecution { + return &dispatcherv1alpha1.PackExecution{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, UID: types.UID("uid-pe-" + name), OwnerReferences: []metav1.OwnerReference{{ - APIVersion: seamv1alpha1.GroupVersion.String(), - Kind: "ClusterPack", + APIVersion: dispatcherv1alpha1.GroupVersion.String(), + Kind: "PackDelivery", Name: cpName, UID: cpUID, Controller: boolPtr(true), BlockOwnerDeletion: boolPtr(true), }}, }, - Spec: seamv1alpha1.InfrastructurePackExecutionSpec{ - ClusterPackRef: seamv1alpha1.InfrastructureClusterPackRef{ + Spec: dispatcherv1alpha1.PackExecutionSpec{ + PackDeliveryRef: dispatcherv1alpha1.PackDeliveryRef{ Name: cpName, Version: cpVersion, }, @@ -198,13 +202,13 @@ func newRBACProfile(name, namespace string, provisioned bool) *unstructured.Unst // newJob returns a batchv1.Job with the given completion counters and an ownerReference // pointing to pe. The ownerRef is required so the stale-job detection in the reconciler // treats this Job as belonging to the current PackExecution (not a GC-lagged leftover). -func newJob(name, namespace string, succeeded, failed int32, pe *seamv1alpha1.InfrastructurePackExecution) *batchv1.Job { +func newJob(name, namespace string, succeeded, failed int32, pe *dispatcherv1alpha1.PackExecution) *batchv1.Job { job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, OwnerReferences: []metav1.OwnerReference{{ - APIVersion: seamv1alpha1.GroupVersion.String(), + APIVersion: dispatcherv1alpha1.GroupVersion.String(), Kind: "PackExecution", Name: pe.Name, UID: pe.UID, @@ -224,8 +228,8 @@ func newJob(name, namespace string, succeeded, failed int32, pe *seamv1alpha1.In // conductor executor after pack-deploy Job success. Uses the single-active-revision // naming convention (T-15): name is "pack-deploy-result-{peName}-r1", labelled with // ontai.dev/pack-execution={peName}, Revision=1. -func newOperationResultPOR(peName, namespace string) *seamv1alpha1.PackOperationResult { - return &seamv1alpha1.PackOperationResult{ +func newOperationResultPOR(peName, namespace string) *dispatcherv1alpha1.PackLog { + return &dispatcherv1alpha1.PackLog{ ObjectMeta: metav1.ObjectMeta{ Name: "pack-deploy-result-" + peName + "-r1", Namespace: namespace, @@ -233,10 +237,10 @@ func newOperationResultPOR(peName, namespace string) *seamv1alpha1.PackOperation "ontai.dev/pack-execution": peName, }, }, - Spec: seamv1alpha1.PackOperationResultSpec{ + Spec: dispatcherv1alpha1.PackLogSpec{ Revision: 1, Capability: "pack-deploy", - Status: seamv1alpha1.PackResultSucceeded, + Status: dispatcherv1alpha1.PackLogResultSucceeded, }, } } diff --git a/test/unit/controller/kueue_job_scheduling_test.go b/test/unit/controller/kueue_job_scheduling_test.go index 90abb2b..4ce1eda 100644 --- a/test/unit/controller/kueue_job_scheduling_test.go +++ b/test/unit/controller/kueue_job_scheduling_test.go @@ -28,7 +28,7 @@ import ( clientevents "k8s.io/client-go/tools/events" "sigs.k8s.io/controller-runtime/pkg/client" - seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/seam-core/pkg/conditions" "github.com/ontai-dev/wrapper/internal/controller" ) @@ -36,7 +36,7 @@ import ( // jobSubmissionSetup returns a fake client with all five gates satisfied and no // pre-existing Job. The returned PackExecution and ClusterPack can be inspected // after reconcile to verify Job labels and env vars. -func jobSubmissionSetup(t *testing.T) (client.Client, *seamcorev1alpha1.InfrastructurePackExecution) { +func jobSubmissionSetup(t *testing.T) (client.Client, *dispatcherv1alpha1.PackExecution) { t.Helper() return allGatesSetup(t, "pe-job", "cilium-pack", "v1.2.3", "cluster-g", "profile-g") } @@ -294,7 +294,7 @@ func TestJobFailed_PackExecutionFailed(t *testing.T) { t.Errorf("expected no requeue after Job failure (human intervention required), got %+v", result) } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -365,7 +365,7 @@ func TestJobSucceeded_PackExecutionSucceeded(t *testing.T) { t.Errorf("expected no requeue after success, got %+v", result) } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -384,17 +384,17 @@ func TestJobSucceeded_PackExecutionSucceeded(t *testing.T) { // PackInstance created in seam-tenant-{clusterRef} (explicit per wrapper-schema.md §9). piName := cpName + "-" + clusterRef - pi := &seamcorev1alpha1.InfrastructurePackInstance{} + pi := &dispatcherv1alpha1.PackInstalled{} if err := fakeClient.Get(ctx, types.NamespacedName{Name: piName, Namespace: "seam-tenant-" + clusterRef}, pi); err != nil { t.Fatalf("PackInstance %q not created after Job success: %v", piName, err) } - if pi.Spec.ClusterPackRef != cpName { - t.Errorf("PackInstance.Spec.ClusterPackRef=%q, want %q", pi.Spec.ClusterPackRef, cpName) + if pi.Spec.PackDeliveryRef != cpName { + t.Errorf("PackInstance.Spec.PackDeliveryRef=%q, want %q", pi.Spec.PackDeliveryRef, cpName) } if pi.Spec.TargetClusterRef != clusterRef { t.Errorf("PackInstance.Spec.TargetClusterRef=%q, want %q", pi.Spec.TargetClusterRef, clusterRef) } - if len(pi.OwnerReferences) != 1 || pi.OwnerReferences[0].Kind != "InfrastructurePackExecution" { + if len(pi.OwnerReferences) != 1 || pi.OwnerReferences[0].Kind != "PackExecution" { t.Errorf("PackInstance ownerRef not pointing to InfrastructurePackExecution: %+v", pi.OwnerReferences) } } @@ -449,7 +449,7 @@ func TestIdempotency_RunningJob_NoNewJob(t *testing.T) { } // PackExecution Running=True. - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } diff --git a/test/unit/controller/packexecution_gates_test.go b/test/unit/controller/packexecution_gates_test.go index 38ccb67..6f13690 100644 --- a/test/unit/controller/packexecution_gates_test.go +++ b/test/unit/controller/packexecution_gates_test.go @@ -29,7 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/seam-core/pkg/conditions" "github.com/ontai-dev/wrapper/internal/controller" ) @@ -49,7 +49,7 @@ func TestAC2_Gate1_PackExecution_BlockedWhenUnsigned(t *testing.T) { c := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}). Build() ctx := context.Background() for _, obj := range []client.Object{tc, rc} { @@ -69,7 +69,7 @@ func TestAC2_Gate1_PackExecution_BlockedWhenUnsigned(t *testing.T) { if result.RequeueAfter == 0 { t.Error("AC-2 gate1: expected requeue when signature pending, got no requeue") } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := c.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("AC-2 gate1: get PackExecution: %v", err) } @@ -100,7 +100,7 @@ func TestAC2_Gate3_PackExecution_BlockedWhenSnapshotStale(t *testing.T) { c := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}). Build() ctx := context.Background() for _, obj := range []client.Object{tc, rc, ps} { @@ -120,7 +120,7 @@ func TestAC2_Gate3_PackExecution_BlockedWhenSnapshotStale(t *testing.T) { if result.RequeueAfter == 0 { t.Error("AC-2 gate3: expected requeue when snapshot stale, got no requeue") } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := c.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("AC-2 gate3: get PackExecution: %v", err) } @@ -152,7 +152,7 @@ func TestAC2_Gate4_PackExecution_BlockedWhenRBACNotProvisioned(t *testing.T) { c := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}). Build() ctx := context.Background() for _, obj := range []client.Object{tc, rc, ps, rp} { @@ -172,7 +172,7 @@ func TestAC2_Gate4_PackExecution_BlockedWhenRBACNotProvisioned(t *testing.T) { if result.RequeueAfter == 0 { t.Error("AC-2 gate4: expected requeue when RBACProfile not provisioned, got no requeue") } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := c.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("AC-2 gate4: get PackExecution: %v", err) } @@ -210,7 +210,7 @@ func TestAC2_Gate2_PackExecution_BlockedWhenRevoked(t *testing.T) { c := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}). Build() ctx := context.Background() for _, obj := range []client.Object{tc, rc} { @@ -231,7 +231,7 @@ func TestAC2_Gate2_PackExecution_BlockedWhenRevoked(t *testing.T) { if result.RequeueAfter != 0 { t.Errorf("AC-2 gate2: revoked pack must not requeue, got RequeueAfter=%v", result.RequeueAfter) } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := c.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("AC-2 gate2: get PackExecution: %v", err) } @@ -279,7 +279,7 @@ func TestAC2_AllGatesPass_JobSubmitted(t *testing.T) { // No blocking conditions must remain set. ctx := context.Background() - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := c.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("AC-2 all-gates: get PackExecution: %v", err) } diff --git a/test/unit/controller/packinstance_lifecycle_test.go b/test/unit/controller/packinstance_lifecycle_test.go index b8818c4..7e294fd 100644 --- a/test/unit/controller/packinstance_lifecycle_test.go +++ b/test/unit/controller/packinstance_lifecycle_test.go @@ -29,7 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/seam-core/pkg/conditions" "github.com/ontai-dev/wrapper/internal/controller" ) @@ -51,7 +51,7 @@ func reconcilePackExecution(t *testing.T, r *controller.PackExecutionReconciler, // - TalosCluster: ConductorReady=True (gate 0 clear) // - PermissionSnapshot: Current=True (gate 3 clear) // - RBACProfile: provisioned=true (gate 4 clear) -func allGatesSetup(t *testing.T, peName, cpName, cpVersion, clusterRef, profileRef string) (client.Client, *seamcorev1alpha1.InfrastructurePackExecution) { +func allGatesSetup(t *testing.T, peName, cpName, cpVersion, clusterRef, profileRef string) (client.Client, *dispatcherv1alpha1.PackExecution) { t.Helper() s := buildTestScheme(t) @@ -64,7 +64,7 @@ func allGatesSetup(t *testing.T, peName, cpName, cpVersion, clusterRef, profileR fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}, &seamcorev1alpha1.InfrastructurePackInstance{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}, &dispatcherv1alpha1.PackInstalled{}). Build() ctx := context.Background() @@ -86,7 +86,7 @@ func allGatesSetup(t *testing.T, peName, cpName, cpVersion, clusterRef, profileR // 1. PackExecution has ownerRef to ClusterPack (ownership chain structure). // 2. PackExecution transitions to Succeeded=True after reconcile. // 3. PackInstance is created with correct ownerRef to PackExecution. -// 4. PackInstance.Spec.ClusterPackRef and TargetClusterRef are correct. +// 4. PackInstance.Spec.PackDeliveryRef and TargetClusterRef are correct. func TestOwnershipChain_TalosClusterExists(t *testing.T) { const ( peName = "pe-happy" @@ -121,8 +121,8 @@ func TestOwnershipChain_TalosClusterExists(t *testing.T) { t.Fatalf("PE must have exactly 1 ownerReference, got %d", len(pe.OwnerReferences)) } ownerRef := pe.OwnerReferences[0] - if ownerRef.Kind != "ClusterPack" || ownerRef.Name != cpName { - t.Errorf("PE ownerRef Kind=%q Name=%q, want Kind=ClusterPack Name=%s", + if ownerRef.Kind != "PackDelivery" || ownerRef.Name != cpName { + t.Errorf("PE ownerRef Kind=%q Name=%q, want Kind=PackDelivery Name=%s", ownerRef.Kind, ownerRef.Name, cpName) } if !*ownerRef.Controller || !*ownerRef.BlockOwnerDeletion { @@ -137,7 +137,7 @@ func TestOwnershipChain_TalosClusterExists(t *testing.T) { } // Assertion 2: PackExecution Succeeded=True. - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -155,7 +155,7 @@ func TestOwnershipChain_TalosClusterExists(t *testing.T) { // Assertion 3: PackInstance created in seam-tenant-{clusterRef} (explicit per schema). piName := cpName + "-" + clusterRef // "my-pack-cluster-a" - pi := &seamcorev1alpha1.InfrastructurePackInstance{} + pi := &dispatcherv1alpha1.PackInstalled{} if err := fakeClient.Get(ctx, types.NamespacedName{Name: piName, Namespace: "seam-tenant-" + clusterRef}, pi); err != nil { t.Fatalf("PackInstance %q not created after Job success: %v", piName, err) } @@ -163,7 +163,7 @@ func TestOwnershipChain_TalosClusterExists(t *testing.T) { t.Fatalf("PackInstance must have exactly 1 ownerReference, got %d", len(pi.OwnerReferences)) } piOwner := pi.OwnerReferences[0] - if piOwner.Kind != "InfrastructurePackExecution" || piOwner.Name != peName { + if piOwner.Kind != "PackExecution" || piOwner.Name != peName { t.Errorf("PackInstance ownerRef Kind=%q Name=%q, want Kind=InfrastructurePackExecution Name=%s", piOwner.Kind, piOwner.Name, peName) } @@ -175,8 +175,8 @@ func TestOwnershipChain_TalosClusterExists(t *testing.T) { } // Assertion 4: PackInstance spec fields. - if pi.Spec.ClusterPackRef != cpName { - t.Errorf("PackInstance.Spec.ClusterPackRef=%q, want %q", pi.Spec.ClusterPackRef, cpName) + if pi.Spec.PackDeliveryRef != cpName { + t.Errorf("PackInstance.Spec.PackDeliveryRef=%q, want %q", pi.Spec.PackDeliveryRef, cpName) } if pi.Spec.TargetClusterRef != clusterRef { t.Errorf("PackInstance.Spec.TargetClusterRef=%q, want %q", pi.Spec.TargetClusterRef, clusterRef) @@ -201,7 +201,7 @@ func TestWaitingForCluster_TalosClusterAbsent(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}). Build() // TalosCluster for "cluster-missing" is deliberately absent. @@ -221,7 +221,7 @@ func TestWaitingForCluster_TalosClusterAbsent(t *testing.T) { } // Waiting=True with AwaitingConductorReady reason. - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -246,7 +246,7 @@ func TestWaitingForCluster_TalosClusterAbsent(t *testing.T) { } // No PackInstances created. - piList := &seamcorev1alpha1.InfrastructurePackInstanceList{} + piList := &dispatcherv1alpha1.PackInstalledList{} if err := fakeClient.List(ctx, piList, client.InNamespace("infra-system")); err != nil { t.Fatalf("list PackInstances: %v", err) } @@ -296,14 +296,14 @@ func TestDeletion_PackExecutionNotFound_NoJobCreated(t *testing.T) { // INV-006: deletion triggers events only. wrapper-design.md §3. func TestDeletion_ClusterPackReconciler_NoJobsSubmitted(t *testing.T) { s := buildTestScheme(t) - cp := &seamcorev1alpha1.InfrastructureClusterPack{ + cp := &dispatcherv1alpha1.PackDelivery{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pack", Namespace: "infra-system", }, - Spec: seamcorev1alpha1.InfrastructureClusterPackSpec{ + Spec: dispatcherv1alpha1.PackDeliverySpec{ Version: "v1.0.0", - RegistryRef: seamcorev1alpha1.InfrastructurePackRegistryRef{ + RegistryRef: dispatcherv1alpha1.PackRegistryRef{ URL: "registry.ontai.dev/packs/test-pack", Digest: "sha256:abc123", }, @@ -313,7 +313,7 @@ func TestDeletion_ClusterPackReconciler_NoJobsSubmitted(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp). - WithStatusSubresource(&seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackDelivery{}). Build() r := &controller.ClusterPackReconciler{ @@ -360,7 +360,7 @@ func TestGate0_RunnerConfigAbsent(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}). Build() if err := fakeClient.Create(context.Background(), tc); err != nil { t.Fatalf("create TalosCluster: %v", err) @@ -380,7 +380,7 @@ func TestGate0_RunnerConfigAbsent(t *testing.T) { } ctx := context.Background() - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -422,7 +422,7 @@ func TestGate1_SignaturePending(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}). Build() ctx := context.Background() for _, obj := range []client.Object{tc, rc} { @@ -444,7 +444,7 @@ func TestGate1_SignaturePending(t *testing.T) { t.Errorf("expected RequeueAfter=15s for signature gate, got %v", result.RequeueAfter) } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -494,7 +494,7 @@ func TestGate2_PackRevoked(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}). Build() ctx := context.Background() for _, obj := range []client.Object{tc, rc, ps, rp} { @@ -517,7 +517,7 @@ func TestGate2_PackRevoked(t *testing.T) { t.Errorf("expected no requeue when pack revoked (human intervention), got %+v", result) } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -556,7 +556,7 @@ func TestGate3_PermissionSnapshotOutOfSync(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}). Build() ctx := context.Background() for _, obj := range []client.Object{tc, rc, ps, rp} { @@ -578,7 +578,7 @@ func TestGate3_PermissionSnapshotOutOfSync(t *testing.T) { t.Error("expected requeue when Gate 3 (PermissionSnapshot) not cleared") } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -620,7 +620,7 @@ func TestGate4_RBACProfileNotProvisioned(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}). Build() ctx := context.Background() for _, obj := range []client.Object{tc, rc, ps, rp} { @@ -642,7 +642,7 @@ func TestGate4_RBACProfileNotProvisioned(t *testing.T) { t.Error("expected requeue when Gate 4 (RBACProfile) not cleared") } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -692,7 +692,7 @@ func TestConductorReady_ManagementClusterFallback_SeamSystem(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}). Build() ctx := context.Background() for _, obj := range []client.Object{tc, rc, ps, rp} { @@ -716,7 +716,7 @@ func TestConductorReady_ManagementClusterFallback_SeamSystem(t *testing.T) { reconcilePackExecution(t, r, peName, "infra-system") - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -758,7 +758,7 @@ func TestConductorReady_RunnerConfigWithCapabilities_ReturnsTrue(t *testing.T) { reconcilePackExecution(t, r, peName, "infra-system") - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -790,7 +790,7 @@ func TestConductorReady_RunnerConfigAbsent_ReturnsFalse(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}). Build() if err := fakeClient.Create(context.Background(), tc); err != nil { t.Fatalf("create TalosCluster: %v", err) @@ -811,7 +811,7 @@ func TestConductorReady_RunnerConfigAbsent_ReturnsFalse(t *testing.T) { } ctx := context.Background() - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -846,7 +846,7 @@ func TestConductorReady_RunnerConfigEmptyCapabilities_ReturnsFalse(t *testing.T) fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}). Build() ctx := context.Background() for _, obj := range []client.Object{tc, rc} { @@ -868,7 +868,7 @@ func TestConductorReady_RunnerConfigEmptyCapabilities_ReturnsFalse(t *testing.T) t.Error("expected requeue when RunnerConfig has empty capabilities (gate 0 not cleared)") } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get PackExecution: %v", err) } diff --git a/test/unit/controller/packversion_test.go b/test/unit/controller/packversion_test.go index c16f008..437dd60 100644 --- a/test/unit/controller/packversion_test.go +++ b/test/unit/controller/packversion_test.go @@ -13,7 +13,7 @@ import ( clientevents "k8s.io/client-go/tools/events" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" - seamv1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" controller "github.com/ontai-dev/wrapper/internal/controller" ) @@ -38,14 +38,14 @@ func deployPack(t *testing.T, cpVersion, existingPIVersion string) string { if existingPIVersion != "" { piName := cpName + "-" + clusterRef piNS := "seam-tenant-" + clusterRef - existing := &seamv1alpha1.InfrastructurePackInstance{ + existing := &dispatcherv1alpha1.PackInstalled{ ObjectMeta: metav1.ObjectMeta{ Name: piName, Namespace: piNS, UID: types.UID("uid-pi-existing"), }, - Spec: seamv1alpha1.InfrastructurePackInstanceSpec{ - ClusterPackRef: cpName, + Spec: dispatcherv1alpha1.PackInstalledSpec{ + PackDeliveryRef: cpName, Version: existingPIVersion, TargetClusterRef: clusterRef, }, @@ -72,7 +72,7 @@ func deployPack(t *testing.T, cpVersion, existingPIVersion string) string { } reconcilePackExecution(t, r, peName, peNS) - pi := &seamv1alpha1.InfrastructurePackInstance{} + pi := &dispatcherv1alpha1.PackInstalled{} piKey := ctrlclient.ObjectKey{ Name: cpName + "-" + clusterRef, Namespace: "seam-tenant-" + clusterRef, @@ -86,7 +86,7 @@ func deployPack(t *testing.T, cpVersion, existingPIVersion string) string { // TestUpgradeDirection_Initial verifies first deployment → Initial. func TestUpgradeDirection_Initial(t *testing.T) { got := deployPack(t, "v4.10.0-r1", "") - if got != string(seamv1alpha1.PackUpgradeDirectionInitial) { + if got != string(dispatcherv1alpha1.PackLogUpgradeDirectionInitial) { t.Errorf("upgradeDirection=%q, want Initial", got) } } @@ -94,7 +94,7 @@ func TestUpgradeDirection_Initial(t *testing.T) { // TestUpgradeDirection_Upgrade verifies newer version → Upgrade. func TestUpgradeDirection_Upgrade(t *testing.T) { got := deployPack(t, "v4.10.0-r1", "v4.9.0-r1") - if got != string(seamv1alpha1.PackUpgradeDirectionUpgrade) { + if got != string(dispatcherv1alpha1.PackLogUpgradeDirectionUpgrade) { t.Errorf("upgradeDirection=%q, want Upgrade", got) } } @@ -102,7 +102,7 @@ func TestUpgradeDirection_Upgrade(t *testing.T) { // TestUpgradeDirection_Rollback verifies older version → Rollback. func TestUpgradeDirection_Rollback(t *testing.T) { got := deployPack(t, "v4.9.0-r1", "v4.10.0-r1") - if got != string(seamv1alpha1.PackUpgradeDirectionRollback) { + if got != string(dispatcherv1alpha1.PackLogUpgradeDirectionRollback) { t.Errorf("upgradeDirection=%q, want Rollback", got) } } @@ -110,7 +110,7 @@ func TestUpgradeDirection_Rollback(t *testing.T) { // TestUpgradeDirection_Redeploy verifies same version → Redeploy. func TestUpgradeDirection_Redeploy(t *testing.T) { got := deployPack(t, "v4.10.0-r1", "v4.10.0-r1") - if got != string(seamv1alpha1.PackUpgradeDirectionRedeploy) { + if got != string(dispatcherv1alpha1.PackLogUpgradeDirectionRedeploy) { t.Errorf("upgradeDirection=%q, want Redeploy", got) } } @@ -118,7 +118,7 @@ func TestUpgradeDirection_Redeploy(t *testing.T) { // TestUpgradeDirection_RevisionBump verifies revision bump (same semver) → Upgrade. func TestUpgradeDirection_RevisionBump(t *testing.T) { got := deployPack(t, "v4.9.0-r2", "v4.9.0-r1") - if got != string(seamv1alpha1.PackUpgradeDirectionUpgrade) { + if got != string(dispatcherv1alpha1.PackLogUpgradeDirectionUpgrade) { t.Errorf("upgradeDirection=%q, want Upgrade (revision bump)", got) } } diff --git a/test/unit/controller/por_label_lookup_test.go b/test/unit/controller/por_label_lookup_test.go index 6464b60..df63b74 100644 --- a/test/unit/controller/por_label_lookup_test.go +++ b/test/unit/controller/por_label_lookup_test.go @@ -9,12 +9,12 @@ import ( clientevents "k8s.io/client-go/tools/events" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" - seamv1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/wrapper/internal/controller" ) -func makePOR(name, namespace, packExecutionRef string, revision int64, status seamv1alpha1.PackResultStatus) *seamv1alpha1.PackOperationResult { - return &seamv1alpha1.PackOperationResult{ +func makePOR(name, namespace, packExecutionRef string, revision int64, status dispatcherv1alpha1.PackLogResultStatus) *dispatcherv1alpha1.PackLog { + return &dispatcherv1alpha1.PackLog{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, @@ -22,7 +22,7 @@ func makePOR(name, namespace, packExecutionRef string, revision int64, status se "ontai.dev/pack-execution": packExecutionRef, }, }, - Spec: seamv1alpha1.PackOperationResultSpec{ + Spec: dispatcherv1alpha1.PackLogSpec{ Revision: revision, Capability: "pack-deploy", Status: status, @@ -45,7 +45,7 @@ func TestFindLatestPOR_SingleResult(t *testing.T) { ctx := context.Background() succeededJob := newJob(packDeployJobName(peName), "infra-system", 1, 0, pe) - por := makePOR("pack-deploy-result-"+peName+"-r1", "infra-system", peName, 1, seamv1alpha1.PackResultSucceeded) + por := makePOR("pack-deploy-result-"+peName+"-r1", "infra-system", peName, 1, dispatcherv1alpha1.PackLogResultSucceeded) if err := fakeClient.Create(ctx, succeededJob); err != nil { t.Fatalf("create Job: %v", err) } @@ -61,7 +61,7 @@ func TestFindLatestPOR_SingleResult(t *testing.T) { } reconcilePackExecution(t, r, peName, "infra-system") - updated := &seamv1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, ctrlclient.ObjectKey{Name: peName, Namespace: "infra-system"}, updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -86,12 +86,12 @@ func TestFindLatestPOR_MultipleRevisions(t *testing.T) { ctx := context.Background() succeededJob := newJob(packDeployJobName(peName), "infra-system", 1, 0, pe) - por1 := makePOR("pack-deploy-result-"+peName+"-r1", "infra-system", peName, 1, seamv1alpha1.PackResultFailed) - por2 := makePOR("pack-deploy-result-"+peName+"-r2", "infra-system", peName, 2, seamv1alpha1.PackResultSucceeded) + por1 := makePOR("pack-deploy-result-"+peName+"-r1", "infra-system", peName, 1, dispatcherv1alpha1.PackLogResultFailed) + por2 := makePOR("pack-deploy-result-"+peName+"-r2", "infra-system", peName, 2, dispatcherv1alpha1.PackLogResultSucceeded) if err := fakeClient.Create(ctx, succeededJob); err != nil { t.Fatalf("create Job: %v", err) } - for _, p := range []*seamv1alpha1.PackOperationResult{por1, por2} { + for _, p := range []*dispatcherv1alpha1.PackLog{por1, por2} { if err := fakeClient.Create(ctx, p); err != nil { t.Fatalf("create POR %q: %v", p.Name, err) } @@ -105,7 +105,7 @@ func TestFindLatestPOR_MultipleRevisions(t *testing.T) { } reconcilePackExecution(t, r, peName, "infra-system") - updated := &seamv1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, ctrlclient.ObjectKey{Name: peName, Namespace: "infra-system"}, updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -145,7 +145,7 @@ func TestFindLatestPOR_NoPOR(t *testing.T) { t.Error("expected RequeueAfter when POR not yet written; got no requeue") } - updated := &seamv1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(ctx, ctrlclient.ObjectKey{Name: peName, Namespace: "infra-system"}, updated); err != nil { t.Fatalf("get PackExecution: %v", err) } diff --git a/test/unit/controller/rollback_test.go b/test/unit/controller/rollback_test.go index 249f19e..fb3eb87 100644 --- a/test/unit/controller/rollback_test.go +++ b/test/unit/controller/rollback_test.go @@ -11,13 +11,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - seamv1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/wrapper/internal/controller" ) // fakePOR builds a PackOperationResult representing a deploy at the given revision. // If superseded is true the ontai.dev/superseded=true label is set (retained history). -func fakePOR(name, namespace, cpName string, revision int64, version, rbacDigest, workloadDigest string, superseded bool) *seamv1alpha1.PackOperationResult { +func fakePOR(name, namespace, cpName string, revision int64, version, rbacDigest, workloadDigest string, superseded bool) *dispatcherv1alpha1.PackLog { labels := map[string]string{ "ontai.dev/cluster-pack": cpName, "ontai.dev/pack-execution": cpName + "-ccs-dev", @@ -25,26 +25,26 @@ func fakePOR(name, namespace, cpName string, revision int64, version, rbacDigest if superseded { labels["ontai.dev/superseded"] = "true" } - return &seamv1alpha1.PackOperationResult{ + return &dispatcherv1alpha1.PackLog{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, Labels: labels, }, - Spec: seamv1alpha1.PackOperationResultSpec{ + Spec: dispatcherv1alpha1.PackLogSpec{ Revision: revision, - ClusterPackRef: cpName, - ClusterPackVersion: version, + PackDeliveryRef: cpName, + PackDeliveryVersion: version, RBACDigest: rbacDigest, WorkloadDigest: workloadDigest, Capability: "pack-deploy", - Status: seamv1alpha1.PackResultSucceeded, + Status: dispatcherv1alpha1.PackLogResultSucceeded, }, } } // buildRollbackCP creates a ClusterPack with RollbackToRevision set to targetRevision. -func buildRollbackCP(name, version, namespace string, targetRevision int64, rbacDigest, workloadDigest string) *seamv1alpha1.InfrastructureClusterPack { +func buildRollbackCP(name, version, namespace string, targetRevision int64, rbacDigest, workloadDigest string) *dispatcherv1alpha1.PackDelivery { cp := newSignedCP(name, version, namespace) cp.Spec.RBACDigest = rbacDigest cp.Spec.WorkloadDigest = workloadDigest @@ -95,7 +95,7 @@ func TestClusterPackReconciler_Rollback_OneStep(t *testing.T) { t.Fatalf("Reconcile: %v", err) } - updated := &seamv1alpha1.InfrastructureClusterPack{} + updated := &dispatcherv1alpha1.PackDelivery{} if err := fakeClient.Get(context.Background(), client.ObjectKey{Name: "nginx-ccs-dev", Namespace: "seam-tenant-ccs-dev"}, updated); err != nil { @@ -167,7 +167,7 @@ func TestClusterPackReconciler_Rollback_NStep(t *testing.T) { t.Fatalf("Reconcile: %v", err) } - updated := &seamv1alpha1.InfrastructureClusterPack{} + updated := &dispatcherv1alpha1.PackDelivery{} if err := fakeClient.Get(context.Background(), client.ObjectKey{Name: "nginx-ccs-dev", Namespace: "seam-tenant-ccs-dev"}, updated); err != nil { @@ -214,7 +214,7 @@ func TestClusterPackReconciler_Rollback_NoPOR_ClearsField(t *testing.T) { t.Fatalf("Reconcile: %v", err) } - updated := &seamv1alpha1.InfrastructureClusterPack{} + updated := &dispatcherv1alpha1.PackDelivery{} if err := fakeClient.Get(context.Background(), client.ObjectKey{Name: "nginx-ccs-dev", Namespace: "seam-tenant-ccs-dev"}, updated); err != nil { @@ -259,7 +259,7 @@ func TestClusterPackReconciler_Rollback_RevisionNotFound_ClearsField(t *testing. t.Fatalf("Reconcile: %v", err) } - updated := &seamv1alpha1.InfrastructureClusterPack{} + updated := &dispatcherv1alpha1.PackDelivery{} if err := fakeClient.Get(context.Background(), client.ObjectKey{Name: "nginx-ccs-dev", Namespace: "seam-tenant-ccs-dev"}, updated); err != nil { diff --git a/test/unit/dispatcher_types_test.go b/test/unit/dispatcher_types_test.go new file mode 100644 index 0000000..45b408d --- /dev/null +++ b/test/unit/dispatcher_types_test.go @@ -0,0 +1,275 @@ +// Package unit contains serialization integrity tests for dispatcher (wrapper) +// CRD types: PackDelivery, PackExecution, PackInstalled, PackReceipt, PackLog. +package unit_test + +import ( + "encoding/json" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + seamv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" + "github.com/ontai-dev/seam-core/pkg/lineage" +) + +// --- PackDelivery --- + +func TestPackDelivery_RequiredFields(t *testing.T) { + t.Parallel() + pd := seamv1alpha1.PackDelivery{ + Spec: seamv1alpha1.PackDeliverySpec{ + Version: "v1.14.0-r1", + RegistryRef: seamv1alpha1.PackRegistryRef{ + URL: "10.20.0.1:5000/ontai-dev/packs/cert-manager-helm", + Digest: "sha256:abc123", + }, + }, + } + if pd.Spec.Version == "" { + t.Fatal("Version must be set") + } + if pd.Spec.RegistryRef.URL == "" { + t.Fatal("RegistryRef.URL must be set") + } +} + +func TestPackDelivery_DigestFieldsRoundTrip(t *testing.T) { + t.Parallel() + pd := seamv1alpha1.PackDelivery{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "seam.ontai.dev/v1alpha1", + Kind: "PackDelivery", + }, + Spec: seamv1alpha1.PackDeliverySpec{ + Version: "v1.14.0-r1", + RegistryRef: seamv1alpha1.PackRegistryRef{URL: "r", Digest: "sha256:a"}, + RBACDigest: "sha256:rbac", + WorkloadDigest: "sha256:workload", + ClusterScopedDigest: "sha256:clusterscoped", + ChartURL: "http://10.20.0.1:8888/cert-manager-v1.14.0.tgz", + ChartName: "cert-manager", + ChartVersion: "v1.14.0", + HelmVersion: "v3.17.3", + }, + } + data, err := json.Marshal(pd) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got seamv1alpha1.PackDelivery + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if got.Spec.RBACDigest != "sha256:rbac" { + t.Errorf("RBACDigest: got %q", got.Spec.RBACDigest) + } + if got.Spec.ClusterScopedDigest != "sha256:clusterscoped" { + t.Errorf("ClusterScopedDigest: got %q", got.Spec.ClusterScopedDigest) + } + if got.Spec.HelmVersion != "v3.17.3" { + t.Errorf("HelmVersion: got %q", got.Spec.HelmVersion) + } +} + +// --- PackExecution --- + +func TestPackExecution_RequiredFields(t *testing.T) { + t.Parallel() + pe := seamv1alpha1.PackExecution{ + Spec: seamv1alpha1.PackExecutionSpec{ + PackDeliveryRef: seamv1alpha1.PackDeliveryRef{ + Name: "cert-manager-helm-v1.14.0-r1", + Version: "v1.14.0-r1", + }, + TargetClusterRef: "ccs-mgmt", + }, + } + if pe.Spec.PackDeliveryRef.Name == "" { + t.Fatal("PackDeliveryRef.Name must be set") + } + if pe.Spec.TargetClusterRef == "" { + t.Fatal("TargetClusterRef must be set") + } +} + +func TestPackExecution_LineageFieldPresent(t *testing.T) { + t.Parallel() + chain := &lineage.SealedCausalChain{ + RootKind: "PackExecution", + RootName: "cert-manager-helm-v1.14.0-r1", + } + pe := seamv1alpha1.PackExecution{ + Spec: seamv1alpha1.PackExecutionSpec{ + PackDeliveryRef: seamv1alpha1.PackDeliveryRef{ + Name: "cert-manager-helm-v1.14.0-r1", + Version: "v1.14.0-r1", + }, + TargetClusterRef: "ccs-mgmt", + Lineage: chain, + }, + } + data, err := json.Marshal(pe) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got seamv1alpha1.PackExecution + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if got.Spec.Lineage == nil { + t.Fatal("Lineage must survive round trip") + } + if got.Spec.Lineage.RootKind != "PackExecution" { + t.Errorf("Lineage.RootKind: got %q", got.Spec.Lineage.RootKind) + } +} + +// --- PackInstalled --- + +func TestPackInstalled_RequiredFields(t *testing.T) { + t.Parallel() + pi := seamv1alpha1.PackInstalled{ + Spec: seamv1alpha1.PackInstalledSpec{ + PackDeliveryRef: "cert-manager-helm-v1.14.0-r1", + Version: "v1.14.0-r1", + TargetClusterRef: "ccs-mgmt", + }, + } + if pi.Spec.Version == "" { + t.Fatal("Version must be set") + } + if pi.Spec.TargetClusterRef == "" { + t.Fatal("TargetClusterRef must be set") + } +} + +func TestPackInstalled_DriftPolicyEnum(t *testing.T) { + t.Parallel() + cases := []seamv1alpha1.DriftPolicy{ + seamv1alpha1.DriftPolicyBlock, + seamv1alpha1.DriftPolicyWarn, + seamv1alpha1.DriftPolicyIgnore, + } + for _, c := range cases { + if c == "" { + t.Errorf("DriftPolicy constant is empty") + } + } +} + +// --- PackReceipt --- + +func TestPackReceipt_SignatureFields(t *testing.T) { + t.Parallel() + pr := seamv1alpha1.PackReceipt{ + Spec: seamv1alpha1.PackReceiptSpec{ + PackInstalledRef: "cert-manager-ccs-dev", + SignatureRef: "seam-pack-signed-ccs-dev-cert-manager-ccs-dev", + PackDeliveryRef: "cert-manager-helm-v1.14.0-r1", + TargetClusterRef: "ccs-mgmt", + RBACDigest: "sha256:rbac", + WorkloadDigest: "sha256:workload", + }, + Status: seamv1alpha1.PackReceiptStatus{ + Verified: true, + Signature: "base64sig==", + }, + } + data, err := json.Marshal(pr) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var got seamv1alpha1.PackReceipt + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !got.Status.Verified { + t.Error("Status.Verified must survive round trip") + } + if got.Status.Signature != "base64sig==" { + t.Errorf("Status.Signature: got %q", got.Status.Signature) + } + if got.Spec.RBACDigest != "sha256:rbac" { + t.Errorf("RBACDigest: got %q", got.Spec.RBACDigest) + } + if got.Spec.PackInstalledRef != "cert-manager-ccs-dev" { + t.Errorf("PackInstalledRef: got %q", got.Spec.PackInstalledRef) + } +} + +// --- PackLog --- + +func TestPackLog_RevisionRequired(t *testing.T) { + t.Parallel() + spec := seamv1alpha1.PackLogSpec{ + Revision: 0, + Capability: "pack-deploy", + Status: seamv1alpha1.PackLogResultSucceeded, + } + data, err := json.Marshal(spec) + if err != nil { + t.Fatalf("json.Marshal: %v", err) + } + var m map[string]interface{} + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("json.Unmarshal: %v", err) + } + if _, ok := m["revision"]; !ok { + t.Error("revision field absent from JSON output; must not use omitempty") + } +} + +func TestPackLog_RevisionSerialization(t *testing.T) { + t.Parallel() + spec := seamv1alpha1.PackLogSpec{ + Revision: 7, + PreviousRevisionRef: "pack-deploy-result-exec-abc-r6", + TalosClusterOperationResultRef: "talos-op-result-xyz", + PackExecutionRef: "exec-abc", + PackDeliveryRef: "cert-manager-v1.13.3", + Capability: "pack-deploy", + Status: seamv1alpha1.PackLogResultSucceeded, + } + data, err := json.Marshal(spec) + if err != nil { + t.Fatalf("json.Marshal: %v", err) + } + var out seamv1alpha1.PackLogSpec + if err := json.Unmarshal(data, &out); err != nil { + t.Fatalf("json.Unmarshal: %v", err) + } + if out.Revision != spec.Revision { + t.Errorf("Revision: got %d, want %d", out.Revision, spec.Revision) + } + if out.PreviousRevisionRef != spec.PreviousRevisionRef { + t.Errorf("PreviousRevisionRef: got %q, want %q", out.PreviousRevisionRef, spec.PreviousRevisionRef) + } + if out.TalosClusterOperationResultRef != spec.TalosClusterOperationResultRef { + t.Errorf("TalosClusterOperationResultRef: got %q, want %q", out.TalosClusterOperationResultRef, spec.TalosClusterOperationResultRef) + } +} + +func TestPackLog_FirstRevisionNoPredecessor(t *testing.T) { + t.Parallel() + spec := seamv1alpha1.PackLogSpec{ + Revision: 1, + Capability: "pack-deploy", + Status: seamv1alpha1.PackLogResultSucceeded, + } + if spec.PreviousRevisionRef != "" { + t.Errorf("PreviousRevisionRef: expected empty for revision 1, got %q", spec.PreviousRevisionRef) + } +} + +func TestPackLog_ResultStatusEnum(t *testing.T) { + t.Parallel() + cases := []seamv1alpha1.PackLogResultStatus{ + seamv1alpha1.PackLogResultSucceeded, + seamv1alpha1.PackLogResultFailed, + } + for _, c := range cases { + if c == "" { + t.Errorf("PackLogResultStatus constant is empty") + } + } +} diff --git a/test/unit/helm_metadata_types_test.go b/test/unit/helm_metadata_types_test.go index 1cc0e16..62765d3 100644 --- a/test/unit/helm_metadata_types_test.go +++ b/test/unit/helm_metadata_types_test.go @@ -14,13 +14,13 @@ import ( "encoding/json" "testing" - seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" ) func TestClusterPackSpec_HelmMetadataFields_RoundTrip(t *testing.T) { - spec := seamcorev1alpha1.InfrastructureClusterPackSpec{ + spec := dispatcherv1alpha1.PackDeliverySpec{ Version: "v1.0.0", - RegistryRef: seamcorev1alpha1.InfrastructurePackRegistryRef{ + RegistryRef: dispatcherv1alpha1.PackRegistryRef{ URL: "registry.example.com/packs/cert-manager", Digest: "sha256:abc123", }, @@ -36,7 +36,7 @@ func TestClusterPackSpec_HelmMetadataFields_RoundTrip(t *testing.T) { t.Fatalf("marshal ClusterPackSpec: %v", err) } - var got seamcorev1alpha1.InfrastructureClusterPackSpec + var got dispatcherv1alpha1.PackDeliverySpec if err := json.Unmarshal(data, &got); err != nil { t.Fatalf("unmarshal ClusterPackSpec: %v", err) } @@ -56,9 +56,9 @@ func TestClusterPackSpec_HelmMetadataFields_RoundTrip(t *testing.T) { } func TestClusterPackSpec_HelmMetadataFields_AbsentWhenZero(t *testing.T) { - spec := seamcorev1alpha1.InfrastructureClusterPackSpec{ + spec := dispatcherv1alpha1.PackDeliverySpec{ Version: "v1.0.0", - RegistryRef: seamcorev1alpha1.InfrastructurePackRegistryRef{ + RegistryRef: dispatcherv1alpha1.PackRegistryRef{ URL: "registry.example.com/packs/raw-pack", }, Checksum: "deadbeef", @@ -83,8 +83,8 @@ func TestClusterPackSpec_HelmMetadataFields_AbsentWhenZero(t *testing.T) { } func TestPackExecutionSpec_HelmMetadataFields_RoundTrip(t *testing.T) { - spec := seamcorev1alpha1.InfrastructurePackExecutionSpec{ - ClusterPackRef: seamcorev1alpha1.InfrastructureClusterPackRef{ + spec := dispatcherv1alpha1.PackExecutionSpec{ + PackDeliveryRef: dispatcherv1alpha1.PackDeliveryRef{ Name: "cert-manager-v1.13.3-r1", Version: "v1.13.3", }, @@ -101,7 +101,7 @@ func TestPackExecutionSpec_HelmMetadataFields_RoundTrip(t *testing.T) { t.Fatalf("marshal PackExecutionSpec: %v", err) } - var got seamcorev1alpha1.InfrastructurePackExecutionSpec + var got dispatcherv1alpha1.PackExecutionSpec if err := json.Unmarshal(data, &got); err != nil { t.Fatalf("unmarshal PackExecutionSpec: %v", err) } @@ -121,8 +121,8 @@ func TestPackExecutionSpec_HelmMetadataFields_RoundTrip(t *testing.T) { } func TestPackExecutionSpec_HelmMetadataFields_AbsentWhenZero(t *testing.T) { - spec := seamcorev1alpha1.InfrastructurePackExecutionSpec{ - ClusterPackRef: seamcorev1alpha1.InfrastructureClusterPackRef{ + spec := dispatcherv1alpha1.PackExecutionSpec{ + PackDeliveryRef: dispatcherv1alpha1.PackDeliveryRef{ Name: "raw-pack-v1.0.0-r1", Version: "v1.0.0", }, @@ -149,8 +149,8 @@ func TestPackExecutionSpec_HelmMetadataFields_AbsentWhenZero(t *testing.T) { } func TestPackInstanceSpec_HelmMetadataFields_RoundTrip(t *testing.T) { - spec := seamcorev1alpha1.InfrastructurePackInstanceSpec{ - ClusterPackRef: "cert-manager-v1.13.3-r1", + spec := dispatcherv1alpha1.PackInstalledSpec{ + PackDeliveryRef: "cert-manager-v1.13.3-r1", TargetClusterRef: "ccs-mgmt", ChartVersion: "v1.13.3", ChartURL: "https://charts.jetstack.io", @@ -163,7 +163,7 @@ func TestPackInstanceSpec_HelmMetadataFields_RoundTrip(t *testing.T) { t.Fatalf("marshal PackInstanceSpec: %v", err) } - var got seamcorev1alpha1.InfrastructurePackInstanceSpec + var got dispatcherv1alpha1.PackInstalledSpec if err := json.Unmarshal(data, &got); err != nil { t.Fatalf("unmarshal PackInstanceSpec: %v", err) } @@ -183,8 +183,8 @@ func TestPackInstanceSpec_HelmMetadataFields_RoundTrip(t *testing.T) { } func TestPackInstanceSpec_HelmMetadataFields_AbsentWhenZero(t *testing.T) { - spec := seamcorev1alpha1.InfrastructurePackInstanceSpec{ - ClusterPackRef: "raw-pack-v1.0.0-r1", + spec := dispatcherv1alpha1.PackInstalledSpec{ + PackDeliveryRef: "raw-pack-v1.0.0-r1", TargetClusterRef: "ccs-mgmt", // No helm metadata -- raw or kustomize pack. } diff --git a/test/unit/packexecution_reconciler_test.go b/test/unit/packexecution_reconciler_test.go index fd21c3a..eec2dd6 100644 --- a/test/unit/packexecution_reconciler_test.go +++ b/test/unit/packexecution_reconciler_test.go @@ -17,6 +17,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/seam-core/pkg/conditions" "github.com/ontai-dev/wrapper/internal/controller" ) @@ -30,11 +31,14 @@ func newPackExecutionScheme(t *testing.T) *runtime.Scheme { if err := seamcorev1alpha1.AddToScheme(s); err != nil { t.Fatalf("AddToScheme seamcorev1alpha1: %v", err) } + if err := dispatcherv1alpha1.AddToScheme(s); err != nil { + t.Fatalf("AddToScheme dispatcherv1alpha1: %v", err) + } return s } -func newSignedClusterPack(name, namespace, version string) *seamcorev1alpha1.InfrastructureClusterPack { - cp := &seamcorev1alpha1.InfrastructureClusterPack{ +func newSignedClusterPack(name, namespace, version string) *dispatcherv1alpha1.PackDelivery { + cp := &dispatcherv1alpha1.PackDelivery{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, @@ -42,15 +46,15 @@ func newSignedClusterPack(name, namespace, version string) *seamcorev1alpha1.Inf "ontai.dev/pack-signature": "validsig==", }, }, - Spec: seamcorev1alpha1.InfrastructureClusterPackSpec{ + Spec: dispatcherv1alpha1.PackDeliverySpec{ Version: version, - RegistryRef: seamcorev1alpha1.InfrastructurePackRegistryRef{ + RegistryRef: dispatcherv1alpha1.PackRegistryRef{ URL: "registry.ontai.dev/packs/" + name, Digest: "sha256:abc123", }, Checksum: "sha256:def456", }, - Status: seamcorev1alpha1.InfrastructureClusterPackStatus{ + Status: dispatcherv1alpha1.PackDeliveryStatus{ Signed: true, PackSignature: "validsig==", }, @@ -58,14 +62,14 @@ func newSignedClusterPack(name, namespace, version string) *seamcorev1alpha1.Inf return cp } -func newPackExecution(name, namespace, packName, packVersion, clusterRef, profileRef string) *seamcorev1alpha1.InfrastructurePackExecution { - return &seamcorev1alpha1.InfrastructurePackExecution{ +func newPackExecution(name, namespace, packName, packVersion, clusterRef, profileRef string) *dispatcherv1alpha1.PackExecution { + return &dispatcherv1alpha1.PackExecution{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: seamcorev1alpha1.InfrastructurePackExecutionSpec{ - ClusterPackRef: seamcorev1alpha1.InfrastructureClusterPackRef{ + Spec: dispatcherv1alpha1.PackExecutionSpec{ + PackDeliveryRef: dispatcherv1alpha1.PackDeliveryRef{ Name: packName, Version: packVersion, }, @@ -163,7 +167,7 @@ func newTalosClusterWithConductorReady(clusterName string, conductorReady bool) return tc } -func reconcilePE(t *testing.T, r *controller.PackExecutionReconciler, pe *seamcorev1alpha1.InfrastructurePackExecution) ctrl.Result { +func reconcilePE(t *testing.T, r *controller.PackExecutionReconciler, pe *dispatcherv1alpha1.PackExecution) ctrl.Result { t.Helper() result, err := r.Reconcile(context.Background(), ctrl.Request{ NamespacedName: types.NamespacedName{Name: pe.Name, Namespace: pe.Namespace}, @@ -177,7 +181,7 @@ func reconcilePE(t *testing.T, r *controller.PackExecutionReconciler, pe *seamco // rbacAllowedStub is a RBACReadyChecker stub that always grants all permissions. // Required for tests that need gate 5 (WrapperRunnerRBAC) to pass so they can reach // job-submission or post-job logic. The real gate requires a live API server SAR. -func rbacAllowedStub(_ context.Context, _ *seamcorev1alpha1.InfrastructurePackExecution) (bool, string, error) { +func rbacAllowedStub(_ context.Context, _ *dispatcherv1alpha1.PackExecution) (bool, string, error) { return true, "", nil } @@ -195,7 +199,7 @@ func TestPackExecutionReconciler_Gate1_SignaturePending(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}, &seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}, &dispatcherv1alpha1.PackDelivery{}). Build() if err := fakeClient.Create(context.Background(), tc); err != nil { t.Fatalf("create TalosCluster: %v", err) @@ -216,7 +220,7 @@ func TestPackExecutionReconciler_Gate1_SignaturePending(t *testing.T) { t.Errorf("expected RequeueAfter=15s for signature gate, got %v", result.RequeueAfter) } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get updated PackExecution: %v", err) } @@ -247,7 +251,7 @@ func TestPackExecutionReconciler_Gate2_PackRevoked(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}, &seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}, &dispatcherv1alpha1.PackDelivery{}). Build() if err := fakeClient.Create(context.Background(), tc); err != nil { t.Fatalf("create TalosCluster: %v", err) @@ -268,7 +272,7 @@ func TestPackExecutionReconciler_Gate2_PackRevoked(t *testing.T) { t.Errorf("expected no requeue for revoked pack, got %+v", result) } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get updated PackExecution: %v", err) } @@ -292,7 +296,7 @@ func TestPackExecutionReconciler_Gate3_SnapshotOutOfSync(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe, profile). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}, &seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}, &dispatcherv1alpha1.PackDelivery{}). Build() // Add unstructured PermissionSnapshot. if err := fakeClient.Create(context.Background(), ps); err != nil { @@ -318,7 +322,7 @@ func TestPackExecutionReconciler_Gate3_SnapshotOutOfSync(t *testing.T) { t.Errorf("expected RequeueAfter=30s for snapshot gate, got %v", result.RequeueAfter) } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get updated PackExecution: %v", err) } @@ -342,7 +346,7 @@ func TestPackExecutionReconciler_Gate4_RBACProfileNotProvisioned(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe, profile). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}, &seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}, &dispatcherv1alpha1.PackDelivery{}). Build() if err := fakeClient.Create(context.Background(), ps); err != nil { t.Fatalf("create PermissionSnapshot: %v", err) @@ -367,7 +371,7 @@ func TestPackExecutionReconciler_Gate4_RBACProfileNotProvisioned(t *testing.T) { t.Errorf("expected RequeueAfter=30s for RBAC gate, got %v", result.RequeueAfter) } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get updated PackExecution: %v", err) } @@ -392,7 +396,7 @@ func TestPackExecutionReconciler_Gate4_AbsentProfileAllowsJobSubmission(t *testi fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}, &seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}, &dispatcherv1alpha1.PackDelivery{}). Build() if err := fakeClient.Create(context.Background(), ps); err != nil { t.Fatalf("create PermissionSnapshot: %v", err) @@ -439,7 +443,7 @@ func TestPackExecutionReconciler_AllGatesClear_JobSubmitted(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe, profile). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}, &seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}, &dispatcherv1alpha1.PackDelivery{}). Build() if err := fakeClient.Create(context.Background(), ps); err != nil { t.Fatalf("create PermissionSnapshot: %v", err) @@ -490,7 +494,7 @@ func TestPackExecutionReconciler_AllGatesClear_JobSubmitted(t *testing.T) { } // Verify PackExecution status shows Running. - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get updated PackExecution: %v", err) } @@ -509,7 +513,7 @@ func TestPackExecutionReconciler_LineageSyncedInitialized(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}, &seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}, &dispatcherv1alpha1.PackDelivery{}). Build() r := &controller.PackExecutionReconciler{ Client: fakeClient, @@ -520,7 +524,7 @@ func TestPackExecutionReconciler_LineageSyncedInitialized(t *testing.T) { reconcilePE(t, r, pe) - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get updated PackExecution: %v", err) } @@ -546,7 +550,7 @@ func TestPackExecutionReconciler_Gate0_ConductorReadyAbsent(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}, &seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}, &dispatcherv1alpha1.PackDelivery{}). Build() r := &controller.PackExecutionReconciler{ Client: fakeClient, @@ -562,7 +566,7 @@ func TestPackExecutionReconciler_Gate0_ConductorReadyAbsent(t *testing.T) { t.Errorf("expected RequeueAfter=30s for ConductorReady gate, got %v", result.RequeueAfter) } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get updated PackExecution: %v", err) } @@ -588,7 +592,7 @@ func TestPackExecutionReconciler_Gate0_ConductorReadyFalse(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}, &seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}, &dispatcherv1alpha1.PackDelivery{}). Build() if err := fakeClient.Create(context.Background(), tc); err != nil { t.Fatalf("create TalosCluster: %v", err) @@ -607,7 +611,7 @@ func TestPackExecutionReconciler_Gate0_ConductorReadyFalse(t *testing.T) { t.Errorf("expected RequeueAfter=30s for ConductorReady gate (False), got %v", result.RequeueAfter) } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get updated PackExecution: %v", err) } @@ -631,7 +635,7 @@ func TestPackExecutionReconciler_Gate0_ConductorReadyTrue_ProceedsToSignatureGat fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}, &seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}, &dispatcherv1alpha1.PackDelivery{}). Build() if err := fakeClient.Create(context.Background(), tc); err != nil { t.Fatalf("create TalosCluster: %v", err) @@ -654,7 +658,7 @@ func TestPackExecutionReconciler_Gate0_ConductorReadyTrue_ProceedsToSignatureGat t.Errorf("expected RequeueAfter=15s (signature gate), got %v — gate 0 may not have cleared", result.RequeueAfter) } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get updated PackExecution: %v", err) } @@ -723,7 +727,7 @@ func TestPackExecutionReconciler_Gate0_RunnerConfigCapabilitiesAppear(t *testing if result.RequeueAfter == 0 { t.Error("expected non-zero RequeueAfter when gate 0 (ConductorReady) not cleared") } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := cl.Get(context.Background(), types.NamespacedName{Name: "cilium-exec", Namespace: "seam-tenant-ccs-test"}, updated); err != nil { t.Fatalf("get PackExecution: %v", err) } @@ -771,7 +775,7 @@ func TestPackExecutionReconciler_Gate0_RunnerConfigCapabilitiesAppear(t *testing if err != nil { t.Fatalf("second reconcile error: %v", err) } - updated2 := &seamcorev1alpha1.InfrastructurePackExecution{} + updated2 := &dispatcherv1alpha1.PackExecution{} if err := cl.Get(context.Background(), types.NamespacedName{Name: "cilium-exec", Namespace: "seam-tenant-ccs-test"}, updated2); err != nil { t.Fatalf("get PackExecution after second reconcile: %v", err) } diff --git a/test/unit/packinstance_reconciler_test.go b/test/unit/packinstance_reconciler_test.go index 4b8ef6b..488a65e 100644 --- a/test/unit/packinstance_reconciler_test.go +++ b/test/unit/packinstance_reconciler_test.go @@ -15,7 +15,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/seam-core/pkg/conditions" "github.com/ontai-dev/wrapper/internal/controller" ) @@ -32,25 +32,25 @@ func newPackInstanceScheme(t *testing.T) *runtime.Scheme { // schema would cause the fake client to convert the unstructured receipt and strip // dynamic status fields (signatureVerified, driftStatus) that conductor writes but // are not yet declared in InfrastructurePackReceiptStatus. - s.AddKnownTypes(seamcorev1alpha1.GroupVersion, - &seamcorev1alpha1.InfrastructurePackInstance{}, - &seamcorev1alpha1.InfrastructurePackInstanceList{}, - &seamcorev1alpha1.InfrastructurePackExecution{}, - &seamcorev1alpha1.InfrastructurePackExecutionList{}, - &seamcorev1alpha1.InfrastructureClusterPack{}, - &seamcorev1alpha1.InfrastructureClusterPackList{}, + s.AddKnownTypes(dispatcherv1alpha1.GroupVersion, + &dispatcherv1alpha1.PackInstalled{}, + &dispatcherv1alpha1.PackInstalledList{}, + &dispatcherv1alpha1.PackExecution{}, + &dispatcherv1alpha1.PackExecutionList{}, + &dispatcherv1alpha1.PackDelivery{}, + &dispatcherv1alpha1.PackDeliveryList{}, ) return s } -func newPackInstance(name, namespace, packRef, clusterRef string) *seamcorev1alpha1.InfrastructurePackInstance { - return &seamcorev1alpha1.InfrastructurePackInstance{ +func newPackInstance(name, namespace, packRef, clusterRef string) *dispatcherv1alpha1.PackInstalled { + return &dispatcherv1alpha1.PackInstalled{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: seamcorev1alpha1.InfrastructurePackInstanceSpec{ - ClusterPackRef: packRef, + Spec: dispatcherv1alpha1.PackInstalledSpec{ + PackDeliveryRef: packRef, TargetClusterRef: clusterRef, }, } @@ -59,9 +59,9 @@ func newPackInstance(name, namespace, packRef, clusterRef string) *seamcorev1alp func newPackReceipt(name, namespace string, signatureVerified bool, driftStatus string) *unstructured.Unstructured { r := &unstructured.Unstructured{} r.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "infrastructure.ontai.dev", + Group: "seam.ontai.dev", Version: "v1alpha1", - Kind: "InfrastructurePackReceipt", + Kind: "PackReceipt", }) r.SetName(name) r.SetNamespace(namespace) @@ -73,7 +73,7 @@ func newPackReceipt(name, namespace string, signatureVerified bool, driftStatus return r } -func reconcilePI(t *testing.T, r *controller.PackInstanceReconciler, pi *seamcorev1alpha1.InfrastructurePackInstance) ctrl.Result { +func reconcilePI(t *testing.T, r *controller.PackInstanceReconciler, pi *dispatcherv1alpha1.PackInstalled) ctrl.Result { t.Helper() result, err := r.Reconcile(context.Background(), ctrl.Request{ NamespacedName: types.NamespacedName{Name: pi.Name, Namespace: pi.Namespace}, @@ -91,7 +91,7 @@ func TestPackInstanceReconciler_NoReceipt(t *testing.T) { pi := newPackInstance("pi-1", "infra-system", "my-pack", "cluster-a") fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(pi). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackInstance{}). + WithStatusSubresource(&dispatcherv1alpha1.PackInstalled{}). Build() r := &controller.PackInstanceReconciler{ Client: fakeClient, @@ -105,7 +105,7 @@ func TestPackInstanceReconciler_NoReceipt(t *testing.T) { t.Errorf("expected RequeueAfter=60s when receipt missing, got %v", result.RequeueAfter) } - updated := &seamcorev1alpha1.InfrastructurePackInstance{} + updated := &dispatcherv1alpha1.PackInstalled{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pi), updated); err != nil { t.Fatalf("get updated PackInstance: %v", err) } @@ -123,7 +123,7 @@ func TestPackInstanceReconciler_SecurityViolation(t *testing.T) { receipt := newPackReceipt("my-pack-cluster-a", "infra-system", false, "InSync") fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(pi). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackInstance{}). + WithStatusSubresource(&dispatcherv1alpha1.PackInstalled{}). Build() if err := fakeClient.Create(context.Background(), receipt); err != nil { t.Fatalf("create PackReceipt: %v", err) @@ -137,7 +137,7 @@ func TestPackInstanceReconciler_SecurityViolation(t *testing.T) { reconcilePI(t, r, pi) - updated := &seamcorev1alpha1.InfrastructurePackInstance{} + updated := &dispatcherv1alpha1.PackInstalled{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pi), updated); err != nil { t.Fatalf("get updated PackInstance: %v", err) } @@ -162,7 +162,7 @@ func TestPackInstanceReconciler_DriftDetected(t *testing.T) { receipt := newPackReceipt("my-pack-cluster-a", "infra-system", true, "Drifted") fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(pi). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackInstance{}). + WithStatusSubresource(&dispatcherv1alpha1.PackInstalled{}). Build() if err := fakeClient.Create(context.Background(), receipt); err != nil { t.Fatalf("create PackReceipt: %v", err) @@ -176,7 +176,7 @@ func TestPackInstanceReconciler_DriftDetected(t *testing.T) { reconcilePI(t, r, pi) - updated := &seamcorev1alpha1.InfrastructurePackInstance{} + updated := &dispatcherv1alpha1.PackInstalled{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pi), updated); err != nil { t.Fatalf("get updated PackInstance: %v", err) } @@ -198,7 +198,7 @@ func TestPackInstanceReconciler_InSync(t *testing.T) { receipt := newPackReceipt("my-pack-cluster-a", "infra-system", true, "InSync") fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(pi). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackInstance{}). + WithStatusSubresource(&dispatcherv1alpha1.PackInstalled{}). Build() if err := fakeClient.Create(context.Background(), receipt); err != nil { t.Fatalf("create PackReceipt: %v", err) @@ -212,7 +212,7 @@ func TestPackInstanceReconciler_InSync(t *testing.T) { reconcilePI(t, r, pi) - updated := &seamcorev1alpha1.InfrastructurePackInstance{} + updated := &dispatcherv1alpha1.PackInstalled{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pi), updated); err != nil { t.Fatalf("get updated PackInstance: %v", err) } @@ -244,15 +244,15 @@ func TestPackInstanceReconciler_DependencyBlock(t *testing.T) { pi := newPackInstance("pi-blocked", "infra-system", "my-pack", "cluster-a") pi.Spec.DependsOn = []string{"dep-pi"} - pi.Spec.DependencyPolicy = &seamcorev1alpha1.InfrastructureDependencyPolicy{ - OnDrift: seamcorev1alpha1.InfrastructureDriftPolicyBlock, + pi.Spec.DependencyPolicy = &dispatcherv1alpha1.DependencyPolicy{ + OnDrift: dispatcherv1alpha1.DriftPolicyBlock, } receipt := newPackReceipt("my-pack-cluster-a", "infra-system", true, "InSync") fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(pi, depPI). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackInstance{}). + WithStatusSubresource(&dispatcherv1alpha1.PackInstalled{}). Build() if err := fakeClient.Create(context.Background(), receipt); err != nil { t.Fatalf("create PackReceipt: %v", err) @@ -266,7 +266,7 @@ func TestPackInstanceReconciler_DependencyBlock(t *testing.T) { reconcilePI(t, r, pi) - updated := &seamcorev1alpha1.InfrastructurePackInstance{} + updated := &dispatcherv1alpha1.PackInstalled{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pi), updated); err != nil { t.Fatalf("get updated PackInstance: %v", err) } @@ -281,21 +281,21 @@ func TestPackInstanceReconciler_DependencyBlock(t *testing.T) { } // newSucceededPackExecution builds a PackExecution with Succeeded=True for use in tests. -func newSucceededPackExecution(name, namespace, packName, packVersion, clusterRef string) *seamcorev1alpha1.InfrastructurePackExecution { - pe := &seamcorev1alpha1.InfrastructurePackExecution{ +func newSucceededPackExecution(name, namespace, packName, packVersion, clusterRef string) *dispatcherv1alpha1.PackExecution { + pe := &dispatcherv1alpha1.PackExecution{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: seamcorev1alpha1.InfrastructurePackExecutionSpec{ - ClusterPackRef: seamcorev1alpha1.InfrastructureClusterPackRef{ + Spec: dispatcherv1alpha1.PackExecutionSpec{ + PackDeliveryRef: dispatcherv1alpha1.PackDeliveryRef{ Name: packName, Version: packVersion, }, TargetClusterRef: clusterRef, AdmissionProfileRef: "rbac-profile", }, - Status: seamcorev1alpha1.InfrastructurePackExecutionStatus{ + Status: dispatcherv1alpha1.PackExecutionStatus{ Conditions: []metav1.Condition{ { Type: conditions.ConditionTypePackExecutionSucceeded, @@ -320,7 +320,7 @@ func TestPackInstanceReconciler_ReadyWhenPackExecutionSucceeded(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(pi). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackInstance{}, &seamcorev1alpha1.InfrastructurePackExecution{}). + WithStatusSubresource(&dispatcherv1alpha1.PackInstalled{}, &dispatcherv1alpha1.PackExecution{}). Build() // Create the PE with status via status subresource. if err := fakeClient.Create(context.Background(), pe); err != nil { @@ -341,7 +341,7 @@ func TestPackInstanceReconciler_ReadyWhenPackExecutionSucceeded(t *testing.T) { t.Errorf("expected RequeueAfter=60s, got %v", result.RequeueAfter) } - updated := &seamcorev1alpha1.InfrastructurePackInstance{} + updated := &dispatcherv1alpha1.PackInstalled{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pi), updated); err != nil { t.Fatalf("get updated PackInstance: %v", err) } @@ -369,7 +369,7 @@ func TestPackInstanceReconciler_AwaitingDeliveryWhenNoPackExecution(t *testing.T pi := newPackInstance("pi-awaiting", "infra-system", "my-pack", "cluster-a") fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(pi). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackInstance{}). + WithStatusSubresource(&dispatcherv1alpha1.PackInstalled{}). Build() r := &controller.PackInstanceReconciler{ @@ -380,7 +380,7 @@ func TestPackInstanceReconciler_AwaitingDeliveryWhenNoPackExecution(t *testing.T reconcilePI(t, r, pi) - updated := &seamcorev1alpha1.InfrastructurePackInstance{} + updated := &dispatcherv1alpha1.PackInstalled{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pi), updated); err != nil { t.Fatalf("get updated PackInstance: %v", err) } @@ -397,7 +397,7 @@ func TestPackInstanceReconciler_LineageSyncedInitialized(t *testing.T) { pi := newPackInstance("pi-lineage", "infra-system", "my-pack", "cluster-a") fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(pi). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackInstance{}). + WithStatusSubresource(&dispatcherv1alpha1.PackInstalled{}). Build() r := &controller.PackInstanceReconciler{ Client: fakeClient, @@ -407,7 +407,7 @@ func TestPackInstanceReconciler_LineageSyncedInitialized(t *testing.T) { reconcilePI(t, r, pi) - updated := &seamcorev1alpha1.InfrastructurePackInstance{} + updated := &dispatcherv1alpha1.PackInstalled{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pi), updated); err != nil { t.Fatalf("get updated PackInstance: %v", err) } diff --git a/test/unit/packinstance_watch_test.go b/test/unit/packinstance_watch_test.go index b05c6e3..d4179ba 100644 --- a/test/unit/packinstance_watch_test.go +++ b/test/unit/packinstance_watch_test.go @@ -11,7 +11,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/wrapper/internal/controller" ) @@ -49,7 +49,7 @@ func TestMapPackInstanceToClusterPack_ReturnsRequestAndDeletesPackExecution(t *t } // PackExecution must have been deleted by the mapping function (WS2). - fetched := &seamcorev1alpha1.InfrastructurePackExecution{} + fetched := &dispatcherv1alpha1.PackExecution{} err := r.Client.Get(context.Background(), client.ObjectKey{Name: peName, Namespace: ns}, fetched) if err == nil { t.Errorf("expected PackExecution %s/%s to be deleted, but it still exists", ns, peName) @@ -90,7 +90,7 @@ func TestMapPackInstanceToClusterPack_NonTenantNamespaceNoPackExecutionDelete(t } // PackExecution must NOT have been deleted -- namespace is not seam-tenant-*. - fetched := &seamcorev1alpha1.InfrastructurePackExecution{} + fetched := &dispatcherv1alpha1.PackExecution{} if err := r.Client.Get(context.Background(), client.ObjectKey{Name: "nginx-infra-system", Namespace: ns}, fetched); err != nil { t.Errorf("PackExecution should survive for non-seam-tenant namespace, got error: %v", err) } diff --git a/test/unit/role_independence_test.go b/test/unit/role_independence_test.go index 9742b54..bb8f014 100644 --- a/test/unit/role_independence_test.go +++ b/test/unit/role_independence_test.go @@ -31,7 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" "github.com/ontai-dev/seam-core/pkg/conditions" "github.com/ontai-dev/wrapper/internal/controller" ) @@ -49,7 +49,7 @@ func TestRoleIndependence_PackExecution_ManagementCluster_Gate0Blocks(t *testing fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}, &seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}, &dispatcherv1alpha1.PackDelivery{}). Build() r := &controller.PackExecutionReconciler{ Client: fakeClient, @@ -65,7 +65,7 @@ func TestRoleIndependence_PackExecution_ManagementCluster_Gate0Blocks(t *testing t.Errorf("expected RequeueAfter=30s for gate 0 (ccs-mgmt), got %v", result.RequeueAfter) } - updated := &seamcorev1alpha1.InfrastructurePackExecution{} + updated := &dispatcherv1alpha1.PackExecution{} if err := fakeClient.Get(context.Background(), client.ObjectKeyFromObject(pe), updated); err != nil { t.Fatalf("get updated PackExecution: %v", err) } @@ -94,7 +94,7 @@ func TestRoleIndependence_PackExecution_ManagementCluster_JobNamespace(t *testin fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe, profile). - WithStatusSubresource(&seamcorev1alpha1.InfrastructurePackExecution{}, &seamcorev1alpha1.InfrastructureClusterPack{}). + WithStatusSubresource(&dispatcherv1alpha1.PackExecution{}, &dispatcherv1alpha1.PackDelivery{}). Build() if err := fakeClient.Create(context.Background(), ps); err != nil { t.Fatalf("create PermissionSnapshot: %v", err) @@ -158,9 +158,9 @@ func TestRoleIndependence_PackExecution_ManagementCluster_PackInstance(t *testin fakeClient := fake.NewClientBuilder().WithScheme(s). WithObjects(cp, pe, profile). WithStatusSubresource( - &seamcorev1alpha1.InfrastructurePackExecution{}, - &seamcorev1alpha1.InfrastructureClusterPack{}, - &seamcorev1alpha1.InfrastructurePackInstance{}, + &dispatcherv1alpha1.PackExecution{}, + &dispatcherv1alpha1.PackDelivery{}, + &dispatcherv1alpha1.PackInstalled{}, ). Build() for _, obj := range []client.Object{ps, tc, rc} { @@ -192,14 +192,14 @@ func TestRoleIndependence_PackExecution_ManagementCluster_PackInstance(t *testin } // Step 3: create a PackOperationResult with status=Succeeded in the PE namespace. - por := &seamcorev1alpha1.PackOperationResult{ + por := &dispatcherv1alpha1.PackLog{ ObjectMeta: metav1.ObjectMeta{ Name: peName + "-por", Namespace: peNS, Labels: map[string]string{"ontai.dev/pack-execution": peName}, }, - Spec: seamcorev1alpha1.PackOperationResultSpec{ - Status: seamcorev1alpha1.PackResultSucceeded, + Spec: dispatcherv1alpha1.PackLogSpec{ + Status: dispatcherv1alpha1.PackLogResultSucceeded, Revision: 1, }, } @@ -211,7 +211,7 @@ func TestRoleIndependence_PackExecution_ManagementCluster_PackInstance(t *testin reconcilePE(t, r, pe) // PackInstance must appear in seam-tenant-ccs-mgmt. - piList := &seamcorev1alpha1.InfrastructurePackInstanceList{} + piList := &dispatcherv1alpha1.PackInstalledList{} if err := fakeClient.List(context.Background(), piList, client.InNamespace(tenantNS)); err != nil { t.Fatalf("list PackInstances in %s: %v", tenantNS, err) } @@ -227,7 +227,7 @@ func TestRoleIndependence_PackExecution_ManagementCluster_PackInstance(t *testin // No PackInstance must exist in seam-system, ont-system, or the PE namespace. for _, forbiddenNS := range []string{"seam-system", "ont-system", peNS} { - wrongList := &seamcorev1alpha1.InfrastructurePackInstanceList{} + wrongList := &dispatcherv1alpha1.PackInstalledList{} if err := fakeClient.List(context.Background(), wrongList, client.InNamespace(forbiddenNS)); err != nil { t.Fatalf("list PackInstances in %s: %v", forbiddenNS, err) } @@ -244,7 +244,7 @@ func TestRoleIndependence_ClusterPack_SignaturePathUnchanged(t *testing.T) { s := newClusterPackScheme(t) // A ClusterPack with an annotation from the management-cluster conductor. - cp := &seamcorev1alpha1.InfrastructureClusterPack{ + cp := &dispatcherv1alpha1.PackDelivery{ ObjectMeta: metav1.ObjectMeta{ Name: "cilium", Namespace: "infra-system", @@ -253,9 +253,9 @@ func TestRoleIndependence_ClusterPack_SignaturePathUnchanged(t *testing.T) { "ontai.dev/pack-signature": "mgmt-conductor-sig==", }, }, - Spec: seamcorev1alpha1.InfrastructureClusterPackSpec{ + Spec: dispatcherv1alpha1.PackDeliverySpec{ Version: "v1.16.0", - RegistryRef: seamcorev1alpha1.InfrastructurePackRegistryRef{ + RegistryRef: dispatcherv1alpha1.PackRegistryRef{ URL: "registry.ontai.dev/packs/cilium", Digest: "sha256:mgmtdigest", }, @@ -264,7 +264,7 @@ func TestRoleIndependence_ClusterPack_SignaturePathUnchanged(t *testing.T) { } fakeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(cp). - WithStatusSubresource(&seamcorev1alpha1.InfrastructureClusterPack{}).Build() + WithStatusSubresource(&dispatcherv1alpha1.PackDelivery{}).Build() r := &controller.ClusterPackReconciler{ Client: fakeClient, Scheme: s, @@ -282,7 +282,7 @@ func TestRoleIndependence_ClusterPack_SignaturePathUnchanged(t *testing.T) { t.Error("expected reconciler to advance past signature-pending when annotation present") } - updated := &seamcorev1alpha1.InfrastructureClusterPack{} + updated := &dispatcherv1alpha1.PackDelivery{} if err := fakeClient.Get(context.Background(), types.NamespacedName{Name: "cilium", Namespace: "infra-system"}, updated); err != nil { t.Fatalf("get updated ClusterPack: %v", err) } From 6c37e7b8eb5c8c904d45ba2368ed0e7a34b15623 Mon Sep 17 00:00:00 2001 From: ontave Date: Tue, 12 May 2026 16:54:50 +0200 Subject: [PATCH 03/19] feat(migration-4.1): rename module path wrapper -> dispatcher Update go.mod module declaration from github.com/ontai-dev/wrapper to github.com/ontai-dev/dispatcher. Update all internal import paths to use github.com/ontai-dev/dispatcher. Update seam-core -> seam replace directive and require entry. Add seam-sdk replace + require. No type or logic changes. --- api/seam/v1alpha1/packdelivery_types.go | 2 +- api/seam/v1alpha1/packexecution_types.go | 2 +- api/seam/v1alpha1/zz_generated.deepcopy.go | 2 +- cmd/wrapper/main.go | 6 +++--- config/crd/bases/seam.ontai.dev_packdeliveries.yaml | 2 ++ go.mod | 9 ++++++--- internal/controller/clusterpack_reconciler.go | 6 +++--- internal/controller/packexecution_reconciler.go | 6 +++--- internal/controller/packinstance_reconciler.go | 4 ++-- test/e2e/ac3_rollback_test.go | 2 +- test/e2e/clusterpack_e2e_test.go | 2 +- test/e2e/suite_test.go | 2 +- test/integration/clusterpack_test.go | 4 ++-- test/integration/suite_test.go | 4 ++-- test/unit/clusterpack_reconciler_test.go | 8 ++++---- test/unit/controller/helpers_test.go | 6 +++--- test/unit/controller/kueue_job_scheduling_test.go | 6 +++--- test/unit/controller/packexecution_gates_test.go | 6 +++--- test/unit/controller/packinstance_lifecycle_test.go | 6 +++--- test/unit/controller/packversion_test.go | 4 ++-- test/unit/controller/por_label_lookup_test.go | 4 ++-- test/unit/controller/rollback_test.go | 4 ++-- test/unit/dispatcher_types_test.go | 4 ++-- test/unit/helm_metadata_types_test.go | 2 +- test/unit/packexecution_reconciler_test.go | 8 ++++---- test/unit/packinstance_reconciler_test.go | 6 +++--- test/unit/packinstance_watch_test.go | 4 ++-- test/unit/role_independence_test.go | 6 +++--- 28 files changed, 66 insertions(+), 61 deletions(-) diff --git a/api/seam/v1alpha1/packdelivery_types.go b/api/seam/v1alpha1/packdelivery_types.go index 542a8b9..6675d8c 100644 --- a/api/seam/v1alpha1/packdelivery_types.go +++ b/api/seam/v1alpha1/packdelivery_types.go @@ -3,7 +3,7 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/ontai-dev/seam-core/pkg/lineage" + "github.com/ontai-dev/seam/pkg/lineage" ) // LifecyclePolicy controls artifact retention behavior. diff --git a/api/seam/v1alpha1/packexecution_types.go b/api/seam/v1alpha1/packexecution_types.go index f2a822c..8155bb6 100644 --- a/api/seam/v1alpha1/packexecution_types.go +++ b/api/seam/v1alpha1/packexecution_types.go @@ -3,7 +3,7 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/ontai-dev/seam-core/pkg/lineage" + "github.com/ontai-dev/seam/pkg/lineage" ) // PackDeliveryRef identifies a specific PackDelivery by name and version. diff --git a/api/seam/v1alpha1/zz_generated.deepcopy.go b/api/seam/v1alpha1/zz_generated.deepcopy.go index 25534f1..e965598 100644 --- a/api/seam/v1alpha1/zz_generated.deepcopy.go +++ b/api/seam/v1alpha1/zz_generated.deepcopy.go @@ -5,7 +5,7 @@ package v1alpha1 import ( - "github.com/ontai-dev/seam-core/pkg/lineage" + "github.com/ontai-dev/seam/pkg/lineage" "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/cmd/wrapper/main.go b/cmd/wrapper/main.go index dc4fd41..1063d3d 100644 --- a/cmd/wrapper/main.go +++ b/cmd/wrapper/main.go @@ -18,9 +18,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - seamv1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/wrapper/internal/controller" + seamv1alpha1 "github.com/ontai-dev/seam/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/dispatcher/internal/controller" ) var scheme = runtime.NewScheme() diff --git a/config/crd/bases/seam.ontai.dev_packdeliveries.yaml b/config/crd/bases/seam.ontai.dev_packdeliveries.yaml index ab92c18..fe8bb07 100644 --- a/config/crd/bases/seam.ontai.dev_packdeliveries.yaml +++ b/config/crd/bases/seam.ontai.dev_packdeliveries.yaml @@ -4,6 +4,8 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.16.1 + labels: + infrastructure.ontai.dev/lineage-root: "true" name: packdeliveries.seam.ontai.dev spec: group: seam.ontai.dev diff --git a/go.mod b/go.mod index 5462158..5d39b95 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,18 @@ -module github.com/ontai-dev/wrapper +module github.com/ontai-dev/dispatcher go 1.25.3 replace github.com/ontai-dev/conductor => ../conductor -replace github.com/ontai-dev/seam-core => ../seam-core +replace github.com/ontai-dev/seam => ../seam-core + +replace github.com/ontai-dev/seam-sdk => ../seam-sdk require ( github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 - github.com/ontai-dev/seam-core v0.1.0-alpha.0.20260425084313-fa4bedc389f6 + github.com/ontai-dev/seam v0.0.0-00010101000000-000000000000 + github.com/ontai-dev/seam-sdk v0.0.0-00010101000000-000000000000 k8s.io/api v0.35.3 k8s.io/apimachinery v0.35.3 k8s.io/client-go v0.35.0 diff --git a/internal/controller/clusterpack_reconciler.go b/internal/controller/clusterpack_reconciler.go index d8afab8..3f83904 100644 --- a/internal/controller/clusterpack_reconciler.go +++ b/internal/controller/clusterpack_reconciler.go @@ -22,9 +22,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" - seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/seam-core/pkg/conditions" + seamcorev1alpha1 "github.com/ontai-dev/seam/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/seam/pkg/conditions" ) // packSignatureAnnotation is the annotation key set by the conductor signing loop diff --git a/internal/controller/packexecution_reconciler.go b/internal/controller/packexecution_reconciler.go index 45c1913..cf740ed 100644 --- a/internal/controller/packexecution_reconciler.go +++ b/internal/controller/packexecution_reconciler.go @@ -25,9 +25,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/seam-core/pkg/conditions" - "github.com/ontai-dev/seam-core/pkg/lineage" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/seam/pkg/conditions" + "github.com/ontai-dev/seam/pkg/lineage" ) const ( diff --git a/internal/controller/packinstance_reconciler.go b/internal/controller/packinstance_reconciler.go index 3fbd3ee..58a24b3 100644 --- a/internal/controller/packinstance_reconciler.go +++ b/internal/controller/packinstance_reconciler.go @@ -20,8 +20,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/seam-core/pkg/conditions" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/seam/pkg/conditions" ) // driftCheckInterval is how often the reconciler re-reads PackReceipt drift status diff --git a/test/e2e/ac3_rollback_test.go b/test/e2e/ac3_rollback_test.go index ac4e067..2343c30 100644 --- a/test/e2e/ac3_rollback_test.go +++ b/test/e2e/ac3_rollback_test.go @@ -28,7 +28,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - e2ehelpers "github.com/ontai-dev/seam-core/pkg/e2e" + e2ehelpers "github.com/ontai-dev/seam/pkg/e2e" ) const ( diff --git a/test/e2e/clusterpack_e2e_test.go b/test/e2e/clusterpack_e2e_test.go index f1eff07..5f19313 100644 --- a/test/e2e/clusterpack_e2e_test.go +++ b/test/e2e/clusterpack_e2e_test.go @@ -27,7 +27,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - e2ehelpers "github.com/ontai-dev/seam-core/pkg/e2e" + e2ehelpers "github.com/ontai-dev/seam/pkg/e2e" ) var ( diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go index 447d395..4128bca 100644 --- a/test/e2e/suite_test.go +++ b/test/e2e/suite_test.go @@ -21,7 +21,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - e2ehelpers "github.com/ontai-dev/seam-core/pkg/e2e" + e2ehelpers "github.com/ontai-dev/seam/pkg/e2e" ) // Suite-level cluster clients, initialized in BeforeSuite. diff --git a/test/integration/clusterpack_test.go b/test/integration/clusterpack_test.go index dc36b6f..260356a 100644 --- a/test/integration/clusterpack_test.go +++ b/test/integration/clusterpack_test.go @@ -14,8 +14,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/seam-core/pkg/conditions" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/seam/pkg/conditions" ) // newClusterPack constructs a minimal valid ClusterPack for testing. diff --git a/test/integration/suite_test.go b/test/integration/suite_test.go index a303a0d..b664e0a 100644 --- a/test/integration/suite_test.go +++ b/test/integration/suite_test.go @@ -31,8 +31,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" - "github.com/ontai-dev/wrapper/internal/controller" + seamcorev1alpha1 "github.com/ontai-dev/seam/api/v1alpha1" + "github.com/ontai-dev/dispatcher/internal/controller" ) var ( diff --git a/test/unit/clusterpack_reconciler_test.go b/test/unit/clusterpack_reconciler_test.go index f242ab4..7617eff 100644 --- a/test/unit/clusterpack_reconciler_test.go +++ b/test/unit/clusterpack_reconciler_test.go @@ -15,10 +15,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/seam-core/pkg/conditions" - "github.com/ontai-dev/wrapper/internal/controller" + seamcorev1alpha1 "github.com/ontai-dev/seam/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/seam/pkg/conditions" + "github.com/ontai-dev/dispatcher/internal/controller" ) func newClusterPackScheme(t *testing.T) *runtime.Scheme { diff --git a/test/unit/controller/helpers_test.go b/test/unit/controller/helpers_test.go index fd79341..c75b5cc 100644 --- a/test/unit/controller/helpers_test.go +++ b/test/unit/controller/helpers_test.go @@ -17,9 +17,9 @@ import ( "k8s.io/apimachinery/pkg/types" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - seamv1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/wrapper/internal/controller" + seamv1alpha1 "github.com/ontai-dev/seam/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/dispatcher/internal/controller" ) // rbacAllowedStub is a RBACReadyChecker stub that always grants all permissions. diff --git a/test/unit/controller/kueue_job_scheduling_test.go b/test/unit/controller/kueue_job_scheduling_test.go index 4ce1eda..c753670 100644 --- a/test/unit/controller/kueue_job_scheduling_test.go +++ b/test/unit/controller/kueue_job_scheduling_test.go @@ -28,9 +28,9 @@ import ( clientevents "k8s.io/client-go/tools/events" "sigs.k8s.io/controller-runtime/pkg/client" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/seam-core/pkg/conditions" - "github.com/ontai-dev/wrapper/internal/controller" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/seam/pkg/conditions" + "github.com/ontai-dev/dispatcher/internal/controller" ) // jobSubmissionSetup returns a fake client with all five gates satisfied and no diff --git a/test/unit/controller/packexecution_gates_test.go b/test/unit/controller/packexecution_gates_test.go index 6f13690..2f5101a 100644 --- a/test/unit/controller/packexecution_gates_test.go +++ b/test/unit/controller/packexecution_gates_test.go @@ -29,9 +29,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/seam-core/pkg/conditions" - "github.com/ontai-dev/wrapper/internal/controller" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/seam/pkg/conditions" + "github.com/ontai-dev/dispatcher/internal/controller" ) // TestAC2_Gate1_PackExecution_BlockedWhenUnsigned verifies that a PackExecution diff --git a/test/unit/controller/packinstance_lifecycle_test.go b/test/unit/controller/packinstance_lifecycle_test.go index 7e294fd..0693de2 100644 --- a/test/unit/controller/packinstance_lifecycle_test.go +++ b/test/unit/controller/packinstance_lifecycle_test.go @@ -29,9 +29,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/seam-core/pkg/conditions" - "github.com/ontai-dev/wrapper/internal/controller" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/seam/pkg/conditions" + "github.com/ontai-dev/dispatcher/internal/controller" ) // reconcilePackExecution calls PackExecutionReconciler.Reconcile and fatals on error. diff --git a/test/unit/controller/packversion_test.go b/test/unit/controller/packversion_test.go index 437dd60..3500a83 100644 --- a/test/unit/controller/packversion_test.go +++ b/test/unit/controller/packversion_test.go @@ -13,8 +13,8 @@ import ( clientevents "k8s.io/client-go/tools/events" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - controller "github.com/ontai-dev/wrapper/internal/controller" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + controller "github.com/ontai-dev/dispatcher/internal/controller" ) // deployPack runs a reconcile cycle with the given ClusterPack version and diff --git a/test/unit/controller/por_label_lookup_test.go b/test/unit/controller/por_label_lookup_test.go index df63b74..6147af0 100644 --- a/test/unit/controller/por_label_lookup_test.go +++ b/test/unit/controller/por_label_lookup_test.go @@ -9,8 +9,8 @@ import ( clientevents "k8s.io/client-go/tools/events" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/wrapper/internal/controller" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/dispatcher/internal/controller" ) func makePOR(name, namespace, packExecutionRef string, revision int64, status dispatcherv1alpha1.PackLogResultStatus) *dispatcherv1alpha1.PackLog { diff --git a/test/unit/controller/rollback_test.go b/test/unit/controller/rollback_test.go index fb3eb87..22c2160 100644 --- a/test/unit/controller/rollback_test.go +++ b/test/unit/controller/rollback_test.go @@ -11,8 +11,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/wrapper/internal/controller" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/dispatcher/internal/controller" ) // fakePOR builds a PackOperationResult representing a deploy at the given revision. diff --git a/test/unit/dispatcher_types_test.go b/test/unit/dispatcher_types_test.go index 45b408d..3df18cb 100644 --- a/test/unit/dispatcher_types_test.go +++ b/test/unit/dispatcher_types_test.go @@ -8,8 +8,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - seamv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/seam-core/pkg/lineage" + seamv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/seam/pkg/lineage" ) // --- PackDelivery --- diff --git a/test/unit/helm_metadata_types_test.go b/test/unit/helm_metadata_types_test.go index 62765d3..6b1550b 100644 --- a/test/unit/helm_metadata_types_test.go +++ b/test/unit/helm_metadata_types_test.go @@ -14,7 +14,7 @@ import ( "encoding/json" "testing" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" ) func TestClusterPackSpec_HelmMetadataFields_RoundTrip(t *testing.T) { diff --git a/test/unit/packexecution_reconciler_test.go b/test/unit/packexecution_reconciler_test.go index eec2dd6..772a36d 100644 --- a/test/unit/packexecution_reconciler_test.go +++ b/test/unit/packexecution_reconciler_test.go @@ -16,10 +16,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - seamcorev1alpha1 "github.com/ontai-dev/seam-core/api/v1alpha1" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/seam-core/pkg/conditions" - "github.com/ontai-dev/wrapper/internal/controller" + seamcorev1alpha1 "github.com/ontai-dev/seam/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/seam/pkg/conditions" + "github.com/ontai-dev/dispatcher/internal/controller" ) func newPackExecutionScheme(t *testing.T) *runtime.Scheme { diff --git a/test/unit/packinstance_reconciler_test.go b/test/unit/packinstance_reconciler_test.go index 488a65e..3d9f433 100644 --- a/test/unit/packinstance_reconciler_test.go +++ b/test/unit/packinstance_reconciler_test.go @@ -15,9 +15,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/seam-core/pkg/conditions" - "github.com/ontai-dev/wrapper/internal/controller" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/seam/pkg/conditions" + "github.com/ontai-dev/dispatcher/internal/controller" ) func newPackInstanceScheme(t *testing.T) *runtime.Scheme { diff --git a/test/unit/packinstance_watch_test.go b/test/unit/packinstance_watch_test.go index d4179ba..cf3bce0 100644 --- a/test/unit/packinstance_watch_test.go +++ b/test/unit/packinstance_watch_test.go @@ -11,8 +11,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/wrapper/internal/controller" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/dispatcher/internal/controller" ) func newWatchTestReconciler(t *testing.T, objs ...client.Object) *controller.ClusterPackReconciler { diff --git a/test/unit/role_independence_test.go b/test/unit/role_independence_test.go index bb8f014..f73f32d 100644 --- a/test/unit/role_independence_test.go +++ b/test/unit/role_independence_test.go @@ -31,9 +31,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - dispatcherv1alpha1 "github.com/ontai-dev/wrapper/api/seam/v1alpha1" - "github.com/ontai-dev/seam-core/pkg/conditions" - "github.com/ontai-dev/wrapper/internal/controller" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" + "github.com/ontai-dev/seam/pkg/conditions" + "github.com/ontai-dev/dispatcher/internal/controller" ) // TestRoleIndependence_PackExecution_ManagementCluster_Gate0Blocks verifies that From ffeef42f68350182845f0245204e1c30e6cc249a Mon Sep 17 00:00:00 2001 From: ontave Date: Tue, 12 May 2026 16:59:58 +0200 Subject: [PATCH 04/19] chore: update replace directive to renamed seam directory Replace ../seam-core with ../seam following the seam-core -> seam filesystem rename. Module path github.com/ontai-dev/seam was already updated in Phase 4; this aligns the local path pointer. --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5d39b95..6b7803b 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.3 replace github.com/ontai-dev/conductor => ../conductor -replace github.com/ontai-dev/seam => ../seam-core +replace github.com/ontai-dev/seam => ../seam replace github.com/ontai-dev/seam-sdk => ../seam-sdk From c8edcef3547408465f16af7d542cab94b4505598 Mon Sep 17 00:00:00 2001 From: ontave Date: Tue, 12 May 2026 17:54:51 +0200 Subject: [PATCH 05/19] migration(phase-5): update guardian GVK group references to guardian.ontai.dev Update all GroupVersionKind Group fields for RBACProfile, RBACPolicy, and PermissionSnapshot from security.ontai.dev to guardian.ontai.dev in the PackExecution reconciler and associated tests. --- internal/controller/packexecution_reconciler.go | 8 ++++---- test/unit/controller/helpers_test.go | 4 ++-- test/unit/packexecution_reconciler_test.go | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/controller/packexecution_reconciler.go b/internal/controller/packexecution_reconciler.go index cf740ed..81b7d76 100644 --- a/internal/controller/packexecution_reconciler.go +++ b/internal/controller/packexecution_reconciler.go @@ -716,7 +716,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques func (r *PackExecutionReconciler) isPermissionSnapshotCurrent(ctx context.Context, pe *dispatcherv1alpha1.PackExecution) (bool, error) { ps := &unstructured.Unstructured{} ps.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "security.ontai.dev", + Group: "guardian.ontai.dev", Version: "v1alpha1", Kind: "PermissionSnapshot", }) @@ -755,7 +755,7 @@ func (r *PackExecutionReconciler) isPermissionSnapshotCurrent(ctx context.Contex func (r *PackExecutionReconciler) isRBACProfileProvisioned(ctx context.Context, pe *dispatcherv1alpha1.PackExecution) (bool, error) { rp := &unstructured.Unstructured{} rp.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "security.ontai.dev", + Group: "guardian.ontai.dev", Version: "v1alpha1", Kind: "RBACProfile", }) @@ -1025,13 +1025,13 @@ func int32Ptr(i int32) *int32 { return &i } func (r *PackExecutionReconciler) SetupWithManager(mgr ctrl.Manager) error { psObj := &unstructured.Unstructured{} psObj.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "security.ontai.dev", + Group: "guardian.ontai.dev", Version: "v1alpha1", Kind: "PermissionSnapshot", }) rpObj := &unstructured.Unstructured{} rpObj.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "security.ontai.dev", + Group: "guardian.ontai.dev", Version: "v1alpha1", Kind: "RBACProfile", }) diff --git a/test/unit/controller/helpers_test.go b/test/unit/controller/helpers_test.go index c75b5cc..ed3cfc2 100644 --- a/test/unit/controller/helpers_test.go +++ b/test/unit/controller/helpers_test.go @@ -164,7 +164,7 @@ func newTalosCluster(clusterRef string, conductorReady bool) *unstructured.Unstr func newPermissionSnapshot(name, namespace string, current bool) *unstructured.Unstructured { ps := &unstructured.Unstructured{} ps.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "security.ontai.dev", + Group: "guardian.ontai.dev", Version: "v1alpha1", Kind: "PermissionSnapshot", }) @@ -189,7 +189,7 @@ func newPermissionSnapshot(name, namespace string, current bool) *unstructured.U func newRBACProfile(name, namespace string, provisioned bool) *unstructured.Unstructured { rp := &unstructured.Unstructured{} rp.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "security.ontai.dev", + Group: "guardian.ontai.dev", Version: "v1alpha1", Kind: "RBACProfile", }) diff --git a/test/unit/packexecution_reconciler_test.go b/test/unit/packexecution_reconciler_test.go index 772a36d..079965a 100644 --- a/test/unit/packexecution_reconciler_test.go +++ b/test/unit/packexecution_reconciler_test.go @@ -82,7 +82,7 @@ func newPackExecution(name, namespace, packName, packVersion, clusterRef, profil func newPermissionSnapshot(name, namespace string, current bool) *unstructured.Unstructured { ps := &unstructured.Unstructured{} ps.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "security.ontai.dev", + Group: "guardian.ontai.dev", Version: "v1alpha1", Kind: "PermissionSnapshot", }) @@ -105,7 +105,7 @@ func newPermissionSnapshot(name, namespace string, current bool) *unstructured.U func newRBACProfile(name, namespace string, provisioned bool) *unstructured.Unstructured { rp := &unstructured.Unstructured{} rp.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "security.ontai.dev", + Group: "guardian.ontai.dev", Version: "v1alpha1", Kind: "RBACProfile", }) From d5027f5443da36d895ef4cc0a9f95caba22bea46 Mon Sep 17 00:00:00 2001 From: ontave Date: Wed, 13 May 2026 08:09:44 +0200 Subject: [PATCH 06/19] migration(phase-7.3): update dispatcher test comments from wrapper naming - Package comments, suite name, test descriptions: wrapper -> dispatcher - Gate condition comments: WrapperRunnerRBAC -> DispatcherRunnerRBAC - Type comments: InfrastructurePackReceipt -> PackReceipt - It() string: "PackExecution is created by wrapper" -> "by dispatcher" wrapper-schema.md / seam-core-schema.md doc refs left unchanged (Phase 8). All 4 dispatcher test packages pass. --- test/e2e/clusterpack_e2e_test.go | 2 +- test/e2e/suite_test.go | 4 ++-- test/integration/suite_test.go | 8 ++++---- test/unit/controller/helpers_test.go | 4 ++-- test/unit/dispatcher_types_test.go | 2 +- test/unit/helm_metadata_types_test.go | 2 +- test/unit/packexecution_reconciler_test.go | 2 +- test/unit/packinstance_reconciler_test.go | 4 ++-- test/unit/role_independence_test.go | 10 +++++----- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/test/e2e/clusterpack_e2e_test.go b/test/e2e/clusterpack_e2e_test.go index 5f19313..97e9648 100644 --- a/test/e2e/clusterpack_e2e_test.go +++ b/test/e2e/clusterpack_e2e_test.go @@ -70,7 +70,7 @@ var _ = Describe("Step 3: ClusterPack deployment end-to-end", func() { validateClusterPackHelmMetadata(context.Background(), mgmtClient, certManagerPackName) }) - It("PackExecution is created by wrapper in seam-tenant-{cluster}", func() { + It("PackExecution is created by dispatcher in seam-tenant-{cluster}", func() { validatePackExecutionCreated(context.Background(), mgmtClient, mgmtClusterName, certManagerPackName, deployTimeout, pollInterval) }) diff --git a/test/e2e/suite_test.go b/test/e2e/suite_test.go index 4128bca..fc3bc65 100644 --- a/test/e2e/suite_test.go +++ b/test/e2e/suite_test.go @@ -1,4 +1,4 @@ -// Package e2e contains the wrapper end-to-end test suite. +// Package e2e contains the dispatcher end-to-end test suite. // // These tests verify the PackExecution full gate sequence and PackInstance drift // detection behaviour on a live cluster. @@ -39,7 +39,7 @@ var ( func TestE2E(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "wrapper E2E Suite") + RunSpecs(t, "dispatcher E2E Suite") } var _ = BeforeSuite(func() { diff --git a/test/integration/suite_test.go b/test/integration/suite_test.go index b664e0a..ad8659b 100644 --- a/test/integration/suite_test.go +++ b/test/integration/suite_test.go @@ -1,4 +1,4 @@ -// Package integration_test contains envtest integration tests for the wrapper +// Package integration_test contains envtest integration tests for the dispatcher // operator reconcilers. // // These tests use envtest to spin up a real API server and etcd, verifying @@ -77,7 +77,7 @@ func TestMain(m *testing.M) { panic("create seam-system namespace: " + err.Error()) } - // Start the controller manager with all wrapper reconcilers. + // Start the controller manager with all dispatcher reconcilers. // Metrics server disabled to avoid port conflicts in parallel test runs. mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme, @@ -102,8 +102,8 @@ func TestMain(m *testing.M) { } // Note: PackExecution reconciler is intentionally not registered here. // PackExecution requires ConductorReady from a platform TalosCluster CR - // via unstructured read, which is not available in wrapper envtest. Gate - // behavior is covered by wrapper unit tests that use fake clients. + // via unstructured read, which is not available in dispatcher envtest. Gate + // behavior is covered by dispatcher unit tests that use fake clients. goCtx, cancel := context.WithCancel(context.Background()) go func() { diff --git a/test/unit/controller/helpers_test.go b/test/unit/controller/helpers_test.go index ed3cfc2..80c9907 100644 --- a/test/unit/controller/helpers_test.go +++ b/test/unit/controller/helpers_test.go @@ -1,4 +1,4 @@ -// Package controller_test provides shared test helpers for wrapper controller unit tests. +// Package controller_test provides shared test helpers for dispatcher controller unit tests. // // Workstream 1 (PackInstance lifecycle) and Workstream 2 (Kueue Job scheduling) // share helpers defined here. All tests use the controller-runtime fake client. @@ -23,7 +23,7 @@ import ( ) // rbacAllowedStub is a RBACReadyChecker stub that always grants all permissions. -// Use this in tests that need gate 5 (WrapperRunnerRBAC) to pass so they can reach +// Use this in tests that need gate 5 (DispatcherRunnerRBAC) to pass so they can reach // job-submission or post-job logic. The real gate requires a live API server SAR. func rbacAllowedStub(_ context.Context, _ *dispatcherv1alpha1.PackExecution) (bool, string, error) { return true, "", nil diff --git a/test/unit/dispatcher_types_test.go b/test/unit/dispatcher_types_test.go index 3df18cb..84c3f9c 100644 --- a/test/unit/dispatcher_types_test.go +++ b/test/unit/dispatcher_types_test.go @@ -1,4 +1,4 @@ -// Package unit contains serialization integrity tests for dispatcher (wrapper) +// Package unit contains serialization integrity tests for dispatcher // CRD types: PackDelivery, PackExecution, PackInstalled, PackReceipt, PackLog. package unit_test diff --git a/test/unit/helm_metadata_types_test.go b/test/unit/helm_metadata_types_test.go index 6b1550b..3111912 100644 --- a/test/unit/helm_metadata_types_test.go +++ b/test/unit/helm_metadata_types_test.go @@ -1,4 +1,4 @@ -// Package unit_test -- T-07 unit tests for helm metadata fields on wrapper types. +// Package unit_test -- T-07 unit tests for helm metadata fields on dispatcher types. // // Tests cover: // - ClusterPackSpec, PackExecutionSpec, PackInstanceSpec all have ChartVersion, diff --git a/test/unit/packexecution_reconciler_test.go b/test/unit/packexecution_reconciler_test.go index 079965a..68a9d5b 100644 --- a/test/unit/packexecution_reconciler_test.go +++ b/test/unit/packexecution_reconciler_test.go @@ -179,7 +179,7 @@ func reconcilePE(t *testing.T, r *controller.PackExecutionReconciler, pe *dispat } // rbacAllowedStub is a RBACReadyChecker stub that always grants all permissions. -// Required for tests that need gate 5 (WrapperRunnerRBAC) to pass so they can reach +// Required for tests that need gate 5 (DispatcherRunnerRBAC) to pass so they can reach // job-submission or post-job logic. The real gate requires a live API server SAR. func rbacAllowedStub(_ context.Context, _ *dispatcherv1alpha1.PackExecution) (bool, string, error) { return true, "", nil diff --git a/test/unit/packinstance_reconciler_test.go b/test/unit/packinstance_reconciler_test.go index 3d9f433..616321c 100644 --- a/test/unit/packinstance_reconciler_test.go +++ b/test/unit/packinstance_reconciler_test.go @@ -27,11 +27,11 @@ func newPackInstanceScheme(t *testing.T) *runtime.Scheme { t.Fatalf("AddToScheme clientgo: %v", err) } // Register only the seam-core types that the PackInstanceReconciler uses as typed - // objects. InfrastructurePackReceipt is intentionally excluded: the reconciler + // objects. PackReceipt is intentionally excluded: the reconciler // accesses it only via unstructured (getPackReceipt). Registering it as a typed // schema would cause the fake client to convert the unstructured receipt and strip // dynamic status fields (signatureVerified, driftStatus) that conductor writes but - // are not yet declared in InfrastructurePackReceiptStatus. + // are not yet declared in PackReceiptStatus. s.AddKnownTypes(dispatcherv1alpha1.GroupVersion, &dispatcherv1alpha1.PackInstalled{}, &dispatcherv1alpha1.PackInstalledList{}, diff --git a/test/unit/role_independence_test.go b/test/unit/role_independence_test.go index f73f32d..f0b359d 100644 --- a/test/unit/role_independence_test.go +++ b/test/unit/role_independence_test.go @@ -1,10 +1,10 @@ -// Package unit_test -- T-VAL-01 Group 3: wrapper role-independence tests. +// Package unit_test -- T-VAL-01 Group 3: dispatcher role-independence tests. // -// These tests verify that the wrapper reconcilers (ClusterPackReconciler, +// These tests verify that the dispatcher reconcilers (ClusterPackReconciler, // PackExecutionReconciler) never read a conductor role field and treat the // management cluster as just another target cluster. // -// The management cluster is identified by TargetClusterRef="ccs-mgmt". The wrapper +// The management cluster is identified by TargetClusterRef="ccs-mgmt". The dispatcher // has no concept of "management vs tenant" -- it uses seam-tenant-{clusterRef} // unconditionally for all cluster namespaces. // @@ -136,7 +136,7 @@ func TestRoleIndependence_PackExecution_ManagementCluster_JobNamespace(t *testin // TestRoleIndependence_PackExecution_ManagementCluster_PackInstance verifies that // PackInstance is created in seam-tenant-ccs-mgmt, not seam-system or ont-system. -// The wrapper always uses seam-tenant-{clusterRef} regardless of cluster role. +// The dispatcher always uses seam-tenant-{clusterRef} regardless of cluster role. // // Two-step flow: first reconcile submits Job; second reconcile reads the // PackOperationResult and creates the PackInstance. @@ -232,7 +232,7 @@ func TestRoleIndependence_PackExecution_ManagementCluster_PackInstance(t *testin t.Fatalf("list PackInstances in %s: %v", forbiddenNS, err) } if len(wrongList.Items) != 0 { - t.Errorf("unexpected PackInstance in %s: wrapper must use seam-tenant-{clusterRef}", forbiddenNS) + t.Errorf("unexpected PackInstance in %s: dispatcher must use seam-tenant-{clusterRef}", forbiddenNS) } } } From ef1bf5ec662ccd02f736622ba5ddaf3adcd42c87 Mon Sep 17 00:00:00 2001 From: ontave Date: Wed, 13 May 2026 08:47:23 +0200 Subject: [PATCH 07/19] docs: session/25m -- Phase 8.3 dispatcher documentation rewrite Fresh documentation from current codebase. wrapper renamed to dispatcher throughout. seam.ontai.dev replaces infra.ontai.dev. Type names updated: PackDelivery, PackExecution, PackInstalled, PackReceipt, PackLog. New dispatcher-schema.md replaces stale wrapper-schema.md (redirect left in place). --- CLAUDE.md | 14 +- README.md | 115 +++++----- docs/dispatcher-schema.md | 428 ++++++++++++++++++++++++++++++++++++++ docs/wrapper-schema.md | 422 +------------------------------------ 4 files changed, 491 insertions(+), 488 deletions(-) create mode 100644 docs/dispatcher-schema.md diff --git a/CLAUDE.md b/CLAUDE.md index 2aa91b8..ced5b0c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,18 +1,18 @@ -## wrapper: Operational Constraints +## dispatcher: Operational Constraints > Read ~/ontai/CLAUDE.md first. The constraints below extend the root constitutional document. ### Schema authority -Primary: docs/wrapper-schema.md +Primary: docs/dispatcher-schema.md Supporting: ~/ontai/conductor/docs/conductor-schema.md (RunnerConfig contract) Supporting: ~/ontai/guardian/docs/guardian-schema.md (execution gatekeeper conditions) ### Invariants -CI-INV-001 -- Runtime delivers only pre-rendered Kubernetes manifests. No Helm or Kustomize at runtime. (root INV-014) -CI-INV-002 -- ClusterPack, once registered, is never modified. Changes require a new PackBuild. Immutability is absolute. -CI-INV-003 -- PackExecution is not submitted until all three execution gatekeeper conditions pass: PermissionSnapshot current, RBACProfile provisioned, ClusterPack not revoked. +CI-INV-001 -- Runtime delivers only pre-rendered Kubernetes manifests. No Helm or Kustomize at runtime. +CI-INV-002 -- PackDelivery, once registered, is never modified. Changes require a new pack build. Immutability is absolute. +CI-INV-003 -- PackExecution is not submitted until all gatekeeper conditions pass: ConductorReady, Signed, not Revoked, PermissionSnapshot current, RBACProfile provisioned, DispatcherRunnerRBAC granted. CI-INV-004 -- Leader election required. CI-INV-005 -- The agent bootstrap exception is the only context where pack application bypasses PackExecution. It is documented and finite. ### Session protocol additions -Step 4a -- Read wrapper-design.md in this repository. -Step 4b -- Verify the pack-compile or pack-deploy capability is declared in RunnerConfig status before implementing any Job submission. +Step 4a -- Read dispatcher-design.md in this repository. +Step 4b -- Verify the pack-deploy capability is declared in RunnerConfig status before implementing any Job submission. diff --git a/README.md b/README.md index 7b33807..3fc8bdc 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,94 @@ -# wrapper +# dispatcher -**Seam Pack Delivery operator** -**API Group:** `infra.ontai.dev` -**Image:** `registry.ontai.dev/ontai-dev/wrapper:` +dispatcher is the pack delivery operator for the ONT platform. It owns the full pack lifecycle: registration, gatekeeper enforcement, Kueue Job submission, and delivery state tracking. ---- +**API group:** seam.ontai.dev +**Status:** Alpha +**Module:** github.com/ontai-dev/dispatcher -## What this repository is +--- -`wrapper` is the pack compile and delivery operator in the Seam platform. It owns -the full lifecycle of Seam packs from compilation intent through cluster delivery -and drift detection. +## CRD Types ---- +All five types are registered under `seam.ontai.dev/v1alpha1`. -## CRDs +| Kind | Short name | Scope | Purpose | +|---|---|---|---| +| PackDelivery | pd | Namespaced | Pack registration record. Immutable after creation. | +| PackExecution | pe | Namespaced | Runtime delivery request for one pack to one cluster. | +| PackInstalled | pi | Namespaced | Delivered state of a pack on a target cluster. | +| PackReceipt | pr | Namespaced | Delivery acknowledgement written by conductor after signature verification. | +| PackLog | pl | Namespaced | Immutable result record written by the Conductor execute-mode Job. | -| Kind | API Group | Role | -|---|---|---| -| `ClusterPack` | `infra.ontai.dev` | Declared pack composition for a target cluster | -| `PackExecution` | `infra.ontai.dev` | Compile-and-deliver intent record for a single pack | -| `PackInstance` | `infra.ontai.dev` | Registered and signed pack artifact record | +Full field reference: [docs/dispatcher-schema.md](docs/dispatcher-schema.md) --- ## Architecture -Wrapper is a thin reconciler. Its reconcile loop is: watch `ClusterPack`, read -`RunnerConfig`, confirm the named `pack-compile` capability exists in the conductor -manifest, build a Kueue Job spec, submit to the tenant `ClusterQueue`, read -`OperationResult`, and update `PackExecution` status. +dispatcher is a thin reconciler. It does not contain execution logic. + +**PackDeliveryReconciler** (`internal/controller/clusterpack_reconciler.go`) + +Watches PackDelivery CRs. On each reconcile it enforces spec immutability via a checksum snapshot annotation, waits for the conductor signing loop to set `status.signed=true`, then creates one PackExecution per target cluster in the corresponding `seam-tenant-{cluster}` namespace. On deletion it removes derived PackInstalled and PackExecution objects and clears DriftSignals before releasing the finalizer. -**Pack compilation** runs as a Kueue Job. The Kueue `ClusterQueue` for each tenant -is provisioned by guardian from `QueueProfile`. Wrapper does not provision queues. +**PackExecutionReconciler** (`internal/controller/packexecution_reconciler.go`) -**PackInstance signing** is performed exclusively by the management cluster Conductor -in agent mode after wrapper confirms `ClusterPack` registration. Wrapper does not sign. +Watches PackExecution CRs. Runs a six-gate check before submitting any Kueue Job: -**Cilium** is delivered as a Seam pack. The `CiliumPending` condition on `TalosCluster` -(owned by platform) is the expected state between CAPI cluster Running and Cilium -`PackInstance` reaching Ready. This window is not an error state. +- Gate 0: ConductorReady -- RunnerConfig in `ont-system` has at least one published capability. +- Gate 1: Signature -- PackDelivery `status.signed=true`. +- Gate 2: Revocation -- PackDelivery is not revoked. +- Gate 3: PermissionSnapshot -- Guardian PermissionSnapshot for the target cluster is Fresh. +- Gate 4: RBACProfile -- RBACProfile referenced by the PackExecution has `provisioned=true`. +- Gate 5: DispatcherRunnerRBAC -- SubjectAccessReview confirms the wrapper-runner ServiceAccount has the required permissions. + +When all gates pass, submits a `pack-deploy` Kueue Job in the tenant namespace. After Job completion reads the PackLog written by the Conductor execute-mode Job, then creates or updates the PackInstalled record. + +**PackInstanceReconciler** (`internal/controller/packinstance_reconciler.go`) + +Watches PackInstalled CRs. Polls PackReceipt drift status from the conductor agent mirror in the tenant namespace. Raises SecurityViolation when signature verification fails. Enforces DependencyPolicy (Block, Warn, Ignore) for declared pack dependencies. Manages the `workload-cleanup` finalizer for resource deletion on the delete path (no Jobs on the delete path; INV-006). --- -## Building +## Build -```sh +``` go build ./cmd/wrapper ``` -The binary is built into a distroless container image: +Run unit tests: -```sh -docker build -t registry.ontai.dev/ontai-dev/wrapper: . +``` +make test ``` ---- - -## Testing +Run e2e tests (requires `MGMT_KUBECONFIG`): -```sh -go test ./test/unit/... +``` +make e2e ``` ---- - -## Schema and design reference +Generate CRD manifests: -- `docs/wrapper-schema.md` - API contract, field definitions, status conditions -- `wrapper-design.md` - Implementation architecture and reconciler design +``` +make manifests generate +``` --- -## Status +## Schema Reference -Alpha. Deployed and tested on management cluster (ccs-mgmt). -Tenant cluster onboarding is not yet verified end to end. -See [docs/wrapper-schema.md](./docs/wrapper-schema.md) -for current capability and known gaps. +- [docs/dispatcher-schema.md](docs/dispatcher-schema.md) -- complete field reference for all five CRD types +- [dispatcher-design.md](dispatcher-design.md) -- reconciler design, gatekeeper sequence, Job spec, signing loop -CRDs are deployed and reconciling on the live management cluster. -The schema specification is published at: -https://schema.ontai.dev/v1alpha1/ - -## Contributing +--- -Read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening a pull -request. Every new reconciliation behavior requires a written -specification and senior engineer sign-off before any code is -written. +## Issues -File issues at https://github.com/ontai-dev/wrapper/issues. -For security issues contact security@ontai.dev directly. +https://github.com/ontai-dev/dispatcher/issues --- -*wrapper - Seam Pack Delivery Operator* -*Apache License, Version 2.0* +dispatcher -- Seam Pack Delivery Operator +Apache License, Version 2.0 diff --git a/docs/dispatcher-schema.md b/docs/dispatcher-schema.md new file mode 100644 index 0000000..cc67dc5 --- /dev/null +++ b/docs/dispatcher-schema.md @@ -0,0 +1,428 @@ +# dispatcher Schema Reference + +> Primary schema authority for the dispatcher operator. +> API group: seam.ontai.dev +> Version: v1alpha1 +> Module: github.com/ontai-dev/dispatcher + +--- + +## 1. Domain Boundary + +dispatcher owns the pack delivery lifecycle on the ONT platform management cluster. Its scope is: + +- Registering compiled pack artifacts as PackDelivery CRs (immutable after creation). +- Enforcing gatekeeper conditions before any pack-deploy Job is submitted. +- Tracking delivery state per pack per cluster via PackInstalled. +- Reading PackLog result records written by Conductor execute-mode Jobs. +- Reading PackReceipt acknowledgements written by Conductor agent mode on tenant clusters. + +dispatcher does not compile packs. Pack compilation runs in CI or on the developer workstation using the Conductor compile mode. dispatcher never runs compile mode. It receives only the output: a PackDelivery CR applied via GitOps plus an OCI artifact in the registry. + +dispatcher does not sign packs. The conductor signing loop on the management cluster owns signing. dispatcher reads `status.signed` on PackDelivery to determine when a pack is ready for delivery. + +dispatcher does not provision RBAC. Guardian owns all RBAC. dispatcher waits for Guardian to provision an RBACProfile before submitting any Job. + +Supporting schema references: +- ~/ontai/conductor/docs/conductor-schema.md -- RunnerConfig contract and capability declarations +- ~/ontai/guardian/docs/guardian-schema.md -- PermissionSnapshot and RBACProfile conditions + +--- + +## 2. Master GVK Table + +| Kind | Group | Version | Scope | Short name | Namespace convention | +|---|---|---|---|---|---| +| PackDelivery | seam.ontai.dev | v1alpha1 | Namespaced | pd | seam-system or seam-tenant-{cluster} | +| PackExecution | seam.ontai.dev | v1alpha1 | Namespaced | pe | seam-tenant-{cluster} | +| PackInstalled | seam.ontai.dev | v1alpha1 | Namespaced | pi | seam-tenant-{cluster} | +| PackReceipt | seam.ontai.dev | v1alpha1 | Namespaced | pr | seam-tenant-{cluster} | +| PackLog | seam.ontai.dev | v1alpha1 | Namespaced | pl | seam-tenant-{cluster} | + +--- + +## 3. PackDelivery + +PackDelivery is the root declaration of a compiled pack artifact. It is immutable after creation. Any change to a pack requires a new PackDelivery CR with a new version. CI-INV-002. + +Columns printed by `kubectl get pd`: Version, Signed, Age. + +### 3.1 spec fields + +| Field | Type | Required | Immutable | Description | +|---|---|---|---|---| +| version | string | yes | yes | Semantic version of this pack (e.g. v1.2.3-r1). | +| registryRef.url | string | yes | yes | OCI registry URL including image name. | +| registryRef.digest | string | yes | yes | OCI image digest (sha256:...). | +| checksum | string | no | yes | Content-addressed checksum of the full artifact manifest set. | +| rbacDigest | string | no | yes | OCI digest of the RBAC layer. Contains ServiceAccount, Role, ClusterRole, RoleBinding, ClusterRoleBinding manifests. | +| workloadDigest | string | no | yes | OCI digest of the workload layer. Applied after Guardian RBACProfile reaches provisioned=true. | +| clusterScopedDigest | string | no | yes | OCI digest of the cluster-scoped non-RBAC layer. Applied after Guardian RBAC intake and before workload manifests. | +| sourceBuildRef | string | no | yes | Opaque reference to the build that produced this pack. Informational. | +| executionOrder | []PackDeliveryStage | no | yes | Ordered stages in which pack manifests are applied. Each stage has a name (rbac, storage, stateful, stateless) and a list of manifest names. | +| provenance.buildID | string | no | yes | CI/CD build identifier. | +| provenance.buildTimestamp | metav1.Time | no | yes | Time the pack artifact was produced. | +| provenance.sourceRef | string | no | yes | Git reference (commit SHA or tag) from which the pack was built. | +| basePackName | string | no | yes | Logical pack name shared across versions (e.g. nginx-ingress). Used to name PackInstalled for in-place version supersession. | +| targetClusters | []string | no | yes | Cluster names to which this PackDelivery is delivered. PackDeliveryReconciler creates one PackExecution per entry. | +| chartVersion | string | no | yes | Helm chart version used to compile this pack. Informational. | +| chartURL | string | no | yes | Helm chart repository URL. Informational. | +| chartName | string | no | yes | Helm chart name. Informational. | +| helmVersion | string | no | yes | Helm SDK version used to render this pack. Informational. | +| valuesFile | string | no | yes | Path to the values file used during pack compilation. Informational. | +| lifecyclePolicies.retainOnDeletion | bool | no | yes | Whether the OCI artifact is retained when the PackDelivery CR is deleted. Default: true. | +| lineage | SealedCausalChain | no | yes | Sealed causal chain record for this root declaration. Written once at creation and immutable. | +| rollbackToRevision | int64 | no | no | Governor-only. When set to N, instructs the reconciler to restore this PackDelivery to the artifact version recorded in PackLog revision N. Cleared after rollback is applied. | + +### 3.2 status fields + +| Field | Type | Description | +|---|---|---| +| signed | bool | True when the conductor signing loop has signed this pack. PackExecution gate 1 reads this field. | +| packSignature | string | Base64-encoded Ed25519 signature produced by the management cluster conductor. | +| observedGeneration | int64 | Generation most recently reconciled. | +| conditions | []metav1.Condition | Standard condition list. See section 3.3. | + +### 3.3 PackDelivery conditions + +| Type | Meaning | +|---|---| +| SignaturePending | True while waiting for the conductor signing loop to set the pack-signature annotation. | +| Available | True when pack is signed and ready for delivery. | +| Revoked | True when the pack has been revoked. All PackExecution CRs blocked. No requeue. | +| ClusterPackImmutabilityViolation | True when a spec mutation is detected against the snapshot annotation. Security event. | +| LineageSynced | False until LineageController is deployed. Informational. | + +### 3.4 Immutability enforcement + +On first reconcile, PackDeliveryReconciler records a checksum snapshot annotation (`infrastructure.ontai.dev/spec-checksum-snapshot`) derived from `spec.checksum`, `spec.registryRef.url`, `spec.registryRef.digest`, and `spec.version`. On every subsequent reconcile the current spec values are compared against the snapshot. Any divergence sets the ClusterPackImmutabilityViolation condition and stops reconciliation. This check is bypassed only during a Governor-authorized rollback (`spec.rollbackToRevision > 0`). + +--- + +## 4. PackExecution + +PackExecution is the runtime delivery request for one PackDelivery to one cluster. It is created by PackDeliveryReconciler, not by humans. One PackExecution per (PackDelivery, targetCluster) pair in namespace `seam-tenant-{cluster}`. + +Columns printed by `kubectl get pe`: Pack, Target, Age. + +### 4.1 spec fields + +| Field | Type | Required | Description | +|---|---|---|---| +| packDeliveryRef.name | string | yes | Name of the PackDelivery CR. | +| packDeliveryRef.version | string | yes | Expected version of the PackDelivery. Guards against name reuse. | +| targetClusterRef | string | yes | Name of the target cluster. | +| admissionProfileRef | string | no | Name of the RBACProfile governing this execution. Read by gate 4. | +| chartVersion | string | no | Carried from PackDelivery. Informational. | +| chartURL | string | no | Carried from PackDelivery. Informational. | +| chartName | string | no | Carried from PackDelivery. Informational. | +| helmVersion | string | no | Carried from PackDelivery. Informational. | +| lineage | SealedCausalChain | no | Sealed causal chain record. Immutable after creation. | + +### 4.2 status fields + +| Field | Type | Description | +|---|---|---| +| observedGeneration | int64 | Generation most recently reconciled. | +| jobName | string | Name of the pack-deploy Kueue Job submitted for this execution. | +| operationResultRef | string | Name of the PackLog CR written after successful pack-deploy Job completion. | +| conditions | []metav1.Condition | Standard condition list. See section 4.3. | + +### 4.3 PackExecution conditions + +| Type | Meaning | +|---|---| +| Waiting | True when ConductorReady gate (gate 0) has not cleared. | +| PackSignaturePending | True when PackDelivery.status.signed is false (gate 1). | +| PackRevoked | True when PackDelivery is revoked (gate 2). Terminal. No requeue. | +| PermissionSnapshotOutOfSync | True when Guardian PermissionSnapshot is not Fresh (gate 3). | +| RBACProfileNotProvisioned | True when RBACProfile.status.provisioned is false (gate 4). | +| DispatcherRunnerRBACNotReady | True when SubjectAccessReview fails for wrapper-runner SA (gate 5). | +| Pending | True while any gate is still clearing. | +| Running | True while the pack-deploy Kueue Job is active. | +| Succeeded | True when the pack-deploy Job completed and PackLog reports success. Terminal. | +| Failed | True when the pack-deploy Job failed at the Job level. | +| LineageSynced | False until LineageController is deployed. | + +--- + +## 5. PackInstalled + +PackInstalled records the delivered state of a pack on a specific cluster. Created by PackExecutionReconciler after a successful pack-deploy Job and confirmed PackLog. One PackInstalled per (basePackName, targetCluster) in namespace `seam-tenant-{cluster}`. When a new version supersedes an existing PackInstalled the spec is patched in place. + +Columns printed by `kubectl get pi`: Pack, Target, Age. + +### 5.1 spec fields + +| Field | Type | Required | Description | +|---|---|---|---| +| packDeliveryRef | string | yes | Name of the PackDelivery CR this instance tracks. | +| version | string | yes | Pack version delivered to the target cluster. | +| targetClusterRef | string | yes | Name of the target cluster. | +| dependsOn | []string | no | Base names of pack packs that must be delivered before this instance can be considered healthy. | +| dependencyPolicy.onDrift | DriftPolicy | no | How to respond when a declared dependency reports Drifted=True. Values: Block, Warn, Ignore. Default: Warn. | +| chartVersion | string | no | Carried from PackDelivery. Informational. | +| chartURL | string | no | Carried from PackDelivery. Informational. | +| chartName | string | no | Carried from PackDelivery. Informational. | +| helmVersion | string | no | Carried from PackDelivery. Informational. | + +### 5.2 status fields + +| Field | Type | Description | +|---|---|---| +| observedGeneration | int64 | Generation most recently reconciled. | +| deliveredAt | metav1.Time | When the pack was most recently confirmed delivered. | +| driftSummary | string | Human-readable summary of current drift state from PackReceipt. | +| upgradeDirection | string | Version transition direction for the last deployment. Values: Initial, Upgrade, Rollback, Redeploy. | +| deployedResources | []DeployedResourceRef | Kubernetes resources applied by the pack-deploy Job. Used by the workload-cleanup finalizer on deletion. | +| conditions | []metav1.Condition | Standard condition list. See section 5.3. | + +### 5.3 PackInstalled conditions + +| Type | Meaning | +|---|---| +| Ready | True when pack is delivered and in sync. False when drift or security violation is detected. | +| Drifted | True when conductor drift detection reports the installed state has diverged from the PackReceipt. | +| DependencyBlocked | True when a dependency PackInstalled is Drifted and DependencyPolicy is Block. | +| SecurityViolation | True when PackReceipt.status.verified is false. Blocks all further pack ops on this cluster. | +| Progressing | True while waiting for PackReceipt to be written or delivery to complete. | +| LineageSynced | False until LineageController is deployed. | + +### 5.4 DeployedResourceRef fields + +| Field | Type | Description | +|---|---|---| +| apiVersion | string | Kubernetes apiVersion (e.g. apps/v1). | +| kind | string | Kubernetes Kind (e.g. Deployment). | +| namespace | string | Resource namespace. Empty for cluster-scoped resources. | +| name | string | Resource name. | + +--- + +## 6. PackReceipt + +PackReceipt is written by the conductor agent on the tenant cluster after it verifies the Ed25519 signature on the delivered pack artifact. The conductor agent mirrors it into the `seam-tenant-{cluster}` namespace on the management cluster. PackInstanceReconciler reads it for drift and signature verification status. + +Columns printed by `kubectl get pr`: Pack, Verified, Age. + +### 6.1 spec fields + +| Field | Type | Required | Description | +|---|---|---|---| +| packDeliveryRef | string | yes | Name of the PackDelivery CR this receipt acknowledges. | +| targetClusterRef | string | yes | Name of the cluster this receipt was generated on. | +| packInstalledRef | string | no | Name of the PackInstalled CR this receipt acknowledges. | +| signatureRef | string | no | Name of the signed artifact Secret on the management cluster from which this receipt was derived. Convention: seam-pack-signed-{cluster}-{packInstalled}. | +| rbacDigest | string | no | OCI digest of the RBAC layer. Carried from PackDelivery for audit. | +| workloadDigest | string | no | OCI digest of the workload layer. Carried from PackDelivery. | +| chartVersion | string | no | Carried from PackDelivery. Informational. | +| chartURL | string | no | Carried from PackDelivery. Informational. | +| chartName | string | no | Carried from PackDelivery. Informational. | +| helmVersion | string | no | Carried from PackDelivery. Informational. | +| deployedResources | []PackReceiptDeployedResource | no | Inventory of Kubernetes resources applied to the tenant cluster. Conductor uses this for drift detection. | + +### 6.2 status fields + +| Field | Type | Description | +|---|---|---| +| verified | bool | True when the Ed25519 signature was successfully verified against the management cluster public key. | +| signature | string | Base64-encoded Ed25519 signature. Stored for auditability. | +| verificationFailedReason | string | Set when verified=false. Describes the specific verification failure. | +| observedGeneration | int64 | Generation most recently reconciled. | +| conditions | []metav1.Condition | Standard condition list. | + +### 6.3 PackReceiptDeployedResource fields + +| Field | Type | Description | +|---|---|---| +| apiVersion | string | Full API version string (e.g. apps/v1). | +| kind | string | Resource kind (e.g. Deployment). | +| namespace | string | Resource namespace. Empty for cluster-scoped resources. | +| name | string | Resource name. | + +--- + +## 7. PackLog + +PackLog is the immutable result record written by the Conductor execute-mode Job after a pack-deploy capability completes. One PackLog per PackExecution, created in `seam-tenant-{clusterName}`. PackLog is controller-authored exclusively; no human or pipeline writes a PackLog. + +Columns printed by `kubectl get pl`: Capability, Status, Cluster, Age. + +### 7.1 spec fields + +| Field | Type | Required | Description | +|---|---|---|---| +| revision | int64 | yes | Monotonically increasing revision counter for this pack operation sequence. | +| capability | string | yes | Name of the Conductor capability that produced this result. | +| status | PackLogResultStatus | yes | Terminal status. Values: Succeeded, Failed. | +| packExecutionRef | string | no | Name of the PackExecution CR that triggered this operation. | +| packDeliveryRef | string | no | Name of the PackDelivery CR that was deployed. | +| targetClusterRef | string | no | Name of the target cluster this operation ran against. | +| previousRevisionRef | string | no | Name of the PackLog CR superseded by this revision. Absent for revision 1. | +| phase | string | no | RunnerConfig phase this result belongs to. | +| startedAt | metav1.Time | no | Time the capability execution began. | +| completedAt | metav1.Time | no | Time the capability execution finished. | +| failureReason.category | string | no | Failure domain classification. Values: ValidationFailure, CapabilityUnavailable, ExecutionFailure, ExternalDependencyFailure, InvariantViolation, LicenseViolation, StorageUnavailable. | +| failureReason.reason | string | no | Human-readable description of the specific failure. | +| failureReason.failedStep | string | no | Name of the step that failed. Empty for single-step capabilities. | +| deployedResources | []PackLogDeployedResource | no | Kubernetes resources applied during this execution. Used by PackInstalledReconciler for deletion cleanup. | +| artifacts | []PackLogArtifact | no | Artifacts produced by this execution. | +| steps | []PackLogStepResult | no | Individual step results for multi-step capabilities. | +| packDeliveryVersion | string | no | PackDelivery spec.version deployed in this operation. Rollback anchor. | +| rbacDigest | string | no | OCI digest of the RBAC layer deployed in this operation. Rollback anchor. | +| workloadDigest | string | no | OCI digest of the workload layer deployed in this operation. Rollback anchor. | +| talosClusterOperationResultRef | string | no | Reserved stub for future cross-reference. Not populated by any current controller. | + +### 7.2 PackLogArtifact fields + +| Field | Type | Description | +|---|---|---| +| name | string | Logical identifier for this artifact. | +| kind | string | Artifact type. Values: ConfigMap, Secret, OCIImage, S3Object. | +| reference | string | Fully qualified reference for the artifact kind. | +| checksum | string | Content-addressed checksum. Format: sha256:. Optional. | + +### 7.3 PackLogStepResult fields + +| Field | Type | Description | +|---|---|---| +| name | string | Step identifier within the capability. | +| status | PackLogResultStatus | Terminal status of this step. Values: Succeeded, Failed. | +| startedAt | metav1.Time | Time this step began. Optional. | +| completedAt | metav1.Time | Time this step finished. Optional. | +| message | string | Additional context about the step outcome. Optional. | + +### 7.4 PackLogDeployedResource fields + +| Field | Type | Description | +|---|---|---| +| apiVersion | string | Kubernetes apiVersion (e.g. apps/v1). | +| kind | string | Kubernetes Kind (e.g. Deployment). | +| namespace | string | Resource namespace. Empty for cluster-scoped resources. | +| name | string | Resource name. | + +--- + +## 8. Compile/Runtime Boundary + +Pack compilation (Helm rendering, manifest generation) runs on the developer workstation or in CI using the Conductor compile mode. The management cluster never runs compile mode. The management cluster receives only: + +- A PackDelivery CR applied via GitOps with the compiled artifact's OCI URL, digest, and checksum. +- The corresponding OCI artifact in the OCI registry. + +The pack-deploy Kueue Job applies already-rendered Kubernetes manifests. No Helm or Kustomize executes at runtime. CI-INV-001. + +--- + +## 9. PackDelivery Signing Lifecycle + +After a PackDelivery CR is created and reaches the management cluster: + +1. PackDeliveryReconciler sets `status.signed=false` and the SignaturePending condition. +2. The conductor signing loop on the management cluster (in `ont-system`) detects the unsigned PackDelivery. +3. Conductor fetches the OCI artifact using `spec.registryRef.digest`. +4. Conductor verifies the artifact checksum against `spec.checksum`. On mismatch: raises a ChecksumMismatch security event and does not sign. +5. Conductor signs the artifact digest with the platform Ed25519 private key. +6. Conductor writes the base64-encoded signature as annotation `ontai.dev/pack-signature` on the PackDelivery CR. +7. PackDeliveryReconciler detects the annotation (AnnotationChangedPredicate), sets `status.signed=true`, copies the signature to `status.packSignature`, and transitions the Available condition to True. + +The platform signing key is mounted as a projected Secret into the conductor Deployment. It is not accessible to any other process. dispatcher reads only the signed status -- it does not own the key, the signing logic, or the signing loop. + +--- + +## 10. Gatekeeper Check Sequence + +PackExecutionReconciler enforces six gates in order before submitting any pack-deploy Job. CI-INV-003. + +**Gate 0 -- ConductorReady** + +Condition type: `Waiting` + +Checks that the TalosCluster for the target cluster is registered (in `seam-tenant-{clusterRef}` or `seam-system`) and that the RunnerConfig named `{targetClusterRef}` in `ont-system` has at least one published capability in `status.capabilities`. A non-empty capabilities list proves the Conductor agent has started, completed leader election, and published its capability manifest. + +Requeue interval: 30 seconds. + +**Gate 1 -- Signature** + +Condition type: `PackSignaturePending` + +Reads PackDelivery for the referenced name and version. If `status.signed=false` the gate does not pass. Also enforces version match: if the PackDelivery version does not match the PackExecution's `spec.packDeliveryRef.version`, the stale PackExecution is deleted and the PackDeliveryReconciler will recreate it. + +Requeue interval: 15 seconds. + +**Gate 2 -- Revocation** + +Condition type: `PackRevoked` + +Checks that the PackDelivery does not have a Revoked condition set to True. A revoked pack blocks the PackExecution permanently with no requeue. Human intervention is required. + +**Gate 3 -- PermissionSnapshot** + +Condition type: `PermissionSnapshotOutOfSync` + +Reads the Guardian PermissionSnapshot named `snapshot-{targetClusterRef}` in `seam-system` via unstructured (no cross-operator type import). The gate passes when the snapshot has a `Fresh=True` condition. + +Requeue interval: 30 seconds. + +**Gate 4 -- RBACProfile** + +Condition type: `RBACProfileNotProvisioned` + +Reads the Guardian RBACProfile named by `spec.admissionProfileRef` in `seam-tenant-{targetClusterRef}` via unstructured. The gate passes when `status.provisioned=true`, or when the RBACProfile does not yet exist (the Conductor Job will create it via rbac-intake). + +Requeue interval: 30 seconds. + +**Gate 5 -- DispatcherRunnerRBAC** + +Condition type: `DispatcherRunnerRBACNotReady` + +Performs SubjectAccessReview checks to verify the `wrapper-runner` ServiceAccount in the PackExecution namespace has the minimum required permissions before the Kueue Job is submitted. Catches stale or missing RBAC before the pod fails with a Forbidden error at runtime. + +Requeue interval: 30 seconds. + +--- + +## 11. Upgrade and Rollback + +PackLog is the rollback anchor. Each successful pack-deploy writes a PackLog with `spec.packDeliveryVersion`, `spec.rbacDigest`, and `spec.workloadDigest` recording the exact artifact state that was deployed. The revision field increments monotonically. + +To roll back a PackDelivery to a previous revision: + +1. Governor sets `spec.rollbackToRevision` to the target PackLog revision number. +2. PackDeliveryReconciler detects `spec.rollbackToRevision > 0` before the immutability gate. +3. Reconciler lists all PackLog CRs labeled `ontai.dev/cluster-pack={packDeliveryName}` and finds the one at the requested revision. +4. Reconciler patches `spec.version`, `spec.rbacDigest`, and `spec.workloadDigest` to the values recorded in that PackLog. +5. Reconciler removes the spec-snapshot annotation so the immutability check re-records the rolled-back state on the next reconcile. +6. Reconciler clears `spec.rollbackToRevision` to 0. +7. Normal reconciliation proceeds: PackDeliveryReconciler creates a new PackExecution for the rolled-back version. + +Only one rollback step per invocation. Any retained revision is reachable by specifying its exact revision number. + +--- + +## 12. Agent Bootstrap Exception + +CI-INV-005. The conductor agent bootstrap exception is the only context where pack application bypasses PackExecution. During initial cluster bootstrap, the conductor agent applies a minimal set of manifests directly to bring the cluster to the state where Guardian can provision RBAC and the normal PackExecution gate sequence can proceed. This path is documented in the conductor schema and is finite. No other context bypasses PackExecution. + +--- + +## 13. Drift Detection + +Conductor agent mode on each tenant cluster runs periodic server-side dry-run comparisons of deployed resources against the inventory recorded in PackReceipt. When drift is detected, the conductor updates `status.driftStatus` on the PackReceipt. + +PackInstanceReconciler reads the mirrored PackReceipt from `seam-tenant-{cluster}` on the management cluster. When `driftStatus=Drifted` it sets the Drifted condition on PackInstalled. It does not auto-remediate. Drift surfaces to the human for decision. + +When `status.verified=false` on a PackReceipt the PackInstanceReconciler raises SecurityViolation on the PackInstalled and blocks all further pack operations on the affected cluster. Human investigation is required to clear the violation. + +Drift detection on PackDelivery and PackExecution is the responsibility of the conductor role=management, as defined in the platform constitution Decision 14. dispatcher does not detect or remediate drift on those types. + +--- + +## 14. Cross-Domain Rules + +- dispatcher reads PermissionSnapshot and RBACProfile from `guardian.ontai.dev/v1alpha1` via unstructured. It does not import guardian Go types. +- dispatcher reads RunnerConfig and TalosCluster from `infrastructure.ontai.dev/v1alpha1` via unstructured. It does not import platform Go types. +- dispatcher does not create any RBAC resources. Guardian owns all RBAC. INV-004. +- dispatcher does not use CNPG. INV-016. +- The pack-deploy Kueue Job uses the `wrapper-runner` ServiceAccount. This ServiceAccount and its permissions are provisioned by Guardian, not by dispatcher. +- Namespaces for tenant objects follow the convention `seam-tenant-{clusterName}`. dispatcher does not create these namespaces; they are created by Platform. diff --git a/docs/wrapper-schema.md b/docs/wrapper-schema.md index 931b877..3980bf2 100644 --- a/docs/wrapper-schema.md +++ b/docs/wrapper-schema.md @@ -1,421 +1 @@ -# Wrapper-schema.md -> CRD types: infrastructure.ontai.dev/v1alpha1 (InfrastructureClusterPack, InfrastructurePackExecution, InfrastructurePackInstance, InfrastructurePackBuild) -- schema owned by seam-core (Decision G). Supersedes infra.ontai.dev (Phase 2B, 2026-04-25). -> Runtime behavior and reconciliation schema: this document -> Operator: Wrapper -> Absorb before any design or implementation work touching pack compile or delivery. -> Amended: 2026-03-30 - compile mode is a workstation/CI operation, never a cluster Job. -> ClusterPack signing lifecycle added. PackBuild is a local spec file, not a cluster trigger. - ---- - -## 1. Domain Boundary - -Wrapper owns the delivery of immutable ClusterPack artifacts to target clusters -and the tracking of their deployed state. It does not own compilation. Compilation -is a workstation or CI/CD pipeline operation performed by the human or an automated -pipeline using conductor in compile mode. - -Wrapper is a thin reconciler. Its pattern is identical to all ONT operators: -watch CR → read RunnerConfig → confirm named capability → build Job spec → submit -to Kueue → read OperationResult → update CR status. - -Runtime delivers only pre-rendered Kubernetes manifests contained in an OCI -artifact. The management cluster never runs Helm or Kustomize. No rendering -happens on any cluster. This is absolute. INV-014. - ---- - -## 2. The Compile / Runtime Boundary - -### 2.1 Compile Time - Workstation or CI/CD Pipeline - -Compilation is not a cluster operation. It is a human or pipeline operation using -conductor in compile mode as a Docker container. No management cluster connection -is required. - -**Invocation pattern:** -The human or CI/CD pipeline runs Compiler with the PackBuild spec -as input. The runner renders Helm charts via its embedded helm goclient, resolves -Kustomize overlays via its embedded kustomize goclient, normalizes all inputs to -flat Kubernetes manifests, validates schemas, computes execution order, pins image -digests, generates the content-addressed checksum, pushes the ClusterPack OCI -artifact to the registry, and emits a ClusterPack CR YAML file as output. - -The ClusterPack OCI artifact contains only raw, fully rendered Kubernetes manifests. -No templates. No variable references. No Helm or Kustomize dependencies at runtime. - -The emitted ClusterPack CR YAML is committed to git. GitOps apply delivers it to -the management cluster. This is the only way a ClusterPack CR appears on the -management cluster. - -**PackBuild is a local spec file and provenance record.** It is the human's -authoritative declaration of source inputs - Helm chart coordinates, values, -overlay paths. It may optionally be committed to git alongside the ClusterPack -output for audit trail. It is never applied to the management cluster as a Job -trigger. No PackBuild controller exists on the management cluster. - -### 2.2 Runtime - Management Cluster (conductor Jobs via Kueue) - -When a PackExecution CR is created, the Wrapper controller verifies execution -gatekeeper conditions and submits a pack-deploy Job. The Job runs conductor, which: -- Fetches the ClusterPack OCI artifact from the registry -- Verifies the checksum and the platform signature -- Applies manifests in declared execution order using server-side apply -- Monitors readiness per stage -- Writes OperationResult to ConfigMap -- Exits - -The pack-deploy Job uses conductor exclusively. conductor is the distroless -runtime binary. It needs only kube goclient to apply manifests. It never invokes -helm goclient or kustomize goclient. The execution seam between compile mode -and runtime that would require a sidecar or full debian image does not exist -because compile mode never runs on any cluster. - ---- - -## 3. ClusterPack Signing Lifecycle - -ClusterPacks require a platform signature before any PackExecution may proceed. -The signing follows the same pattern as PermissionSnapshot signing. - -**Signing flow:** - -Step 1 - GitOps applies ClusterPack CR to the management cluster. -Wrapper controller detects the new ClusterPack CR with Status.Signed=false -(absent signature annotation). - -Step 2 - Management cluster conductor signing loop detects the unsigned ClusterPack -CR. It fetches the OCI artifact, verifies the content checksum matches the CR spec, -signs the artifact digest with the platform private key, and writes the signature -annotation: ontai.dev/pack-signature={base64-encoded-signature}. -Status.Signed transitions to true. - -Step 3 - PackExecution gate includes a signature check. The pack-deploy Job will -not be submitted until ClusterPack Status.Signed=true. If the ClusterPack is not -yet signed, Wrapper sets PackSignaturePending on the PackExecution status and -requeues. - -Step 4 - On the target cluster, the pack-deploy Job (running conductor) fetches -the ClusterPack artifact and verifies the signature against the platform public key -embedded in the conductor binary before applying any manifest. An artifact that -fails signature verification causes immediate ExecutionFailure with SignatureInvalid -category. Nothing is applied. - -**Why this matters:** Unsigned or tampered ClusterPacks cannot be deployed to any -cluster. An artifact produced outside the official compile flow and injected into -the registry without a valid signature is rejected at the Job execution layer. - ---- - -## 4. CRDs - Management Cluster - -### InfrastructurePackBuild (Local Spec and Provenance Record - Not a Cluster CRD) - -PackBuild is not a Kubernetes CRD applied to the management cluster. It is the -human's local specification file used as input to conductor compile mode. It may -be committed to git alongside the ClusterPack CR output for provenance and audit -trail. The management cluster has no PackBuild controller. - -Key fields in the PackBuild spec file (consumed by conductor compile mode): - -source.helm.repository: Helm chart repository URL. -source.helm.chart: chart name. -source.helm.version: the admin's version record. The single field to change when - upgrading a component. -source.helm.values: inline structured data equivalent to the admin's values.yaml. - Canonical record of all non-sensitive configuration values. -source.helm.valuesSecretRef: optional path to a local file or environment variable - containing sensitive values - credentials, tokens, API keys. These are merged - at compile time. They never appear in the ClusterPack CR or OCI artifact metadata. -source.kustomize.path: Kustomize overlay directory path. -source.kustomize.secretRef: optional path to sensitive kustomize patch file. -source.raw: list of raw manifest file paths. -targetVersion: the version string to assign to the produced ClusterPack. Must be - unique. The runner fails compilation with VersionConflict if a ClusterPack with - this version already exists in the registry. -outputDir: filesystem path where the runner writes the ClusterPack CR YAML output - for git commit. - ---- - -### InfrastructureClusterPack - -Kind: InfrastructureClusterPack. API group: infrastructure.ontai.dev/v1alpha1. Schema owned by seam-core (Decision G). -Scope: Namespaced - seam-tenant-{cluster-name} -Short name: cp -Lives in: OCI registry (artifact), git (CR YAML), management cluster (applied via GitOps). - -The management cluster's record of an immutable deployed artifact. Applied to -the management cluster via GitOps from the output of conductor compile mode. -Never created by an operator or controller on the management cluster. - -Key spec fields: -- version: declared version string. Immutable after creation. -- registryRef: OCI registry URL and content digest (sha256). -- checksum: content-addressed checksum of the full artifact manifest set. -- rbacDigest: OCI digest of the RBAC layer. Contains ServiceAccount, Role, - ClusterRole, RoleBinding, ClusterRoleBinding manifests extracted at compile - time. Guardian /rbac-intake processes this layer before workload apply - proceeds. Absent on pre-split ClusterPack specs (backward compatibility). -- workloadDigest: OCI digest of the workload layer. Contains all non-RBAC - manifests. Applied after guardian RBACProfile for this pack reaches - provisioned=true. Absent on pre-split ClusterPack specs (backward - compatibility). -- sourceBuildRef: optional reference to the PackBuild spec file path in git - (provenance only - not a cluster object reference). -- executionOrder: stage ordering derived from the compiled execution graph. - Stages in order: rbac, storage, stateful, stateless. -- lifecyclePolicies: resource retention rules for upgrade and delete operations. -- provenance: build identity, timestamp, Helm chart digest, Kustomize overlay - digest, compiler version, compilation timestamp. -- targetClusters: list of cluster names (strings) to which this ClusterPack - should be delivered. The ClusterPackReconciler creates one RunnerConfig per - entry in seam-tenant-{clusterName}. ClusterAssignment is removed - pack-to-cluster - binding is declared directly here. - -Two-layer OCI artifact contract: -ClusterPack OCI artifacts carry two layers. The compiler packbuild command -separates RBAC resources from workload resources at compile time. The -pack-deploy capability processes the RBAC layer via guardian /rbac-intake -before applying the workload layer. This ensures all ServiceAccount, Role, -ClusterRole, RoleBinding, and ClusterRoleBinding resources in any ClusterPack -are guardian-owned before any workload resource is applied. - -The RBAC layer is pushed with media type application/vnd.ontai.rbac.v1. -The workload layer is pushed with media type application/vnd.ontai.workload.v1. - -When rbacDigest is absent on a ClusterPack spec (pre-split artifact), pack-deploy -skips the guardian intake step and applies all manifests directly. This preserves -backward compatibility with artifacts produced before the two-layer split was -introduced. - -ClusterPack spec never contains: Helm templates, Kustomize overlays, variable -references, runtime decision logic, or values from valuesSecretRef. Their presence -is an invariant violation. - -Status fields: -- Signed: bool. Set to true by the management cluster conductor signing loop after - signature verification and annotation. False on creation. -- packSignature: the base64-encoded platform signature written as annotation - ontai.dev/pack-signature by the signing loop. - -Status conditions: Available (signed and registry-verified), Revoked, SignaturePending. - -A ClusterPack in SignaturePending state blocks all PackExecution creation for that -version. The signing loop is the only process that can advance this state. - ---- - -### InfrastructurePackExecution - -Kind: InfrastructurePackExecution. API group: infrastructure.ontai.dev/v1alpha1. Schema owned by seam-core (Decision G). -Scope: Namespaced - seam-tenant-{cluster-name} -Short name: pe -Named capability: pack-deploy - -Runtime request to apply a specific ClusterPack version to a specific target -cluster. Contains no execution logic. A pure reference that the PackExecution -controller acts on by submitting a pack-deploy Job to Kueue. - -Gates verified before Job submission: -- ClusterPack Status.Signed=true (signature present and valid in annotation). -- Target cluster PermissionSnapshot is current and acknowledged. -- Requesting principal's RBACProfile is provisioned and permits this operation. -- ClusterPack version is Available and not Revoked. - -If any gate fails: set specific blocking condition on PackExecution status, requeue -with backoff. Do not submit Job. - -If ClusterPack is SignaturePending: set PackSignaturePending. Requeue with 15-second -backoff. Do not submit Job. - -Key spec fields: clusterPackRef (name and version), targetClusterRef, -admissionProfileRef. -Status conditions: Pending, PackSignaturePending, Running, Succeeded, Failed. - ---- - -### InfrastructurePackInstance - -Kind: InfrastructurePackInstance. API group: infrastructure.ontai.dev/v1alpha1. Schema owned by seam-core (Decision G). -Scope: Namespaced - seam-tenant-{cluster-name} -Short name: pi - -Tracks currently deployed state of a specific pack on a specific target cluster. -One PackInstance per pack per target cluster. Continuously compares expected state -from ClusterPack with PackReceipt drift status from the target cluster conductor. - -Key spec fields: -- clusterPackRef: currently active ClusterPack name and version. -- targetClusterRef: target cluster this instance tracks. -- dependsOn: list of PackInstance names that must be Ready before this is - deployable. Dependencies must exist on the same target cluster. -- dependencyPolicy.onDrift: Block (default for infra packs), Warn (default for - application packs), Ignore. - -Status conditions: Ready, Progressing, Drifted, DependencyBlocked. - ---- - -## 5. CRDs - Target Cluster (Agent-Managed) - -### InfrastructurePackReceipt - -Kind: InfrastructurePackReceipt. API group: infrastructure.ontai.dev/v1alpha1. Schema owned by seam-core (Decision G). -Scope: Namespaced - ont-system on target cluster. -Short name: pr - -Local record of deployed ClusterPack versions and drift status. Created and -maintained exclusively by the conductor Deployment in ont-system on the target -cluster. Never authored by humans or other controllers. - -One PackReceipt per deployed pack per target cluster. The management cluster's -PackInstance trusts PackReceipt as the ground truth for delivery confirmation. - -Key fields (agent-managed): packRef, appliedAt, checksum, signatureVerified (bool - -set true only after the pack-deploy Job verifies the platform signature before -applying), driftStatus (Clean or Drifted), driftDetails, lastCheckedAt. - -The signatureVerified field is critical. A PackReceipt where signatureVerified=false -is treated as a security incident. The PackInstance on the management cluster raises -a SecurityViolation condition and blocks further pack operations on that cluster. - ---- - -## 6. Upgrade and Rollback - -### 6.1 Upgrade Flow - -A new ClusterPack version is compiled locally or via CI pipeline. The ClusterPack -CR YAML output is committed to git. GitOps applies it to the management cluster. -The conductor signing loop signs it. Once Available, a new PackExecution references -the new version. - -Before execution, the runner diff engine in the pack-deploy Job computes the -resource delta between the current PackInstance's active ClusterPack version and -the target version by comparing their respective OCI artifact manifests. - -| Resource condition | Action | -|-----------------------------|-------------------------------------| -| Exists in both versions | Patch via server-side apply | -| Only in current version | Apply lifecycle policy | -| Only in target version | Create | - -Lifecycle policies: retain, delete, orphan, replace. - -Stateful defaults (require explicit human approval to override): -- PersistentVolumeClaims: retain. -- StatefulSets: retain with protected status. -- Breaking schema changes to stateful resources: mandatory human approval gate. - -### 6.2 Rollback - -Rollback to any retained historical revision is triggered by setting -`spec.rollbackToRevision` on the ClusterPack CR to the target POR revision number. -N-step rollback is supported: any revision retained in the superseded POR history -is reachable in one operation. - -**Mechanism:** - -The POR writer retains superseded PORs by labeling them `ontai.dev/superseded=true` -instead of deleting them. Each superseded POR carries its original `clusterPackVersion`, -`rbacDigest`, and `workloadDigest` fields unchanged. Up to 10 superseded PORs are -retained per ClusterPack; the oldest (lowest revision) is pruned when the cap is exceeded. - -When `spec.rollbackToRevision > 0`, the ClusterPackReconciler: -1. Lists ALL PORs labeled `ontai.dev/cluster-pack={cp.Name}` in the ClusterPack namespace (both active and superseded). -2. Finds the POR where `spec.revision == rollbackToRevision`. If not found, clears the field without patching spec. -3. Reads `clusterPackVersion`, `rbacDigest`, `workloadDigest` directly from that POR. -4. Patches `ClusterPack.spec`: sets `version`, `rbacDigest`, `workloadDigest` to the target values; clears `rollbackToRevision`. -5. Removes the `infrastructure.ontai.dev/spec-checksum-snapshot` annotation so the immutability check re-records the rolled-back state on the next reconcile pass. -6. Returns -- the version change triggers normal PackExecution creation. - -The resulting PackExecution runs a normal pack-deploy Job against the target OCI -layer digests. The new POR records `upgradeDirection=Rollback`. The PackReceipt on -the tenant cluster is overwritten with the target version's resource inventory. - ---- - -## 7. Agent Bootstrap Exception - -The agent ClusterPack is the first pack applied to any target cluster. During -bootstrap, conductor applies it directly via kube goclient without going through -the PackExecution flow. Signature verification still occurs - the bootstrap -bootstrap sequence includes signature verification using the embedded public key -before any manifest is applied. No PackReceipt tracking exists yet because no -infra agent is running. This is the single documented exception to the PackExecution -flow model. - -After the agent pack is applied, the infra agent comes online, creates its own -PackReceipt for the agent pack with signatureVerified=true, and all subsequent -deliveries follow the normal PackExecution model. - ---- - -## 8. Drift Detection - -The conductor on target clusters runs periodic server-side dry-run comparisons -between the expected state from the current PackReceipt and actual live cluster -state. Updates PackReceipt driftStatus. PackInstance on the management cluster -reflects this via its Drifted condition. Remediation is a runner Job submitted -via a new PackExecution - the agent never auto-remediates. - ---- - -## 9. Cross-Domain Rules - -Reads: security.ontai.dev/PermissionSnapshot delivery status before admitting -PackExecution. Does not write to security.ontai.dev. -ClusterAssignment is removed. Pack-to-cluster binding is declared directly in -InfrastructureClusterPack.spec.targetClusters. Wrapper does not read from platform.ontai.dev. -Reads: infrastructure.ontai.dev/InfrastructureRunnerConfig status (capability confirmation). -Writes: infrastructure.ontai.dev/InfrastructureRunnerConfig (generates from ClusterPack/PackExecution - context via shared runner library - no PackBuild controller). -Writes: infrastructure.ontai.dev resources on management cluster (InfrastructureClusterPack, InfrastructurePackExecution, InfrastructurePackInstance). -Writes: InfrastructurePackReceipt on target clusters via conductor. - -Pack delivery ownership chain (locked): -- InfrastructureClusterPack: human/GitOps authored. Immutable after creation. -- InfrastructureRunnerConfig: created by ClusterPackReconciler, one per targetClusters entry, - in seam-tenant-{clusterName}, with labels infrastructure.ontai.dev/cluster and - infrastructure.ontai.dev/pack. -- InfrastructurePackExecution: created by management cluster Conductor agent from RunnerConfig - (not by Wrapper). Conductor watches RunnerConfigs labeled infrastructure.ontai.dev/pack - and creates one PackExecution per RunnerConfig that lacks one. -- InfrastructurePackInstance: created by Wrapper PackExecutionReconciler after observing that - the pack-deploy Job succeeded (OperationResult ConfigMap present and Succeeded). - Namespace: seam-tenant-{clusterRef}. Label: infrastructure.ontai.dev/pack. -- InfrastructurePackReceipt: created by Conductor agent on the target cluster after verifying - the PackInstance signature and applying the manifests. - -Note: The signing loop is an conductor responsibility. The Wrapper controller -does not sign ClusterPacks. It reads the Signed status and blocks PackExecution -until signing is complete. - ---- - -*Wrapper behavioral schema - Wrapper* -*CRD type schema authority: seam-core (infrastructure.ontai.dev). Supersedes infra.ontai.dev. Decision G, Phase 2B 2026-04-25.* -*Amendments:* -*2026-03-30 - Compile mode is a workstation/CI operation, never a cluster Job.* -* PackBuild removed as a management cluster CRD. No PackBuild controller.* -* ClusterPack signing lifecycle added (Section 3). Signing is conductor responsibility.* -* pack-deploy Jobs use conductor (distroless). No execution seam with compile mode.* -* PackReceipt.signatureVerified field added. SecurityViolation condition added.* -* PackExecution.PackSignaturePending gate condition added.* -*2026-04-10 - Namespace model locked: ClusterPack, PackExecution, PackInstance all live* -* in seam-tenant-{cluster-name}, not infra-system. ClusterAssignment removed.* -* ClusterPack.spec.targetClusters added - pack-to-cluster binding declared directly.* -* Pack delivery ownership chain locked (Section 9): ClusterPack (human/GitOps),* -* RunnerConfig (ClusterPackReconciler), PackExecution (Conductor agent from RunnerConfig),* -* PackInstance (Wrapper after Job success), PackReceipt (target Conductor).* -*2026-04-21 - Two-layer ClusterPack OCI artifact contract added (session/13-clusterpack-rbac-split).* -* ClusterPack spec gains rbacDigest and workloadDigest fields.* -* Compiler packbuild separates RBAC resources from workload resources at compile time.* -* Pack-deploy calls guardian /rbac-intake for RBAC layer before applying workload layer.* -* Backward compatibility: rbacDigest absent means single-layer fallback.* -* wrapper-runner Role restricted to workload resource kinds only.* -* wrapper-runner ClusterRole added for cluster-scoped bucket 2 resources (Phase 2B, 2026-04-25).* -*2026-04-25 - Phase 2B: all CRD types migrated to seam-core infrastructure.ontai.dev.* -* infra.ontai.dev API group superseded. Kind names carry Infrastructure prefix.* -* All reconcilers, labels, annotations, and finalizers updated to infrastructure.ontai.dev.* +# Moved to dispatcher-schema.md From 3ebc64e94967845ad6e9cf477fddcb368042ca16 Mon Sep 17 00:00:00 2001 From: ontave Date: Sun, 17 May 2026 23:36:53 +0200 Subject: [PATCH 08/19] fix: update CI workflow (wrapper->dispatcher, seam-core->seam, add seam-sdk); fix integration test scheme registration and CRD path --- .github/workflows/ci.yaml | 30 ++++++++++++++++++------------ test/integration/suite_test.go | 4 +++- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7e315da..c8c7044 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,16 +10,22 @@ jobs: ci: runs-on: ubuntu-latest steps: - - name: Checkout wrapper + - name: Checkout dispatcher uses: actions/checkout@v4 with: - path: wrapper + path: dispatcher - - name: Checkout seam-core (replace dep) + - name: Checkout seam (replace dep) uses: actions/checkout@v4 with: - repository: ontai-dev/seam-core - path: seam-core + repository: ontai-dev/seam + path: seam + + - name: Checkout seam-sdk (replace dep) + uses: actions/checkout@v4 + with: + repository: ontai-dev/seam-sdk + path: seam-sdk - name: Checkout conductor (replace dep) uses: actions/checkout@v4 @@ -30,36 +36,36 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version-file: wrapper/go.mod + go-version-file: dispatcher/go.mod cache: true - name: Build - working-directory: wrapper + working-directory: dispatcher run: make build - name: Lint - working-directory: wrapper + working-directory: dispatcher run: | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest golangci-lint run --timeout 5m ./... - name: Unit tests - working-directory: wrapper + working-directory: dispatcher run: make test-unit - name: Install envtest binaries - working-directory: wrapper + working-directory: dispatcher run: | go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest ENVTEST_BIN=$(setup-envtest use --bin-dir /tmp/envtest-bins --print path 1.35.0) echo "KUBEBUILDER_ASSETS=${ENVTEST_BIN}" >> "$GITHUB_ENV" - name: Integration tests - working-directory: wrapper + working-directory: dispatcher run: make test-integration - name: e2e tests - working-directory: wrapper + working-directory: dispatcher run: | E2E_OUT=$(go test ./test/e2e/... -v 2>&1 || true) SKIPPED=$(echo "$E2E_OUT" | grep -c '\[SKIPPED\]\|S\[' || echo 0) diff --git a/test/integration/suite_test.go b/test/integration/suite_test.go index ad8659b..420ed0f 100644 --- a/test/integration/suite_test.go +++ b/test/integration/suite_test.go @@ -32,6 +32,7 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" seamcorev1alpha1 "github.com/ontai-dev/seam/api/v1alpha1" + dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" "github.com/ontai-dev/dispatcher/internal/controller" ) @@ -52,8 +53,9 @@ func TestMain(m *testing.M) { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(seamcorev1alpha1.AddToScheme(scheme)) + utilruntime.Must(dispatcherv1alpha1.AddToScheme(scheme)) - crdPath := filepath.Join("..", "..", "config", "crd") + crdPath := filepath.Join("..", "..", "config", "crd", "bases") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{crdPath}, From c0b96aeb4b2a7528c38f537bf69ca519f089f9f6 Mon Sep 17 00:00:00 2001 From: ontave Date: Mon, 18 May 2026 06:28:26 +0200 Subject: [PATCH 09/19] feat(dispatcher): implement SeamOperator interface and startup SeamMembership --- cmd/wrapper/main.go | 16 ++++- internal/identity/identity.go | 64 ++++++++++++++++++++ internal/identity/identity_test.go | 95 ++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 internal/identity/identity.go create mode 100644 internal/identity/identity_test.go diff --git a/cmd/wrapper/main.go b/cmd/wrapper/main.go index 1063d3d..e09c104 100644 --- a/cmd/wrapper/main.go +++ b/cmd/wrapper/main.go @@ -7,6 +7,7 @@ package main import ( + "context" "flag" "os" @@ -14,6 +15,7 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" @@ -21,6 +23,7 @@ import ( seamv1alpha1 "github.com/ontai-dev/seam/api/v1alpha1" dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" "github.com/ontai-dev/dispatcher/internal/controller" + "github.com/ontai-dev/dispatcher/internal/identity" ) var scheme = runtime.NewScheme() @@ -60,9 +63,20 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) setupLog := ctrl.Log.WithName("setup") + cfg := ctrl.GetConfigOrDie() + startupClient, err := client.New(cfg, client.Options{Scheme: scheme}) + if err != nil { + setupLog.Error(err, "unable to create startup client") + os.Exit(1) + } + if err := identity.EnsureSeamMembership(context.Background(), startupClient); err != nil { + setupLog.Error(err, "unable to ensure SeamMembership") + os.Exit(1) + } + // CI-INV-004: leader election required. Lease name: wrapper-leader. // Lease namespace: seam-system (canonical operator namespace). - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{BindAddress: metricsAddr}, HealthProbeBindAddress: healthProbeAddr, diff --git a/internal/identity/identity.go b/internal/identity/identity.go new file mode 100644 index 0000000..72f8666 --- /dev/null +++ b/internal/identity/identity.go @@ -0,0 +1,64 @@ +package identity + +import ( + "context" + + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + seamv1alpha1 "github.com/ontai-dev/seam/api/v1alpha1" + "github.com/ontai-dev/seam-sdk/conditions" + "github.com/ontai-dev/seam-sdk/labels" + "github.com/ontai-dev/seam-sdk/operator" +) + +// SeamIdentity implements operator.SeamOperator for the dispatcher operator. +type SeamIdentity struct{} + +var _ operator.SeamOperator = (*SeamIdentity)(nil) + +func (s *SeamIdentity) OperatorName() string { return "dispatcher" } +func (s *SeamIdentity) MembershipCRName() string { return "seam-dispatcher" } +func (s *SeamIdentity) ReadyConditionType() string { return conditions.ConditionReady } +func (s *SeamIdentity) Domain() string { return "seam.ontai.dev" } +func (s *SeamIdentity) Subdomain() string { return "pack" } +func (s *SeamIdentity) ConditionTypes() []string { + return []string{ + conditions.ConditionReady, + conditions.ConditionSeamMembershipProvisioned, + conditions.ConditionRBACProfileActive, + conditions.ConditionReconciling, + conditions.ConditionDegraded, + } +} +func (s *SeamIdentity) LineageLabelSchema() map[string]string { + return map[string]string{ + labels.LabelManagedBy: "dispatcher", + labels.LabelRootDeclarationKind: "", + labels.LabelRootDeclarationName: "", + labels.LabelRootDeclarationNamespace: "", + } +} + +// EnsureSeamMembership creates the SeamMembership CR for the dispatcher operator +// in seam-system. Idempotent: AlreadyExists is not an error. +func EnsureSeamMembership(ctx context.Context, c client.Client) error { + id := &SeamIdentity{} + sm := &seamv1alpha1.SeamMembership{ + ObjectMeta: metav1.ObjectMeta{ + Name: id.MembershipCRName(), + Namespace: "seam-system", + }, + Spec: seamv1alpha1.SeamMembershipSpec{ + AppIdentityRef: id.OperatorName(), + DomainIdentityRef: id.OperatorName(), + PrincipalRef: "system:serviceaccount:seam-system:" + id.OperatorName(), + Tier: "infrastructure", + }, + } + if err := c.Create(ctx, sm); err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + return nil +} diff --git a/internal/identity/identity_test.go b/internal/identity/identity_test.go new file mode 100644 index 0000000..6a0f0d5 --- /dev/null +++ b/internal/identity/identity_test.go @@ -0,0 +1,95 @@ +package identity_test + +import ( + "context" + "testing" + + k8sruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + seamv1alpha1 "github.com/ontai-dev/seam/api/v1alpha1" + "github.com/ontai-dev/dispatcher/internal/identity" + "github.com/ontai-dev/seam-sdk/conditions" + "github.com/ontai-dev/seam-sdk/operator" +) + +var _ operator.SeamOperator = (*identity.SeamIdentity)(nil) + +func newScheme(t *testing.T) *k8sruntime.Scheme { + t.Helper() + s := k8sruntime.NewScheme() + if err := seamv1alpha1.AddToScheme(s); err != nil { + t.Fatalf("AddToScheme: %v", err) + } + return s +} + +func TestSeamIdentity_Values(t *testing.T) { + id := &identity.SeamIdentity{} + if got := id.OperatorName(); got != "dispatcher" { + t.Errorf("OperatorName() = %q, want %q", got, "dispatcher") + } + if got := id.MembershipCRName(); got != "seam-dispatcher" { + t.Errorf("MembershipCRName() = %q, want %q", got, "seam-dispatcher") + } + if got := id.ReadyConditionType(); got != conditions.ConditionReady { + t.Errorf("ReadyConditionType() = %q, want %q", got, conditions.ConditionReady) + } + if got := id.Domain(); got != "seam.ontai.dev" { + t.Errorf("Domain() = %q, want %q", got, "seam.ontai.dev") + } + if got := id.Subdomain(); got != "pack" { + t.Errorf("Subdomain() = %q, want %q", got, "pack") + } +} + +func TestSeamIdentity_ConditionTypes_ContainsReady(t *testing.T) { + id := &identity.SeamIdentity{} + for _, ct := range id.ConditionTypes() { + if ct == conditions.ConditionReady { + return + } + } + t.Error("ConditionTypes() does not include conditions.ConditionReady") +} + +func TestSeamIdentity_LineageLabelSchema_HasManagedBy(t *testing.T) { + id := &identity.SeamIdentity{} + schema := id.LineageLabelSchema() + v, ok := schema["seam.ontai.dev/managed-by"] + if !ok { + t.Fatal("LineageLabelSchema() missing seam.ontai.dev/managed-by") + } + if v != "dispatcher" { + t.Errorf("seam.ontai.dev/managed-by = %q, want %q", v, "dispatcher") + } +} + +func TestEnsureSeamMembership_Creates(t *testing.T) { + c := fake.NewClientBuilder().WithScheme(newScheme(t)).Build() + if err := identity.EnsureSeamMembership(context.Background(), c); err != nil { + t.Fatalf("EnsureSeamMembership: %v", err) + } + sm := &seamv1alpha1.SeamMembership{} + key := types.NamespacedName{Name: "seam-dispatcher", Namespace: "seam-system"} + if err := c.Get(context.Background(), key, sm); err != nil { + t.Fatalf("Get SeamMembership: %v", err) + } + if sm.Spec.AppIdentityRef != "dispatcher" { + t.Errorf("AppIdentityRef = %q, want %q", sm.Spec.AppIdentityRef, "dispatcher") + } + if sm.Spec.Tier != "infrastructure" { + t.Errorf("Tier = %q, want %q", sm.Spec.Tier, "infrastructure") + } +} + +func TestEnsureSeamMembership_Idempotent(t *testing.T) { + c := fake.NewClientBuilder().WithScheme(newScheme(t)).Build() + if err := identity.EnsureSeamMembership(context.Background(), c); err != nil { + t.Fatalf("first call: %v", err) + } + if err := identity.EnsureSeamMembership(context.Background(), c); err != nil { + t.Fatalf("second call (idempotency): %v", err) + } +} From a5a3000eec22bb133a133acec02e2a5cf8017892 Mon Sep 17 00:00:00 2001 From: ontave Date: Mon, 18 May 2026 11:40:45 +0200 Subject: [PATCH 10/19] fix(dispatcher): mount kubeconfig secret key as file via SubPath The pack-deploy Job mounts the cluster kubeconfig secret without SubPath, causing the secret key 'value' to appear as a directory entry at /var/run/secrets/kubeconfig/value rather than as a plain file at /var/run/secrets/kubeconfig. Add SubPath: "value" so conductor execute mode can open the path as a regular file. --- internal/controller/packexecution_reconciler.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/controller/packexecution_reconciler.go b/internal/controller/packexecution_reconciler.go index 81b7d76..eba792f 100644 --- a/internal/controller/packexecution_reconciler.go +++ b/internal/controller/packexecution_reconciler.go @@ -975,6 +975,7 @@ func (r *PackExecutionReconciler) buildPackDeployJob( { Name: "kubeconfig", MountPath: "/var/run/secrets/kubeconfig", + SubPath: "value", ReadOnly: true, }, }, From f5e4041a57d53e244e774dd2c7e8bc1a3d7da8b2 Mon Sep 17 00:00:00 2001 From: ontave Date: Mon, 18 May 2026 16:14:47 +0200 Subject: [PATCH 11/19] feat(watchdog): add RemediationPolicyRef to PackInstalled, remediationAttempts to PackLog PackInstalled gains spec.remediationPolicyRef for conductor watchdog escalation policy resolution. PackLog gains status.remediationAttempts slice for per-reason attempt tracking. Deepcopy regenerated. pack-name label injection wired into PackExecution reconciler (T-CW-12 through T-CW-20). --- api/seam/v1alpha1/packinstalled_types.go | 16 +++++++ api/seam/v1alpha1/packlog_types.go | 23 +++++++++- api/seam/v1alpha1/zz_generated.deepcopy.go | 46 +++++++++++++++++++ .../controller/packexecution_reconciler.go | 12 +++++ 4 files changed, 96 insertions(+), 1 deletion(-) diff --git a/api/seam/v1alpha1/packinstalled_types.go b/api/seam/v1alpha1/packinstalled_types.go index ce0060f..ed1987a 100644 --- a/api/seam/v1alpha1/packinstalled_types.go +++ b/api/seam/v1alpha1/packinstalled_types.go @@ -85,6 +85,22 @@ type PackInstalledSpec struct { // HelmVersion is the Helm SDK version used to render the pack. Carried from PackDelivery. // +optional HelmVersion string `json:"helmVersion,omitempty"` + + // RemediationPolicyRef is an optional reference to a RemediationPolicy CR + // (conductor.ontai.dev) in the same namespace. When absent, Conductor Watchdog + // applies the platform defaults (threshold=3, per-reason default strategies, + // MaxAttempts=3, 5m window). + // +optional + RemediationPolicyRef *RemediationPolicyRefSpec `json:"remediationPolicyRef,omitempty"` +} + +// RemediationPolicyRefSpec is a name+namespace reference to a RemediationPolicy CR. +type RemediationPolicyRefSpec struct { + // Name is the RemediationPolicy CR name. + Name string `json:"name"` + + // Namespace is the namespace of the RemediationPolicy CR. + Namespace string `json:"namespace"` } // PackInstalledStatus is the observed state of a PackInstalled. diff --git a/api/seam/v1alpha1/packlog_types.go b/api/seam/v1alpha1/packlog_types.go index b6799a1..71eccf6 100644 --- a/api/seam/v1alpha1/packlog_types.go +++ b/api/seam/v1alpha1/packlog_types.go @@ -181,12 +181,33 @@ type PackLogSpec struct { WorkloadDigest string `json:"workloadDigest,omitempty"` } +// RemediationAttemptRecord tracks one remediation attempt series for a specific +// FailureReason. Written by the management Conductor after each remediation Job +// completes. The management Conductor is the sole writer; tenant conductor +// observes failure counts but does not write here. +type RemediationAttemptRecord struct { + // FailureReason is the seam-sdk FailureReason value this record tracks. + // +kubebuilder:validation:Enum=CrashLoopBackOff;OOMKilled;ImagePullBackOff;FailedMount;MultiAttachError + FailureReason string `json:"failureReason"` + + // AttemptCount is the total number of remediation Jobs submitted for this reason. + AttemptCount int32 `json:"attemptCount"` + + // LastAttemptAt is the time the most recent remediation Job was submitted. + // +optional + LastAttemptAt *metav1.Time `json:"lastAttemptAt,omitempty"` +} + // PackLogStatus is the observed state of a PackLog. -// Currently empty; reserved for future controller-set conditions. type PackLogStatus struct { // ObservedGeneration is the last generation processed by any consumer. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // RemediationAttempts records the Conductor Watchdog remediation attempt history + // per FailureReason. Written by management Conductor exec mode after each Job. + // +optional + RemediationAttempts []RemediationAttemptRecord `json:"remediationAttempts,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/seam/v1alpha1/zz_generated.deepcopy.go b/api/seam/v1alpha1/zz_generated.deepcopy.go index e965598..8495f5e 100644 --- a/api/seam/v1alpha1/zz_generated.deepcopy.go +++ b/api/seam/v1alpha1/zz_generated.deepcopy.go @@ -375,6 +375,40 @@ func (in *PackInstalledList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RemediationPolicyRefSpec) DeepCopyInto(out *RemediationPolicyRefSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemediationPolicyRefSpec. +func (in *RemediationPolicyRefSpec) DeepCopy() *RemediationPolicyRefSpec { + if in == nil { + return nil + } + out := new(RemediationPolicyRefSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RemediationAttemptRecord) DeepCopyInto(out *RemediationAttemptRecord) { + *out = *in + if in.LastAttemptAt != nil { + in, out := &in.LastAttemptAt, &out.LastAttemptAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemediationAttemptRecord. +func (in *RemediationAttemptRecord) DeepCopy() *RemediationAttemptRecord { + if in == nil { + return nil + } + out := new(RemediationAttemptRecord) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PackInstalledSpec) DeepCopyInto(out *PackInstalledSpec) { *out = *in @@ -388,6 +422,11 @@ func (in *PackInstalledSpec) DeepCopyInto(out *PackInstalledSpec) { *out = new(DependencyPolicy) **out = **in } + if in.RemediationPolicyRef != nil { + in, out := &in.RemediationPolicyRef, &out.RemediationPolicyRef + *out = new(RemediationPolicyRefSpec) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackInstalledSpec. @@ -583,6 +622,13 @@ func (in *PackLogSpec) DeepCopy() *PackLogSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PackLogStatus) DeepCopyInto(out *PackLogStatus) { *out = *in + if in.RemediationAttempts != nil { + in, out := &in.RemediationAttempts, &out.RemediationAttempts + *out = make([]RemediationAttemptRecord, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackLogStatus. diff --git a/internal/controller/packexecution_reconciler.go b/internal/controller/packexecution_reconciler.go index eba792f..0b80bd8 100644 --- a/internal/controller/packexecution_reconciler.go +++ b/internal/controller/packexecution_reconciler.go @@ -896,6 +896,17 @@ func (r *PackExecutionReconciler) isDispatcherRunnerRBACReady(ctx context.Contex return true, "", nil } +// packInstalledName returns the deterministic PackInstalled CR name for the given +// PackExecution and its owning PackDelivery. Matches the piName formula in the +// completion handler above. Used to stamp PACK_INSTALLED_NAME into the pack-deploy Job. +func packInstalledName(pe *dispatcherv1alpha1.PackExecution, cp *dispatcherv1alpha1.PackDelivery) string { + base := cp.Spec.BasePackName + if base == "" { + base = pe.Spec.PackDeliveryRef.Name + } + return base + "-" + pe.Spec.TargetClusterRef +} + // buildPackDeployJob constructs the pack-deploy Job spec for Kueue admission. // The Job carries the kueue.x-k8s.io/queue-name label — without this label, // the Job will not be admitted. wrapper-design.md §4. @@ -970,6 +981,7 @@ func (r *PackExecutionReconciler) buildPackDeployJob( {Name: "PACK_REGISTRY_REF", Value: cp.Spec.RegistryRef.URL + "@" + cp.Spec.RegistryRef.Digest}, {Name: "PACK_CHECKSUM", Value: cp.Spec.Checksum}, {Name: "PACK_SIGNATURE", Value: cp.Status.PackSignature}, + {Name: "PACK_INSTALLED_NAME", Value: packInstalledName(pe, cp)}, }, VolumeMounts: []corev1.VolumeMount{ { From b30672e32d0bda3e8d22af369e6716e067d189c3 Mon Sep 17 00:00:00 2001 From: ontave Date: Wed, 20 May 2026 16:23:06 +0200 Subject: [PATCH 12/19] fix(dispatcher): rename image wrapper->dispatcher in Dockerfile and Makefile --- Dockerfile | 20 +++++++++++--------- Makefile | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 85f0f13..871d3ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,24 @@ -# Dockerfile — Wrapper operator (distroless). +# Dockerfile — Dispatcher operator (distroless). # -# Wrapper is a long-running Deployment in seam-system. It manages ClusterPack -# compilation and delivery. Distroless: zero attack surface. INV-022. -# wrapper-schema.md §3. +# Dispatcher is a long-running Deployment in seam-system. It manages pack +# delivery and lifecycle. Distroless: zero attack surface. INV-022. +# dispatcher-schema.md §3. FROM golang:1.25 AS builder WORKDIR /build -COPY wrapper/ . +COPY dispatcher/ . COPY conductor/ ../conductor/ -COPY seam-core/ ../seam-core/ +COPY seam/ ../seam/ +COPY seam-sdk/ ../seam-sdk/ +COPY conductor-sdk/ ../conductor-sdk/ RUN CGO_ENABLED=0 GOOS=linux go build \ -trimpath \ -ldflags="-s -w" \ - -o /bin/wrapper \ + -o /bin/dispatcher \ ./cmd/wrapper FROM gcr.io/distroless/base:nonroot -COPY --from=builder /bin/wrapper /usr/local/bin/wrapper +COPY --from=builder /bin/dispatcher /usr/local/bin/dispatcher USER 65532:65532 -ENTRYPOINT ["/usr/local/bin/wrapper"] +ENTRYPOINT ["/usr/local/bin/dispatcher"] diff --git a/Makefile b/Makefile index 3073378..5a2c824 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ CONTROLLER_GEN ?= $(shell which controller-gen 2>/dev/null || echo $(HOME)/go/bin/controller-gen) IMAGE_REGISTRY ?= 10.20.0.1:5000/ontai-dev -IMAGE_NAME := wrapper +IMAGE_NAME := dispatcher TAG ?= dev build: From 30080fbab8208db955e227e8b47f3ac0c83bc39b Mon Sep 17 00:00:00 2001 From: ontave Date: Wed, 20 May 2026 20:22:44 +0200 Subject: [PATCH 13/19] fix(dispatcher): post-migration GVK, SA name, and label key fixes Replace all infrastructure.ontai.dev references with seam.ontai.dev across controllers and tests. Fix InfrastructureRunnerConfig -> RunnerConfig and InfrastructureTalosCluster -> TalosCluster GVKs in SetupWithManager watch, isConductorReadyForCluster lookups, and all test helpers. Rename wrapper-runner SA to dispatcher-runner throughout. Update finalizer, annotation, and label keys to seam.ontai.dev prefix. Update unit tests to use post-migration GVKs so gate 0 clears correctly in test scenarios. --- internal/controller/clusterpack_reconciler.go | 12 ++-- .../controller/packexecution_reconciler.go | 64 +++++++++---------- .../controller/packinstance_reconciler.go | 2 +- test/unit/clusterpack_reconciler_test.go | 10 +-- test/unit/controller/helpers_test.go | 8 +-- .../controller/kueue_job_scheduling_test.go | 4 +- test/unit/controller/rollback_test.go | 4 +- test/unit/packexecution_reconciler_test.go | 14 ++-- test/unit/role_independence_test.go | 4 +- 9 files changed, 61 insertions(+), 61 deletions(-) diff --git a/internal/controller/clusterpack_reconciler.go b/internal/controller/clusterpack_reconciler.go index 3f83904..02c2565 100644 --- a/internal/controller/clusterpack_reconciler.go +++ b/internal/controller/clusterpack_reconciler.go @@ -36,7 +36,7 @@ const packSignatureAnnotation = "ontai.dev/pack-signature" // clusterPackFinalizer is added to every ClusterPack on first reconcile. // On deletion it triggers cleanup of all PackInstances and RunnerConfigs // derived from this ClusterPack before the object is removed from etcd. -const clusterPackFinalizer = "infrastructure.ontai.dev/clusterpack-cleanup" +const clusterPackFinalizer = "seam.ontai.dev/clusterpack-cleanup" // ClusterPackReconciler watches ClusterPack CRs and manages their signing lifecycle // and immutability enforcement. @@ -110,7 +110,7 @@ func (r *ClusterPackReconciler) Reconcile(ctx context.Context, req ctrl.Request) // r.Client.Patch() after the status patch setup would overwrite the in-memory // object with the stored state, losing any status mutations made before the call. // CI-INV-002: ClusterPack spec is immutable after creation. - const specSnapshotAnnotation = "infrastructure.ontai.dev/spec-checksum-snapshot" + const specSnapshotAnnotation = "seam.ontai.dev/spec-checksum-snapshot" currentChecksum := cp.Spec.Checksum + "|" + cp.Spec.RegistryRef.URL + "|" + cp.Spec.RegistryRef.Digest + "|" + cp.Spec.Version if _, ok := cp.Annotations[specSnapshotAnnotation]; !ok { metaPatch := client.MergeFrom(cp.DeepCopy()) @@ -271,9 +271,9 @@ func (r *ClusterPackReconciler) Reconcile(ctx context.Context, req ctrl.Request) Name: peName, Namespace: tenantNS, Labels: map[string]string{ - "infrastructure.ontai.dev/pack": cp.Name, - "infrastructure.ontai.dev/pack-version": cp.Spec.Version, - "infrastructure.ontai.dev/cluster": clusterName, + "seam.ontai.dev/pack": cp.Name, + "seam.ontai.dev/pack-version": cp.Spec.Version, + "seam.ontai.dev/cluster": clusterName, }, }, Spec: dispatcherv1alpha1.PackExecutionSpec{ @@ -355,7 +355,7 @@ func (r *ClusterPackReconciler) handleRollback(ctx context.Context, cp *dispatch // Patch spec back to target version + digests, clear rollbackToRevision, // and remove the spec-snapshot annotation so the immutability check re-records // the rolled-back state on the next reconcile pass. - const specSnapshotAnnotation = "infrastructure.ontai.dev/spec-checksum-snapshot" + const specSnapshotAnnotation = "seam.ontai.dev/spec-checksum-snapshot" patch := client.MergeFrom(cp.DeepCopy()) cp.Spec.Version = targetVersion cp.Spec.RBACDigest = targetRBACDigest diff --git a/internal/controller/packexecution_reconciler.go b/internal/controller/packexecution_reconciler.go index 0b80bd8..20aa571 100644 --- a/internal/controller/packexecution_reconciler.go +++ b/internal/controller/packexecution_reconciler.go @@ -60,10 +60,10 @@ const ( // RBACProfile gates have not yet cleared. gateRequeueInterval = 30 * time.Second - // wrapperRunnerServiceAccount is the ServiceAccount used by pack-deploy Jobs. + // dispatcherRunnerServiceAccount is the ServiceAccount used by pack-deploy Jobs. // The ServiceAccount must exist in the Job's namespace (seam-tenant-{clusterRef}). - // wrapper-design.md §4. - wrapperRunnerServiceAccount = "wrapper-runner" + // dispatcher-design.md §4. + dispatcherRunnerServiceAccount = "dispatcher-runner" // conductorImageEnvVar is the env var the operator reads for the conductor image. // Defaults to the local dev registry if not set. @@ -376,7 +376,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques ) // Gate 5: DispatcherRunnerRBAC gate. - // SubjectAccessReview verifies wrapper-runner SA has required permissions + // SubjectAccessReview verifies dispatcher-runner SA has required permissions // before submitting the Job. Catches stale or missing RBAC before runtime failure. rbacCheckFn := r.isDispatcherRunnerRBACReady if r.RBACChecker != nil { @@ -384,7 +384,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques } rbacSARReady, rbacSARDenyReason, err := rbacCheckFn(ctx, pe) if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to check wrapper-runner RBAC: %w", err) + return ctrl.Result{}, fmt.Errorf("failed to check dispatcher-runner RBAC: %w", err) } if !rbacSARReady { conditions.SetCondition( @@ -412,7 +412,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques conditions.ConditionTypeDispatcherRunnerRBACNotReady, metav1.ConditionFalse, conditions.ReasonDispatcherRunnerRBACNotReady, - "wrapper-runner has required RBAC permissions.", + "dispatcher-runner has required RBAC permissions.", pe.Generation, ) @@ -586,8 +586,8 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques Name: piName, Namespace: piNamespace, Labels: map[string]string{ - "infrastructure.ontai.dev/pack": pe.Spec.PackDeliveryRef.Name, - "infrastructure.ontai.dev/cluster": pe.Spec.TargetClusterRef, + "seam.ontai.dev/pack": pe.Spec.PackDeliveryRef.Name, + "seam.ontai.dev/cluster": pe.Spec.TargetClusterRef, }, OwnerReferences: []metav1.OwnerReference{{ APIVersion: dispatcherv1alpha1.GroupVersion.String(), @@ -606,7 +606,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques } // Wire descendant lineage so the DescendantReconciler can append this // PackInstance to the PackExecution's ILI. seam-core-schema.md §3. - lineage.SetDescendantLabels(pi, lineage.IndexName("PackExecution", pe.Name), pe.Namespace, "wrapper", lineage.PackExecution, pe.GetAnnotations()[lineage.AnnotationDeclaringPrincipal]) + lineage.SetDescendantLabels(pi, lineage.IndexName("PackExecution", pe.Name), pe.Namespace, "dispatcher", lineage.PackExecution, pe.GetAnnotations()[lineage.AnnotationDeclaringPrincipal]) if err := r.Client.Create(ctx, pi); err != nil { if !apierrors.IsAlreadyExists(err) { return ctrl.Result{}, fmt.Errorf("failed to create PackInstance %s: %w", piName, err) @@ -780,10 +780,10 @@ func (r *PackExecutionReconciler) isRBACProfileProvisioned(ctx context.Context, // isConductorReadyForCluster determines whether the Conductor agent for the // target cluster is live and ready to accept Jobs. It performs two lookups: // -// 1. InfrastructureTalosCluster namespace (Fix 1): tries seam-tenant-{clusterRef} -// first; if not found, falls back to seam-system. The management cluster -// InfrastructureTalosCluster lives in seam-system, tenant cluster in -// seam-tenant-{name}. If absent in both namespaces, cluster not yet registered. +// 1. TalosCluster namespace: tries seam-tenant-{clusterRef} first; if not found, +// falls back to seam-system. The management cluster TalosCluster lives in +// seam-system, tenant cluster in seam-tenant-{name}. +// If absent in both namespaces, cluster not yet registered. // // 2. RunnerConfig readiness (Fix 2): looks up the RunnerConfig named // {targetClusterRef} in ont-system and checks status.capabilities. A @@ -799,9 +799,9 @@ func (r *PackExecutionReconciler) isRBACProfileProvisioned(ctx context.Context, func (r *PackExecutionReconciler) isConductorReadyForCluster(ctx context.Context, pe *dispatcherv1alpha1.PackExecution) (bool, error) { clusterRef := pe.Spec.TargetClusterRef tcGVK := schema.GroupVersionKind{ - Group: "infrastructure.ontai.dev", + Group: "seam.ontai.dev", Version: "v1alpha1", - Kind: "InfrastructureTalosCluster", + Kind: "TalosCluster", } // Fix 1: try seam-tenant-{clusterRef} then fall back to seam-system. @@ -828,9 +828,9 @@ func (r *PackExecutionReconciler) isConductorReadyForCluster(ctx context.Context // correct signal that Conductor is live. conductor-schema.md §5, §10 step 3. rc := &unstructured.Unstructured{} rc.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "infrastructure.ontai.dev", + Group: "seam.ontai.dev", Version: "v1alpha1", - Kind: "InfrastructureRunnerConfig", + Kind: "RunnerConfig", }) if err := r.Client.Get(ctx, types.NamespacedName{Name: clusterRef, Namespace: "ont-system"}, rc); err != nil { if apierrors.IsNotFound(err) { @@ -847,27 +847,27 @@ func (r *PackExecutionReconciler) isConductorReadyForCluster(ctx context.Context } // isDispatcherRunnerRBACReady performs SubjectAccessReview checks to verify that -// the wrapper-runner ServiceAccount in pe.Namespace has the required RBAC +// the dispatcher-runner ServiceAccount in pe.Namespace has the required RBAC // permissions before a Kueue Job is submitted. Returns (true, "", nil) when // all permissions are granted. Returns (false, denyReason, nil) when any // permission is denied. Returns (false, "", err) on SAR API failure. // // This is gate 5 of the PackExecution gate check. It catches stale or missing -// RBAC before the Job pod runs and fails with a Forbidden error. The three -// checks mirror the permissions declared in the compiler-generated -// wrapper-runner Role (05-post-bootstrap/wrapper-runner.yaml). +// RBAC before the Job pod runs and fails with a Forbidden error. The checks +// mirror the permissions declared in the compiler-generated +// dispatcher-runner Role (05-post-bootstrap/dispatcher-runner.yaml). func (r *PackExecutionReconciler) isDispatcherRunnerRBACReady(ctx context.Context, pe *dispatcherv1alpha1.PackExecution) (bool, string, error) { - saUser := "system:serviceaccount:" + pe.Namespace + ":wrapper-runner" + saUser := "system:serviceaccount:" + pe.Namespace + ":dispatcher-runner" checks := []struct { verb string group string resource string }{ - {"list", "infrastructure.ontai.dev", "infrastructurepackexecutions"}, - {"get", "infrastructure.ontai.dev", "infrastructurerunnerconfigs"}, - {"create", "infrastructure.ontai.dev", "packoperationresults"}, - {"delete", "infrastructure.ontai.dev", "packoperationresults"}, + {"list", "seam.ontai.dev", "packexecutions"}, + {"get", "seam.ontai.dev", "runnerconfigs"}, + {"create", "seam.ontai.dev", "packlogs"}, + {"delete", "seam.ontai.dev", "packlogs"}, } for _, chk := range checks { @@ -888,7 +888,7 @@ func (r *PackExecutionReconciler) isDispatcherRunnerRBACReady(ctx context.Contex if !sar.Status.Allowed { reason := sar.Status.Reason if reason == "" { - reason = fmt.Sprintf("wrapper-runner cannot %s %s in %s: permission denied", chk.verb, chk.resource, chk.group) + reason = fmt.Sprintf("dispatcher-runner cannot %s %s in %s: permission denied", chk.verb, chk.resource, chk.group) } return false, reason, nil } @@ -930,8 +930,8 @@ func (r *PackExecutionReconciler) buildPackDeployJob( Namespace: pe.Namespace, Labels: map[string]string{ kueueQueueLabel: packDeployQueue, - "app.kubernetes.io/part-of": "wrapper", - "infrastructure.ontai.dev/pe-name": pe.Name, + "app.kubernetes.io/part-of": "dispatcher", + "seam.ontai.dev/pe-name": pe.Name, }, OwnerReferences: []metav1.OwnerReference{ { @@ -950,7 +950,7 @@ func (r *PackExecutionReconciler) buildPackDeployJob( BackoffLimit: int32Ptr(0), Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ - ServiceAccountName: wrapperRunnerServiceAccount, + ServiceAccountName: dispatcherRunnerServiceAccount, RestartPolicy: corev1.RestartPolicyNever, SecurityContext: &corev1.PodSecurityContext{ RunAsNonRoot: boolPtr(true), @@ -1050,9 +1050,9 @@ func (r *PackExecutionReconciler) SetupWithManager(mgr ctrl.Manager) error { }) rcObj := &unstructured.Unstructured{} rcObj.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "infrastructure.ontai.dev", + Group: "seam.ontai.dev", Version: "v1alpha1", - Kind: "InfrastructureRunnerConfig", + Kind: "RunnerConfig", }) return ctrl.NewControllerManagedBy(mgr). For(&dispatcherv1alpha1.PackExecution{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). diff --git a/internal/controller/packinstance_reconciler.go b/internal/controller/packinstance_reconciler.go index 58a24b3..1c5d8bf 100644 --- a/internal/controller/packinstance_reconciler.go +++ b/internal/controller/packinstance_reconciler.go @@ -32,7 +32,7 @@ const driftCheckInterval = 60 * time.Second // non-empty DeployedResources list. The deletion handler removes all deployed // resources from the target cluster before allowing the PackInstance to be deleted. // INV-006: no Jobs on the delete path. wrapper-schema.md §3, Decision 11. -const workloadCleanupFinalizer = "infrastructure.ontai.dev/workload-cleanup" +const workloadCleanupFinalizer = "seam.ontai.dev/workload-cleanup" // PackInstanceReconciler watches PackInstance CRs and reflects drift state from // the target cluster conductor's PackReceipt. diff --git a/test/unit/clusterpack_reconciler_test.go b/test/unit/clusterpack_reconciler_test.go index 7617eff..952c38b 100644 --- a/test/unit/clusterpack_reconciler_test.go +++ b/test/unit/clusterpack_reconciler_test.go @@ -41,7 +41,7 @@ func newClusterPack(name, namespace, version string) *dispatcherv1alpha1.PackDel ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, - Finalizers: []string{"infrastructure.ontai.dev/clusterpack-cleanup"}, + Finalizers: []string{"seam.ontai.dev/clusterpack-cleanup"}, }, Spec: dispatcherv1alpha1.PackDeliverySpec{ Version: version, @@ -116,7 +116,7 @@ func TestClusterPackReconciler_SignedTransitionsToAvailable(t *testing.T) { cp := newClusterPack("signed-pack", "infra-system", "v1.0.0") cp.Annotations = map[string]string{ "ontai.dev/pack-signature": "base64sig==", - "infrastructure.ontai.dev/spec-checksum-snapshot": cp.Spec.Checksum + "|" + cp.Spec.RegistryRef.URL + "|" + cp.Spec.RegistryRef.Digest + "|" + cp.Spec.Version, + "seam.ontai.dev/spec-checksum-snapshot": cp.Spec.Checksum + "|" + cp.Spec.RegistryRef.URL + "|" + cp.Spec.RegistryRef.Digest + "|" + cp.Spec.Version, } fakeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(cp). WithStatusSubresource(&dispatcherv1alpha1.PackDelivery{}).Build() @@ -192,7 +192,7 @@ func TestClusterPackReconciler_ImmutabilityViolation(t *testing.T) { cp := newClusterPack("immutable-pack", "infra-system", "v1.0.0") // Pre-set the snapshot annotation with a different checksum to simulate mutation. cp.Annotations = map[string]string{ - "infrastructure.ontai.dev/spec-checksum-snapshot": "sha256:different|old-url|old-digest|v0.9.0", + "seam.ontai.dev/spec-checksum-snapshot": "sha256:different|old-url|old-digest|v0.9.0", } fakeClient := fake.NewClientBuilder().WithScheme(s).WithObjects(cp). WithStatusSubresource(&dispatcherv1alpha1.PackDelivery{}).Build() @@ -292,7 +292,7 @@ func TestClusterPackReconciler_PackExecutionDeletedRecreatesPE(t *testing.T) { cp.Status.PackSignature = "base64sig==" cp.Annotations = map[string]string{ "ontai.dev/pack-signature": "base64sig==", - "infrastructure.ontai.dev/spec-checksum-snapshot": cp.Spec.Checksum + "|" + + "seam.ontai.dev/spec-checksum-snapshot": cp.Spec.Checksum + "|" + cp.Spec.RegistryRef.URL + "|" + cp.Spec.RegistryRef.Digest + "|" + cp.Spec.Version, } @@ -352,7 +352,7 @@ func TestClusterPackReconciler_RevokedNoRequeue(t *testing.T) { cp := newClusterPack("revoked-pack", "infra-system", "v1.0.0") // Pre-set revoked condition and snapshot. cp.Annotations = map[string]string{ - "infrastructure.ontai.dev/spec-checksum-snapshot": cp.Spec.Checksum + "|" + cp.Spec.RegistryRef.URL + "|" + cp.Spec.RegistryRef.Digest + "|" + cp.Spec.Version, + "seam.ontai.dev/spec-checksum-snapshot": cp.Spec.Checksum + "|" + cp.Spec.RegistryRef.URL + "|" + cp.Spec.RegistryRef.Digest + "|" + cp.Spec.Version, } cp.Status.Conditions = []metav1.Condition{ { diff --git a/test/unit/controller/helpers_test.go b/test/unit/controller/helpers_test.go index 80c9907..a18a071 100644 --- a/test/unit/controller/helpers_test.go +++ b/test/unit/controller/helpers_test.go @@ -114,9 +114,9 @@ func newPE(name, cpName, cpVersion string, cpUID types.UID, clusterRef, profileR func newRunnerConfig(clusterRef string, capCount int) *unstructured.Unstructured { rc := &unstructured.Unstructured{} rc.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "infrastructure.ontai.dev", + Group: "seam.ontai.dev", Version: "v1alpha1", - Kind: "InfrastructureRunnerConfig", + Kind: "RunnerConfig", }) rc.SetName(clusterRef) rc.SetNamespace("ont-system") @@ -139,9 +139,9 @@ func newRunnerConfig(clusterRef string, capCount int) *unstructured.Unstructured func newTalosCluster(clusterRef string, conductorReady bool) *unstructured.Unstructured { tc := &unstructured.Unstructured{} tc.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "infrastructure.ontai.dev", + Group: "seam.ontai.dev", Version: "v1alpha1", - Kind: "InfrastructureTalosCluster", + Kind: "TalosCluster", }) tc.SetName(clusterRef) tc.SetNamespace("seam-tenant-" + clusterRef) diff --git a/test/unit/controller/kueue_job_scheduling_test.go b/test/unit/controller/kueue_job_scheduling_test.go index c753670..aa6de12 100644 --- a/test/unit/controller/kueue_job_scheduling_test.go +++ b/test/unit/controller/kueue_job_scheduling_test.go @@ -73,12 +73,12 @@ func TestJobSubmission_KueueQueueLabel(t *testing.T) { } // Part-of label. - if v := job.Labels["app.kubernetes.io/part-of"]; v != "wrapper" { + if v := job.Labels["app.kubernetes.io/part-of"]; v != "dispatcher" { t.Errorf("app.kubernetes.io/part-of=%q, want wrapper", v) } // PE name label — used for filtering. - if v := job.Labels["infrastructure.ontai.dev/pe-name"]; v != pe.Name { + if v := job.Labels["seam.ontai.dev/pe-name"]; v != pe.Name { t.Errorf("infrastructure.ontai.dev/pe-name=%q, want %q", v, pe.Name) } } diff --git a/test/unit/controller/rollback_test.go b/test/unit/controller/rollback_test.go index 22c2160..ce47ef8 100644 --- a/test/unit/controller/rollback_test.go +++ b/test/unit/controller/rollback_test.go @@ -50,7 +50,7 @@ func buildRollbackCP(name, version, namespace string, targetRevision int64, rbac cp.Spec.WorkloadDigest = workloadDigest cp.Spec.TargetClusters = []string{"ccs-dev"} cp.Spec.RollbackToRevision = targetRevision - cp.Annotations["infrastructure.ontai.dev/spec-checksum-snapshot"] = "stale-checksum" + cp.Annotations["seam.ontai.dev/spec-checksum-snapshot"] = "stale-checksum" return cp } @@ -114,7 +114,7 @@ func TestClusterPackReconciler_Rollback_OneStep(t *testing.T) { if updated.Spec.RollbackToRevision != 0 { t.Errorf("spec.rollbackToRevision=%d, want 0 (cleared)", updated.Spec.RollbackToRevision) } - if _, ok := updated.Annotations["infrastructure.ontai.dev/spec-checksum-snapshot"]; ok { + if _, ok := updated.Annotations["seam.ontai.dev/spec-checksum-snapshot"]; ok { t.Error("spec-checksum-snapshot annotation should be removed after rollback") } } diff --git a/test/unit/packexecution_reconciler_test.go b/test/unit/packexecution_reconciler_test.go index 68a9d5b..d91cf15 100644 --- a/test/unit/packexecution_reconciler_test.go +++ b/test/unit/packexecution_reconciler_test.go @@ -121,9 +121,9 @@ func newRBACProfile(name, namespace string, provisioned bool) *unstructured.Unst func newRunnerConfig(clusterName string, capCount int) *unstructured.Unstructured { rc := &unstructured.Unstructured{} rc.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "infrastructure.ontai.dev", + Group: "seam.ontai.dev", Version: "v1alpha1", - Kind: "InfrastructureRunnerConfig", + Kind: "RunnerConfig", }) rc.SetName(clusterName) rc.SetNamespace("ont-system") @@ -146,9 +146,9 @@ func newRunnerConfig(clusterName string, capCount int) *unstructured.Unstructure func newTalosClusterWithConductorReady(clusterName string, conductorReady bool) *unstructured.Unstructured { tc := &unstructured.Unstructured{} tc.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "infrastructure.ontai.dev", + Group: "seam.ontai.dev", Version: "v1alpha1", - Kind: "InfrastructureTalosCluster", + Kind: "TalosCluster", }) tc.SetName(clusterName) tc.SetNamespace("seam-tenant-" + clusterName) @@ -741,9 +741,9 @@ func TestPackExecutionReconciler_Gate0_RunnerConfigCapabilitiesAppear(t *testing rcKey := types.NamespacedName{Name: "ccs-test", Namespace: "ont-system"} rcLive := &unstructured.Unstructured{} rcLive.SetGroupVersionKind(schema.GroupVersionKind{ - Group: "infrastructure.ontai.dev", + Group: "seam.ontai.dev", Version: "v1alpha1", - Kind: "InfrastructureRunnerConfig", + Kind: "RunnerConfig", }) if err := cl.Get(context.Background(), rcKey, rcLive); err != nil { t.Fatalf("get RunnerConfig: %v", err) @@ -759,7 +759,7 @@ func TestPackExecutionReconciler_Gate0_RunnerConfigCapabilitiesAppear(t *testing } // Verify capabilities stored before proceeding. rcCheck := &unstructured.Unstructured{} - rcCheck.SetGroupVersionKind(schema.GroupVersionKind{Group: "infrastructure.ontai.dev", Version: "v1alpha1", Kind: "InfrastructureRunnerConfig"}) + rcCheck.SetGroupVersionKind(schema.GroupVersionKind{Group: "seam.ontai.dev", Version: "v1alpha1", Kind: "RunnerConfig"}) if err := cl.Get(context.Background(), rcKey, rcCheck); err != nil { t.Fatalf("get RunnerConfig after update: %v", err) } diff --git a/test/unit/role_independence_test.go b/test/unit/role_independence_test.go index f0b359d..cc9e2b3 100644 --- a/test/unit/role_independence_test.go +++ b/test/unit/role_independence_test.go @@ -129,7 +129,7 @@ func TestRoleIndependence_PackExecution_ManagementCluster_JobNamespace(t *testin // The Job must carry the management cluster reference in its labels. job := jobList.Items[0] - if got := job.Labels["infrastructure.ontai.dev/pe-name"]; got != pe.Name { + if got := job.Labels["seam.ontai.dev/pe-name"]; got != pe.Name { t.Errorf("Job label pe-name = %q, want %q", got, pe.Name) } } @@ -248,7 +248,7 @@ func TestRoleIndependence_ClusterPack_SignaturePathUnchanged(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "cilium", Namespace: "infra-system", - Finalizers: []string{"infrastructure.ontai.dev/clusterpack-cleanup"}, + Finalizers: []string{"seam.ontai.dev/clusterpack-cleanup"}, Annotations: map[string]string{ "ontai.dev/pack-signature": "mgmt-conductor-sig==", }, From 938b3482d161713f2ce2df7f3994bdcbd76bb551 Mon Sep 17 00:00:00 2001 From: ontave Date: Wed, 20 May 2026 20:42:21 +0200 Subject: [PATCH 14/19] fix(dispatcher): PackDelivery lookup in seam-system, not pe namespace PackExecution reconciler was looking for PackDelivery in pe.Namespace (seam-tenant-{cluster}) but PackDeliveries live in seam-system. This caused Gate 1 to block indefinitely with "ClusterPack not found". Fix: hardcode seam-system in the Gate 1 lookup. Update all test fixtures to use seam-system for PackDelivery namespace. --- .../controller/packexecution_reconciler.go | 4 ++-- .../controller/packexecution_gates_test.go | 8 +++---- .../controller/packinstance_lifecycle_test.go | 20 ++++++++--------- test/unit/packexecution_reconciler_test.go | 22 +++++++++---------- test/unit/role_independence_test.go | 6 ++--- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/internal/controller/packexecution_reconciler.go b/internal/controller/packexecution_reconciler.go index 20aa571..24519ad 100644 --- a/internal/controller/packexecution_reconciler.go +++ b/internal/controller/packexecution_reconciler.go @@ -218,9 +218,9 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques pe.Generation, ) - // Gate 1: Signature gate. + // Gate 1: Signature gate. PackDeliveries are always in seam-system (management namespace). cp := &dispatcherv1alpha1.PackDelivery{} - cpKey := client.ObjectKey{Name: pe.Spec.PackDeliveryRef.Name, Namespace: pe.Namespace} + cpKey := client.ObjectKey{Name: pe.Spec.PackDeliveryRef.Name, Namespace: "seam-system"} if err := r.Client.Get(ctx, cpKey, cp); err != nil { if apierrors.IsNotFound(err) { conditions.SetCondition( diff --git a/test/unit/controller/packexecution_gates_test.go b/test/unit/controller/packexecution_gates_test.go index 2f5101a..b43bbb1 100644 --- a/test/unit/controller/packexecution_gates_test.go +++ b/test/unit/controller/packexecution_gates_test.go @@ -40,7 +40,7 @@ import ( // AC-2 gate: ClusterPack signature contract. func TestAC2_Gate1_PackExecution_BlockedWhenUnsigned(t *testing.T) { s := buildTestScheme(t) - cp := newSignedCP("unsigned-pack-ac2", "v1.0.0", "infra-system") + cp := newSignedCP("unsigned-pack-ac2", "v1.0.0", "seam-system") cp.Status.Signed = false cp.Annotations["ontai.dev/pack-signature"] = "" pe := newPE("pe-ac2-gate1", "unsigned-pack-ac2", "v1.0.0", cp.UID, "cluster-ac2g1", "profile-ac2g1", "infra-system") @@ -92,7 +92,7 @@ func TestAC2_Gate1_PackExecution_BlockedWhenUnsigned(t *testing.T) { // AC-2 gate: PermissionSnapshot freshness contract. func TestAC2_Gate3_PackExecution_BlockedWhenSnapshotStale(t *testing.T) { s := buildTestScheme(t) - cp := newSignedCP("stale-snap-pack-ac2", "v1.0.0", "infra-system") + cp := newSignedCP("stale-snap-pack-ac2", "v1.0.0", "seam-system") pe := newPE("pe-ac2-gate3", "stale-snap-pack-ac2", "v1.0.0", cp.UID, "cluster-ac2g3", "profile-ac2g3", "infra-system") tc := newTalosCluster("cluster-ac2g3", true) rc := newRunnerConfig("cluster-ac2g3", 1) @@ -143,7 +143,7 @@ func TestAC2_Gate3_PackExecution_BlockedWhenSnapshotStale(t *testing.T) { // AC-2 gate: RBACProfile provisioning contract. func TestAC2_Gate4_PackExecution_BlockedWhenRBACNotProvisioned(t *testing.T) { s := buildTestScheme(t) - cp := newSignedCP("rbac-pack-ac2", "v1.0.0", "infra-system") + cp := newSignedCP("rbac-pack-ac2", "v1.0.0", "seam-system") pe := newPE("pe-ac2-gate4", "rbac-pack-ac2", "v1.0.0", cp.UID, "cluster-ac2g4", "profile-ac2g4", "infra-system") tc := newTalosCluster("cluster-ac2g4", true) rc := newRunnerConfig("cluster-ac2g4", 1) @@ -195,7 +195,7 @@ func TestAC2_Gate4_PackExecution_BlockedWhenRBACNotProvisioned(t *testing.T) { // AC-2 gate: ClusterPack revocation contract (human intervention required). func TestAC2_Gate2_PackExecution_BlockedWhenRevoked(t *testing.T) { s := buildTestScheme(t) - cp := newSignedCP("revoked-pack-ac2", "v1.0.0", "infra-system") + cp := newSignedCP("revoked-pack-ac2", "v1.0.0", "seam-system") conditions.SetCondition( &cp.Status.Conditions, conditions.ConditionTypeClusterPackRevoked, diff --git a/test/unit/controller/packinstance_lifecycle_test.go b/test/unit/controller/packinstance_lifecycle_test.go index 0693de2..0ae3e73 100644 --- a/test/unit/controller/packinstance_lifecycle_test.go +++ b/test/unit/controller/packinstance_lifecycle_test.go @@ -55,7 +55,7 @@ func allGatesSetup(t *testing.T, peName, cpName, cpVersion, clusterRef, profileR t.Helper() s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "infra-system") + cp := newSignedCP(cpName, cpVersion, "seam-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) rc := newRunnerConfig(clusterRef, 1) // gate 0: RunnerConfig with 1 capability @@ -196,7 +196,7 @@ func TestWaitingForCluster_TalosClusterAbsent(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "infra-system") + cp := newSignedCP(cpName, cpVersion, "seam-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") fakeClient := fake.NewClientBuilder().WithScheme(s). @@ -354,7 +354,7 @@ func TestGate0_RunnerConfigAbsent(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "infra-system") + cp := newSignedCP(cpName, cpVersion, "seam-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) // TalosCluster present; no RunnerConfig @@ -414,7 +414,7 @@ func TestGate1_SignaturePending(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "infra-system") + cp := newSignedCP(cpName, cpVersion, "seam-system") cp.Status.Signed = false // Override: not yet signed. pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) @@ -478,7 +478,7 @@ func TestGate2_PackRevoked(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "infra-system") + cp := newSignedCP(cpName, cpVersion, "seam-system") cp.Status.Conditions = []metav1.Condition{{ Type: conditions.ConditionTypeClusterPackRevoked, Status: metav1.ConditionTrue, @@ -547,7 +547,7 @@ func TestGate3_PermissionSnapshotOutOfSync(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "infra-system") + cp := newSignedCP(cpName, cpVersion, "seam-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) rc := newRunnerConfig(clusterRef, 1) // gate 0 must clear to reach gate 3 @@ -611,7 +611,7 @@ func TestGate4_RBACProfileNotProvisioned(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "infra-system") + cp := newSignedCP(cpName, cpVersion, "seam-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) rc := newRunnerConfig(clusterRef, 1) // gate 0 must clear to reach gate 4 @@ -679,7 +679,7 @@ func TestConductorReady_ManagementClusterFallback_SeamSystem(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "infra-system") + cp := newSignedCP(cpName, cpVersion, "seam-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") // Management cluster TalosCluster lives in seam-system, not seam-tenant-*. @@ -784,7 +784,7 @@ func TestConductorReady_RunnerConfigAbsent_ReturnsFalse(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "infra-system") + cp := newSignedCP(cpName, cpVersion, "seam-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) // TalosCluster present, RunnerConfig absent @@ -839,7 +839,7 @@ func TestConductorReady_RunnerConfigEmptyCapabilities_ReturnsFalse(t *testing.T) ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "infra-system") + cp := newSignedCP(cpName, cpVersion, "seam-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) rc := newRunnerConfig(clusterRef, 0) // capCount=0 → empty capabilities list diff --git a/test/unit/packexecution_reconciler_test.go b/test/unit/packexecution_reconciler_test.go index d91cf15..5a9ad9d 100644 --- a/test/unit/packexecution_reconciler_test.go +++ b/test/unit/packexecution_reconciler_test.go @@ -191,7 +191,7 @@ var _ controller.RBACReadyChecker = rbacAllowedStub // the ClusterPack is not yet signed (gate 0 already cleared via ConductorReady=True). func TestPackExecutionReconciler_Gate1_SignaturePending(t *testing.T) { s := newPackExecutionScheme(t) - cp := newClusterPack("my-pack", "infra-system", "v1.0.0") + cp := newClusterPack("my-pack", "seam-system", "v1.0.0") pe := newPackExecution("exec-1", "infra-system", "my-pack", "v1.0.0", "cluster-a", "profile-a") // TalosCluster + RunnerConfig with capabilities satisfies gate 0; gate 1 is the first to block. tc := newTalosClusterWithConductorReady("cluster-a", true) @@ -234,7 +234,7 @@ func TestPackExecutionReconciler_Gate1_SignaturePending(t *testing.T) { // sets PackRevoked on the PackExecution and does not requeue. func TestPackExecutionReconciler_Gate2_PackRevoked(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") cp.Status.Conditions = []metav1.Condition{ { Type: conditions.ConditionTypeClusterPackRevoked, @@ -286,7 +286,7 @@ func TestPackExecutionReconciler_Gate2_PackRevoked(t *testing.T) { // when the PermissionSnapshot is not current. func TestPackExecutionReconciler_Gate3_SnapshotOutOfSync(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") pe := newPackExecution("exec-snap", "infra-system", "my-pack", "v1.0.0", "cluster-a", "profile-a") ps := newPermissionSnapshot("snapshot-cluster-a", "seam-system", false) profile := newRBACProfile("profile-a", "seam-tenant-cluster-a", true) @@ -336,7 +336,7 @@ func TestPackExecutionReconciler_Gate3_SnapshotOutOfSync(t *testing.T) { // requeues when the RBACProfile is not yet provisioned. func TestPackExecutionReconciler_Gate4_RBACProfileNotProvisioned(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") pe := newPackExecution("exec-rbac", "infra-system", "my-pack", "v1.0.0", "cluster-a", "profile-a") ps := newPermissionSnapshot("snapshot-cluster-a", "seam-system", true) profile := newRBACProfile("profile-a", "seam-tenant-cluster-a", false) @@ -387,7 +387,7 @@ func TestPackExecutionReconciler_Gate4_RBACProfileNotProvisioned(t *testing.T) { // permanently block first-deploy because the profile only exists after the Job runs. func TestPackExecutionReconciler_Gate4_AbsentProfileAllowsJobSubmission(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") pe := newPackExecution("exec-absent-profile", "infra-system", "my-pack", "v1.0.0", "cluster-a", "profile-absent") ps := newPermissionSnapshot("snapshot-cluster-a", "seam-system", true) tc := newTalosClusterWithConductorReady("cluster-a", true) @@ -433,7 +433,7 @@ func TestPackExecutionReconciler_Gate4_AbsentProfileAllowsJobSubmission(t *testi // submitted with the Kueue queue label. Gap 27. func TestPackExecutionReconciler_AllGatesClear_JobSubmitted(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") pe := newPackExecution("exec-submit", "infra-system", "my-pack", "v1.0.0", "cluster-a", "profile-a") ps := newPermissionSnapshot("snapshot-cluster-a", "seam-system", true) profile := newRBACProfile("profile-a", "seam-tenant-cluster-a", true) @@ -508,7 +508,7 @@ func TestPackExecutionReconciler_AllGatesClear_JobSubmitted(t *testing.T) { // condition is initialized on first reconcile. func TestPackExecutionReconciler_LineageSyncedInitialized(t *testing.T) { s := newPackExecutionScheme(t) - cp := newClusterPack("my-pack", "infra-system", "v1.0.0") + cp := newClusterPack("my-pack", "seam-system", "v1.0.0") pe := newPackExecution("exec-lineage", "infra-system", "my-pack", "v1.0.0", "cluster-a", "profile-a") fakeClient := fake.NewClientBuilder().WithScheme(s). @@ -544,7 +544,7 @@ func TestPackExecutionReconciler_LineageSyncedInitialized(t *testing.T) { // blocks Job submission, sets Waiting condition, and requeues. func TestPackExecutionReconciler_Gate0_ConductorReadyAbsent(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") pe := newPackExecution("exec-gate0-absent", "infra-system", "my-pack", "v1.0.0", "cluster-b", "profile-b") // No TalosCluster for cluster-b in the fake client. @@ -585,7 +585,7 @@ func TestPackExecutionReconciler_Gate0_ConductorReadyAbsent(t *testing.T) { // submission, sets Waiting condition, and requeues. func TestPackExecutionReconciler_Gate0_ConductorReadyFalse(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") pe := newPackExecution("exec-gate0-false", "infra-system", "my-pack", "v1.0.0", "cluster-b", "profile-b") // TalosCluster exists but ConductorReady=False. tc := newTalosClusterWithConductorReady("cluster-b", false) @@ -627,7 +627,7 @@ func TestPackExecutionReconciler_Gate0_ConductorReadyFalse(t *testing.T) { func TestPackExecutionReconciler_Gate0_ConductorReadyTrue_ProceedsToSignatureGate(t *testing.T) { s := newPackExecutionScheme(t) // Use an unsigned ClusterPack so gate 1 (signature) blocks after gate 0 passes. - cp := newClusterPack("my-pack", "infra-system", "v1.0.0") + cp := newClusterPack("my-pack", "seam-system", "v1.0.0") pe := newPackExecution("exec-gate0-true", "infra-system", "my-pack", "v1.0.0", "cluster-c", "profile-c") // TalosCluster + RunnerConfig with capabilities — gate 0 passes. tc := newTalosClusterWithConductorReady("cluster-c", true) @@ -683,7 +683,7 @@ func TestPackExecutionReconciler_Gate0_ConductorReadyTrue_ProceedsToSignatureGat // RunnerConfig watch in SetupWithManager fires at the right time. func TestPackExecutionReconciler_Gate0_RunnerConfigCapabilitiesAppear(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("cilium", "seam-tenant-ccs-test", "v1.2.0") + cp := newSignedClusterPack("cilium", "seam-system", "v1.2.0") pe := newPackExecution("cilium-exec", "seam-tenant-ccs-test", "cilium", "v1.2.0", "ccs-test", "rbac-wrapper") snapshot := newPermissionSnapshot("snapshot-ccs-test", "security-system", true) diff --git a/test/unit/role_independence_test.go b/test/unit/role_independence_test.go index cc9e2b3..4cb7fb7 100644 --- a/test/unit/role_independence_test.go +++ b/test/unit/role_independence_test.go @@ -42,7 +42,7 @@ import ( // The reconciler must not special-case the management cluster namespace. func TestRoleIndependence_PackExecution_ManagementCluster_Gate0Blocks(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("cilium", "infra-system", "v1.16.0") + cp := newSignedClusterPack("cilium", "seam-system", "v1.16.0") // TargetClusterRef=ccs-mgmt: the management cluster treated as a tenant. pe := newPackExecution("cilium-exec-mgmt", "infra-system", "cilium", "v1.16.0", "ccs-mgmt", "profile-mgmt") // No TalosCluster or RunnerConfig in the fake client for ccs-mgmt. @@ -84,7 +84,7 @@ func TestRoleIndependence_PackExecution_ManagementCluster_JobNamespace(t *testin tenantNS = "seam-tenant-ccs-mgmt" ) s := newPackExecutionScheme(t) - cp := newSignedClusterPack("cilium", "infra-system", "v1.16.0") + cp := newSignedClusterPack("cilium", "seam-system", "v1.16.0") // PackExecution lives in infra-system; Job will be submitted there too (PE namespace). pe := newPackExecution("cilium-exec-mgmt", "infra-system", "cilium", "v1.16.0", clusterRef, "cilium") ps := newPermissionSnapshot("snapshot-"+clusterRef, "seam-system", true) @@ -148,7 +148,7 @@ func TestRoleIndependence_PackExecution_ManagementCluster_PackInstance(t *testin peNS = "infra-system" ) s := newPackExecutionScheme(t) - cp := newSignedClusterPack("cilium", peNS, "v1.16.0") + cp := newSignedClusterPack("cilium", "seam-system", "v1.16.0") pe := newPackExecution(peName, peNS, "cilium", "v1.16.0", clusterRef, "cilium") ps := newPermissionSnapshot("snapshot-"+clusterRef, "seam-system", true) profile := newRBACProfile("cilium", tenantNS, true) From a92a7a2a315bc6e4a550969731c482e8518334e4 Mon Sep 17 00:00:00 2001 From: ontave Date: Wed, 20 May 2026 20:44:49 +0200 Subject: [PATCH 15/19] fix(dispatcher): PermissionSnapshot fallback to snapshot-management for mgmt cluster The management cluster's PermissionSnapshot is named snapshot-management (role-derived) not snapshot-ccs-mgmt (cluster-name-derived). G-BL-SNAPSHOT-ALIAS. isPermissionSnapshotCurrent now tries snapshot-{clusterRef} first then falls back to snapshot-management, matching Guardian's naming convention for the management cluster self-import case. --- .../controller/packexecution_reconciler.go | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/internal/controller/packexecution_reconciler.go b/internal/controller/packexecution_reconciler.go index 24519ad..501d388 100644 --- a/internal/controller/packexecution_reconciler.go +++ b/internal/controller/packexecution_reconciler.go @@ -710,37 +710,41 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques // isPermissionSnapshotCurrent reads the PermissionSnapshot for the target cluster // via unstructured to avoid importing guardian types. Returns true if the snapshot -// has a Fresh=True condition. PermissionSnapshots live exclusively in seam-system -// and are named "snapshot-{clusterRef}". guardian-schema.md §PS condition vocabulary. +// has a Fresh=True condition. PermissionSnapshots live exclusively in seam-system. +// Naming: management cluster uses "snapshot-management"; all other clusters use +// "snapshot-{clusterRef}". Tries clusterRef first then falls back to "management" +// so the management cluster self-import case (ccs-mgmt targeting itself) works. +// guardian-schema.md §PS condition vocabulary. G-BL-SNAPSHOT-ALIAS. // Returns false (not error) if the snapshot is not found or not fresh. func (r *PackExecutionReconciler) isPermissionSnapshotCurrent(ctx context.Context, pe *dispatcherv1alpha1.PackExecution) (bool, error) { - ps := &unstructured.Unstructured{} - ps.SetGroupVersionKind(schema.GroupVersionKind{ + psGVK := schema.GroupVersionKind{ Group: "guardian.ontai.dev", Version: "v1alpha1", Kind: "PermissionSnapshot", - }) - psKey := types.NamespacedName{ - Name: "snapshot-" + pe.Spec.TargetClusterRef, - Namespace: "seam-system", } - if err := r.Client.Get(ctx, psKey, ps); err != nil { - if apierrors.IsNotFound(err) { - return false, nil + // Try snapshot-{clusterRef} first; fall back to snapshot-management for + // the management cluster whose snapshot name is role-derived, not cluster-derived. + for _, name := range []string{"snapshot-" + pe.Spec.TargetClusterRef, "snapshot-management"} { + ps := &unstructured.Unstructured{} + ps.SetGroupVersionKind(psGVK) + if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: "seam-system"}, ps); err != nil { + if apierrors.IsNotFound(err) { + continue + } + return false, err } - return false, err - } - conditions, found, err := unstructured.NestedSlice(ps.Object, "status", "conditions") - if err != nil || !found { - return false, nil - } - for _, raw := range conditions { - cond, ok := raw.(map[string]interface{}) - if !ok { + conditions, found, err := unstructured.NestedSlice(ps.Object, "status", "conditions") + if err != nil || !found { continue } - if cond["type"] == "Fresh" && cond["status"] == "True" { - return true, nil + for _, raw := range conditions { + cond, ok := raw.(map[string]interface{}) + if !ok { + continue + } + if cond["type"] == "Fresh" && cond["status"] == "True" { + return true, nil + } } } return false, nil From c0d36f584fc4826be7e166f5f985d95606d3d02b Mon Sep 17 00:00:00 2001 From: ontave Date: Wed, 20 May 2026 20:52:50 +0200 Subject: [PATCH 16/19] fix(dispatcher): read runnerImage from RunnerConfig for pack-deploy Job buildPackDeployJob was using a hardcoded conductorImageDefault ("conductor-execute:dev") instead of reading the RunnerConfig's spec.runnerImage. Changed buildPackDeployJob to accept runnerImage parameter; call site reads it from RunnerConfig in ont-system with conductorImageDefault as fallback. --- internal/controller/packexecution_reconciler.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/controller/packexecution_reconciler.go b/internal/controller/packexecution_reconciler.go index 501d388..591cd62 100644 --- a/internal/controller/packexecution_reconciler.go +++ b/internal/controller/packexecution_reconciler.go @@ -688,7 +688,19 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques // Step J — Submit pack-deploy Job via Kueue. Direct Job creation without // Kueue admission is an invariant violation. wrapper-design.md §4. - job := r.buildPackDeployJob(pe, cp, jobName) + // Read the RunnerConfig to get the executor image (conductor-exec:v...). + // Falls back to conductorImageDefault if RunnerConfig absent or image empty. + runnerImage := conductorImageDefault + { + rcObj := &unstructured.Unstructured{} + rcObj.SetGroupVersionKind(schema.GroupVersionKind{Group: "seam.ontai.dev", Version: "v1alpha1", Kind: "RunnerConfig"}) + if getErr := r.Client.Get(ctx, types.NamespacedName{Name: pe.Spec.TargetClusterRef, Namespace: "ont-system"}, rcObj); getErr == nil { + if img, found, _ := unstructured.NestedString(rcObj.Object, "spec", "runnerImage"); found && img != "" { + runnerImage = img + } + } + } + job := r.buildPackDeployJob(pe, cp, jobName, runnerImage) if err := r.Client.Create(ctx, job); err != nil { return ctrl.Result{}, fmt.Errorf("failed to create pack-deploy Job %s: %w", jobName, err) } @@ -918,9 +930,9 @@ func (r *PackExecutionReconciler) buildPackDeployJob( pe *dispatcherv1alpha1.PackExecution, cp *dispatcherv1alpha1.PackDelivery, jobName string, + conductorImage string, ) *batchv1.Job { ttl := packDeployJobTTL - conductorImage := conductorImageDefault // resultCMName is the packExecutionRef passed to the Conductor Job via // OPERATION_RESULT_CM. Conductor uses this value as the label key for From 6b707be38e293e6cb04db918822efe4ae19b0c06 Mon Sep 17 00:00:00 2001 From: ontave Date: Wed, 20 May 2026 21:01:10 +0200 Subject: [PATCH 17/19] fix: dispatcher-leader lease name; add generated CRDs cmd/wrapper/main.go: LeaderElectionID was "wrapper-leader"; corrected to "dispatcher-leader" per post-migration naming. config/crd/: add generated seam.ontai.dev CRD manifests for PackDelivery, PackExecution, PackInstalled, PackLog, PackReceipt. api/seam/v1alpha1/zz_generated.deepcopy.go: regenerated deep copy. --- api/seam/v1alpha1/zz_generated.deepcopy.go | 70 ++-- cmd/wrapper/main.go | 4 +- config/crd/seam.ontai.dev_packdeliveries.yaml | 350 ++++++++++++++++++ config/crd/seam.ontai.dev_packexecutions.yaml | 251 +++++++++++++ config/crd/seam.ontai.dev_packinstalleds.yaml | 248 +++++++++++++ config/crd/seam.ontai.dev_packlogs.yaml | 318 ++++++++++++++++ config/crd/seam.ontai.dev_packreceipts.yaml | 222 +++++++++++ 7 files changed, 1426 insertions(+), 37 deletions(-) create mode 100644 config/crd/seam.ontai.dev_packdeliveries.yaml create mode 100644 config/crd/seam.ontai.dev_packexecutions.yaml create mode 100644 config/crd/seam.ontai.dev_packinstalleds.yaml create mode 100644 config/crd/seam.ontai.dev_packlogs.yaml create mode 100644 config/crd/seam.ontai.dev_packreceipts.yaml diff --git a/api/seam/v1alpha1/zz_generated.deepcopy.go b/api/seam/v1alpha1/zz_generated.deepcopy.go index 8495f5e..0ffefd0 100644 --- a/api/seam/v1alpha1/zz_generated.deepcopy.go +++ b/api/seam/v1alpha1/zz_generated.deepcopy.go @@ -375,40 +375,6 @@ func (in *PackInstalledList) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RemediationPolicyRefSpec) DeepCopyInto(out *RemediationPolicyRefSpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemediationPolicyRefSpec. -func (in *RemediationPolicyRefSpec) DeepCopy() *RemediationPolicyRefSpec { - if in == nil { - return nil - } - out := new(RemediationPolicyRefSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RemediationAttemptRecord) DeepCopyInto(out *RemediationAttemptRecord) { - *out = *in - if in.LastAttemptAt != nil { - in, out := &in.LastAttemptAt, &out.LastAttemptAt - *out = (*in).DeepCopy() - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemediationAttemptRecord. -func (in *RemediationAttemptRecord) DeepCopy() *RemediationAttemptRecord { - if in == nil { - return nil - } - out := new(RemediationAttemptRecord) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PackInstalledSpec) DeepCopyInto(out *PackInstalledSpec) { *out = *in @@ -476,7 +442,7 @@ func (in *PackLog) DeepCopyInto(out *PackLog) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackLog. @@ -813,3 +779,37 @@ func (in *PackRegistryRef) DeepCopy() *PackRegistryRef { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RemediationAttemptRecord) DeepCopyInto(out *RemediationAttemptRecord) { + *out = *in + if in.LastAttemptAt != nil { + in, out := &in.LastAttemptAt, &out.LastAttemptAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemediationAttemptRecord. +func (in *RemediationAttemptRecord) DeepCopy() *RemediationAttemptRecord { + if in == nil { + return nil + } + out := new(RemediationAttemptRecord) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RemediationPolicyRefSpec) DeepCopyInto(out *RemediationPolicyRefSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemediationPolicyRefSpec. +func (in *RemediationPolicyRefSpec) DeepCopy() *RemediationPolicyRefSpec { + if in == nil { + return nil + } + out := new(RemediationPolicyRefSpec) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/wrapper/main.go b/cmd/wrapper/main.go index e09c104..a1d5d71 100644 --- a/cmd/wrapper/main.go +++ b/cmd/wrapper/main.go @@ -74,14 +74,14 @@ func main() { os.Exit(1) } - // CI-INV-004: leader election required. Lease name: wrapper-leader. + // CI-INV-004: leader election required. Lease name: dispatcher-leader. // Lease namespace: seam-system (canonical operator namespace). mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{BindAddress: metricsAddr}, HealthProbeBindAddress: healthProbeAddr, LeaderElection: enableLeaderElection, - LeaderElectionID: "wrapper-leader", + LeaderElectionID: "dispatcher-leader", LeaderElectionNamespace: "seam-system", }) if err != nil { diff --git a/config/crd/seam.ontai.dev_packdeliveries.yaml b/config/crd/seam.ontai.dev_packdeliveries.yaml new file mode 100644 index 0000000..ab92c18 --- /dev/null +++ b/config/crd/seam.ontai.dev_packdeliveries.yaml @@ -0,0 +1,350 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: packdeliveries.seam.ontai.dev +spec: + group: seam.ontai.dev + names: + kind: PackDelivery + listKind: PackDeliveryList + plural: packdeliveries + shortNames: + - pd + singular: packdelivery + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.version + name: Version + type: string + - jsonPath: .status.signed + name: Signed + type: boolean + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + PackDelivery is the dispatcher CRD for pack registration. + Records an OCI artifact that has been compiled and is ready for runtime delivery. + Spec is immutable after creation. wrapper-schema.md §3. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + PackDeliverySpec defines the desired state of a PackDelivery. + All fields are immutable after creation. wrapper-schema.md §3. + properties: + basePackName: + description: BasePackName is the logical pack name shared across versions + (e.g., "nginx-ingress"). + type: string + chartName: + description: ChartName is the name of the Helm chart used to compile + this pack. + type: string + chartURL: + description: ChartURL is the URL of the Helm chart repository used + to compile this pack. + type: string + chartVersion: + description: ChartVersion is the version of the Helm chart used to + compile this pack. + type: string + checksum: + description: Checksum is the content-addressed checksum of the full + artifact manifest set. + type: string + clusterScopedDigest: + description: |- + ClusterScopedDigest is the OCI digest of the cluster-scoped non-RBAC layer. + Applied after guardian RBAC intake and before workload manifests. wrapper-schema.md §4. + type: string + executionOrder: + description: ExecutionOrder defines the ordered stages in which pack + manifests are applied. + items: + description: PackDeliveryStage is a single stage in the pack execution + order. + properties: + manifests: + description: Manifests is the list of manifest names to apply + in this stage. + items: + type: string + type: array + name: + description: 'Name is the stage name. Must be one of: rbac, + storage, stateful, stateless.' + enum: + - rbac + - storage + - stateful + - stateless + type: string + required: + - name + type: object + type: array + helmVersion: + description: HelmVersion is the version of the Helm SDK used to render + this pack. + type: string + lifecyclePolicies: + description: LifecyclePolicies controls artifact retention behavior. + properties: + retainOnDeletion: + default: true + description: |- + RetainOnDeletion controls whether the OCI artifact is retained when the + PackDelivery CR is deleted. Default: true (artifact retained). + type: boolean + type: object + lineage: + description: |- + Lineage is the sealed causal chain record for this root declaration. + Authored once at object creation time and immutable thereafter. + seam-core-schema.md §5, CLAUDE.md §14 Decision 1. + properties: + creatingOperator: + description: |- + CreatingOperator identifies the Seam Operator that created this object. + This is a structured identity carrying the operator name and its deployed + version at creation time. + properties: + name: + description: |- + Name is the canonical name of the Seam Operator (e.g., platform, guardian, + wrapper, conductor). + type: string + version: + description: |- + Version is the deployed version of the operator at the time the object was + created (e.g., v1.26.5-r3). This allows audit tooling to correlate objects + with the operator version that produced them. + type: string + required: + - name + - version + type: object + creationRationale: + description: |- + CreationRationale is the reason this object was created, drawn from the + Seam Core controlled vocabulary defined in rationale.go. It is not a + free-text field. + enum: + - ClusterProvision + - ClusterDecommission + - SecurityEnforcement + - PackExecution + - VirtualizationFulfillment + - ConductorAssignment + - VortexBinding + type: string + rootGenerationAtCreation: + description: |- + RootGenerationAtCreation is the metadata.generation of the root declaration + at the time this object was created. Together with RootUID, it provides a + complete temporal anchor for the derivation record. + format: int64 + type: integer + rootKind: + description: |- + RootKind is the kind of the root declaration that caused this object to + exist (e.g., TalosCluster, PackExecution, RBACPolicy). + type: string + rootName: + description: RootName is the name of the root declaration. + type: string + rootNamespace: + description: RootNamespace is the namespace of the root declaration. + type: string + rootUID: + description: |- + RootUID is the UID of the root declaration at the time this object was + created. Used to verify that no root declaration replacement has occurred. + type: string + required: + - creatingOperator + - creationRationale + - rootGenerationAtCreation + - rootKind + - rootName + - rootNamespace + - rootUID + type: object + provenance: + description: Provenance records build-time metadata for audit and + traceability. + properties: + buildID: + description: BuildID is the CI/CD build identifier that produced + this pack. + type: string + buildTimestamp: + description: BuildTimestamp is when the pack artifact was produced. + format: date-time + type: string + sourceRef: + description: SourceRef is the git reference (commit SHA or tag) + from which the pack was built. + type: string + type: object + rbacDigest: + description: |- + RBACDigest is the OCI digest of the RBAC layer of this PackDelivery artifact. + Contains ServiceAccount, Role, ClusterRole, RoleBinding, ClusterRoleBinding manifests. + type: string + registryRef: + description: RegistryRef identifies the OCI artifact for this pack. + Immutable after creation. + properties: + digest: + description: Digest is the OCI image digest (e.g., sha256:abc123...). + Immutable after creation. + type: string + url: + description: URL is the OCI registry URL including image name. + type: string + required: + - digest + - url + type: object + rollbackToRevision: + description: |- + RollbackToRevision instructs the dispatcher to restore this PackDelivery to the + artifact version deployed at PackLog revision N-1. When set, the PackDeliveryReconciler + reads PreviousClusterPackVersion/PreviousRBACDigest/PreviousWorkloadDigest from + the current PackLog, patches spec.version and spec.*Digest to those values, removes + the spec-checksum-snapshot annotation, and clears this field. Only one-step + rollback (to revision N-1) is supported per invocation. Set to 0 to disable. + Governor-controlled only. wrapper-schema.md §6.2. seam-core-schema.md §7.8. + format: int64 + type: integer + sourceBuildRef: + description: SourceBuildRef is an opaque reference to the build that + produced this pack. Informational. + type: string + targetClusters: + description: TargetClusters is the list of cluster names to which + this PackDelivery should be delivered. + items: + type: string + type: array + valuesFile: + description: ValuesFile is the path to the values file used during + pack compilation. + type: string + version: + description: Version is the semantic version of this pack. Immutable + after creation. + type: string + workloadDigest: + description: |- + WorkloadDigest is the OCI digest of the workload layer of this PackDelivery artifact. + Applied after guardian RBACProfile reaches provisioned=true. wrapper-schema.md §4. + type: string + required: + - registryRef + - version + type: object + status: + description: PackDeliveryStatus is the observed state of a PackDelivery. + properties: + conditions: + description: Conditions is the list of status conditions for this + PackDelivery. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: ObservedGeneration is the generation most recently reconciled. + format: int64 + type: integer + packSignature: + description: PackSignature is the base64-encoded Ed25519 signature + produced by the management cluster conductor. + type: string + signed: + description: Signed indicates whether the conductor signing loop has + signed this pack. + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/seam.ontai.dev_packexecutions.yaml b/config/crd/seam.ontai.dev_packexecutions.yaml new file mode 100644 index 0000000..4f158e8 --- /dev/null +++ b/config/crd/seam.ontai.dev_packexecutions.yaml @@ -0,0 +1,251 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: packexecutions.seam.ontai.dev +spec: + group: seam.ontai.dev + names: + kind: PackExecution + listKind: PackExecutionList + plural: packexecutions + shortNames: + - pe + singular: packexecution + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.packDeliveryRef.name + name: Pack + type: string + - jsonPath: .spec.targetClusterRef + name: Target + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + PackExecution is the dispatcher CRD for a runtime pack delivery request. + wrapper-schema.md §3. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + PackExecutionSpec defines the desired state of a PackExecution. + wrapper-schema.md §3. + properties: + admissionProfileRef: + description: AdmissionProfileRef is the name of the RBACProfile governing + this execution. + type: string + chartName: + description: ChartName is the Helm chart name. Carried from PackDelivery. + type: string + chartURL: + description: ChartURL is the Helm chart repository URL. Carried from + PackDelivery. + type: string + chartVersion: + description: ChartVersion is the Helm chart version for this execution. + Carried from PackDelivery. + type: string + helmVersion: + description: HelmVersion is the Helm SDK version used to render the + pack. Carried from PackDelivery. + type: string + lineage: + description: Lineage is the sealed causal chain record for this root + declaration. Immutable after creation. + properties: + creatingOperator: + description: |- + CreatingOperator identifies the Seam Operator that created this object. + This is a structured identity carrying the operator name and its deployed + version at creation time. + properties: + name: + description: |- + Name is the canonical name of the Seam Operator (e.g., platform, guardian, + wrapper, conductor). + type: string + version: + description: |- + Version is the deployed version of the operator at the time the object was + created (e.g., v1.26.5-r3). This allows audit tooling to correlate objects + with the operator version that produced them. + type: string + required: + - name + - version + type: object + creationRationale: + description: |- + CreationRationale is the reason this object was created, drawn from the + Seam Core controlled vocabulary defined in rationale.go. It is not a + free-text field. + enum: + - ClusterProvision + - ClusterDecommission + - SecurityEnforcement + - PackExecution + - VirtualizationFulfillment + - ConductorAssignment + - VortexBinding + type: string + rootGenerationAtCreation: + description: |- + RootGenerationAtCreation is the metadata.generation of the root declaration + at the time this object was created. Together with RootUID, it provides a + complete temporal anchor for the derivation record. + format: int64 + type: integer + rootKind: + description: |- + RootKind is the kind of the root declaration that caused this object to + exist (e.g., TalosCluster, PackExecution, RBACPolicy). + type: string + rootName: + description: RootName is the name of the root declaration. + type: string + rootNamespace: + description: RootNamespace is the namespace of the root declaration. + type: string + rootUID: + description: |- + RootUID is the UID of the root declaration at the time this object was + created. Used to verify that no root declaration replacement has occurred. + type: string + required: + - creatingOperator + - creationRationale + - rootGenerationAtCreation + - rootKind + - rootName + - rootNamespace + - rootUID + type: object + packDeliveryRef: + description: PackDeliveryRef identifies the PackDelivery to deploy. + properties: + name: + description: Name is the name of the PackDelivery CR. + minLength: 1 + type: string + version: + description: Version is the expected version of the PackDelivery. + Guards against name reuse. + minLength: 1 + type: string + required: + - name + - version + type: object + targetClusterRef: + description: TargetClusterRef is the name of the target cluster to + deliver the pack to. + type: string + required: + - packDeliveryRef + - targetClusterRef + type: object + status: + description: PackExecutionStatus is the observed state of a PackExecution. + properties: + conditions: + description: Conditions is the list of status conditions for this + PackExecution. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + jobName: + description: JobName is the name of the pack-deploy Kueue Job submitted + for this execution. + type: string + observedGeneration: + description: ObservedGeneration is the generation most recently reconciled. + format: int64 + type: integer + operationResultRef: + description: |- + OperationResultRef is the name of the PackLog CR written after + successful pack-deploy Job completion. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/seam.ontai.dev_packinstalleds.yaml b/config/crd/seam.ontai.dev_packinstalleds.yaml new file mode 100644 index 0000000..3dd1edd --- /dev/null +++ b/config/crd/seam.ontai.dev_packinstalleds.yaml @@ -0,0 +1,248 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: packinstalleds.seam.ontai.dev +spec: + group: seam.ontai.dev + names: + kind: PackInstalled + listKind: PackInstalledList + plural: packinstalleds + shortNames: + - pi + singular: packinstalled + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.packDeliveryRef + name: Pack + type: string + - jsonPath: .spec.targetClusterRef + name: Target + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + PackInstalled is the dispatcher CRD recording the delivered state of a pack on a cluster. + wrapper-schema.md §3. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + PackInstalledSpec defines the desired state of a PackInstalled. + wrapper-schema.md §3. + properties: + chartName: + description: ChartName is the Helm chart name. Carried from PackDelivery. + type: string + chartURL: + description: ChartURL is the Helm chart repository URL. Carried from + PackDelivery. + type: string + chartVersion: + description: ChartVersion is the Helm chart version for this instance. + Carried from PackDelivery. + type: string + dependencyPolicy: + description: DependencyPolicy defines behavior when a dependency reports + drift. + properties: + onDrift: + default: Warn + description: |- + OnDrift controls how this PackInstalled responds when a declared dependency + PackInstalled reports Drifted=True. + enum: + - Block + - Warn + - Ignore + type: string + type: object + dependsOn: + description: DependsOn is the list of pack base names that must be + Delivered before this instance. + items: + type: string + type: array + helmVersion: + description: HelmVersion is the Helm SDK version used to render the + pack. Carried from PackDelivery. + type: string + packDeliveryRef: + description: PackDeliveryRef is the name of the PackDelivery CR this + instance tracks. + type: string + remediationPolicyRef: + description: |- + RemediationPolicyRef is an optional reference to a RemediationPolicy CR + (conductor.ontai.dev) in the same namespace. When absent, Conductor Watchdog + applies the platform defaults (threshold=3, per-reason default strategies, + MaxAttempts=3, 5m window). + properties: + name: + description: Name is the RemediationPolicy CR name. + type: string + namespace: + description: Namespace is the namespace of the RemediationPolicy + CR. + type: string + required: + - name + - namespace + type: object + targetClusterRef: + description: TargetClusterRef is the name of the target cluster this + instance is installed on. + type: string + version: + description: Version is the pack version delivered to the target cluster. + type: string + required: + - packDeliveryRef + - targetClusterRef + - version + type: object + status: + description: PackInstalledStatus is the observed state of a PackInstalled. + properties: + conditions: + description: Conditions is the list of status conditions for this + PackInstalled. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + deliveredAt: + description: DeliveredAt records when the pack was most recently confirmed + delivered. + format: date-time + type: string + deployedResources: + description: |- + DeployedResources is the list of Kubernetes resources applied by the pack-deploy job. + Used by the PackInstalled deletion handler for cleanup. + items: + description: |- + DeployedResourceRef records a single Kubernetes resource applied + by the pack-deploy job. Used by the PackInstalled deletion handler to clean up + deployed workload when the PackDelivery is deleted. wrapper-schema.md §3. + properties: + apiVersion: + description: APIVersion is the Kubernetes apiVersion (e.g., + apps/v1, v1). + type: string + kind: + description: Kind is the Kubernetes resource Kind (e.g., Deployment, + Namespace). + type: string + name: + description: Name is the resource name. + type: string + namespace: + description: Namespace is the resource namespace. Empty for + cluster-scoped resources. + type: string + required: + - apiVersion + - kind + - name + type: object + type: array + driftSummary: + description: DriftSummary is a human-readable summary of the current + drift state. + type: string + observedGeneration: + description: ObservedGeneration is the generation most recently reconciled. + format: int64 + type: integer + upgradeDirection: + description: |- + UpgradeDirection records the version transition direction for the last deployment. + Initial: first deployment. Upgrade: newer version. Rollback: older version. Redeploy: same version. + enum: + - Initial + - Upgrade + - Rollback + - Redeploy + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/seam.ontai.dev_packlogs.yaml b/config/crd/seam.ontai.dev_packlogs.yaml new file mode 100644 index 0000000..e4fd58d --- /dev/null +++ b/config/crd/seam.ontai.dev_packlogs.yaml @@ -0,0 +1,318 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: packlogs.seam.ontai.dev +spec: + group: seam.ontai.dev + names: + kind: PackLog + listKind: PackLogList + plural: packlogs + shortNames: + - pl + singular: packlog + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.capability + name: Capability + type: string + - jsonPath: .spec.status + name: Status + type: string + - jsonPath: .spec.targetClusterRef + name: Cluster + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + PackLog is the immutable result record written by the Conductor + execute-mode Job after a pack-deploy capability completes. One PackLog + per PackExecution, created in namespace seam-tenant-{clusterName}. + seam-core-schema.md §8, Decision 11. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + PackLogSpec is the complete result document written by the + Conductor execute-mode Job before exit. Written by conductor; read by dispatcher. + seam-core-schema.md §8, Decision 11. + properties: + artifacts: + description: Artifacts is the list of artifacts produced by this execution. + items: + description: |- + PackLogArtifact is a structured reference to an artifact produced + by a pack-deploy execution. Never contains raw artifact content. + properties: + checksum: + description: 'Checksum is the content-addressed checksum. Format: + sha256:.' + type: string + kind: + description: 'Kind declares the artifact type. One of: ConfigMap, + Secret, OCIImage, S3Object.' + enum: + - ConfigMap + - Secret + - OCIImage + - S3Object + type: string + name: + description: Name is a logical identifier for this artifact. + type: string + reference: + description: Reference is the fully qualified reference for + the artifact kind. + type: string + required: + - kind + - name + - reference + type: object + type: array + capability: + description: Capability is the name of the Conductor capability that + produced this result. + type: string + completedAt: + description: CompletedAt is the time the capability execution finished. + format: date-time + type: string + deployedResources: + description: |- + DeployedResources is the list of Kubernetes resources applied during this + execution. Populated by pack-deploy on success. Used by PackInstalledReconciler + for deletion cleanup. + items: + description: |- + PackLogDeployedResource records a single Kubernetes resource applied + during a pack-deploy execution. + properties: + apiVersion: + description: APIVersion is the Kubernetes apiVersion (e.g., + apps/v1, v1). + type: string + kind: + description: Kind is the Kubernetes resource Kind (e.g., Deployment, + Namespace). + type: string + name: + description: Name is the resource name. + type: string + namespace: + description: Namespace is the resource namespace. Empty for + cluster-scoped resources. + type: string + required: + - apiVersion + - kind + - name + type: object + type: array + failureReason: + description: FailureReason is populated when Status is Failed. Nil + on success. + properties: + category: + description: Category classifies the failure domain. + enum: + - ValidationFailure + - CapabilityUnavailable + - ExecutionFailure + - ExternalDependencyFailure + - InvariantViolation + - LicenseViolation + - StorageUnavailable + type: string + failedStep: + description: FailedStep is the name of the step that failed. Empty + for single-step capabilities. + type: string + reason: + description: Reason is a human-readable description of the specific + failure. + type: string + required: + - category + - reason + type: object + packDeliveryRef: + description: PackDeliveryRef is the name of the PackDelivery CR that + was deployed. + type: string + packDeliveryVersion: + description: |- + PackDeliveryVersion is the PackDelivery spec.version deployed in this operation. + Populated by the Conductor executor at write time. Rollback anchor. + seam-core-schema.md §7.8. + type: string + packExecutionRef: + description: PackExecutionRef is the name of the PackExecution CR + that triggered this operation. + type: string + phase: + description: Phase identifies the RunnerConfig phase this result belongs + to. + type: string + previousRevisionRef: + description: |- + PreviousRevisionRef is the name of the PackLog CR that was superseded when + this revision was written. Absent for revision 1 (no predecessor). + type: string + rbacDigest: + description: |- + RBACDigest is the OCI digest of the RBAC layer deployed in this operation. + Copied from PackDelivery.spec.rbacDigest at deploy time. Rollback anchor. + type: string + revision: + description: |- + Revision is the monotonically increasing revision counter for this pack operation + sequence. Incremented each time a new result supersedes the previous one. + format: int64 + type: integer + startedAt: + description: StartedAt is the time the capability execution began. + format: date-time + type: string + status: + allOf: + - enum: + - Succeeded + - Failed + - enum: + - Succeeded + - Failed + description: Status is the terminal status of the capability execution. + type: string + steps: + description: Steps contains individual step results for multi-step + capabilities. + items: + description: |- + PackLogStepResult is the execution result for one step within a + multi-step capability. + properties: + completedAt: + description: CompletedAt is the time this step finished execution. + format: date-time + type: string + message: + description: Message provides additional context about the step + outcome. + type: string + name: + description: Name is the step identifier within the capability. + type: string + startedAt: + description: StartedAt is the time this step began execution. + format: date-time + type: string + status: + allOf: + - enum: + - Succeeded + - Failed + - enum: + - Succeeded + - Failed + description: Status is the terminal status of this step. + type: string + required: + - name + - status + type: object + type: array + talosClusterOperationResultRef: + description: |- + TalosClusterOperationResultRef is reserved for future cross-reference to a + TalosCluster-scoped OperationResult. Stub field; not populated by any current + controller. + type: string + targetClusterRef: + description: TargetClusterRef is the name of the target cluster this + operation ran against. + type: string + workloadDigest: + description: |- + WorkloadDigest is the OCI digest of the workload layer deployed in this operation. + Copied from PackDelivery.spec.workloadDigest at deploy time. Rollback anchor. + type: string + required: + - capability + - revision + - status + type: object + status: + description: PackLogStatus is the observed state of a PackLog. + properties: + observedGeneration: + description: ObservedGeneration is the last generation processed by + any consumer. + format: int64 + type: integer + remediationAttempts: + description: |- + RemediationAttempts records the Conductor Watchdog remediation attempt history + per FailureReason. Written by management Conductor exec mode after each Job. + items: + description: |- + RemediationAttemptRecord tracks one remediation attempt series for a specific + FailureReason. Written by the management Conductor after each remediation Job + completes. The management Conductor is the sole writer; tenant conductor + observes failure counts but does not write here. + properties: + attemptCount: + description: AttemptCount is the total number of remediation + Jobs submitted for this reason. + format: int32 + type: integer + failureReason: + description: FailureReason is the seam-sdk FailureReason value + this record tracks. + enum: + - CrashLoopBackOff + - OOMKilled + - ImagePullBackOff + - FailedMount + - MultiAttachError + type: string + lastAttemptAt: + description: LastAttemptAt is the time the most recent remediation + Job was submitted. + format: date-time + type: string + required: + - attemptCount + - failureReason + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/seam.ontai.dev_packreceipts.yaml b/config/crd/seam.ontai.dev_packreceipts.yaml new file mode 100644 index 0000000..dc65fb1 --- /dev/null +++ b/config/crd/seam.ontai.dev_packreceipts.yaml @@ -0,0 +1,222 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: packreceipts.seam.ontai.dev +spec: + group: seam.ontai.dev + names: + kind: PackReceipt + listKind: PackReceiptList + plural: packreceipts + shortNames: + - pr + singular: packreceipt + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.packDeliveryRef + name: Pack + type: string + - jsonPath: .status.verified + name: Verified + type: boolean + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + PackReceipt is the dispatcher CRD for pack delivery acknowledgement on a tenant cluster. + Written by conductor agent after signature verification. INV-026. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + PackReceiptSpec defines the desired state of a PackReceipt. + Written by the packinstalled pull loop on the tenant cluster conductor after + Ed25519 signature verification. INV-026. conductor-schema.md. + properties: + chartName: + description: ChartName is the Helm chart name. Carried from PackDelivery. + type: string + chartURL: + description: ChartURL is the Helm chart repository URL. Carried from + PackDelivery. + type: string + chartVersion: + description: ChartVersion is the Helm chart version. Carried from + PackDelivery. + type: string + deployedResources: + description: |- + DeployedResources is the inventory of Kubernetes resources applied to the tenant cluster + during the pack-deploy Job. Conductor role=tenant uses this list to detect drift by + verifying each resource still exists with the expected state. + CLUSTERPACK-BL-VERSION-CLEANUP, conductor-schema.md. + items: + description: |- + PackReceiptDeployedResource records a single Kubernetes resource that was applied + to the tenant cluster as part of a pack-deploy Job. Used by conductor role=tenant + to detect drift between declared and actual cluster state. + CLUSTERPACK-BL-VERSION-CLEANUP. conductor-schema.md. + properties: + apiVersion: + description: APIVersion is the full API version string (e.g., + "apps/v1"). + type: string + kind: + description: Kind is the resource kind (e.g., "Deployment"). + type: string + name: + description: Name is the resource name. + type: string + namespace: + description: Namespace is the namespace the resource was applied + to. Empty for cluster-scoped resources. + type: string + required: + - apiVersion + - kind + - name + type: object + type: array + helmVersion: + description: HelmVersion is the Helm SDK version. Carried from PackDelivery. + type: string + packDeliveryRef: + description: PackDeliveryRef is the name of the PackDelivery CR this + receipt acknowledges. + type: string + packInstalledRef: + description: PackInstalledRef is the name of the PackInstalled CR + this receipt acknowledges. + type: string + rbacDigest: + description: RBACDigest is the OCI digest of the RBAC layer. Carried + from PackDelivery for audit. + type: string + signatureRef: + description: |- + SignatureRef is the name of the signed artifact Secret on the management cluster + (seam-pack-signed-{cluster}-{packInstalled}) from which this receipt was derived. + type: string + targetClusterRef: + description: TargetClusterRef is the name of the cluster this receipt + was generated on. + type: string + workloadDigest: + description: WorkloadDigest is the OCI digest of the workload layer. + Carried from PackDelivery. + type: string + required: + - packDeliveryRef + - targetClusterRef + type: object + status: + description: |- + PackReceiptStatus is the observed state of a PackReceipt. + Written by the packinstalled pull loop after signature verification. INV-026. + properties: + conditions: + description: Conditions is the list of status conditions for this + PackReceipt. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: ObservedGeneration is the generation most recently reconciled. + format: int64 + type: integer + signature: + description: |- + Signature is the base64-encoded Ed25519 signature from the signed artifact Secret. + Stored for auditability and idempotency checking. INV-026. + type: string + verificationFailedReason: + description: |- + VerificationFailedReason is set when Verified=false and describes the + specific verification failure (e.g., "Ed25519 signature verification failed (INV-026)"). + type: string + verified: + description: |- + Verified indicates whether the Ed25519 signature on the PackInstalled artifact + was successfully verified against the management cluster's public key. INV-026. + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} From 05e016ea8bbe2d10fed7898c87348ab98343bbec Mon Sep 17 00:00:00 2001 From: ontave Date: Thu, 21 May 2026 04:46:40 +0200 Subject: [PATCH 18/19] fix(namespaces): replace hardcoded namespace strings with seam/pkg/namespaces constants Eliminates all hardcoded namespace strings from dispatcher controllers per Governor invariant 2026-05-20: never hardcode namespaces in code. Key fix: PackDelivery lookup now uses pe.Namespace (seam-tenant-{clusterRef}) instead of hardcoded seam-system. This was the root cause of TC-MC-2 failing when PackDelivery was correctly placed in the tenant namespace. - packexecution_reconciler.go: 8 literals replaced with namespaces.* calls - clusterpack_reconciler.go: 3 literals replaced; removed unused strings import - packinstance_reconciler.go: 1 literal replaced - identity/identity.go: 2 literals replaced - Tests updated: capabilities format changed from []CapabilityEntry to []string; PackDelivery namespace in test fixtures aligned with PackExecution namespace --- internal/controller/clusterpack_reconciler.go | 10 +++---- .../controller/packexecution_reconciler.go | 24 ++++++++------- .../controller/packinstance_reconciler.go | 3 +- internal/identity/identity.go | 5 ++-- internal/identity/identity_test.go | 3 +- test/unit/controller/helpers_test.go | 5 +--- .../controller/packexecution_gates_test.go | 8 ++--- .../controller/packinstance_lifecycle_test.go | 20 ++++++------- test/unit/packexecution_reconciler_test.go | 29 +++++++++---------- test/unit/role_independence_test.go | 6 ++-- 10 files changed, 56 insertions(+), 57 deletions(-) diff --git a/internal/controller/clusterpack_reconciler.go b/internal/controller/clusterpack_reconciler.go index 02c2565..05aebff 100644 --- a/internal/controller/clusterpack_reconciler.go +++ b/internal/controller/clusterpack_reconciler.go @@ -3,7 +3,6 @@ package controller import ( "context" "fmt" - "strings" "time" batchv1 "k8s.io/api/batch/v1" @@ -25,6 +24,7 @@ import ( seamcorev1alpha1 "github.com/ontai-dev/seam/api/v1alpha1" dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" "github.com/ontai-dev/seam/pkg/conditions" + "github.com/ontai-dev/seam/pkg/namespaces" ) // packSignatureAnnotation is the annotation key set by the conductor signing loop @@ -233,7 +233,7 @@ func (r *ClusterPackReconciler) Reconcile(ctx context.Context, req ctrl.Request) // with current version already exists (delivery complete) or PackExecution already // exists (delivery in progress). wrapper-schema.md §9 delivery chain. for _, clusterName := range cp.Spec.TargetClusters { - tenantNS := "seam-tenant-" + clusterName + tenantNS := namespaces.Tenant(clusterName) peName := cp.Name + "-" + clusterName // If PackInstance exists with current version, delivery is already complete — skip. @@ -435,7 +435,7 @@ func (r *ClusterPackReconciler) handleClusterPackDeletion(ctx context.Context, c // 2.5. Delete DriftSignals for each target cluster. // Convention: DriftSignal name = "drift-{cp.Name}", namespace = "seam-tenant-{clusterName}". for _, clusterName := range cp.Spec.TargetClusters { - tenantNS := "seam-tenant-" + clusterName + tenantNS := namespaces.Tenant(clusterName) signalName := "drift-" + cp.Name signal := &seamcorev1alpha1.DriftSignal{ ObjectMeta: metav1.ObjectMeta{Name: signalName, Namespace: tenantNS}, @@ -571,8 +571,8 @@ func (r *ClusterPackReconciler) MapPackInstanceToClusterPack( // WS2: Delete the corresponding PackExecution so the ClusterPackReconciler // creates a fresh one on the next reconcile pass. ns := pi.GetNamespace() - clusterName := strings.TrimPrefix(ns, "seam-tenant-") - if clusterName != ns { + clusterName := namespaces.ClusterNameFromTenant(ns) + if clusterName != "" { peName := cpName + "-" + clusterName pe := &dispatcherv1alpha1.PackExecution{} if getErr := r.Client.Get(ctx, client.ObjectKey{Name: peName, Namespace: ns}, pe); getErr == nil { diff --git a/internal/controller/packexecution_reconciler.go b/internal/controller/packexecution_reconciler.go index 591cd62..071a821 100644 --- a/internal/controller/packexecution_reconciler.go +++ b/internal/controller/packexecution_reconciler.go @@ -28,6 +28,7 @@ import ( dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" "github.com/ontai-dev/seam/pkg/conditions" "github.com/ontai-dev/seam/pkg/lineage" + "github.com/ontai-dev/seam/pkg/namespaces" ) const ( @@ -218,9 +219,10 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques pe.Generation, ) - // Gate 1: Signature gate. PackDeliveries are always in seam-system (management namespace). + // Gate 1: Signature gate. PackDelivery lives in the same namespace as the PackExecution + // (seam-tenant-{clusterRef}). All day2 operation CRs are scoped to the tenant namespace. cp := &dispatcherv1alpha1.PackDelivery{} - cpKey := client.ObjectKey{Name: pe.Spec.PackDeliveryRef.Name, Namespace: "seam-system"} + cpKey := client.ObjectKey{Name: pe.Spec.PackDeliveryRef.Name, Namespace: pe.Namespace} if err := r.Client.Get(ctx, cpKey, cp); err != nil { if apierrors.IsNotFound(err) { conditions.SetCondition( @@ -576,7 +578,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques piBaseName = pe.Spec.PackDeliveryRef.Name } piName := piBaseName + "-" + pe.Spec.TargetClusterRef - piNamespace := "seam-tenant-" + pe.Spec.TargetClusterRef + piNamespace := namespaces.Tenant(pe.Spec.TargetClusterRef) // Determine upgradeDirection by comparing existing PackInstance version. upgradeDir := packUpgradeDirection(ctx, r.Client, piName, piNamespace, cp.Spec.Version) @@ -694,7 +696,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques { rcObj := &unstructured.Unstructured{} rcObj.SetGroupVersionKind(schema.GroupVersionKind{Group: "seam.ontai.dev", Version: "v1alpha1", Kind: "RunnerConfig"}) - if getErr := r.Client.Get(ctx, types.NamespacedName{Name: pe.Spec.TargetClusterRef, Namespace: "ont-system"}, rcObj); getErr == nil { + if getErr := r.Client.Get(ctx, types.NamespacedName{Name: pe.Spec.TargetClusterRef, Namespace: namespaces.OntSystem}, rcObj); getErr == nil { if img, found, _ := unstructured.NestedString(rcObj.Object, "spec", "runnerImage"); found && img != "" { runnerImage = img } @@ -739,7 +741,7 @@ func (r *PackExecutionReconciler) isPermissionSnapshotCurrent(ctx context.Contex for _, name := range []string{"snapshot-" + pe.Spec.TargetClusterRef, "snapshot-management"} { ps := &unstructured.Unstructured{} ps.SetGroupVersionKind(psGVK) - if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: "seam-system"}, ps); err != nil { + if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespaces.SeamSystem}, ps); err != nil { if apierrors.IsNotFound(err) { continue } @@ -777,7 +779,7 @@ func (r *PackExecutionReconciler) isRBACProfileProvisioned(ctx context.Context, }) rpKey := types.NamespacedName{ Name: pe.Spec.AdmissionProfileRef, - Namespace: "seam-tenant-" + pe.Spec.TargetClusterRef, + Namespace: namespaces.Tenant(pe.Spec.TargetClusterRef), } if err := r.Client.Get(ctx, rpKey, rp); err != nil { if apierrors.IsNotFound(err) { @@ -822,7 +824,7 @@ func (r *PackExecutionReconciler) isConductorReadyForCluster(ctx context.Context // Fix 1: try seam-tenant-{clusterRef} then fall back to seam-system. tcFound := false - for _, ns := range []string{"seam-tenant-" + clusterRef, "seam-system"} { + for _, ns := range []string{namespaces.Tenant(clusterRef), namespaces.SeamSystem} { tc := &unstructured.Unstructured{} tc.SetGroupVersionKind(tcGVK) if err := r.Client.Get(ctx, types.NamespacedName{Name: clusterRef, Namespace: ns}, tc); err != nil { @@ -848,7 +850,7 @@ func (r *PackExecutionReconciler) isConductorReadyForCluster(ctx context.Context Version: "v1alpha1", Kind: "RunnerConfig", }) - if err := r.Client.Get(ctx, types.NamespacedName{Name: clusterRef, Namespace: "ont-system"}, rc); err != nil { + if err := r.Client.Get(ctx, types.NamespacedName{Name: clusterRef, Namespace: namespaces.OntSystem}, rc); err != nil { if apierrors.IsNotFound(err) { return false, nil } @@ -1092,7 +1094,7 @@ func (r *PackExecutionReconciler) mapSnapshotToPackExecutions( // Not a snapshot-{cluster} name pattern — ignore. return nil } - ns := "seam-tenant-" + clusterRef + ns := namespaces.Tenant(clusterRef) peList := &dispatcherv1alpha1.PackExecutionList{} if err := r.Client.List(ctx, peList, client.InNamespace(ns)); err != nil { return nil @@ -1182,11 +1184,11 @@ func (r *PackExecutionReconciler) mapRunnerConfigToPackExecutions( obj client.Object, ) []reconcile.Request { // RunnerConfig name == cluster name; lives in ont-system. - if obj.GetNamespace() != "ont-system" { + if obj.GetNamespace() != namespaces.OntSystem { return nil } clusterRef := obj.GetName() - ns := "seam-tenant-" + clusterRef + ns := namespaces.Tenant(clusterRef) peList := &dispatcherv1alpha1.PackExecutionList{} if err := r.Client.List(ctx, peList, client.InNamespace(ns)); err != nil { return nil diff --git a/internal/controller/packinstance_reconciler.go b/internal/controller/packinstance_reconciler.go index 1c5d8bf..b1250c7 100644 --- a/internal/controller/packinstance_reconciler.go +++ b/internal/controller/packinstance_reconciler.go @@ -22,6 +22,7 @@ import ( dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" "github.com/ontai-dev/seam/pkg/conditions" + "github.com/ontai-dev/seam/pkg/namespaces" ) // driftCheckInterval is how often the reconciler re-reads PackReceipt drift status @@ -430,7 +431,7 @@ func (r *PackInstanceReconciler) cleanupDeployedResources(ctx context.Context, p targetCluster := pi.Spec.TargetClusterRef kubeconfigSecretName := "seam-mc-" + targetCluster + "-kubeconfig" - kubeconfigNamespace := "seam-tenant-" + targetCluster + kubeconfigNamespace := namespaces.Tenant(targetCluster) kubeconfigSecret := &corev1.Secret{} if err := r.Client.Get(ctx, client.ObjectKey{Name: kubeconfigSecretName, Namespace: kubeconfigNamespace}, kubeconfigSecret); err != nil { diff --git a/internal/identity/identity.go b/internal/identity/identity.go index 72f8666..72008a3 100644 --- a/internal/identity/identity.go +++ b/internal/identity/identity.go @@ -11,6 +11,7 @@ import ( "github.com/ontai-dev/seam-sdk/conditions" "github.com/ontai-dev/seam-sdk/labels" "github.com/ontai-dev/seam-sdk/operator" + "github.com/ontai-dev/seam/pkg/namespaces" ) // SeamIdentity implements operator.SeamOperator for the dispatcher operator. @@ -48,12 +49,12 @@ func EnsureSeamMembership(ctx context.Context, c client.Client) error { sm := &seamv1alpha1.SeamMembership{ ObjectMeta: metav1.ObjectMeta{ Name: id.MembershipCRName(), - Namespace: "seam-system", + Namespace: namespaces.SeamSystem, }, Spec: seamv1alpha1.SeamMembershipSpec{ AppIdentityRef: id.OperatorName(), DomainIdentityRef: id.OperatorName(), - PrincipalRef: "system:serviceaccount:seam-system:" + id.OperatorName(), + PrincipalRef: "system:serviceaccount:" + namespaces.SeamSystem + ":" + id.OperatorName(), Tier: "infrastructure", }, } diff --git a/internal/identity/identity_test.go b/internal/identity/identity_test.go index 6a0f0d5..6a5d772 100644 --- a/internal/identity/identity_test.go +++ b/internal/identity/identity_test.go @@ -12,6 +12,7 @@ import ( "github.com/ontai-dev/dispatcher/internal/identity" "github.com/ontai-dev/seam-sdk/conditions" "github.com/ontai-dev/seam-sdk/operator" + "github.com/ontai-dev/seam/pkg/namespaces" ) var _ operator.SeamOperator = (*identity.SeamIdentity)(nil) @@ -72,7 +73,7 @@ func TestEnsureSeamMembership_Creates(t *testing.T) { t.Fatalf("EnsureSeamMembership: %v", err) } sm := &seamv1alpha1.SeamMembership{} - key := types.NamespacedName{Name: "seam-dispatcher", Namespace: "seam-system"} + key := types.NamespacedName{Name: "seam-dispatcher", Namespace: namespaces.SeamSystem} if err := c.Get(context.Background(), key, sm); err != nil { t.Fatalf("Get SeamMembership: %v", err) } diff --git a/test/unit/controller/helpers_test.go b/test/unit/controller/helpers_test.go index a18a071..2d83123 100644 --- a/test/unit/controller/helpers_test.go +++ b/test/unit/controller/helpers_test.go @@ -123,10 +123,7 @@ func newRunnerConfig(clusterRef string, capCount int) *unstructured.Unstructured if capCount > 0 { caps := make([]interface{}, capCount) for i := 0; i < capCount; i++ { - caps[i] = map[string]interface{}{ - "name": "pack-deploy", - "version": "v1.0.0", - } + caps[i] = "pack-deploy" } _ = unstructured.SetNestedSlice(rc.Object, caps, "status", "capabilities") } diff --git a/test/unit/controller/packexecution_gates_test.go b/test/unit/controller/packexecution_gates_test.go index b43bbb1..2f5101a 100644 --- a/test/unit/controller/packexecution_gates_test.go +++ b/test/unit/controller/packexecution_gates_test.go @@ -40,7 +40,7 @@ import ( // AC-2 gate: ClusterPack signature contract. func TestAC2_Gate1_PackExecution_BlockedWhenUnsigned(t *testing.T) { s := buildTestScheme(t) - cp := newSignedCP("unsigned-pack-ac2", "v1.0.0", "seam-system") + cp := newSignedCP("unsigned-pack-ac2", "v1.0.0", "infra-system") cp.Status.Signed = false cp.Annotations["ontai.dev/pack-signature"] = "" pe := newPE("pe-ac2-gate1", "unsigned-pack-ac2", "v1.0.0", cp.UID, "cluster-ac2g1", "profile-ac2g1", "infra-system") @@ -92,7 +92,7 @@ func TestAC2_Gate1_PackExecution_BlockedWhenUnsigned(t *testing.T) { // AC-2 gate: PermissionSnapshot freshness contract. func TestAC2_Gate3_PackExecution_BlockedWhenSnapshotStale(t *testing.T) { s := buildTestScheme(t) - cp := newSignedCP("stale-snap-pack-ac2", "v1.0.0", "seam-system") + cp := newSignedCP("stale-snap-pack-ac2", "v1.0.0", "infra-system") pe := newPE("pe-ac2-gate3", "stale-snap-pack-ac2", "v1.0.0", cp.UID, "cluster-ac2g3", "profile-ac2g3", "infra-system") tc := newTalosCluster("cluster-ac2g3", true) rc := newRunnerConfig("cluster-ac2g3", 1) @@ -143,7 +143,7 @@ func TestAC2_Gate3_PackExecution_BlockedWhenSnapshotStale(t *testing.T) { // AC-2 gate: RBACProfile provisioning contract. func TestAC2_Gate4_PackExecution_BlockedWhenRBACNotProvisioned(t *testing.T) { s := buildTestScheme(t) - cp := newSignedCP("rbac-pack-ac2", "v1.0.0", "seam-system") + cp := newSignedCP("rbac-pack-ac2", "v1.0.0", "infra-system") pe := newPE("pe-ac2-gate4", "rbac-pack-ac2", "v1.0.0", cp.UID, "cluster-ac2g4", "profile-ac2g4", "infra-system") tc := newTalosCluster("cluster-ac2g4", true) rc := newRunnerConfig("cluster-ac2g4", 1) @@ -195,7 +195,7 @@ func TestAC2_Gate4_PackExecution_BlockedWhenRBACNotProvisioned(t *testing.T) { // AC-2 gate: ClusterPack revocation contract (human intervention required). func TestAC2_Gate2_PackExecution_BlockedWhenRevoked(t *testing.T) { s := buildTestScheme(t) - cp := newSignedCP("revoked-pack-ac2", "v1.0.0", "seam-system") + cp := newSignedCP("revoked-pack-ac2", "v1.0.0", "infra-system") conditions.SetCondition( &cp.Status.Conditions, conditions.ConditionTypeClusterPackRevoked, diff --git a/test/unit/controller/packinstance_lifecycle_test.go b/test/unit/controller/packinstance_lifecycle_test.go index 0ae3e73..0693de2 100644 --- a/test/unit/controller/packinstance_lifecycle_test.go +++ b/test/unit/controller/packinstance_lifecycle_test.go @@ -55,7 +55,7 @@ func allGatesSetup(t *testing.T, peName, cpName, cpVersion, clusterRef, profileR t.Helper() s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "seam-system") + cp := newSignedCP(cpName, cpVersion, "infra-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) rc := newRunnerConfig(clusterRef, 1) // gate 0: RunnerConfig with 1 capability @@ -196,7 +196,7 @@ func TestWaitingForCluster_TalosClusterAbsent(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "seam-system") + cp := newSignedCP(cpName, cpVersion, "infra-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") fakeClient := fake.NewClientBuilder().WithScheme(s). @@ -354,7 +354,7 @@ func TestGate0_RunnerConfigAbsent(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "seam-system") + cp := newSignedCP(cpName, cpVersion, "infra-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) // TalosCluster present; no RunnerConfig @@ -414,7 +414,7 @@ func TestGate1_SignaturePending(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "seam-system") + cp := newSignedCP(cpName, cpVersion, "infra-system") cp.Status.Signed = false // Override: not yet signed. pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) @@ -478,7 +478,7 @@ func TestGate2_PackRevoked(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "seam-system") + cp := newSignedCP(cpName, cpVersion, "infra-system") cp.Status.Conditions = []metav1.Condition{{ Type: conditions.ConditionTypeClusterPackRevoked, Status: metav1.ConditionTrue, @@ -547,7 +547,7 @@ func TestGate3_PermissionSnapshotOutOfSync(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "seam-system") + cp := newSignedCP(cpName, cpVersion, "infra-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) rc := newRunnerConfig(clusterRef, 1) // gate 0 must clear to reach gate 3 @@ -611,7 +611,7 @@ func TestGate4_RBACProfileNotProvisioned(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "seam-system") + cp := newSignedCP(cpName, cpVersion, "infra-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) rc := newRunnerConfig(clusterRef, 1) // gate 0 must clear to reach gate 4 @@ -679,7 +679,7 @@ func TestConductorReady_ManagementClusterFallback_SeamSystem(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "seam-system") + cp := newSignedCP(cpName, cpVersion, "infra-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") // Management cluster TalosCluster lives in seam-system, not seam-tenant-*. @@ -784,7 +784,7 @@ func TestConductorReady_RunnerConfigAbsent_ReturnsFalse(t *testing.T) { ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "seam-system") + cp := newSignedCP(cpName, cpVersion, "infra-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) // TalosCluster present, RunnerConfig absent @@ -839,7 +839,7 @@ func TestConductorReady_RunnerConfigEmptyCapabilities_ReturnsFalse(t *testing.T) ) s := buildTestScheme(t) - cp := newSignedCP(cpName, cpVersion, "seam-system") + cp := newSignedCP(cpName, cpVersion, "infra-system") pe := newPE(peName, cpName, cpVersion, cp.UID, clusterRef, profileRef, "infra-system") tc := newTalosCluster(clusterRef, true) rc := newRunnerConfig(clusterRef, 0) // capCount=0 → empty capabilities list diff --git a/test/unit/packexecution_reconciler_test.go b/test/unit/packexecution_reconciler_test.go index 5a9ad9d..33004d0 100644 --- a/test/unit/packexecution_reconciler_test.go +++ b/test/unit/packexecution_reconciler_test.go @@ -130,10 +130,7 @@ func newRunnerConfig(clusterName string, capCount int) *unstructured.Unstructure if capCount > 0 { caps := make([]interface{}, capCount) for i := 0; i < capCount; i++ { - caps[i] = map[string]interface{}{ - "name": "pack-deploy", - "version": "v1.0.0", - } + caps[i] = "pack-deploy" } _ = unstructured.SetNestedSlice(rc.Object, caps, "status", "capabilities") } @@ -191,7 +188,7 @@ var _ controller.RBACReadyChecker = rbacAllowedStub // the ClusterPack is not yet signed (gate 0 already cleared via ConductorReady=True). func TestPackExecutionReconciler_Gate1_SignaturePending(t *testing.T) { s := newPackExecutionScheme(t) - cp := newClusterPack("my-pack", "seam-system", "v1.0.0") + cp := newClusterPack("my-pack", "infra-system", "v1.0.0") pe := newPackExecution("exec-1", "infra-system", "my-pack", "v1.0.0", "cluster-a", "profile-a") // TalosCluster + RunnerConfig with capabilities satisfies gate 0; gate 1 is the first to block. tc := newTalosClusterWithConductorReady("cluster-a", true) @@ -234,7 +231,7 @@ func TestPackExecutionReconciler_Gate1_SignaturePending(t *testing.T) { // sets PackRevoked on the PackExecution and does not requeue. func TestPackExecutionReconciler_Gate2_PackRevoked(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") cp.Status.Conditions = []metav1.Condition{ { Type: conditions.ConditionTypeClusterPackRevoked, @@ -286,7 +283,7 @@ func TestPackExecutionReconciler_Gate2_PackRevoked(t *testing.T) { // when the PermissionSnapshot is not current. func TestPackExecutionReconciler_Gate3_SnapshotOutOfSync(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") pe := newPackExecution("exec-snap", "infra-system", "my-pack", "v1.0.0", "cluster-a", "profile-a") ps := newPermissionSnapshot("snapshot-cluster-a", "seam-system", false) profile := newRBACProfile("profile-a", "seam-tenant-cluster-a", true) @@ -336,7 +333,7 @@ func TestPackExecutionReconciler_Gate3_SnapshotOutOfSync(t *testing.T) { // requeues when the RBACProfile is not yet provisioned. func TestPackExecutionReconciler_Gate4_RBACProfileNotProvisioned(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") pe := newPackExecution("exec-rbac", "infra-system", "my-pack", "v1.0.0", "cluster-a", "profile-a") ps := newPermissionSnapshot("snapshot-cluster-a", "seam-system", true) profile := newRBACProfile("profile-a", "seam-tenant-cluster-a", false) @@ -387,7 +384,7 @@ func TestPackExecutionReconciler_Gate4_RBACProfileNotProvisioned(t *testing.T) { // permanently block first-deploy because the profile only exists after the Job runs. func TestPackExecutionReconciler_Gate4_AbsentProfileAllowsJobSubmission(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") pe := newPackExecution("exec-absent-profile", "infra-system", "my-pack", "v1.0.0", "cluster-a", "profile-absent") ps := newPermissionSnapshot("snapshot-cluster-a", "seam-system", true) tc := newTalosClusterWithConductorReady("cluster-a", true) @@ -433,7 +430,7 @@ func TestPackExecutionReconciler_Gate4_AbsentProfileAllowsJobSubmission(t *testi // submitted with the Kueue queue label. Gap 27. func TestPackExecutionReconciler_AllGatesClear_JobSubmitted(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") pe := newPackExecution("exec-submit", "infra-system", "my-pack", "v1.0.0", "cluster-a", "profile-a") ps := newPermissionSnapshot("snapshot-cluster-a", "seam-system", true) profile := newRBACProfile("profile-a", "seam-tenant-cluster-a", true) @@ -508,7 +505,7 @@ func TestPackExecutionReconciler_AllGatesClear_JobSubmitted(t *testing.T) { // condition is initialized on first reconcile. func TestPackExecutionReconciler_LineageSyncedInitialized(t *testing.T) { s := newPackExecutionScheme(t) - cp := newClusterPack("my-pack", "seam-system", "v1.0.0") + cp := newClusterPack("my-pack", "infra-system", "v1.0.0") pe := newPackExecution("exec-lineage", "infra-system", "my-pack", "v1.0.0", "cluster-a", "profile-a") fakeClient := fake.NewClientBuilder().WithScheme(s). @@ -544,7 +541,7 @@ func TestPackExecutionReconciler_LineageSyncedInitialized(t *testing.T) { // blocks Job submission, sets Waiting condition, and requeues. func TestPackExecutionReconciler_Gate0_ConductorReadyAbsent(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") pe := newPackExecution("exec-gate0-absent", "infra-system", "my-pack", "v1.0.0", "cluster-b", "profile-b") // No TalosCluster for cluster-b in the fake client. @@ -585,7 +582,7 @@ func TestPackExecutionReconciler_Gate0_ConductorReadyAbsent(t *testing.T) { // submission, sets Waiting condition, and requeues. func TestPackExecutionReconciler_Gate0_ConductorReadyFalse(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("my-pack", "seam-system", "v1.0.0") + cp := newSignedClusterPack("my-pack", "infra-system", "v1.0.0") pe := newPackExecution("exec-gate0-false", "infra-system", "my-pack", "v1.0.0", "cluster-b", "profile-b") // TalosCluster exists but ConductorReady=False. tc := newTalosClusterWithConductorReady("cluster-b", false) @@ -627,7 +624,7 @@ func TestPackExecutionReconciler_Gate0_ConductorReadyFalse(t *testing.T) { func TestPackExecutionReconciler_Gate0_ConductorReadyTrue_ProceedsToSignatureGate(t *testing.T) { s := newPackExecutionScheme(t) // Use an unsigned ClusterPack so gate 1 (signature) blocks after gate 0 passes. - cp := newClusterPack("my-pack", "seam-system", "v1.0.0") + cp := newClusterPack("my-pack", "infra-system", "v1.0.0") pe := newPackExecution("exec-gate0-true", "infra-system", "my-pack", "v1.0.0", "cluster-c", "profile-c") // TalosCluster + RunnerConfig with capabilities — gate 0 passes. tc := newTalosClusterWithConductorReady("cluster-c", true) @@ -683,7 +680,7 @@ func TestPackExecutionReconciler_Gate0_ConductorReadyTrue_ProceedsToSignatureGat // RunnerConfig watch in SetupWithManager fires at the right time. func TestPackExecutionReconciler_Gate0_RunnerConfigCapabilitiesAppear(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("cilium", "seam-system", "v1.2.0") + cp := newSignedClusterPack("cilium", "seam-tenant-ccs-test", "v1.2.0") pe := newPackExecution("cilium-exec", "seam-tenant-ccs-test", "cilium", "v1.2.0", "ccs-test", "rbac-wrapper") snapshot := newPermissionSnapshot("snapshot-ccs-test", "security-system", true) @@ -750,7 +747,7 @@ func TestPackExecutionReconciler_Gate0_RunnerConfigCapabilitiesAppear(t *testing } // Mutate the live object in-place and use regular Update (RunnerConfig is not in // WithStatusSubresource so the fake client stores the full object including status). - caps := []interface{}{map[string]interface{}{"name": "pack-deploy", "version": "v1.0.0"}} + caps := []interface{}{"pack-deploy"} if err := unstructured.SetNestedSlice(rcLive.Object, caps, "status", "capabilities"); err != nil { t.Fatalf("set capabilities on rcLive: %v", err) } diff --git a/test/unit/role_independence_test.go b/test/unit/role_independence_test.go index 4cb7fb7..8ca5aee 100644 --- a/test/unit/role_independence_test.go +++ b/test/unit/role_independence_test.go @@ -42,7 +42,7 @@ import ( // The reconciler must not special-case the management cluster namespace. func TestRoleIndependence_PackExecution_ManagementCluster_Gate0Blocks(t *testing.T) { s := newPackExecutionScheme(t) - cp := newSignedClusterPack("cilium", "seam-system", "v1.16.0") + cp := newSignedClusterPack("cilium", "infra-system", "v1.16.0") // TargetClusterRef=ccs-mgmt: the management cluster treated as a tenant. pe := newPackExecution("cilium-exec-mgmt", "infra-system", "cilium", "v1.16.0", "ccs-mgmt", "profile-mgmt") // No TalosCluster or RunnerConfig in the fake client for ccs-mgmt. @@ -84,7 +84,7 @@ func TestRoleIndependence_PackExecution_ManagementCluster_JobNamespace(t *testin tenantNS = "seam-tenant-ccs-mgmt" ) s := newPackExecutionScheme(t) - cp := newSignedClusterPack("cilium", "seam-system", "v1.16.0") + cp := newSignedClusterPack("cilium", "infra-system", "v1.16.0") // PackExecution lives in infra-system; Job will be submitted there too (PE namespace). pe := newPackExecution("cilium-exec-mgmt", "infra-system", "cilium", "v1.16.0", clusterRef, "cilium") ps := newPermissionSnapshot("snapshot-"+clusterRef, "seam-system", true) @@ -148,7 +148,7 @@ func TestRoleIndependence_PackExecution_ManagementCluster_PackInstance(t *testin peNS = "infra-system" ) s := newPackExecutionScheme(t) - cp := newSignedClusterPack("cilium", "seam-system", "v1.16.0") + cp := newSignedClusterPack("cilium", "infra-system", "v1.16.0") pe := newPackExecution(peName, peNS, "cilium", "v1.16.0", clusterRef, "cilium") ps := newPermissionSnapshot("snapshot-"+clusterRef, "seam-system", true) profile := newRBACProfile("cilium", tenantNS, true) From 9043f34670b57d4e70336be0d63d50a3910dd029 Mon Sep 17 00:00:00 2001 From: ontave Date: Thu, 21 May 2026 20:32:39 +0200 Subject: [PATCH 19/19] fix(dispatcher): correct ILI label to packdelivery-{name} + unit test for lineage label - clusterpack_reconciler: SetDescendantLabels on new PackExecution using IndexName("PackDelivery", cp.Name) so the descendant registry lookup resolves to the correct LineageRecord (packdelivery-{name} not packexecution-{name}) - packexecution_reconciler: same fix for PackInstalled label at reconcile time - helpers_test: fix RunnerCapabilityEntry format from plain string to map[string]interface{}{"name":...} matching the seam type definition - packinstance_lifecycle_test: add TestPackInstalled_LineageLabelUsesPackDeliveryName verifying the root-ili label value (unblocks TC-MC-13, TC-MC-23) - packexecution_reconciler_test: same RunnerCapabilityEntry format fix --- internal/controller/clusterpack_reconciler.go | 4 ++ .../controller/packexecution_reconciler.go | 2 +- test/unit/controller/helpers_test.go | 2 +- .../controller/packinstance_lifecycle_test.go | 46 +++++++++++++++++++ test/unit/packexecution_reconciler_test.go | 4 +- 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/internal/controller/clusterpack_reconciler.go b/internal/controller/clusterpack_reconciler.go index 05aebff..2201dc3 100644 --- a/internal/controller/clusterpack_reconciler.go +++ b/internal/controller/clusterpack_reconciler.go @@ -24,6 +24,7 @@ import ( seamcorev1alpha1 "github.com/ontai-dev/seam/api/v1alpha1" dispatcherv1alpha1 "github.com/ontai-dev/dispatcher/api/seam/v1alpha1" "github.com/ontai-dev/seam/pkg/conditions" + "github.com/ontai-dev/seam/pkg/lineage" "github.com/ontai-dev/seam/pkg/namespaces" ) @@ -285,6 +286,9 @@ func (r *ClusterPackReconciler) Reconcile(ctx context.Context, req ctrl.Request) AdmissionProfileRef: cp.Name, }, } + // Wire descendant lineage: DescendantReconciler appends this PackExecution to the + // PackDelivery's LineageRecord descendantRegistry. seam-core-schema.md §3. + lineage.SetDescendantLabels(newPE, lineage.IndexName("PackDelivery", cp.Name), tenantNS, "dispatcher", lineage.PackExecution, cp.GetAnnotations()[lineage.AnnotationDeclaringPrincipal]) if err := r.Client.Create(ctx, newPE); err != nil && !apierrors.IsAlreadyExists(err) { return ctrl.Result{}, fmt.Errorf("create PackExecution %s/%s: %w", tenantNS, peName, err) } diff --git a/internal/controller/packexecution_reconciler.go b/internal/controller/packexecution_reconciler.go index 071a821..2b32d0d 100644 --- a/internal/controller/packexecution_reconciler.go +++ b/internal/controller/packexecution_reconciler.go @@ -608,7 +608,7 @@ func (r *PackExecutionReconciler) Reconcile(ctx context.Context, req ctrl.Reques } // Wire descendant lineage so the DescendantReconciler can append this // PackInstance to the PackExecution's ILI. seam-core-schema.md §3. - lineage.SetDescendantLabels(pi, lineage.IndexName("PackExecution", pe.Name), pe.Namespace, "dispatcher", lineage.PackExecution, pe.GetAnnotations()[lineage.AnnotationDeclaringPrincipal]) + lineage.SetDescendantLabels(pi, lineage.IndexName("PackDelivery", pe.Spec.PackDeliveryRef.Name), pe.Namespace, "dispatcher", lineage.PackExecution, pe.GetAnnotations()[lineage.AnnotationDeclaringPrincipal]) if err := r.Client.Create(ctx, pi); err != nil { if !apierrors.IsAlreadyExists(err) { return ctrl.Result{}, fmt.Errorf("failed to create PackInstance %s: %w", piName, err) diff --git a/test/unit/controller/helpers_test.go b/test/unit/controller/helpers_test.go index 2d83123..ea9af84 100644 --- a/test/unit/controller/helpers_test.go +++ b/test/unit/controller/helpers_test.go @@ -123,7 +123,7 @@ func newRunnerConfig(clusterRef string, capCount int) *unstructured.Unstructured if capCount > 0 { caps := make([]interface{}, capCount) for i := 0; i < capCount; i++ { - caps[i] = "pack-deploy" + caps[i] = map[string]interface{}{"name": "pack-deploy"} } _ = unstructured.SetNestedSlice(rc.Object, caps, "status", "capabilities") } diff --git a/test/unit/controller/packinstance_lifecycle_test.go b/test/unit/controller/packinstance_lifecycle_test.go index 0693de2..52aaa22 100644 --- a/test/unit/controller/packinstance_lifecycle_test.go +++ b/test/unit/controller/packinstance_lifecycle_test.go @@ -183,6 +183,52 @@ func TestOwnershipChain_TalosClusterExists(t *testing.T) { } } +// TestPackInstalled_LineageLabelUsesPackDeliveryName verifies that when a PackInstalled +// is created after Job success, the infrastructure.ontai.dev/root-ili label is set to +// "packdelivery-{cpName}" (not "packexecution-{peName}"). This ensures the DescendantReconciler +// can find the correct LineageRecord by its deterministic IndexName. TC-MC-13/TC-MC-23. +func TestPackInstalled_LineageLabelUsesPackDeliveryName(t *testing.T) { + const ( + peName = "pe-lineage-label" + cpName = "nginx" + cpVersion = "v1.0.0" + clusterRef = "ccs-mgmt" + profileRef = "profile-lineage" + ) + + fakeClient, pe := allGatesSetup(t, peName, cpName, cpVersion, clusterRef, profileRef) + ctx := context.Background() + + job := newJob(packDeployJobName(peName), "infra-system", 1, 0, pe) + por := newOperationResultPOR(peName, "infra-system") + if err := fakeClient.Create(ctx, job); err != nil { + t.Fatalf("create Job: %v", err) + } + if err := fakeClient.Create(ctx, por); err != nil { + t.Fatalf("create PackOperationResult: %v", err) + } + + r := &controller.PackExecutionReconciler{ + Client: fakeClient, + Scheme: buildTestScheme(t), + Recorder: clientevents.NewFakeRecorder(32), + RBACChecker: rbacAllowedStub, + } + reconcilePackExecution(t, r, peName, "infra-system") + + piName := cpName + "-" + clusterRef + pi := &dispatcherv1alpha1.PackInstalled{} + if err := fakeClient.Get(ctx, types.NamespacedName{Name: piName, Namespace: "seam-tenant-" + clusterRef}, pi); err != nil { + t.Fatalf("PackInstalled %q not created: %v", piName, err) + } + + wantILI := "packdelivery-" + cpName + gotILI := pi.GetLabels()["infrastructure.ontai.dev/root-ili"] + if gotILI != wantILI { + t.Errorf("PackInstalled root-ili label = %q, want %q (must reference PackDelivery LineageRecord, not PackExecution)", gotILI, wantILI) + } +} + // TestWaitingForCluster_TalosClusterAbsent verifies that when the target TalosCluster // does not exist, the PackExecutionReconciler sets Waiting=True with // ReasonAwaitingConductorReady, requeues, and creates neither a Job nor a PackInstance. diff --git a/test/unit/packexecution_reconciler_test.go b/test/unit/packexecution_reconciler_test.go index 33004d0..c631c63 100644 --- a/test/unit/packexecution_reconciler_test.go +++ b/test/unit/packexecution_reconciler_test.go @@ -130,7 +130,7 @@ func newRunnerConfig(clusterName string, capCount int) *unstructured.Unstructure if capCount > 0 { caps := make([]interface{}, capCount) for i := 0; i < capCount; i++ { - caps[i] = "pack-deploy" + caps[i] = map[string]interface{}{"name": "pack-deploy"} } _ = unstructured.SetNestedSlice(rc.Object, caps, "status", "capabilities") } @@ -747,7 +747,7 @@ func TestPackExecutionReconciler_Gate0_RunnerConfigCapabilitiesAppear(t *testing } // Mutate the live object in-place and use regular Update (RunnerConfig is not in // WithStatusSubresource so the fake client stores the full object including status). - caps := []interface{}{"pack-deploy"} + caps := []interface{}{map[string]interface{}{"name": "pack-deploy"}} if err := unstructured.SetNestedSlice(rcLive.Object, caps, "status", "capabilities"); err != nil { t.Fatalf("set capabilities on rcLive: %v", err) }