@@ -25,8 +25,11 @@ import (
2525 spinv1alpha1 "github.com/spinframework/spin-operator/api/v1alpha1"
2626 "github.com/spinframework/spin-operator/internal/generics"
2727 "github.com/stretchr/testify/require"
28+ corev1 "k8s.io/api/core/v1"
29+ apierrors "k8s.io/apimachinery/pkg/api/errors"
2830 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2931 ctrl "sigs.k8s.io/controller-runtime"
32+ "sigs.k8s.io/controller-runtime/pkg/client"
3033 "sigs.k8s.io/controller-runtime/pkg/client/fake"
3134 controllerconfig "sigs.k8s.io/controller-runtime/pkg/config"
3235 "sigs.k8s.io/controller-runtime/pkg/log/zap"
@@ -145,3 +148,99 @@ func testContainerdShimSpinExecutor() *spinv1alpha1.SpinAppExecutor {
145148 },
146149 }
147150}
151+
152+ func TestSpinAppExecutorReconcile_CrossNamespaceDoesNotBlockDeletion (t * testing.T ) {
153+ t .Parallel ()
154+
155+ envTest , mgr , _ := setupExecutorController (t )
156+
157+ ctx , cancelFunc := context .WithTimeout (context .Background (), 10 * time .Second )
158+ defer cancelFunc ()
159+
160+ var wg sync.WaitGroup
161+ wg .Add (1 )
162+ go func () {
163+ require .NoError (t , mgr .Start (ctx ))
164+ wg .Done ()
165+ }()
166+
167+ // Create two namespaces
168+ nsA := & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "test-ns-a" }}
169+ nsB := & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : "test-ns-b" }}
170+ require .NoError (t , envTest .k8sClient .Create (ctx , nsA ))
171+ require .NoError (t , envTest .k8sClient .Create (ctx , nsB ))
172+
173+ // Create an executor with the same name in both namespaces
174+ executorA := & spinv1alpha1.SpinAppExecutor {
175+ ObjectMeta : metav1.ObjectMeta {
176+ Name : "containerd-shim-spin" ,
177+ Namespace : "test-ns-a" ,
178+ },
179+ Spec : spinv1alpha1.SpinAppExecutorSpec {
180+ CreateDeployment : true ,
181+ DeploymentConfig : & spinv1alpha1.ExecutorDeploymentConfig {
182+ RuntimeClassName : generics .Ptr ("wasmtime-spin-v2" ),
183+ },
184+ },
185+ }
186+ executorB := & spinv1alpha1.SpinAppExecutor {
187+ ObjectMeta : metav1.ObjectMeta {
188+ Name : "containerd-shim-spin" ,
189+ Namespace : "test-ns-b" ,
190+ },
191+ Spec : spinv1alpha1.SpinAppExecutorSpec {
192+ CreateDeployment : true ,
193+ DeploymentConfig : & spinv1alpha1.ExecutorDeploymentConfig {
194+ RuntimeClassName : generics .Ptr ("wasmtime-spin-v2" ),
195+ },
196+ },
197+ }
198+ require .NoError (t , envTest .k8sClient .Create (ctx , executorA ))
199+ require .NoError (t , envTest .k8sClient .Create (ctx , executorB ))
200+
201+ // Wait for finalizers to be added by the controller
202+ require .Eventually (t , func () bool {
203+ var exec spinv1alpha1.SpinAppExecutor
204+ if err := envTest .k8sClient .Get (ctx , client .ObjectKeyFromObject (executorA ), & exec ); err != nil {
205+ return false
206+ }
207+ return len (exec .Finalizers ) > 0
208+ }, 5 * time .Second , 100 * time .Millisecond , "executor A should have finalizer" )
209+
210+ require .Eventually (t , func () bool {
211+ var exec spinv1alpha1.SpinAppExecutor
212+ if err := envTest .k8sClient .Get (ctx , client .ObjectKeyFromObject (executorB ), & exec ); err != nil {
213+ return false
214+ }
215+ return len (exec .Finalizers ) > 0
216+ }, 5 * time .Second , 100 * time .Millisecond , "executor B should have finalizer" )
217+
218+ // Create a SpinApp in namespace B that references the executor by name
219+ spinApp := & spinv1alpha1.SpinApp {
220+ ObjectMeta : metav1.ObjectMeta {
221+ Name : "test-app" ,
222+ Namespace : "test-ns-b" ,
223+ },
224+ Spec : spinv1alpha1.SpinAppSpec {
225+ Executor : "containerd-shim-spin" ,
226+ Image : "ghcr.io/spinkube/containerd-shim-spin/examples/spin-rust-hello:v0.15.1" ,
227+ Replicas : 1 ,
228+ },
229+ }
230+ require .NoError (t , envTest .k8sClient .Create (ctx , spinApp ))
231+
232+ // Delete executor in namespace A — this should succeed because
233+ // the SpinApp is in namespace B, not namespace A.
234+ require .NoError (t , envTest .k8sClient .Delete (ctx , executorA ))
235+
236+ // The executor in namespace A should be fully deleted (finalizer removed)
237+ require .Eventually (t , func () bool {
238+ var exec spinv1alpha1.SpinAppExecutor
239+ err := envTest .k8sClient .Get (ctx , client .ObjectKeyFromObject (executorA ), & exec )
240+ return apierrors .IsNotFound (err ) // NotFound means it was deleted
241+ }, 5 * time .Second , 100 * time .Millisecond , "executor A should be deleted — SpinApp in namespace B should not block it" )
242+
243+ // Verify executor in namespace B still exists (it has a dependent SpinApp)
244+ var execB spinv1alpha1.SpinAppExecutor
245+ require .NoError (t , envTest .k8sClient .Get (ctx , client .ObjectKeyFromObject (executorB ), & execB ))
246+ }
0 commit comments