diff --git a/pkg/api/pod/warnings.go b/pkg/api/pod/warnings.go index d5511ab3ec2f9..c30c99b02d37a 100644 --- a/pkg/api/pod/warnings.go +++ b/pkg/api/pod/warnings.go @@ -19,6 +19,7 @@ package pod import ( "context" "fmt" + "os" "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -168,6 +169,10 @@ func warningsForPodSpecAndMeta(fieldPath *field.Path, podSpec *api.PodSpec, meta } } + if overlaps := warningsForOverlappingVirtualPaths(podSpec.Volumes); len(overlaps) > 0 { + warnings = append(warnings, overlaps...) + } + // duplicate hostAliases (#91670, #58477) if len(podSpec.HostAliases) > 1 { items := sets.New[string]() @@ -354,3 +359,166 @@ func warningsForWeightedPodAffinityTerms(terms []api.WeightedPodAffinityTerm, fi } return warnings } + +// warningsForOverlappingVirtualPaths validates that there are no overlapping paths in single ConfigMapVolume, SecretVolume, DownwardAPIVolume and ProjectedVolume. +// A volume can try to load different keys to the same path which will result in overwriting of the value from the latest registered key +// Another possible scenario is when one of the path contains the other key path. Example: +// configMap: +// +// name: myconfig +// items: +// - key: key1 +// path: path +// - key: key2 +// path: path/path2 +// +// In such cases we either get `is directory` or 'file exists' error message. +func warningsForOverlappingVirtualPaths(volumes []api.Volume) []string { + var warnings []string + + mkWarn := func(volName, volDesc, body string) string { + return fmt.Sprintf("volume %q (%s): overlapping paths: %s", volName, volDesc, body) + } + + for _, v := range volumes { + if v.ConfigMap != nil && v.ConfigMap.Items != nil { + overlaps := checkVolumeMappingForOverlap(extractPaths(v.ConfigMap.Items, "")) + for _, ol := range overlaps { + warnings = append(warnings, mkWarn(v.Name, fmt.Sprintf("ConfigMap %q", v.ConfigMap.Name), ol)) + } + } + + if v.Secret != nil && v.Secret.Items != nil { + overlaps := checkVolumeMappingForOverlap(extractPaths(v.Secret.Items, "")) + for _, ol := range overlaps { + warnings = append(warnings, mkWarn(v.Name, fmt.Sprintf("Secret %q", v.Secret.SecretName), ol)) + } + } + + if v.DownwardAPI != nil && v.DownwardAPI.Items != nil { + overlaps := checkVolumeMappingForOverlap(extractPathsDownwardAPI(v.DownwardAPI.Items, "")) + for _, ol := range overlaps { + warnings = append(warnings, mkWarn(v.Name, "DownwardAPI", ol)) + } + } + + if v.Projected != nil { + var sourcePaths []pathAndSource + var allPaths []pathAndSource + + for _, source := range v.Projected.Sources { + if source == (api.VolumeProjection{}) { + warnings = append(warnings, fmt.Sprintf("volume %q (Projected) has no sources provided", v.Name)) + continue + } + + switch { + case source.ConfigMap != nil && source.ConfigMap.Items != nil: + sourcePaths = extractPaths(source.ConfigMap.Items, fmt.Sprintf("ConfigMap %q", source.ConfigMap.Name)) + case source.Secret != nil && source.Secret.Items != nil: + sourcePaths = extractPaths(source.Secret.Items, fmt.Sprintf("Secret %q", source.Secret.Name)) + case source.DownwardAPI != nil && source.DownwardAPI.Items != nil: + sourcePaths = extractPathsDownwardAPI(source.DownwardAPI.Items, "DownwardAPI") + case source.ServiceAccountToken != nil: + sourcePaths = []pathAndSource{{source.ServiceAccountToken.Path, "ServiceAccountToken"}} + case source.ClusterTrustBundle != nil: + name := "" + if source.ClusterTrustBundle.Name != nil { + name = *source.ClusterTrustBundle.Name + } else { + name = *source.ClusterTrustBundle.SignerName + } + sourcePaths = []pathAndSource{{source.ClusterTrustBundle.Path, fmt.Sprintf("ClusterTrustBundle %q", name)}} + } + + if len(sourcePaths) == 0 { + continue + } + + for _, ps := range sourcePaths { + ps.path = strings.TrimRight(ps.path, string(os.PathSeparator)) + if collisions := checkForOverlap(allPaths, ps); len(collisions) > 0 { + for _, c := range collisions { + warnings = append(warnings, mkWarn(v.Name, "Projected", fmt.Sprintf("%s with %s", ps.String(), c.String()))) + } + } + allPaths = append(allPaths, ps) + } + } + } + } + return warnings +} + +// this lets us track a path and where it came from, for better errors +type pathAndSource struct { + path string + source string +} + +func (ps pathAndSource) String() string { + if ps.source != "" { + return fmt.Sprintf("%q (%s)", ps.path, ps.source) + } + return fmt.Sprintf("%q", ps.path) +} + +func extractPaths(mapping []api.KeyToPath, source string) []pathAndSource { + result := make([]pathAndSource, 0, len(mapping)) + + for _, v := range mapping { + result = append(result, pathAndSource{v.Path, source}) + } + return result +} + +func extractPathsDownwardAPI(mapping []api.DownwardAPIVolumeFile, source string) []pathAndSource { + result := make([]pathAndSource, 0, len(mapping)) + + for _, v := range mapping { + result = append(result, pathAndSource{v.Path, source}) + } + return result +} + +func checkVolumeMappingForOverlap(paths []pathAndSource) []string { + pathSeparator := string(os.PathSeparator) + var warnings []string + var allPaths []pathAndSource + + for _, ps := range paths { + ps.path = strings.TrimRight(ps.path, pathSeparator) + if collisions := checkForOverlap(allPaths, ps); len(collisions) > 0 { + for _, c := range collisions { + warnings = append(warnings, fmt.Sprintf("%s with %s", ps.String(), c.String())) + } + } + allPaths = append(allPaths, ps) + } + + return warnings +} + +func checkForOverlap(haystack []pathAndSource, needle pathAndSource) []pathAndSource { + pathSeparator := string(os.PathSeparator) + + if needle.path == "" { + return nil + } + + var result []pathAndSource + for _, item := range haystack { + switch { + case item.path == "": + continue + case item == needle: + result = append(result, item) + case strings.HasPrefix(item.path+pathSeparator, needle.path+pathSeparator): + result = append(result, item) + case strings.HasPrefix(needle.path+pathSeparator, item.path+pathSeparator): + result = append(result, item) + } + } + + return result +} diff --git a/pkg/api/pod/warnings_test.go b/pkg/api/pod/warnings_test.go index 6405977b7cba1..f3f0687131d1d 100644 --- a/pkg/api/pod/warnings_test.go +++ b/pkg/api/pod/warnings_test.go @@ -18,6 +18,8 @@ package pod import ( "context" + "reflect" + "strings" "testing" "k8s.io/apimachinery/pkg/api/resource" @@ -143,6 +145,7 @@ func TestWarnings(t *testing.T) { api.ResourceMemory: resource.MustParse("4m"), api.ResourceEphemeralStorage: resource.MustParse("4m"), } + testName := "Test" testcases := []struct { name string template *api.PodTemplateSpec @@ -235,6 +238,464 @@ func TestWarnings(t *testing.T) { }, expected: []string{`spec.volumes[0].rbd: deprecated in v1.28, non-functional in v1.31+`}, }, + { + name: "overlapping paths in a configmap volume", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "Test", + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{Name: "foo"}, + Items: []api.KeyToPath{ + {Key: "foo", Path: "test"}, + {Key: "bar", Path: "test"}, + }, + }, + }, + }}, + }}, + expected: []string{ + `volume "Test" (ConfigMap "foo"): overlapping paths: "test" with "test"`, + }, + }, + { + name: "overlapping paths in a configmap volume - try to mount dir path into a file", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "Test", + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{Name: "foo"}, + Items: []api.KeyToPath{ + {Key: "foo", Path: "test"}, + {Key: "bar", Path: "test/app"}, + }, + }, + }, + }}, + }}, + expected: []string{ + `volume "Test" (ConfigMap "foo"): overlapping paths: "test/app" with "test"`, + }, + }, + { + name: "overlapping paths in a configmap volume - try to mount file into a dir path", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "Test", + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{Name: "foo"}, + Items: []api.KeyToPath{ + {Key: "bar", Path: "test/app"}, + {Key: "foo", Path: "test"}, + }, + }, + }, + }}, + }}, + expected: []string{ + `volume "Test" (ConfigMap "foo"): overlapping paths: "test" with "test/app"`, + }, + }, + { + name: "overlapping paths in a secret volume", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "Test", + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: "foo", + Items: []api.KeyToPath{ + {Key: "foo", Path: "test"}, + {Key: "bar", Path: "test"}, + }, + }, + }, + }}, + }}, + expected: []string{ + `volume "Test" (Secret "foo"): overlapping paths: "test" with "test"`, + }, + }, + { + name: "overlapping paths in a downward api volume", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "Test", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + Items: []api.DownwardAPIVolumeFile{ + {FieldRef: &api.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.name"}, Path: "test"}, + {FieldRef: &api.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.labels"}, Path: "test"}, + }, + }, + }, + }}, + }}, + expected: []string{ + `volume "Test" (DownwardAPI): overlapping paths: "test" with "test"`, + }, + }, + { + name: "overlapping paths in projected volume - service account and config", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "foo", + VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{{ + ConfigMap: &api.ConfigMapProjection{ + LocalObjectReference: api.LocalObjectReference{Name: "Test"}, + Items: []api.KeyToPath{ + {Key: "foo", Path: "test"}, + }, + }, + }, { + ServiceAccountToken: &api.ServiceAccountTokenProjection{ + Path: "test", + }, + }}, + }, + }, + }}, + }}, + expected: []string{ + `volume "foo" (Projected): overlapping paths: "test" (ServiceAccountToken) with "test" (ConfigMap "Test")`, + }, + }, + { + name: "overlapping paths in projected volume volume: service account dir and config file", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "foo", + VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{{ + ConfigMap: &api.ConfigMapProjection{ + LocalObjectReference: api.LocalObjectReference{Name: "Test"}, + Items: []api.KeyToPath{ + {Key: "foo", Path: "test"}, + }, + }, + }, { + ServiceAccountToken: &api.ServiceAccountTokenProjection{ + Path: "test/file", + }, + }}, + }, + }, + }}, + }}, + expected: []string{ + `volume "foo" (Projected): overlapping paths: "test/file" (ServiceAccountToken) with "test" (ConfigMap "Test")`, + }, + }, + { + name: "overlapping paths in projected volume - service account file and config dir", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "foo", + VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{{ + ConfigMap: &api.ConfigMapProjection{ + LocalObjectReference: api.LocalObjectReference{Name: "Test"}, + Items: []api.KeyToPath{ + {Key: "foo", Path: "test/file"}, + }, + }, + }, { + ServiceAccountToken: &api.ServiceAccountTokenProjection{ + Path: "test", + }, + }}, + }, + }, + }}, + }}, + expected: []string{ + `volume "foo" (Projected): overlapping paths: "test" (ServiceAccountToken) with "test/file" (ConfigMap "Test")`, + }, + }, + { + name: "overlapping paths in projected volume - service account and secret", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "foo", + VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{{ + Secret: &api.SecretProjection{ + LocalObjectReference: api.LocalObjectReference{Name: "Test"}, + Items: []api.KeyToPath{ + {Key: "foo", Path: "test"}, + }, + }, + }, { + ServiceAccountToken: &api.ServiceAccountTokenProjection{ + Path: "test", + }, + }}, + }, + }, + }}, + }}, + expected: []string{ + `volume "foo" (Projected): overlapping paths: "test" (ServiceAccountToken) with "test" (Secret "Test")`, + }, + }, + { + name: "overlapping paths in projected volume - service account and downward api", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "foo", + VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{{ + DownwardAPI: &api.DownwardAPIProjection{ + Items: []api.DownwardAPIVolumeFile{{ + FieldRef: &api.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.name"}, + Path: "test", + }}, + }, + }, { + ServiceAccountToken: &api.ServiceAccountTokenProjection{ + Path: "test", + }, + }}, + }, + }, + }}, + }}, + expected: []string{ + `volume "foo" (Projected): overlapping paths: "test" (ServiceAccountToken) with "test" (DownwardAPI)`, + }, + }, + { + name: "overlapping paths in projected volume - service account and cluster trust bundle", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "foo", + VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{{ + ClusterTrustBundle: &api.ClusterTrustBundleProjection{ + Name: &testName, Path: "test", + }, + }, { + ServiceAccountToken: &api.ServiceAccountTokenProjection{ + Path: "test", + }, + }}, + }, + }, + }}, + }}, + expected: []string{ + `volume "foo" (Projected): overlapping paths: "test" (ServiceAccountToken) with "test" (ClusterTrustBundle "Test")`, + }, + }, + { + name: "overlapping paths in projected volume - service account and cluster trust bundle with signer name", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "foo", + VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{{ + ClusterTrustBundle: &api.ClusterTrustBundleProjection{ + SignerName: &testName, Path: "test", + }, + }, { + ServiceAccountToken: &api.ServiceAccountTokenProjection{ + Path: "test", + }, + }}, + }, + }, + }}, + }}, + expected: []string{ + `volume "foo" (Projected): overlapping paths: "test" (ServiceAccountToken) with "test" (ClusterTrustBundle "Test")`, + }, + }, + { + name: "overlapping paths in projected volume - secret and config map", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "foo", + VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{{ + Secret: &api.SecretProjection{ + LocalObjectReference: api.LocalObjectReference{Name: "TestSecret"}, + Items: []api.KeyToPath{ + {Key: "mykey", Path: "test"}, + }, + }, + }, { + ConfigMap: &api.ConfigMapProjection{ + LocalObjectReference: api.LocalObjectReference{Name: "TestConfigMap"}, + Items: []api.KeyToPath{ + {Key: "mykey", Path: "test/test1"}, + }, + }, + }}, + }, + }, + }}, + }}, + expected: []string{ + `volume "foo" (Projected): overlapping paths: "test/test1" (ConfigMap "TestConfigMap") with "test" (Secret "TestSecret")`, + }, + }, + { + name: "overlapping paths in projected volume - config map and downward api", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "foo", + VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{{ + Secret: &api.SecretProjection{ + LocalObjectReference: api.LocalObjectReference{Name: "TestSecret"}, + Items: []api.KeyToPath{ + {Key: "mykey", Path: "test"}, + }, + }, + }, { + DownwardAPI: &api.DownwardAPIProjection{ + Items: []api.DownwardAPIVolumeFile{{ + FieldRef: &api.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.name"}, + Path: "test/test2", + }}, + }, + }}, + }, + }, + }}, + }}, + expected: []string{ + `volume "foo" (Projected): overlapping paths: "test/test2" (DownwardAPI) with "test" (Secret "TestSecret")`, + }, + }, + { + name: "overlapping paths in projected volume - downward api and cluster thrust bundle api", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "foo", + VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{{ + DownwardAPI: &api.DownwardAPIProjection{ + Items: []api.DownwardAPIVolumeFile{{ + FieldRef: &api.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.name"}, + Path: "test/test2", + }}, + }, + }, { + ClusterTrustBundle: &api.ClusterTrustBundleProjection{ + Name: &testName, Path: "test", + }, + }}, + }, + }, + }}, + }}, + expected: []string{ + `volume "foo" (Projected): overlapping paths: "test" (ClusterTrustBundle "Test") with "test/test2" (DownwardAPI)`, + }, + }, + { + name: "overlapping paths in projected volume - multiple sources", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "foo", + VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{{ + ClusterTrustBundle: &api.ClusterTrustBundleProjection{ + SignerName: &testName, Path: "test/test"}, + }, { + DownwardAPI: &api.DownwardAPIProjection{ + Items: []api.DownwardAPIVolumeFile{{ + FieldRef: &api.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.name"}, + Path: "test", + }}, + }, + }, { + Secret: &api.SecretProjection{ + LocalObjectReference: api.LocalObjectReference{Name: "Test"}, + Items: []api.KeyToPath{ + {Key: "foo", Path: "test"}, + }, + }, + }, { + ServiceAccountToken: &api.ServiceAccountTokenProjection{ + Path: "test", + }, + }}, + }, + }, + }}, + }}, + expected: []string{ + `volume "foo" (Projected): overlapping paths: "test" (DownwardAPI) with "test/test" (ClusterTrustBundle "Test")`, + `volume "foo" (Projected): overlapping paths: "test" (Secret "Test") with "test/test" (ClusterTrustBundle "Test")`, + `volume "foo" (Projected): overlapping paths: "test" (Secret "Test") with "test" (DownwardAPI)`, + `volume "foo" (Projected): overlapping paths: "test" (ServiceAccountToken) with "test/test" (ClusterTrustBundle "Test")`, + `volume "foo" (Projected): overlapping paths: "test" (ServiceAccountToken) with "test" (DownwardAPI)`, + `volume "foo" (Projected): overlapping paths: "test" (ServiceAccountToken) with "test" (Secret "Test")`, + }, + }, + { + name: "overlapping paths in projected volume - ServiceAccount vs. DownwardAPI", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "foo", + VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{{ + ServiceAccountToken: &api.ServiceAccountTokenProjection{ + Path: "test/test2", + }, + }, { + DownwardAPI: &api.DownwardAPIProjection{ + Items: []api.DownwardAPIVolumeFile{ + {FieldRef: &api.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.name"}, Path: "test"}, + {FieldRef: &api.ObjectFieldSelector{APIVersion: "v1", FieldPath: "metadata.name"}, Path: "test"}, + }, + }, + }}, + }, + }, + }}, + }}, + expected: []string{ + `volume "foo" (Projected): overlapping paths: "test" (DownwardAPI) with "test/test2" (ServiceAccountToken)`, + `volume "foo" (Projected): overlapping paths: "test" (DownwardAPI) with "test/test2" (ServiceAccountToken)`, + `volume "foo" (Projected): overlapping paths: "test" (DownwardAPI) with "test" (DownwardAPI)`, + }, + }, + { + name: "empty sources in projected volume", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{{ + Name: "foo", + VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{ + {}, // one item, no fields set + }, + }, + }, + }}, + }}, + expected: []string{ + `volume "foo" (Projected) has no sources provided`, + }, + }, { name: "duplicate hostAlias", template: &api.PodTemplateSpec{Spec: api.PodSpec{ @@ -1067,13 +1528,17 @@ func TestWarnings(t *testing.T) { if tc.oldTemplate != nil { oldTemplate = tc.oldTemplate } - actual := sets.New[string](GetWarningsForPodTemplate(context.TODO(), nil, tc.template, oldTemplate)...) - expected := sets.New[string](tc.expected...) - for _, missing := range sets.List[string](expected.Difference(actual)) { + actual := GetWarningsForPodTemplate(context.TODO(), nil, tc.template, oldTemplate) + if len(actual) != len(tc.expected) { + t.Errorf("expected %d errors, got %d:\n%v", len(tc.expected), len(actual), strings.Join(actual, "\n")) + } + actualSet := sets.New(actual...) + expectedSet := sets.New(tc.expected...) + for _, missing := range sets.List(expectedSet.Difference(actualSet)) { t.Errorf("missing: %s", missing) } - for _, extra := range sets.List[string](actual.Difference(expected)) { - t.Errorf("extra: %s", extra) + for _, extra := range sets.List(actualSet.Difference(expectedSet)) { + t.Errorf("extra: %s", extra) } }) @@ -1085,13 +1550,17 @@ func TestWarnings(t *testing.T) { Spec: tc.template.Spec, } } - actual := sets.New[string](GetWarningsForPod(context.TODO(), pod, &api.Pod{})...) - expected := sets.New[string](tc.expected...) - for _, missing := range sets.List[string](expected.Difference(actual)) { + actual := GetWarningsForPod(context.TODO(), pod, &api.Pod{}) + if len(actual) != len(tc.expected) { + t.Errorf("expected %d errors, got %d:\n%v", len(tc.expected), len(actual), strings.Join(actual, "\n")) + } + actualSet := sets.New(actual...) + expectedSet := sets.New(tc.expected...) + for _, missing := range sets.List(expectedSet.Difference(actualSet)) { t.Errorf("missing: %s", missing) } - for _, extra := range sets.List[string](actual.Difference(expected)) { - t.Errorf("extra: %s", extra) + for _, extra := range sets.List(actualSet.Difference(expectedSet)) { + t.Errorf("extra: %s", extra) } }) } @@ -1183,3 +1652,112 @@ func TestTemplateOnlyWarnings(t *testing.T) { }) } } + +func TestCheckForOverLap(t *testing.T) { + testCase := map[string]struct { + checkPaths []pathAndSource + path pathAndSource + found bool + expected []pathAndSource + }{ + "exact match": { + checkPaths: []pathAndSource{{"path/path1", "src1"}}, + path: pathAndSource{"path/path1", "src2"}, + found: true, + expected: []pathAndSource{{"path/path1", "src1"}}, + }, + "no match": { + checkPaths: []pathAndSource{{"path/path1", "src1"}}, + path: pathAndSource{"path2/path1", "src2"}, + found: false, + }, + "empty checkPaths": { + checkPaths: []pathAndSource{}, + path: pathAndSource{"path2/path1", "src2"}, + found: false, + }, + "empty string in checkPaths": { + checkPaths: []pathAndSource{{"", "src1"}}, + path: pathAndSource{"path2/path1", "src2"}, + found: false, + }, + "empty path": { + checkPaths: []pathAndSource{{"test", "src1"}}, + path: pathAndSource{"", ""}, + found: false, + }, + "empty strings in checkPaths and path": { + checkPaths: []pathAndSource{{"", "src1"}}, + path: pathAndSource{"", ""}, + expected: []pathAndSource{{"", ""}}, + found: false, + }, + "between file and dir": { + checkPaths: []pathAndSource{{"path/path1", "src1"}}, + path: pathAndSource{"path", "src2"}, + found: true, + expected: []pathAndSource{{"path/path1", "src1"}}, + }, + "between dir and file": { + checkPaths: []pathAndSource{{"path", "src1"}}, + path: pathAndSource{"path/path1", "src2"}, + found: true, + expected: []pathAndSource{{"path", "src1"}}, + }, + "multiple paths without overlap": { + checkPaths: []pathAndSource{{"path1/path", "src1"}, {"path2/path", "src2"}, {"path3/path", "src3"}}, + path: pathAndSource{"path4/path", "src4"}, + found: false, + }, + "multiple paths with 1 overlap": { + checkPaths: []pathAndSource{{"path1/path", "src1"}, {"path2/path", "src2"}, {"path3/path", "src3"}}, + path: pathAndSource{"path3/path", "src4"}, + found: true, + expected: []pathAndSource{{"path3/path", "src3"}}, + }, + "multiple paths with multiple overlap": { + checkPaths: []pathAndSource{{"path/path1", "src1"}, {"path/path2", "src2"}, {"path/path3", "src3"}}, + path: pathAndSource{"path", "src4"}, + found: true, + expected: []pathAndSource{{"path/path1", "src1"}, {"path/path2", "src2"}, {"path/path3", "src3"}}, + }, + "partial overlap": { + checkPaths: []pathAndSource{{"path1/path", "src1"}, {"path2/path", "src2"}, {"path3/path", "src3"}}, + path: pathAndSource{"path101/path3", "src4"}, + found: false, + }, + "partial overlap in path": { + checkPaths: []pathAndSource{{"dir/path1", "src1"}, {"dir/path2", "src2"}, {"dir/path3", "src3"}}, + path: pathAndSource{"dir/path345", "src4"}, + found: false, + }, + "trailing slash in path": { + checkPaths: []pathAndSource{{"path1/path3", "src1"}}, + path: pathAndSource{"path1/path3/", "src2"}, + found: true, + expected: []pathAndSource{{"path1/path3", "src1"}}, + }, + "trailing slash in checkPaths": { + checkPaths: []pathAndSource{{"path1/path3/", "src1"}}, + path: pathAndSource{"path1/path3", "src2"}, + found: true, + expected: []pathAndSource{{"path1/path3/", "src1"}}, + }, + } + + for name, tc := range testCase { + t.Run(name, func(t *testing.T) { + result := checkForOverlap(tc.checkPaths, tc.path) + found := len(result) > 0 + if found && !tc.found { + t.Errorf("unexpected match for %q: %q", tc.path, result) + } + if !found && tc.found { + t.Errorf("expected match for %q: %q", tc.path, tc.expected) + } + if tc.found && !reflect.DeepEqual(result, tc.expected) { + t.Errorf("expected %q, got %q", tc.expected, result) + } + }) + } +}