Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 0 additions & 92 deletions CODEBASE.md

This file was deleted.

46 changes: 44 additions & 2 deletions internal/controller/clusterpack_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,40 @@ func (r *ClusterPackReconciler) handleClusterPackDeletion(ctx context.Context, c
return ctrl.Result{}, nil
}

// MapPackExecutionToClusterPack maps a PackExecution delete event back to the
// ClusterPack that owns it. It also deletes the corresponding PackInstance so that
// Step I of Reconcile (which skips PE creation when a PackInstance at the current
// version already exists) does not suppress PE recreation after an external retrigger
// (e.g. conductor DriftSignalHandler). Mirrors the inverse logic in
// MapPackInstanceToClusterPack, which deletes the PE when a PackInstance is deleted.
func (r *ClusterPackReconciler) MapPackExecutionToClusterPack(
ctx context.Context,
obj client.Object,
) []reconcile.Request {
pe, ok := obj.(*seamcorev1alpha1.InfrastructurePackExecution)
if !ok {
return nil
}
cpName := pe.Spec.ClusterPackRef.Name
if cpName == "" {
return nil
}

// Delete the PackInstance with the same name so that the ClusterPackReconciler's
// version guard (Step I) does not skip PE recreation. The PE name and PackInstance
// 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{}
if getErr := r.Client.Get(ctx, client.ObjectKey{Name: pe.GetName(), Namespace: ns}, pi); getErr == nil {
_ = r.Client.Delete(ctx, pi)
}

return []reconcile.Request{
{NamespacedName: types.NamespacedName{Name: cpName, Namespace: ns}},
}
}

// containsString reports whether slice contains s.
func containsString(slice []string, s string) bool {
for _, v := range slice {
Expand Down Expand Up @@ -486,8 +520,12 @@ func removeString(slice []string, s string) []string {
// Watches PackInstance with a delete-only predicate. When a PackInstance is
// deleted, the reconciler is notified so it can check whether redelivery is
// needed and create a fresh PackExecution.
//
// Watches PackExecution with a delete-only predicate. When a PackExecution is
// externally deleted (e.g. by conductor's DriftSignalHandler to retrigger
// delivery), the reconciler is notified so it can create a fresh PackExecution.
func (r *ClusterPackReconciler) SetupWithManager(mgr ctrl.Manager) error {
packInstanceDeletePredicate := predicate.Funcs{
deleteOnlyPredicate := predicate.Funcs{
CreateFunc: func(_ event.CreateEvent) bool { return false },
UpdateFunc: func(_ event.UpdateEvent) bool { return false },
DeleteFunc: func(_ event.DeleteEvent) bool { return true },
Expand All @@ -501,7 +539,11 @@ func (r *ClusterPackReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&batchv1.Job{}).
Watches(&seamcorev1alpha1.InfrastructurePackInstance{},
handler.EnqueueRequestsFromMapFunc(r.MapPackInstanceToClusterPack),
builder.WithPredicates(packInstanceDeletePredicate),
builder.WithPredicates(deleteOnlyPredicate),
).
Watches(&seamcorev1alpha1.InfrastructurePackExecution{},
handler.EnqueueRequestsFromMapFunc(r.MapPackExecutionToClusterPack),
builder.WithPredicates(deleteOnlyPredicate),
).
Complete(r)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/controller/packexecution_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1037,9 +1037,9 @@ func (r *PackExecutionReconciler) SetupWithManager(mgr ctrl.Manager) error {
})
rcObj := &unstructured.Unstructured{}
rcObj.SetGroupVersionKind(schema.GroupVersionKind{
Group: "runner.ontai.dev",
Group: "infrastructure.ontai.dev",
Version: "v1alpha1",
Kind: "RunnerConfig",
Kind: "InfrastructureRunnerConfig",
})
return ctrl.NewControllerManagedBy(mgr).
For(&seamv1alpha1.InfrastructurePackExecution{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Expand Down
73 changes: 73 additions & 0 deletions test/unit/clusterpack_reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,79 @@ func TestClusterPackReconciler_DeletionCascadesDriftSignal(t *testing.T) {
}
}

// TestClusterPackReconciler_PackExecutionDeletedRecreatesPE verifies that when a
// PackExecution is externally deleted (drift retrigger), MapPackExecutionToClusterPack
// deletes the PackInstance so the version guard in Step I does not suppress PE
// recreation, and that the subsequent Reconcile creates a fresh PackExecution.
func TestClusterPackReconciler_PackExecutionDeletedRecreatesPE(t *testing.T) {
s := newClusterPackScheme(t)

clusterName := "ccs-dev"
tenantNS := "seam-tenant-" + clusterName
cpName := "nginx-ccs-dev"
version := "v4.9.0"
peName := cpName + "-" + clusterName

// ClusterPack: signed and available, targets ccs-dev.
cp := newClusterPack(cpName, tenantNS, version)
cp.Spec.TargetClusters = []string{clusterName}
cp.Status.Signed = true
cp.Status.PackSignature = "base64sig=="
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,
}

// PackInstance exists at current version — without the fix, this would suppress PE recreation.
pi := &seamcorev1alpha1.InfrastructurePackInstance{
ObjectMeta: metav1.ObjectMeta{Name: peName, Namespace: tenantNS},
Spec: seamcorev1alpha1.InfrastructurePackInstanceSpec{Version: version, ClusterPackRef: cpName},
}

// PackExecution is absent — it was externally deleted by DriftSignalHandler.
fakeClient := fake.NewClientBuilder().WithScheme(s).
WithObjects(cp, pi).
WithStatusSubresource(&seamcorev1alpha1.InfrastructureClusterPack{}).
Build()
r := &controller.ClusterPackReconciler{
Client: fakeClient,
Scheme: s,
Recorder: clientevents.NewFakeRecorder(10),
}

// Simulate the PE deletion watch firing: call the mapper with the deleted PE object.
deletedPE := &seamcorev1alpha1.InfrastructurePackExecution{
ObjectMeta: metav1.ObjectMeta{Name: peName, Namespace: tenantNS},
Spec: seamcorev1alpha1.InfrastructurePackExecutionSpec{
ClusterPackRef: seamcorev1alpha1.InfrastructureClusterPackRef{Name: cpName, Version: version},
TargetClusterRef: clusterName,
},
}
requests := r.MapPackExecutionToClusterPack(context.Background(), deletedPE)

if len(requests) != 1 {
t.Fatalf("expected 1 reconcile request, got %d", len(requests))
}
if requests[0].Name != cpName || requests[0].Namespace != tenantNS {
t.Errorf("expected request for %s/%s, got %v", tenantNS, cpName, requests[0])
}

// Mapper must have deleted the PackInstance.
remainingPI := &seamcorev1alpha1.InfrastructurePackInstance{}
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)
}

// Reconcile: PackInstance gone, PE gone → new PE must be created.
reconcileCP(t, r, cp)

newPE := &seamcorev1alpha1.InfrastructurePackExecution{}
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)
}
}

// TestClusterPackReconciler_RevokedNoRequeue verifies that a revoked ClusterPack
// stops reconciliation without requeue.
func TestClusterPackReconciler_RevokedNoRequeue(t *testing.T) {
Expand Down
Loading