@@ -18,9 +18,11 @@ import (
1818 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1919 "k8s.io/apimachinery/pkg/types"
2020 "k8s.io/apimachinery/pkg/util/intstr"
21+ "k8s.io/apimachinery/pkg/util/yaml"
2122 "k8s.io/client-go/kubernetes/scheme"
2223 "sigs.k8s.io/controller-runtime/pkg/client"
2324 "sigs.k8s.io/kustomize/api/resmap"
25+ kresource "sigs.k8s.io/kustomize/api/resource"
2426 "sigs.k8s.io/kustomize/kyaml/filesys"
2527)
2628
@@ -628,3 +630,283 @@ func TestRemoveDeploymentReplicas(t *testing.T) {
628630
629631 require .False (t , hasReplicas , "replicas should be removed from all deployments when autoscaling is enabled" )
630632}
633+
634+ // TestHasLegacyCABundleVolumes tests the detection of legacy CA bundle volumes.
635+ func TestHasLegacyCABundleVolumes (t * testing.T ) {
636+ ctx := context .Background ()
637+
638+ t .Run ("detects legacy emptyDir ca-bundle volume" , func (t * testing.T ) {
639+ deployment := newTestResource (t , "apps/v1" , "Deployment" , "test-deploy" , "test-ns" , map [string ]any {
640+ "template" : map [string ]any {
641+ "spec" : map [string ]any {
642+ "volumes" : []any {
643+ map [string ]any {
644+ "name" : "ca-bundle" ,
645+ "emptyDir" : map [string ]any {},
646+ },
647+ },
648+ },
649+ },
650+ })
651+
652+ u , err := resourceToUnstructured (t , deployment )
653+ require .NoError (t , err )
654+
655+ result := hasLegacyCABundleVolumes (ctx , u )
656+ require .True (t , result , "should detect legacy ca-bundle emptyDir volume" )
657+ })
658+
659+ t .Run ("detects legacy ca-bundle-source volume" , func (t * testing.T ) {
660+ deployment := newTestResource (t , "apps/v1" , "Deployment" , "test-deploy" , "test-ns" , map [string ]any {
661+ "template" : map [string ]any {
662+ "spec" : map [string ]any {
663+ "volumes" : []any {
664+ map [string ]any {
665+ "name" : "ca-bundle-source" ,
666+ "configMap" : map [string ]any {
667+ "name" : "odh-trusted-ca-bundle" ,
668+ },
669+ },
670+ },
671+ },
672+ },
673+ })
674+
675+ u , err := resourceToUnstructured (t , deployment )
676+ require .NoError (t , err )
677+
678+ result := hasLegacyCABundleVolumes (ctx , u )
679+ require .True (t , result , "should detect legacy ca-bundle-source volume" )
680+ })
681+
682+ t .Run ("does not detect new-style ca-bundle configMap" , func (t * testing.T ) {
683+ deployment := newTestResource (t , "apps/v1" , "Deployment" , "test-deploy" , "test-ns" , map [string ]any {
684+ "template" : map [string ]any {
685+ "spec" : map [string ]any {
686+ "volumes" : []any {
687+ map [string ]any {
688+ "name" : "ca-bundle" ,
689+ "configMap" : map [string ]any {
690+ "name" : "managed-ca-bundle" ,
691+ },
692+ },
693+ },
694+ },
695+ },
696+ })
697+
698+ u , err := resourceToUnstructured (t , deployment )
699+ require .NoError (t , err )
700+
701+ result := hasLegacyCABundleVolumes (ctx , u )
702+ require .False (t , result , "should not detect new-style ca-bundle ConfigMap as legacy" )
703+ })
704+
705+ t .Run ("does not detect unrelated volumes" , func (t * testing.T ) {
706+ deployment := newTestResource (t , "apps/v1" , "Deployment" , "test-deploy" , "test-ns" , map [string ]any {
707+ "template" : map [string ]any {
708+ "spec" : map [string ]any {
709+ "volumes" : []any {
710+ map [string ]any {
711+ "name" : "data" ,
712+ "emptyDir" : map [string ]any {},
713+ },
714+ map [string ]any {
715+ "name" : "config" ,
716+ "configMap" : map [string ]any {
717+ "name" : "app-config" ,
718+ },
719+ },
720+ },
721+ },
722+ },
723+ })
724+
725+ u , err := resourceToUnstructured (t , deployment )
726+ require .NoError (t , err )
727+
728+ result := hasLegacyCABundleVolumes (ctx , u )
729+ require .False (t , result , "should not detect unrelated volumes as legacy" )
730+ })
731+
732+ t .Run ("returns false when no volumes present" , func (t * testing.T ) {
733+ deployment := newTestResource (t , "apps/v1" , "Deployment" , "test-deploy" , "test-ns" , map [string ]any {})
734+
735+ u , err := resourceToUnstructured (t , deployment )
736+ require .NoError (t , err )
737+
738+ result := hasLegacyCABundleVolumes (ctx , u )
739+ require .False (t , result , "should return false when no volumes present" )
740+ })
741+ }
742+
743+ // TestLegacyCABundleUpgrade tests that deployments with legacy CA bundle volumes
744+ // are replaced instead of patched to avoid SSA conflicts.
745+ func TestLegacyCABundleUpgrade (t * testing.T ) {
746+ ctx , testNs , owner := setupApplyResourcesTest (t , "legacy-ca-upgrade" )
747+
748+ // Create an existing deployment with legacy CA bundle volumes (old operator pattern)
749+ existingDeployment := & appsv1.Deployment {
750+ ObjectMeta : metav1.ObjectMeta {
751+ Name : "test-deployment" ,
752+ Namespace : testNs ,
753+ Labels : map [string ]string {"version" : "old" },
754+ OwnerReferences : []metav1.OwnerReference {
755+ * metav1 .NewControllerRef (owner , owner .GroupVersionKind ()),
756+ },
757+ },
758+ Spec : appsv1.DeploymentSpec {
759+ Replicas : ptr (int32 (1 )),
760+ Selector : & metav1.LabelSelector {
761+ MatchLabels : map [string ]string {"app" : "test" },
762+ },
763+ Template : corev1.PodTemplateSpec {
764+ ObjectMeta : metav1.ObjectMeta {
765+ Labels : map [string ]string {"app" : "test" },
766+ },
767+ Spec : corev1.PodSpec {
768+ Containers : []corev1.Container {
769+ {
770+ Name : "main" ,
771+ Image : "test:v1" ,
772+ },
773+ },
774+ // Legacy CA bundle volumes (old operator pattern)
775+ Volumes : []corev1.Volume {
776+ {
777+ Name : "data" ,
778+ VolumeSource : corev1.VolumeSource {
779+ EmptyDir : & corev1.EmptyDirVolumeSource {},
780+ },
781+ },
782+ {
783+ Name : "ca-bundle" ,
784+ VolumeSource : corev1.VolumeSource {
785+ EmptyDir : & corev1.EmptyDirVolumeSource {},
786+ },
787+ },
788+ {
789+ Name : "ca-bundle-source" ,
790+ VolumeSource : corev1.VolumeSource {
791+ ConfigMap : & corev1.ConfigMapVolumeSource {
792+ LocalObjectReference : corev1.LocalObjectReference {
793+ Name : "odh-trusted-ca-bundle" ,
794+ },
795+ },
796+ },
797+ },
798+ },
799+ InitContainers : []corev1.Container {
800+ {
801+ Name : "ca-bundle-init" ,
802+ Image : "busybox" ,
803+ },
804+ },
805+ },
806+ },
807+ },
808+ }
809+ require .NoError (t , k8sClient .Create (ctx , existingDeployment ))
810+
811+ // Create desired deployment with new CA bundle pattern (new operator pattern)
812+ // Must use same selector as existing deployment (selector is immutable)
813+ desiredDeployment := newTestResource (t , "apps/v1" , "Deployment" , "test-deployment" , testNs , map [string ]any {
814+ "replicas" : int32 (1 ),
815+ "selector" : map [string ]any {
816+ "matchLabels" : map [string ]any {
817+ "app" : "test" ,
818+ },
819+ },
820+ "template" : map [string ]any {
821+ "metadata" : map [string ]any {
822+ "labels" : map [string ]any {
823+ "app" : "test" ,
824+ },
825+ },
826+ "spec" : map [string ]any {
827+ "containers" : []any {
828+ map [string ]any {
829+ "name" : "main" ,
830+ "image" : "test:v2" ,
831+ },
832+ },
833+ "volumes" : []any {
834+ map [string ]any {
835+ "name" : "data" ,
836+ "emptyDir" : map [string ]any {},
837+ },
838+ // New CA bundle pattern - single ConfigMap volume
839+ map [string ]any {
840+ "name" : "ca-bundle" ,
841+ "configMap" : map [string ]any {
842+ "name" : "managed-ca-bundle" ,
843+ },
844+ },
845+ },
846+ },
847+ },
848+ })
849+ desiredDeployment .SetLabels (map [string ]string {"version" : "new" })
850+
851+ resMap := resmap .New ()
852+ require .NoError (t , resMap .Append (desiredDeployment ))
853+
854+ // Apply the resources (should trigger replacement instead of patch)
855+ require .NoError (t , ApplyResources (ctx , k8sClient , scheme .Scheme , owner , & resMap ))
856+
857+ // Verify the deployment was updated
858+ updatedDeployment := & appsv1.Deployment {}
859+ deploymentKey := types.NamespacedName {Name : "test-deployment" , Namespace : testNs }
860+ require .NoError (t , k8sClient .Get (ctx , deploymentKey , updatedDeployment ))
861+
862+ // Verify labels were updated (proves Update was used, not just patch)
863+ require .Equal (t , "new" , updatedDeployment .Labels ["version" ], "deployment labels should be updated" )
864+
865+ // Verify container image was updated
866+ require .Equal (t , "test:v2" , updatedDeployment .Spec .Template .Spec .Containers [0 ].Image , "container image should be updated" )
867+
868+ // Verify legacy volumes are removed
869+ volumeNames := make ([]string , len (updatedDeployment .Spec .Template .Spec .Volumes ))
870+ for i , vol := range updatedDeployment .Spec .Template .Spec .Volumes {
871+ volumeNames [i ] = vol .Name
872+ }
873+ require .NotContains (t , volumeNames , "ca-bundle-source" , "legacy ca-bundle-source volume should be removed" )
874+
875+ // Verify new ca-bundle is a ConfigMap (not emptyDir)
876+ var caBundleVolume * corev1.Volume
877+ for i := range updatedDeployment .Spec .Template .Spec .Volumes {
878+ if updatedDeployment .Spec .Template .Spec .Volumes [i ].Name == "ca-bundle" {
879+ caBundleVolume = & updatedDeployment .Spec .Template .Spec .Volumes [i ]
880+ break
881+ }
882+ }
883+ require .NotNil (t , caBundleVolume , "ca-bundle volume should exist" )
884+ require .NotNil (t , caBundleVolume .ConfigMap , "ca-bundle should be a ConfigMap volume" )
885+ require .Nil (t , caBundleVolume .EmptyDir , "ca-bundle should not be an emptyDir volume" )
886+ require .Equal (t , "managed-ca-bundle" , caBundleVolume .ConfigMap .Name , "ca-bundle ConfigMap name should be correct" )
887+
888+ // Verify init containers are removed
889+ require .Empty (t , updatedDeployment .Spec .Template .Spec .InitContainers , "legacy init containers should be removed" )
890+ }
891+
892+ // resourceToUnstructured converts a kustomize resource to an unstructured object.
893+ func resourceToUnstructured (t * testing.T , res * kresource.Resource ) (* unstructured.Unstructured , error ) {
894+ t .Helper ()
895+
896+ yamlBytes , err := res .AsYAML ()
897+ if err != nil {
898+ return nil , err
899+ }
900+
901+ u := & unstructured.Unstructured {}
902+ if err := yaml .Unmarshal (yamlBytes , u ); err != nil {
903+ return nil , err
904+ }
905+
906+ return u , nil
907+ }
908+
909+ // ptr is a helper function to get a pointer to a value.
910+ func ptr [T any ](v T ) * T {
911+ return & v
912+ }
0 commit comments