@@ -20,10 +20,12 @@ import (
20
20
"github.com/stretchr/testify/require"
21
21
corev1 "k8s.io/api/core/v1"
22
22
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23
+ "k8s.io/apimachinery/pkg/runtime"
23
24
"k8s.io/apimachinery/pkg/runtime/schema"
24
25
"k8s.io/apimachinery/pkg/version"
25
26
fakediscovery "k8s.io/client-go/discovery/fake"
26
27
"k8s.io/client-go/kubernetes/fake"
28
+ k8stesting "k8s.io/client-go/testing"
27
29
logf "sigs.k8s.io/controller-runtime/pkg/log"
28
30
)
29
31
@@ -688,6 +690,287 @@ func Test_PlaceHolderSecretCreated_WhenPackageInstallUpdated(t *testing.T) {
688
690
assert .Equal (t , "instl-pkg-fetch-0" , app .Spec .Fetch [0 ].ImgpkgBundle .SecretRef .Name )
689
691
}
690
692
693
+ // Test_StatusUpdaterClosureWithAppUpdateFromReconcileFailed tests this exact scenario:
694
+ // 1. App exists in ReconcileFailed state (generation == observedGeneration)
695
+ // 2. PackageInstall gets updated to reference a new package version
696
+ // 3. This updates the App spec (and in real K8s would increment generation)
697
+ // 4. PackageInstall status must now reflect the updated App state
698
+ func Test_StatusUpdaterClosureWithAppUpdateFromReconcileFailed (t * testing.T ) {
699
+ log := logf .Log .WithName ("kc" )
700
+
701
+ // Create a package
702
+ pkg := datapkgingv1alpha1.Package {
703
+ ObjectMeta : metav1.ObjectMeta {
704
+ Name : "test-pkg.2.0.0" ,
705
+ },
706
+ Spec : datapkgingv1alpha1.PackageSpec {
707
+ RefName : "test-pkg" ,
708
+ Version : "2.0.0" ,
709
+ Template : datapkgingv1alpha1.AppTemplateSpec {
710
+ Spec : & v1alpha1.AppSpec {
711
+ Fetch : []v1alpha1.AppFetch {
712
+ {
713
+ ImgpkgBundle : & v1alpha1.AppFetchImgpkgBundle {
714
+ Image : "test-pkg:2.0.0" ,
715
+ },
716
+ },
717
+ },
718
+ Template : []v1alpha1.AppTemplate {
719
+ {
720
+ Ytt : & v1alpha1.AppTemplateYtt {},
721
+ },
722
+ },
723
+ Deploy : []v1alpha1.AppDeploy {
724
+ {
725
+ Kapp : & v1alpha1.AppDeployKapp {},
726
+ },
727
+ },
728
+ },
729
+ },
730
+ },
731
+ }
732
+
733
+ fakePkgClient := fakeapiserver .NewSimpleClientset (& pkg )
734
+
735
+ model := & pkgingv1alpha1.PackageInstall {
736
+ ObjectMeta : metav1.ObjectMeta {
737
+ Name : "test-pkg-install" ,
738
+ },
739
+ Spec : pkgingv1alpha1.PackageInstallSpec {
740
+ PackageRef : & pkgingv1alpha1.PackageRef {
741
+ RefName : "test-pkg" ,
742
+ VersionSelection : & versions.VersionSelectionSemver {
743
+ Constraints : "2.0.0" ,
744
+ },
745
+ },
746
+ ServiceAccountName : "use-local-cluster-sa" ,
747
+ },
748
+ }
749
+
750
+ // Create an existing App that will be updated
751
+ existingApp := & v1alpha1.App {
752
+ ObjectMeta : metav1.ObjectMeta {
753
+ Name : "test-pkg-install" ,
754
+ Generation : 3 ,
755
+ Annotations : map [string ]string {
756
+ "packaging.carvel.dev/package-ref-name" : "test-pkg" ,
757
+ "packaging.carvel.dev/package-version" : "1.0.0" , // Old version
758
+ },
759
+ },
760
+ Spec : v1alpha1.AppSpec {
761
+ Fetch : []v1alpha1.AppFetch {
762
+ {
763
+ ImgpkgBundle : & v1alpha1.AppFetchImgpkgBundle {
764
+ Image : "test-pkg:1.0.0" , // Old image
765
+ },
766
+ },
767
+ },
768
+ Template : []v1alpha1.AppTemplate {
769
+ {
770
+ Ytt : & v1alpha1.AppTemplateYtt {},
771
+ },
772
+ },
773
+ Deploy : []v1alpha1.AppDeploy {
774
+ {
775
+ Kapp : & v1alpha1.AppDeployKapp {},
776
+ },
777
+ },
778
+ ServiceAccountName : "use-local-cluster-sa" ,
779
+ },
780
+ Status : v1alpha1.AppStatus {
781
+ GenericStatus : v1alpha1.GenericStatus {
782
+ ObservedGeneration : 3 , // Generation == ObservedGeneration
783
+ Conditions : []v1alpha1.Condition {
784
+ {
785
+ Type : v1alpha1 .ReconcileFailed ,
786
+ Status : corev1 .ConditionTrue ,
787
+ },
788
+ },
789
+ FriendlyDescription : "Reconcile failed" ,
790
+ UsefulErrorMessage : "Original error from v1.0.0" ,
791
+ },
792
+ },
793
+ }
794
+
795
+ // Create a custom fake client that increments generation on update (like real K8s)
796
+ fakekctrl := fakekappctrl .NewSimpleClientset (model , existingApp )
797
+
798
+ // Add a reactor to simulate generation increment on App updates
799
+ fakekctrl .PrependReactor ("update" , "apps" , func (action k8stesting.Action ) (handled bool , ret runtime.Object , err error ) {
800
+ updateAction := action .(k8stesting.UpdateAction )
801
+ if app , ok := updateAction .GetObject ().(* v1alpha1.App ); ok {
802
+ // Simulate generation increment when App is updated (like real K8s)
803
+ app .Generation = app .Generation + 1
804
+ }
805
+ return false , nil , nil // Let the default handler process the update
806
+ })
807
+
808
+ fakek8s := fake .NewSimpleClientset ()
809
+ fakeDiscovery , _ := fakek8s .Discovery ().(* fakediscovery.FakeDiscovery )
810
+ fakeDiscovery .FakedServerVersion = & version.Info {
811
+ GitVersion : "v0.20.0" ,
812
+ }
813
+
814
+ ip := NewPackageInstallCR (model , log , fakekctrl , fakePkgClient , fakek8s ,
815
+ FakeComponentInfo {KCVersion : semver .MustParse ("0.42.31337" )}, Opts {},
816
+ metrics .NewMetrics ())
817
+
818
+ // Reconcile should update the App and set PackageInstall status
819
+ _ , err := ip .Reconcile ()
820
+ assert .Nil (t , err )
821
+
822
+ // Verify the App was updated
823
+ gvr := schema.GroupVersionResource {"kappctrl.k14s.io" , "v1alpha1" , "apps" }
824
+ obj , err := fakekctrl .Tracker ().Get (gvr , "" , "test-pkg-install" )
825
+ assert .Nil (t , err )
826
+ require .NotNil (t , obj )
827
+ updatedApp := obj .(* v1alpha1.App )
828
+
829
+ // Verify App was updated with new package content
830
+ assert .Equal (t , "test-pkg:2.0.0" , updatedApp .Spec .Fetch [0 ].ImgpkgBundle .Image )
831
+ assert .Equal (t , "2.0.0" , updatedApp .Annotations ["packaging.carvel.dev/package-version" ])
832
+ assert .Equal (t , int64 (4 ), updatedApp .Generation , "Generation should have incremented from 3 to 4" )
833
+
834
+ // Verify PackageInstall status reflects the updated App state
835
+ assert .Len (t , ip .model .Status .Conditions , 1 )
836
+ assert .Equal (t , v1alpha1 .Reconciling , ip .model .Status .Conditions [0 ].Type )
837
+ assert .Equal (t , corev1 .ConditionTrue , ip .model .Status .Conditions [0 ].Status )
838
+ assert .Equal (t , "Reconciling" , ip .model .Status .FriendlyDescription )
839
+ }
840
+
841
+ // Test_StatusUpdaterClosureWithNoAppUpdate verifies the closure works when App doesn't need updating:
842
+ // 1. App exists in ReconcileFailed state (generation == observedGeneration)
843
+ // 2. PackageInstall points to same package version as App
844
+ // 3. No App update needed, so PackageInstall status should reflect existing App state
845
+ func Test_StatusUpdaterClosureWithNoAppUpdate (t * testing.T ) {
846
+ log := logf .Log .WithName ("kc" )
847
+
848
+ // Create a package
849
+ pkg := datapkgingv1alpha1.Package {
850
+ ObjectMeta : metav1.ObjectMeta {
851
+ Name : "test-pkg.1.0.0" ,
852
+ },
853
+ Spec : datapkgingv1alpha1.PackageSpec {
854
+ RefName : "test-pkg" ,
855
+ Version : "1.0.0" ,
856
+ Template : datapkgingv1alpha1.AppTemplateSpec {
857
+ Spec : & v1alpha1.AppSpec {
858
+ Fetch : []v1alpha1.AppFetch {
859
+ {
860
+ ImgpkgBundle : & v1alpha1.AppFetchImgpkgBundle {
861
+ Image : "test-pkg:1.0.0" ,
862
+ },
863
+ },
864
+ },
865
+ Template : []v1alpha1.AppTemplate {
866
+ {
867
+ Ytt : & v1alpha1.AppTemplateYtt {},
868
+ },
869
+ },
870
+ Deploy : []v1alpha1.AppDeploy {
871
+ {
872
+ Kapp : & v1alpha1.AppDeployKapp {},
873
+ },
874
+ },
875
+ },
876
+ },
877
+ },
878
+ }
879
+
880
+ fakePkgClient := fakeapiserver .NewSimpleClientset (& pkg )
881
+
882
+ // Create a PackageInstall pointing to v1.0.0
883
+ model := & pkgingv1alpha1.PackageInstall {
884
+ ObjectMeta : metav1.ObjectMeta {
885
+ Name : "test-pkg-install" ,
886
+ },
887
+ Spec : pkgingv1alpha1.PackageInstallSpec {
888
+ PackageRef : & pkgingv1alpha1.PackageRef {
889
+ RefName : "test-pkg" ,
890
+ VersionSelection : & versions.VersionSelectionSemver {
891
+ Constraints : "1.0.0" ,
892
+ },
893
+ },
894
+ ServiceAccountName : "use-local-cluster-sa" ,
895
+ },
896
+ }
897
+
898
+ // Create an existing App in ReconcileFailed state with matching spec (no update needed)
899
+ existingApp := & v1alpha1.App {
900
+ ObjectMeta : metav1.ObjectMeta {
901
+ Name : "test-pkg-install" ,
902
+ Generation : 3 ,
903
+ },
904
+ Spec : v1alpha1.AppSpec {
905
+ Fetch : []v1alpha1.AppFetch {
906
+ {
907
+ ImgpkgBundle : & v1alpha1.AppFetchImgpkgBundle {
908
+ Image : "test-pkg:1.0.0" , // Same as package spec
909
+ },
910
+ },
911
+ },
912
+ Template : []v1alpha1.AppTemplate {
913
+ {
914
+ Ytt : & v1alpha1.AppTemplateYtt {},
915
+ },
916
+ },
917
+ Deploy : []v1alpha1.AppDeploy {
918
+ {
919
+ Kapp : & v1alpha1.AppDeployKapp {},
920
+ },
921
+ },
922
+ ServiceAccountName : "use-local-cluster-sa" ,
923
+ },
924
+ Status : v1alpha1.AppStatus {
925
+ GenericStatus : v1alpha1.GenericStatus {
926
+ ObservedGeneration : 3 , // Generation == ObservedGeneration (stable failed state)
927
+ Conditions : []v1alpha1.Condition {
928
+ {
929
+ Type : v1alpha1 .ReconcileFailed ,
930
+ Status : corev1 .ConditionTrue ,
931
+ },
932
+ },
933
+ FriendlyDescription : "Reconcile failed" ,
934
+ UsefulErrorMessage : "Deploy failed for v1.0.0" ,
935
+ },
936
+ },
937
+ }
938
+
939
+ fakekctrl := fakekappctrl .NewSimpleClientset (model , existingApp )
940
+ fakek8s := fake .NewSimpleClientset ()
941
+
942
+ // mock the kubernetes server version
943
+ fakeDiscovery , _ := fakek8s .Discovery ().(* fakediscovery.FakeDiscovery )
944
+ fakeDiscovery .FakedServerVersion = & version.Info {
945
+ GitVersion : "v0.20.0" ,
946
+ }
947
+
948
+ ip := NewPackageInstallCR (model , log , fakekctrl , fakePkgClient , fakek8s ,
949
+ FakeComponentInfo {KCVersion : semver .MustParse ("0.42.31337" )}, Opts {},
950
+ metrics .NewMetrics ())
951
+
952
+ // Reconcile should NOT update the App since specs match
953
+ _ , err := ip .Reconcile ()
954
+ assert .Nil (t , err )
955
+
956
+ // Verify the App was NOT updated (generation should remain the same)
957
+ gvr := schema.GroupVersionResource {"kappctrl.k14s.io" , "v1alpha1" , "apps" }
958
+ obj , err := fakekctrl .Tracker ().Get (gvr , "" , "test-pkg-install" )
959
+ assert .Nil (t , err )
960
+ require .NotNil (t , obj )
961
+ appAfterReconcile := obj .(* v1alpha1.App )
962
+
963
+ // App should be unchanged
964
+ assert .Equal (t , int64 (3 ), appAfterReconcile .Generation , "App generation should not have changed" )
965
+ assert .Equal (t , "test-pkg:1.0.0" , appAfterReconcile .Spec .Fetch [0 ].ImgpkgBundle .Image )
966
+
967
+ // PackageInstall status should reflect the existing App state (ReconcileFailed)
968
+ assert .Len (t , ip .model .Status .Conditions , 1 )
969
+ assert .Equal (t , v1alpha1 .ReconcileFailed , ip .model .Status .Conditions [0 ].Type )
970
+ assert .Equal (t , corev1 .ConditionTrue , ip .model .Status .Conditions [0 ].Status )
971
+ assert .Equal (t , "Deploy failed for v1.0.0" , ip .model .Status .UsefulErrorMessage )
972
+ }
973
+
691
974
func generatePackageWithConstraints (name , version , kcConstraint string , k8sConstraint string ) datapkgingv1alpha1.Package {
692
975
return datapkgingv1alpha1.Package {
693
976
ObjectMeta : metav1.ObjectMeta {
0 commit comments