k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/api/v1/pod/util_test.go (about)

     1  /*
     2  Copyright 2015 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package pod
    18  
    19  import (
    20  	"reflect"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	"github.com/stretchr/testify/assert"
    27  	v1 "k8s.io/api/core/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/util/intstr"
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  	"k8s.io/apimachinery/pkg/util/validation/field"
    32  )
    33  
    34  func TestFindPort(t *testing.T) {
    35  	testCases := []struct {
    36  		name       string
    37  		containers []v1.Container
    38  		port       intstr.IntOrString
    39  		expected   int
    40  		pass       bool
    41  	}{{
    42  		name:       "valid int, no ports",
    43  		containers: []v1.Container{{}},
    44  		port:       intstr.FromInt32(93),
    45  		expected:   93,
    46  		pass:       true,
    47  	}, {
    48  		name: "valid int, with ports",
    49  		containers: []v1.Container{{Ports: []v1.ContainerPort{{
    50  			Name:          "",
    51  			ContainerPort: 11,
    52  			Protocol:      "TCP",
    53  		}, {
    54  			Name:          "p",
    55  			ContainerPort: 22,
    56  			Protocol:      "TCP",
    57  		}}}},
    58  		port:     intstr.FromInt32(93),
    59  		expected: 93,
    60  		pass:     true,
    61  	}, {
    62  		name:       "valid str, no ports",
    63  		containers: []v1.Container{{}},
    64  		port:       intstr.FromString("p"),
    65  		expected:   0,
    66  		pass:       false,
    67  	}, {
    68  		name: "valid str, one ctr with ports",
    69  		containers: []v1.Container{{Ports: []v1.ContainerPort{{
    70  			Name:          "",
    71  			ContainerPort: 11,
    72  			Protocol:      "UDP",
    73  		}, {
    74  			Name:          "p",
    75  			ContainerPort: 22,
    76  			Protocol:      "TCP",
    77  		}, {
    78  			Name:          "q",
    79  			ContainerPort: 33,
    80  			Protocol:      "TCP",
    81  		}}}},
    82  		port:     intstr.FromString("q"),
    83  		expected: 33,
    84  		pass:     true,
    85  	}, {
    86  		name: "valid str, two ctr with ports",
    87  		containers: []v1.Container{{}, {Ports: []v1.ContainerPort{{
    88  			Name:          "",
    89  			ContainerPort: 11,
    90  			Protocol:      "UDP",
    91  		}, {
    92  			Name:          "p",
    93  			ContainerPort: 22,
    94  			Protocol:      "TCP",
    95  		}, {
    96  			Name:          "q",
    97  			ContainerPort: 33,
    98  			Protocol:      "TCP",
    99  		}}}},
   100  		port:     intstr.FromString("q"),
   101  		expected: 33,
   102  		pass:     true,
   103  	}, {
   104  		name: "valid str, two ctr with same port",
   105  		containers: []v1.Container{{}, {Ports: []v1.ContainerPort{{
   106  			Name:          "",
   107  			ContainerPort: 11,
   108  			Protocol:      "UDP",
   109  		}, {
   110  			Name:          "p",
   111  			ContainerPort: 22,
   112  			Protocol:      "TCP",
   113  		}, {
   114  			Name:          "q",
   115  			ContainerPort: 22,
   116  			Protocol:      "TCP",
   117  		}}}},
   118  		port:     intstr.FromString("q"),
   119  		expected: 22,
   120  		pass:     true,
   121  	}, {
   122  		name: "valid str, invalid protocol",
   123  		containers: []v1.Container{{}, {Ports: []v1.ContainerPort{{
   124  			Name:          "a",
   125  			ContainerPort: 11,
   126  			Protocol:      "snmp",
   127  		},
   128  		}}},
   129  		port:     intstr.FromString("a"),
   130  		expected: 0,
   131  		pass:     false,
   132  	}, {
   133  		name: "valid hostPort",
   134  		containers: []v1.Container{{}, {Ports: []v1.ContainerPort{{
   135  			Name:          "a",
   136  			ContainerPort: 11,
   137  			HostPort:      81,
   138  			Protocol:      "TCP",
   139  		},
   140  		}}},
   141  		port:     intstr.FromString("a"),
   142  		expected: 11,
   143  		pass:     true,
   144  	},
   145  		{
   146  			name: "invalid hostPort",
   147  			containers: []v1.Container{{}, {Ports: []v1.ContainerPort{{
   148  				Name:          "a",
   149  				ContainerPort: 11,
   150  				HostPort:      -1,
   151  				Protocol:      "TCP",
   152  			},
   153  			}}},
   154  			port:     intstr.FromString("a"),
   155  			expected: 11,
   156  			pass:     true,
   157  			//this should fail but passes.
   158  		},
   159  		{
   160  			name: "invalid ContainerPort",
   161  			containers: []v1.Container{{}, {Ports: []v1.ContainerPort{{
   162  				Name:          "a",
   163  				ContainerPort: -1,
   164  				Protocol:      "TCP",
   165  			},
   166  			}}},
   167  			port:     intstr.FromString("a"),
   168  			expected: -1,
   169  			pass:     true,
   170  			//this should fail but passes
   171  		},
   172  		{
   173  			name: "HostIP Address",
   174  			containers: []v1.Container{{}, {Ports: []v1.ContainerPort{{
   175  				Name:          "a",
   176  				ContainerPort: 11,
   177  				HostIP:        "192.168.1.1",
   178  				Protocol:      "TCP",
   179  			},
   180  			}}},
   181  			port:     intstr.FromString("a"),
   182  			expected: 11,
   183  			pass:     true,
   184  		},
   185  	}
   186  
   187  	for _, tc := range testCases {
   188  		port, err := FindPort(&v1.Pod{Spec: v1.PodSpec{Containers: tc.containers}},
   189  			&v1.ServicePort{Protocol: "TCP", TargetPort: tc.port})
   190  		if err != nil && tc.pass {
   191  			t.Errorf("unexpected error for %s: %v", tc.name, err)
   192  		}
   193  		if err == nil && !tc.pass {
   194  			t.Errorf("unexpected non-error for %s: %d", tc.name, port)
   195  		}
   196  		if port != tc.expected {
   197  			t.Errorf("wrong result for %s: expected %d, got %d", tc.name, tc.expected, port)
   198  		}
   199  	}
   200  }
   201  
   202  func TestVisitContainers(t *testing.T) {
   203  	setAllFeatureEnabledContainersDuringTest := ContainerType(0)
   204  	testCases := []struct {
   205  		desc           string
   206  		spec           *v1.PodSpec
   207  		wantContainers []string
   208  		mask           ContainerType
   209  	}{
   210  		{
   211  			desc:           "empty podspec",
   212  			spec:           &v1.PodSpec{},
   213  			wantContainers: []string{},
   214  			mask:           AllContainers,
   215  		},
   216  		{
   217  			desc: "regular containers",
   218  			spec: &v1.PodSpec{
   219  				Containers: []v1.Container{
   220  					{Name: "c1"},
   221  					{Name: "c2"},
   222  				},
   223  				InitContainers: []v1.Container{
   224  					{Name: "i1"},
   225  					{Name: "i2"},
   226  				},
   227  				EphemeralContainers: []v1.EphemeralContainer{
   228  					{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e1"}},
   229  					{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e2"}},
   230  				},
   231  			},
   232  			wantContainers: []string{"c1", "c2"},
   233  			mask:           Containers,
   234  		},
   235  		{
   236  			desc: "init containers",
   237  			spec: &v1.PodSpec{
   238  				Containers: []v1.Container{
   239  					{Name: "c1"},
   240  					{Name: "c2"},
   241  				},
   242  				InitContainers: []v1.Container{
   243  					{Name: "i1"},
   244  					{Name: "i2"},
   245  				},
   246  				EphemeralContainers: []v1.EphemeralContainer{
   247  					{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e1"}},
   248  					{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e2"}},
   249  				},
   250  			},
   251  			wantContainers: []string{"i1", "i2"},
   252  			mask:           InitContainers,
   253  		},
   254  		{
   255  			desc: "ephemeral containers",
   256  			spec: &v1.PodSpec{
   257  				Containers: []v1.Container{
   258  					{Name: "c1"},
   259  					{Name: "c2"},
   260  				},
   261  				InitContainers: []v1.Container{
   262  					{Name: "i1"},
   263  					{Name: "i2"},
   264  				},
   265  				EphemeralContainers: []v1.EphemeralContainer{
   266  					{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e1"}},
   267  					{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e2"}},
   268  				},
   269  			},
   270  			wantContainers: []string{"e1", "e2"},
   271  			mask:           EphemeralContainers,
   272  		},
   273  		{
   274  			desc: "all container types",
   275  			spec: &v1.PodSpec{
   276  				Containers: []v1.Container{
   277  					{Name: "c1"},
   278  					{Name: "c2"},
   279  				},
   280  				InitContainers: []v1.Container{
   281  					{Name: "i1"},
   282  					{Name: "i2"},
   283  				},
   284  				EphemeralContainers: []v1.EphemeralContainer{
   285  					{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e1"}},
   286  					{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e2"}},
   287  				},
   288  			},
   289  			wantContainers: []string{"i1", "i2", "c1", "c2", "e1", "e2"},
   290  			mask:           AllContainers,
   291  		},
   292  		{
   293  			desc: "all feature enabled container types",
   294  			spec: &v1.PodSpec{
   295  				Containers: []v1.Container{
   296  					{Name: "c1"},
   297  					{Name: "c2", SecurityContext: &v1.SecurityContext{}},
   298  				},
   299  				InitContainers: []v1.Container{
   300  					{Name: "i1"},
   301  					{Name: "i2", SecurityContext: &v1.SecurityContext{}},
   302  				},
   303  				EphemeralContainers: []v1.EphemeralContainer{
   304  					{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e1"}},
   305  					{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e2"}},
   306  				},
   307  			},
   308  			wantContainers: []string{"i1", "i2", "c1", "c2", "e1", "e2"},
   309  			mask:           setAllFeatureEnabledContainersDuringTest,
   310  		},
   311  		{
   312  			desc: "dropping fields",
   313  			spec: &v1.PodSpec{
   314  				Containers: []v1.Container{
   315  					{Name: "c1"},
   316  					{Name: "c2", SecurityContext: &v1.SecurityContext{}},
   317  				},
   318  				InitContainers: []v1.Container{
   319  					{Name: "i1"},
   320  					{Name: "i2", SecurityContext: &v1.SecurityContext{}},
   321  				},
   322  				EphemeralContainers: []v1.EphemeralContainer{
   323  					{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e1"}},
   324  					{EphemeralContainerCommon: v1.EphemeralContainerCommon{Name: "e2", SecurityContext: &v1.SecurityContext{}}},
   325  				},
   326  			},
   327  			wantContainers: []string{"i1", "i2", "c1", "c2", "e1", "e2"},
   328  			mask:           AllContainers,
   329  		},
   330  	}
   331  
   332  	for _, tc := range testCases {
   333  		t.Run(tc.desc, func(t *testing.T) {
   334  			if tc.mask == setAllFeatureEnabledContainersDuringTest {
   335  				tc.mask = AllFeatureEnabledContainers()
   336  			}
   337  
   338  			gotContainers := []string{}
   339  			VisitContainers(tc.spec, tc.mask, func(c *v1.Container, containerType ContainerType) bool {
   340  				gotContainers = append(gotContainers, c.Name)
   341  				if c.SecurityContext != nil {
   342  					c.SecurityContext = nil
   343  				}
   344  				return true
   345  			})
   346  			if !cmp.Equal(gotContainers, tc.wantContainers) {
   347  				t.Errorf("VisitContainers() = %+v, want %+v", gotContainers, tc.wantContainers)
   348  			}
   349  			for _, c := range tc.spec.Containers {
   350  				if c.SecurityContext != nil {
   351  					t.Errorf("VisitContainers() did not drop SecurityContext for container %q", c.Name)
   352  				}
   353  			}
   354  			for _, c := range tc.spec.InitContainers {
   355  				if c.SecurityContext != nil {
   356  					t.Errorf("VisitContainers() did not drop SecurityContext for init container %q", c.Name)
   357  				}
   358  			}
   359  			for _, c := range tc.spec.EphemeralContainers {
   360  				if c.SecurityContext != nil {
   361  					t.Errorf("VisitContainers() did not drop SecurityContext for ephemeral container %q", c.Name)
   362  				}
   363  			}
   364  		})
   365  	}
   366  }
   367  
   368  func TestPodSecrets(t *testing.T) {
   369  	// Stub containing all possible secret references in a pod.
   370  	// The names of the referenced secrets match struct paths detected by reflection.
   371  	pod := &v1.Pod{
   372  		Spec: v1.PodSpec{
   373  			Containers: []v1.Container{{
   374  				EnvFrom: []v1.EnvFromSource{{
   375  					SecretRef: &v1.SecretEnvSource{
   376  						LocalObjectReference: v1.LocalObjectReference{
   377  							Name: "Spec.Containers[*].EnvFrom[*].SecretRef"}}}},
   378  				Env: []v1.EnvVar{{
   379  					ValueFrom: &v1.EnvVarSource{
   380  						SecretKeyRef: &v1.SecretKeySelector{
   381  							LocalObjectReference: v1.LocalObjectReference{
   382  								Name: "Spec.Containers[*].Env[*].ValueFrom.SecretKeyRef"}}}}}}},
   383  			ImagePullSecrets: []v1.LocalObjectReference{{
   384  				Name: "Spec.ImagePullSecrets"}},
   385  			InitContainers: []v1.Container{{
   386  				EnvFrom: []v1.EnvFromSource{{
   387  					SecretRef: &v1.SecretEnvSource{
   388  						LocalObjectReference: v1.LocalObjectReference{
   389  							Name: "Spec.InitContainers[*].EnvFrom[*].SecretRef"}}}},
   390  				Env: []v1.EnvVar{{
   391  					ValueFrom: &v1.EnvVarSource{
   392  						SecretKeyRef: &v1.SecretKeySelector{
   393  							LocalObjectReference: v1.LocalObjectReference{
   394  								Name: "Spec.InitContainers[*].Env[*].ValueFrom.SecretKeyRef"}}}}}}},
   395  			Volumes: []v1.Volume{{
   396  				VolumeSource: v1.VolumeSource{
   397  					AzureFile: &v1.AzureFileVolumeSource{
   398  						SecretName: "Spec.Volumes[*].VolumeSource.AzureFile.SecretName"}}}, {
   399  				VolumeSource: v1.VolumeSource{
   400  					CephFS: &v1.CephFSVolumeSource{
   401  						SecretRef: &v1.LocalObjectReference{
   402  							Name: "Spec.Volumes[*].VolumeSource.CephFS.SecretRef"}}}}, {
   403  				VolumeSource: v1.VolumeSource{
   404  					Cinder: &v1.CinderVolumeSource{
   405  						SecretRef: &v1.LocalObjectReference{
   406  							Name: "Spec.Volumes[*].VolumeSource.Cinder.SecretRef"}}}}, {
   407  				VolumeSource: v1.VolumeSource{
   408  					FlexVolume: &v1.FlexVolumeSource{
   409  						SecretRef: &v1.LocalObjectReference{
   410  							Name: "Spec.Volumes[*].VolumeSource.FlexVolume.SecretRef"}}}}, {
   411  				VolumeSource: v1.VolumeSource{
   412  					Projected: &v1.ProjectedVolumeSource{
   413  						Sources: []v1.VolumeProjection{{
   414  							Secret: &v1.SecretProjection{
   415  								LocalObjectReference: v1.LocalObjectReference{
   416  									Name: "Spec.Volumes[*].VolumeSource.Projected.Sources[*].Secret"}}}}}}}, {
   417  				VolumeSource: v1.VolumeSource{
   418  					RBD: &v1.RBDVolumeSource{
   419  						SecretRef: &v1.LocalObjectReference{
   420  							Name: "Spec.Volumes[*].VolumeSource.RBD.SecretRef"}}}}, {
   421  				VolumeSource: v1.VolumeSource{
   422  					Secret: &v1.SecretVolumeSource{
   423  						SecretName: "Spec.Volumes[*].VolumeSource.Secret.SecretName"}}}, {
   424  				VolumeSource: v1.VolumeSource{
   425  					Secret: &v1.SecretVolumeSource{
   426  						SecretName: "Spec.Volumes[*].VolumeSource.Secret"}}}, {
   427  				VolumeSource: v1.VolumeSource{
   428  					ScaleIO: &v1.ScaleIOVolumeSource{
   429  						SecretRef: &v1.LocalObjectReference{
   430  							Name: "Spec.Volumes[*].VolumeSource.ScaleIO.SecretRef"}}}}, {
   431  				VolumeSource: v1.VolumeSource{
   432  					ISCSI: &v1.ISCSIVolumeSource{
   433  						SecretRef: &v1.LocalObjectReference{
   434  							Name: "Spec.Volumes[*].VolumeSource.ISCSI.SecretRef"}}}}, {
   435  				VolumeSource: v1.VolumeSource{
   436  					StorageOS: &v1.StorageOSVolumeSource{
   437  						SecretRef: &v1.LocalObjectReference{
   438  							Name: "Spec.Volumes[*].VolumeSource.StorageOS.SecretRef"}}}}, {
   439  				VolumeSource: v1.VolumeSource{
   440  					CSI: &v1.CSIVolumeSource{
   441  						NodePublishSecretRef: &v1.LocalObjectReference{
   442  							Name: "Spec.Volumes[*].VolumeSource.CSI.NodePublishSecretRef"}}}}},
   443  			EphemeralContainers: []v1.EphemeralContainer{{
   444  				EphemeralContainerCommon: v1.EphemeralContainerCommon{
   445  					EnvFrom: []v1.EnvFromSource{{
   446  						SecretRef: &v1.SecretEnvSource{
   447  							LocalObjectReference: v1.LocalObjectReference{
   448  								Name: "Spec.EphemeralContainers[*].EphemeralContainerCommon.EnvFrom[*].SecretRef"}}}},
   449  					Env: []v1.EnvVar{{
   450  						ValueFrom: &v1.EnvVarSource{
   451  							SecretKeyRef: &v1.SecretKeySelector{
   452  								LocalObjectReference: v1.LocalObjectReference{
   453  									Name: "Spec.EphemeralContainers[*].EphemeralContainerCommon.Env[*].ValueFrom.SecretKeyRef"}}}}}}}},
   454  		},
   455  	}
   456  	extractedNames := sets.New[string]()
   457  	VisitPodSecretNames(pod, func(name string) bool {
   458  		extractedNames.Insert(name)
   459  		return true
   460  	})
   461  
   462  	// excludedSecretPaths holds struct paths to fields with "secret" in the name that are not actually references to secret API objects
   463  	excludedSecretPaths := sets.New[string](
   464  		"Spec.Volumes[*].VolumeSource.CephFS.SecretFile",
   465  	)
   466  	// expectedSecretPaths holds struct paths to fields with "secret" in the name that are references to secret API objects.
   467  	// every path here should be represented as an example in the Pod stub above, with the secret name set to the path.
   468  	expectedSecretPaths := sets.New[string](
   469  		"Spec.Containers[*].EnvFrom[*].SecretRef",
   470  		"Spec.Containers[*].Env[*].ValueFrom.SecretKeyRef",
   471  		"Spec.EphemeralContainers[*].EphemeralContainerCommon.EnvFrom[*].SecretRef",
   472  		"Spec.EphemeralContainers[*].EphemeralContainerCommon.Env[*].ValueFrom.SecretKeyRef",
   473  		"Spec.ImagePullSecrets",
   474  		"Spec.InitContainers[*].EnvFrom[*].SecretRef",
   475  		"Spec.InitContainers[*].Env[*].ValueFrom.SecretKeyRef",
   476  		"Spec.Volumes[*].VolumeSource.AzureFile.SecretName",
   477  		"Spec.Volumes[*].VolumeSource.CephFS.SecretRef",
   478  		"Spec.Volumes[*].VolumeSource.Cinder.SecretRef",
   479  		"Spec.Volumes[*].VolumeSource.FlexVolume.SecretRef",
   480  		"Spec.Volumes[*].VolumeSource.Projected.Sources[*].Secret",
   481  		"Spec.Volumes[*].VolumeSource.RBD.SecretRef",
   482  		"Spec.Volumes[*].VolumeSource.Secret",
   483  		"Spec.Volumes[*].VolumeSource.Secret.SecretName",
   484  		"Spec.Volumes[*].VolumeSource.ScaleIO.SecretRef",
   485  		"Spec.Volumes[*].VolumeSource.ISCSI.SecretRef",
   486  		"Spec.Volumes[*].VolumeSource.StorageOS.SecretRef",
   487  		"Spec.Volumes[*].VolumeSource.CSI.NodePublishSecretRef",
   488  	)
   489  	secretPaths := collectResourcePaths(t, "secret", nil, "", reflect.TypeOf(&v1.Pod{}))
   490  	secretPaths = secretPaths.Difference(excludedSecretPaths)
   491  	if missingPaths := expectedSecretPaths.Difference(secretPaths); len(missingPaths) > 0 {
   492  		t.Logf("Missing expected secret paths:\n%s", strings.Join(sets.List[string](missingPaths), "\n"))
   493  		t.Error("Missing expected secret paths. Verify VisitPodSecretNames() is correctly finding the missing paths, then correct expectedSecretPaths")
   494  	}
   495  	if extraPaths := secretPaths.Difference(expectedSecretPaths); len(extraPaths) > 0 {
   496  		t.Logf("Extra secret paths:\n%s", strings.Join(sets.List(extraPaths), "\n"))
   497  		t.Error("Extra fields with 'secret' in the name found. Verify VisitPodSecretNames() is including these fields if appropriate, then correct expectedSecretPaths")
   498  	}
   499  
   500  	if missingNames := expectedSecretPaths.Difference(extractedNames); len(missingNames) > 0 {
   501  		t.Logf("Missing expected secret names:\n%s", strings.Join(sets.List[string](missingNames), "\n"))
   502  		t.Error("Missing expected secret names. Verify the pod stub above includes these references, then verify VisitPodSecretNames() is correctly finding the missing names")
   503  	}
   504  	if extraNames := extractedNames.Difference(expectedSecretPaths); len(extraNames) > 0 {
   505  		t.Logf("Extra secret names:\n%s", strings.Join(sets.List[string](extraNames), "\n"))
   506  		t.Error("Extra secret names extracted. Verify VisitPodSecretNames() is correctly extracting secret names")
   507  	}
   508  
   509  	// emptyPod is a stub containing empty object names
   510  	emptyPod := &v1.Pod{
   511  		Spec: v1.PodSpec{
   512  			Containers: []v1.Container{{
   513  				EnvFrom: []v1.EnvFromSource{{
   514  					SecretRef: &v1.SecretEnvSource{
   515  						LocalObjectReference: v1.LocalObjectReference{
   516  							Name: ""}}}}}},
   517  		},
   518  	}
   519  	VisitPodSecretNames(emptyPod, func(name string) bool {
   520  		t.Fatalf("expected no empty names collected, got %q", name)
   521  		return false
   522  	})
   523  }
   524  
   525  // collectResourcePaths traverses the object, computing all the struct paths that lead to fields with resourcename in the name.
   526  func collectResourcePaths(t *testing.T, resourcename string, path *field.Path, name string, tp reflect.Type) sets.Set[string] {
   527  	resourcename = strings.ToLower(resourcename)
   528  	resourcePaths := sets.New[string]()
   529  
   530  	if tp.Kind() == reflect.Pointer {
   531  		resourcePaths.Insert(sets.List[string](collectResourcePaths(t, resourcename, path, name, tp.Elem()))...)
   532  		return resourcePaths
   533  	}
   534  
   535  	if strings.Contains(strings.ToLower(name), resourcename) {
   536  		resourcePaths.Insert(path.String())
   537  	}
   538  
   539  	switch tp.Kind() {
   540  	case reflect.Pointer:
   541  		resourcePaths.Insert(sets.List[string](collectResourcePaths(t, resourcename, path, name, tp.Elem()))...)
   542  	case reflect.Struct:
   543  		// ObjectMeta is generic and therefore should never have a field with a specific resource's name;
   544  		// it contains cycles so it's easiest to just skip it.
   545  		if name == "ObjectMeta" {
   546  			break
   547  		}
   548  		for i := 0; i < tp.NumField(); i++ {
   549  			field := tp.Field(i)
   550  			resourcePaths.Insert(sets.List[string](collectResourcePaths(t, resourcename, path.Child(field.Name), field.Name, field.Type))...)
   551  		}
   552  	case reflect.Interface:
   553  		t.Errorf("cannot find %s fields in interface{} field %s", resourcename, path.String())
   554  	case reflect.Map:
   555  		resourcePaths.Insert(sets.List[string](collectResourcePaths(t, resourcename, path.Key("*"), "", tp.Elem()))...)
   556  	case reflect.Slice:
   557  		resourcePaths.Insert(sets.List[string](collectResourcePaths(t, resourcename, path.Key("*"), "", tp.Elem()))...)
   558  	default:
   559  		// all primitive types
   560  	}
   561  
   562  	return resourcePaths
   563  }
   564  
   565  func TestPodConfigmaps(t *testing.T) {
   566  	// Stub containing all possible ConfigMap references in a pod.
   567  	// The names of the referenced ConfigMaps match struct paths detected by reflection.
   568  	pod := &v1.Pod{
   569  		Spec: v1.PodSpec{
   570  			Containers: []v1.Container{{
   571  				EnvFrom: []v1.EnvFromSource{{
   572  					ConfigMapRef: &v1.ConfigMapEnvSource{
   573  						LocalObjectReference: v1.LocalObjectReference{
   574  							Name: "Spec.Containers[*].EnvFrom[*].ConfigMapRef"}}}},
   575  				Env: []v1.EnvVar{{
   576  					ValueFrom: &v1.EnvVarSource{
   577  						ConfigMapKeyRef: &v1.ConfigMapKeySelector{
   578  							LocalObjectReference: v1.LocalObjectReference{
   579  								Name: "Spec.Containers[*].Env[*].ValueFrom.ConfigMapKeyRef"}}}}}}},
   580  			EphemeralContainers: []v1.EphemeralContainer{{
   581  				EphemeralContainerCommon: v1.EphemeralContainerCommon{
   582  					EnvFrom: []v1.EnvFromSource{{
   583  						ConfigMapRef: &v1.ConfigMapEnvSource{
   584  							LocalObjectReference: v1.LocalObjectReference{
   585  								Name: "Spec.EphemeralContainers[*].EphemeralContainerCommon.EnvFrom[*].ConfigMapRef"}}}},
   586  					Env: []v1.EnvVar{{
   587  						ValueFrom: &v1.EnvVarSource{
   588  							ConfigMapKeyRef: &v1.ConfigMapKeySelector{
   589  								LocalObjectReference: v1.LocalObjectReference{
   590  									Name: "Spec.EphemeralContainers[*].EphemeralContainerCommon.Env[*].ValueFrom.ConfigMapKeyRef"}}}}}}}},
   591  			InitContainers: []v1.Container{{
   592  				EnvFrom: []v1.EnvFromSource{{
   593  					ConfigMapRef: &v1.ConfigMapEnvSource{
   594  						LocalObjectReference: v1.LocalObjectReference{
   595  							Name: "Spec.InitContainers[*].EnvFrom[*].ConfigMapRef"}}}},
   596  				Env: []v1.EnvVar{{
   597  					ValueFrom: &v1.EnvVarSource{
   598  						ConfigMapKeyRef: &v1.ConfigMapKeySelector{
   599  							LocalObjectReference: v1.LocalObjectReference{
   600  								Name: "Spec.InitContainers[*].Env[*].ValueFrom.ConfigMapKeyRef"}}}}}}},
   601  			Volumes: []v1.Volume{{
   602  				VolumeSource: v1.VolumeSource{
   603  					Projected: &v1.ProjectedVolumeSource{
   604  						Sources: []v1.VolumeProjection{{
   605  							ConfigMap: &v1.ConfigMapProjection{
   606  								LocalObjectReference: v1.LocalObjectReference{
   607  									Name: "Spec.Volumes[*].VolumeSource.Projected.Sources[*].ConfigMap"}}}}}}}, {
   608  				VolumeSource: v1.VolumeSource{
   609  					ConfigMap: &v1.ConfigMapVolumeSource{
   610  						LocalObjectReference: v1.LocalObjectReference{
   611  							Name: "Spec.Volumes[*].VolumeSource.ConfigMap"}}}}},
   612  		},
   613  	}
   614  	extractedNames := sets.New[string]()
   615  	VisitPodConfigmapNames(pod, func(name string) bool {
   616  		extractedNames.Insert(name)
   617  		return true
   618  	})
   619  
   620  	// expectedPaths holds struct paths to fields with "ConfigMap" in the name that are references to ConfigMap API objects.
   621  	// every path here should be represented as an example in the Pod stub above, with the ConfigMap name set to the path.
   622  	expectedPaths := sets.New[string](
   623  		"Spec.Containers[*].EnvFrom[*].ConfigMapRef",
   624  		"Spec.Containers[*].Env[*].ValueFrom.ConfigMapKeyRef",
   625  		"Spec.EphemeralContainers[*].EphemeralContainerCommon.EnvFrom[*].ConfigMapRef",
   626  		"Spec.EphemeralContainers[*].EphemeralContainerCommon.Env[*].ValueFrom.ConfigMapKeyRef",
   627  		"Spec.InitContainers[*].EnvFrom[*].ConfigMapRef",
   628  		"Spec.InitContainers[*].Env[*].ValueFrom.ConfigMapKeyRef",
   629  		"Spec.Volumes[*].VolumeSource.Projected.Sources[*].ConfigMap",
   630  		"Spec.Volumes[*].VolumeSource.ConfigMap",
   631  	)
   632  	collectPaths := collectResourcePaths(t, "ConfigMap", nil, "", reflect.TypeOf(&v1.Pod{}))
   633  	if missingPaths := expectedPaths.Difference(collectPaths); len(missingPaths) > 0 {
   634  		t.Logf("Missing expected paths:\n%s", strings.Join(sets.List[string](missingPaths), "\n"))
   635  		t.Error("Missing expected paths. Verify VisitPodConfigmapNames() is correctly finding the missing paths, then correct expectedPaths")
   636  	}
   637  	if extraPaths := collectPaths.Difference(expectedPaths); len(extraPaths) > 0 {
   638  		t.Logf("Extra paths:\n%s", strings.Join(sets.List[string](extraPaths), "\n"))
   639  		t.Error("Extra fields with resource in the name found. Verify VisitPodConfigmapNames() is including these fields if appropriate, then correct expectedPaths")
   640  	}
   641  
   642  	if missingNames := expectedPaths.Difference(extractedNames); len(missingNames) > 0 {
   643  		t.Logf("Missing expected names:\n%s", strings.Join(sets.List[string](missingNames), "\n"))
   644  		t.Error("Missing expected names. Verify the pod stub above includes these references, then verify VisitPodConfigmapNames() is correctly finding the missing names")
   645  	}
   646  	if extraNames := extractedNames.Difference(expectedPaths); len(extraNames) > 0 {
   647  		t.Logf("Extra names:\n%s", strings.Join(sets.List[string](extraNames), "\n"))
   648  		t.Error("Extra names extracted. Verify VisitPodConfigmapNames() is correctly extracting resource names")
   649  	}
   650  
   651  	// emptyPod is a stub containing empty object names
   652  	emptyPod := &v1.Pod{
   653  		Spec: v1.PodSpec{
   654  			Containers: []v1.Container{{
   655  				EnvFrom: []v1.EnvFromSource{{
   656  					ConfigMapRef: &v1.ConfigMapEnvSource{
   657  						LocalObjectReference: v1.LocalObjectReference{
   658  							Name: ""}}}}}},
   659  		},
   660  	}
   661  	VisitPodConfigmapNames(emptyPod, func(name string) bool {
   662  		t.Fatalf("expected no empty names collected, got %q", name)
   663  		return false
   664  	})
   665  }
   666  
   667  func newPod(now metav1.Time, ready bool, beforeSec int) *v1.Pod {
   668  	conditionStatus := v1.ConditionFalse
   669  	if ready {
   670  		conditionStatus = v1.ConditionTrue
   671  	}
   672  	return &v1.Pod{
   673  		Status: v1.PodStatus{
   674  			Conditions: []v1.PodCondition{
   675  				{
   676  					Type:               v1.PodReady,
   677  					LastTransitionTime: metav1.NewTime(now.Time.Add(-1 * time.Duration(beforeSec) * time.Second)),
   678  					Status:             conditionStatus,
   679  				},
   680  			},
   681  		},
   682  	}
   683  }
   684  
   685  func TestIsPodAvailable(t *testing.T) {
   686  	now := metav1.Now()
   687  	tests := []struct {
   688  		pod             *v1.Pod
   689  		minReadySeconds int32
   690  		expected        bool
   691  	}{
   692  		{
   693  			pod:             newPod(now, false, 0),
   694  			minReadySeconds: 0,
   695  			expected:        false,
   696  		},
   697  		{
   698  			pod:             newPod(now, true, 0),
   699  			minReadySeconds: 1,
   700  			expected:        false,
   701  		},
   702  		{
   703  			pod:             newPod(now, true, 0),
   704  			minReadySeconds: 0,
   705  			expected:        true,
   706  		},
   707  		{
   708  			pod:             newPod(now, true, 51),
   709  			minReadySeconds: 50,
   710  			expected:        true,
   711  		},
   712  	}
   713  
   714  	for i, test := range tests {
   715  		isAvailable := IsPodAvailable(test.pod, test.minReadySeconds, now)
   716  		if isAvailable != test.expected {
   717  			t.Errorf("[tc #%d] expected available pod: %t, got: %t", i, test.expected, isAvailable)
   718  		}
   719  	}
   720  }
   721  
   722  func TestIsPodTerminal(t *testing.T) {
   723  	now := metav1.Now()
   724  
   725  	tests := []struct {
   726  		podPhase v1.PodPhase
   727  		expected bool
   728  	}{
   729  		{
   730  			podPhase: v1.PodFailed,
   731  			expected: true,
   732  		},
   733  		{
   734  			podPhase: v1.PodSucceeded,
   735  			expected: true,
   736  		},
   737  		{
   738  			podPhase: v1.PodUnknown,
   739  			expected: false,
   740  		},
   741  		{
   742  			podPhase: v1.PodPending,
   743  			expected: false,
   744  		},
   745  		{
   746  			podPhase: v1.PodRunning,
   747  			expected: false,
   748  		},
   749  		{
   750  			expected: false,
   751  		},
   752  	}
   753  
   754  	for i, test := range tests {
   755  		pod := newPod(now, true, 0)
   756  		pod.Status.Phase = test.podPhase
   757  		isTerminal := IsPodTerminal(pod)
   758  		if isTerminal != test.expected {
   759  			t.Errorf("[tc #%d] expected terminal pod: %t, got: %t", i, test.expected, isTerminal)
   760  		}
   761  	}
   762  }
   763  
   764  func TestGetContainerStatus(t *testing.T) {
   765  	type ExpectedStruct struct {
   766  		status v1.ContainerStatus
   767  		exists bool
   768  	}
   769  
   770  	tests := []struct {
   771  		status   []v1.ContainerStatus
   772  		name     string
   773  		expected ExpectedStruct
   774  		desc     string
   775  	}{
   776  		{
   777  			status:   []v1.ContainerStatus{{Name: "test1", Ready: false, Image: "image1"}, {Name: "test2", Ready: true, Image: "image1"}},
   778  			name:     "test1",
   779  			expected: ExpectedStruct{status: v1.ContainerStatus{Name: "test1", Ready: false, Image: "image1"}, exists: true},
   780  			desc:     "retrieve ContainerStatus with Name=\"test1\"",
   781  		},
   782  		{
   783  			status:   []v1.ContainerStatus{{Name: "test2", Ready: false, Image: "image2"}},
   784  			name:     "test1",
   785  			expected: ExpectedStruct{status: v1.ContainerStatus{}, exists: false},
   786  			desc:     "no matching ContainerStatus with Name=\"test1\"",
   787  		},
   788  		{
   789  			status:   []v1.ContainerStatus{{Name: "test3", Ready: false, Image: "image3"}},
   790  			name:     "",
   791  			expected: ExpectedStruct{status: v1.ContainerStatus{}, exists: false},
   792  			desc:     "retrieve an empty ContainerStatus with container name empty",
   793  		},
   794  		{
   795  			status:   nil,
   796  			name:     "",
   797  			expected: ExpectedStruct{status: v1.ContainerStatus{}, exists: false},
   798  			desc:     "retrieve an empty ContainerStatus with status nil",
   799  		},
   800  	}
   801  
   802  	for _, test := range tests {
   803  		resultStatus, exists := GetContainerStatus(test.status, test.name)
   804  		assert.Equal(t, test.expected.status, resultStatus, "GetContainerStatus: "+test.desc)
   805  		assert.Equal(t, test.expected.exists, exists, "GetContainerStatus: "+test.desc)
   806  
   807  		resultStatus = GetExistingContainerStatus(test.status, test.name)
   808  		assert.Equal(t, test.expected.status, resultStatus, "GetExistingContainerStatus: "+test.desc)
   809  	}
   810  }
   811  
   812  func TestGetIndexOfContainerStatus(t *testing.T) {
   813  	testStatus := []v1.ContainerStatus{
   814  		{
   815  			Name:  "c1",
   816  			Ready: false,
   817  			Image: "image1",
   818  		},
   819  		{
   820  			Name:  "c2",
   821  			Ready: true,
   822  			Image: "image1",
   823  		},
   824  	}
   825  
   826  	tests := []struct {
   827  		desc           string
   828  		containerName  string
   829  		expectedExists bool
   830  		expectedIndex  int
   831  	}{
   832  		{
   833  			desc:           "first container",
   834  			containerName:  "c1",
   835  			expectedExists: true,
   836  			expectedIndex:  0,
   837  		},
   838  		{
   839  			desc:           "second container",
   840  			containerName:  "c2",
   841  			expectedExists: true,
   842  			expectedIndex:  1,
   843  		},
   844  		{
   845  			desc:           "non-existent container",
   846  			containerName:  "c3",
   847  			expectedExists: false,
   848  			expectedIndex:  0,
   849  		},
   850  	}
   851  
   852  	for _, test := range tests {
   853  		idx, exists := GetIndexOfContainerStatus(testStatus, test.containerName)
   854  		assert.Equal(t, test.expectedExists, exists, "GetIndexOfContainerStatus: "+test.desc)
   855  		assert.Equal(t, test.expectedIndex, idx, "GetIndexOfContainerStatus: "+test.desc)
   856  	}
   857  }
   858  
   859  func TestUpdatePodCondition(t *testing.T) {
   860  	time := metav1.Now()
   861  
   862  	podStatus := v1.PodStatus{
   863  		Conditions: []v1.PodCondition{
   864  			{
   865  				Type:               v1.PodReady,
   866  				Status:             v1.ConditionTrue,
   867  				Reason:             "successfully",
   868  				Message:            "sync pod successfully",
   869  				LastProbeTime:      time,
   870  				LastTransitionTime: metav1.NewTime(time.Add(1000)),
   871  			},
   872  		},
   873  	}
   874  	tests := []struct {
   875  		status     *v1.PodStatus
   876  		conditions v1.PodCondition
   877  		expected   bool
   878  		desc       string
   879  	}{
   880  		{
   881  			status: &podStatus,
   882  			conditions: v1.PodCondition{
   883  				Type:               v1.PodReady,
   884  				Status:             v1.ConditionTrue,
   885  				Reason:             "successfully",
   886  				Message:            "sync pod successfully",
   887  				LastProbeTime:      time,
   888  				LastTransitionTime: metav1.NewTime(time.Add(1000))},
   889  			expected: false,
   890  			desc:     "all equal, no update",
   891  		},
   892  		{
   893  			status: &podStatus,
   894  			conditions: v1.PodCondition{
   895  				Type:               v1.PodScheduled,
   896  				Status:             v1.ConditionTrue,
   897  				Reason:             "successfully",
   898  				Message:            "sync pod successfully",
   899  				LastProbeTime:      time,
   900  				LastTransitionTime: metav1.NewTime(time.Add(1000))},
   901  			expected: true,
   902  			desc:     "not equal Type, should get updated",
   903  		},
   904  		{
   905  			status: &podStatus,
   906  			conditions: v1.PodCondition{
   907  				Type:               v1.PodReady,
   908  				Status:             v1.ConditionFalse,
   909  				Reason:             "successfully",
   910  				Message:            "sync pod successfully",
   911  				LastProbeTime:      time,
   912  				LastTransitionTime: metav1.NewTime(time.Add(1000))},
   913  			expected: true,
   914  			desc:     "not equal Status, should get updated",
   915  		},
   916  	}
   917  
   918  	for _, test := range tests {
   919  		resultStatus := UpdatePodCondition(test.status, &test.conditions)
   920  
   921  		assert.Equal(t, test.expected, resultStatus, test.desc)
   922  	}
   923  }
   924  
   925  func TestGetContainersReadyCondition(t *testing.T) {
   926  	time := metav1.Now()
   927  
   928  	containersReadyCondition := v1.PodCondition{
   929  		Type:               v1.ContainersReady,
   930  		Status:             v1.ConditionTrue,
   931  		Reason:             "successfully",
   932  		Message:            "sync pod successfully",
   933  		LastProbeTime:      time,
   934  		LastTransitionTime: metav1.NewTime(time.Add(1000)),
   935  	}
   936  
   937  	tests := []struct {
   938  		desc              string
   939  		podStatus         v1.PodStatus
   940  		expectedCondition *v1.PodCondition
   941  	}{
   942  		{
   943  			desc: "containers ready condition exists",
   944  			podStatus: v1.PodStatus{
   945  				Conditions: []v1.PodCondition{containersReadyCondition},
   946  			},
   947  			expectedCondition: &containersReadyCondition,
   948  		},
   949  		{
   950  			desc: "containers ready condition does not exist",
   951  			podStatus: v1.PodStatus{
   952  				Conditions: []v1.PodCondition{},
   953  			},
   954  			expectedCondition: nil,
   955  		},
   956  	}
   957  
   958  	for _, test := range tests {
   959  		containersReadyCondition := GetContainersReadyCondition(test.podStatus)
   960  		assert.Equal(t, test.expectedCondition, containersReadyCondition, test.desc)
   961  	}
   962  }
   963  
   964  func TestIsContainersReadyConditionTrue(t *testing.T) {
   965  	time := metav1.Now()
   966  
   967  	tests := []struct {
   968  		desc      string
   969  		podStatus v1.PodStatus
   970  		expected  bool
   971  	}{
   972  		{
   973  			desc: "containers ready condition is true",
   974  			podStatus: v1.PodStatus{
   975  				Conditions: []v1.PodCondition{
   976  					{
   977  						Type:               v1.ContainersReady,
   978  						Status:             v1.ConditionTrue,
   979  						Reason:             "successfully",
   980  						Message:            "sync pod successfully",
   981  						LastProbeTime:      time,
   982  						LastTransitionTime: metav1.NewTime(time.Add(1000)),
   983  					},
   984  				},
   985  			},
   986  			expected: true,
   987  		},
   988  		{
   989  			desc: "containers ready condition is false",
   990  			podStatus: v1.PodStatus{
   991  				Conditions: []v1.PodCondition{
   992  					{
   993  						Type:               v1.ContainersReady,
   994  						Status:             v1.ConditionFalse,
   995  						Reason:             "successfully",
   996  						Message:            "sync pod successfully",
   997  						LastProbeTime:      time,
   998  						LastTransitionTime: metav1.NewTime(time.Add(1000)),
   999  					},
  1000  				},
  1001  			},
  1002  			expected: false,
  1003  		},
  1004  		{
  1005  			desc: "containers ready condition is empty",
  1006  			podStatus: v1.PodStatus{
  1007  				Conditions: []v1.PodCondition{},
  1008  			},
  1009  			expected: false,
  1010  		},
  1011  	}
  1012  
  1013  	for _, test := range tests {
  1014  		isContainersReady := IsContainersReadyConditionTrue(test.podStatus)
  1015  		assert.Equal(t, test.expected, isContainersReady, test.desc)
  1016  	}
  1017  }