diff --git a/cmd/kubeadm/app/util/BUILD b/cmd/kubeadm/app/util/BUILD index 7eed2645d16..a919803f21a 100644 --- a/cmd/kubeadm/app/util/BUILD +++ b/cmd/kubeadm/app/util/BUILD @@ -89,6 +89,7 @@ filegroup( "//cmd/kubeadm/app/util/kubeconfig:all-srcs", "//cmd/kubeadm/app/util/kustomize:all-srcs", "//cmd/kubeadm/app/util/output:all-srcs", + "//cmd/kubeadm/app/util/patches:all-srcs", "//cmd/kubeadm/app/util/pkiutil:all-srcs", "//cmd/kubeadm/app/util/pubkeypin:all-srcs", "//cmd/kubeadm/app/util/runtime:all-srcs", diff --git a/cmd/kubeadm/app/util/patches/BUILD b/cmd/kubeadm/app/util/patches/BUILD new file mode 100644 index 00000000000..298bc3aa121 --- /dev/null +++ b/cmd/kubeadm/app/util/patches/BUILD @@ -0,0 +1,40 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["patches.go"], + importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/patches", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library", + "//vendor/github.com/evanphx/json-patch:go_default_library", + "//vendor/github.com/pkg/errors:go_default_library", + "//vendor/sigs.k8s.io/yaml:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["patches_test.go"], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/cmd/kubeadm/app/util/patches/patches.go b/cmd/kubeadm/app/util/patches/patches.go new file mode 100644 index 00000000000..3b1ad23d0e3 --- /dev/null +++ b/cmd/kubeadm/app/util/patches/patches.go @@ -0,0 +1,342 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package patches + +import ( + "bufio" + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + + jsonpatch "github.com/evanphx/json-patch" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/strategicpatch" + utilyaml "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/yaml" +) + +// PatchTarget defines a target to be patched, such as a control-plane static Pod. +type PatchTarget struct { + // Name must be the name of a known target. In the case of Kubernetes objects + // this is likely to match the ObjectMeta.Name of a target. + Name string + + // StrategicMergePatchObject is only used for strategic merge patches. + // It represents the underlying object type that is patched - e.g. "v1.Pod" + StrategicMergePatchObject interface{} + + // Data must contain the bytes that will be patched. + Data []byte +} + +// PatchManager defines an object that can apply patches. +type PatchManager struct { + patchSets []*patchSet + knownTargets []string + output io.Writer +} + +// patchSet defines a set of patches of a certain type that can patch a PatchTarget. +type patchSet struct { + targetName string + patchType types.PatchType + patches []string +} + +// String() is used for unit-testing. +func (ps *patchSet) String() string { + return fmt.Sprintf( + "{%q, %q, %#v}", + ps.targetName, + ps.patchType, + ps.patches, + ) +} + +var ( + pathLock = &sync.RWMutex{} + pathCache = map[string]*PatchManager{} + + patchTypes = map[string]types.PatchType{ + "json": types.JSONPatchType, + "merge": types.MergePatchType, + "strategic": types.StrategicMergePatchType, + "": types.StrategicMergePatchType, // Default + } + patchTypeList = []string{"json", "merge", "strategic"} + patchTypesJoined = strings.Join(patchTypeList, "|") + knownExtensions = []string{"json", "yaml"} + + regExtension = regexp.MustCompile(`.+\.(` + strings.Join(knownExtensions, "|") + `)$`) +) + +// GetPatchManagerForPath creates a patch manager that can be used to apply patches to "knownTargets". +// "path" should contain patches that can be used to patch the "knownTargets". +// If "output" is non-nil, messages about actions performed by the manager would go on this io.Writer. +func GetPatchManagerForPath(path string, knownTargets []string, output io.Writer) (*PatchManager, error) { + pathLock.RLock() + if pm, known := pathCache[path]; known { + pathLock.RUnlock() + return pm, nil + } + pathLock.RUnlock() + + if output == nil { + output = ioutil.Discard + } + + fmt.Fprintf(output, "[patches] Reading patches from path %q\n", path) + + // Get the files in the path. + patchSets, patchFiles, ignoredFiles, err := getPatchSetsFromPath(path, knownTargets, output) + if err != nil { + return nil, err + } + + if len(patchFiles) > 0 { + fmt.Fprintf(output, "[patches] Found the following patch files: %v\n", patchFiles) + } + if len(ignoredFiles) > 0 { + fmt.Fprintf(output, "[patches] Ignored the following files: %v\n", ignoredFiles) + } + + pm := &PatchManager{ + patchSets: patchSets, + knownTargets: knownTargets, + output: output, + } + pathLock.Lock() + pathCache[path] = pm + pathLock.Unlock() + + return pm, nil +} + +// ApplyPatchesToTarget takes a patch target and patches its "Data" using the patches +// stored in the patch manager. The resulted "Data" is always converted to JSON. +func (pm *PatchManager) ApplyPatchesToTarget(patchTarget *PatchTarget) error { + var err error + var patchedData []byte + + var found bool + for _, pt := range pm.knownTargets { + if pt == patchTarget.Name { + found = true + break + } + } + if !found { + return errors.Errorf("unknown patch target name %q, must be one of %v", patchTarget.Name, pm.knownTargets) + } + + // Always convert the target data to JSON. + patchedData, err = yaml.YAMLToJSON(patchTarget.Data) + if err != nil { + return err + } + + // Iterate over the patchSets. + for _, patchSet := range pm.patchSets { + if patchSet.targetName != patchTarget.Name { + continue + } + + // Iterate over the patches in the patchSets. + for _, patch := range patchSet.patches { + patchBytes := []byte(patch) + + // Patch based on the patch type. + switch patchSet.patchType { + + // JSON patch. + case types.JSONPatchType: + var patchObj jsonpatch.Patch + patchObj, err = jsonpatch.DecodePatch(patchBytes) + if err == nil { + patchedData, err = patchObj.Apply(patchedData) + } + + // Merge patch. + case types.MergePatchType: + patchedData, err = jsonpatch.MergePatch(patchedData, patchBytes) + + // Strategic merge patch. + case types.StrategicMergePatchType: + patchedData, err = strategicpatch.StrategicMergePatch( + patchedData, + patchBytes, + patchTarget.StrategicMergePatchObject, + ) + } + + if err != nil { + return errors.Wrapf(err, "could not apply the following patch of type %q to target %q:\n%s\n", + patchSet.patchType, + patchTarget.Name, + patch) + } + fmt.Fprintf(pm.output, "[patches] Applied patch of type %q to target %q\n", patchSet.patchType, patchTarget.Name) + } + + // Update the data for this patch target. + patchTarget.Data = patchedData + } + + return nil +} + +// parseFilename validates a file name and retrieves the encoded target name and patch type. +// - On unknown extension or target name it returns a warning +// - On unknown patch type it returns an error +// - On success it returns a target name and patch type +func parseFilename(fileName string, knownTargets []string) (string, types.PatchType, error, error) { + // Return a warning if the extension cannot be matched. + if !regExtension.MatchString(fileName) { + return "", "", errors.Errorf("the file extension must be one of %v", knownExtensions), nil + } + + regFileNameSplit := regexp.MustCompile( + fmt.Sprintf(`^(%s)([^.+\n]*)?(\+)?(%s)?`, strings.Join(knownTargets, "|"), patchTypesJoined), + ) + // Extract the target name and patch type. The resulting sub-string slice would look like this: + // [full-match, targetName, suffix, +, patchType] + sub := regFileNameSplit.FindStringSubmatch(fileName) + if sub == nil { + return "", "", errors.Errorf("unknown target, must be one of %v", knownTargets), nil + } + targetName := sub[1] + + if len(sub[3]) > 0 && len(sub[4]) == 0 { + return "", "", nil, errors.Errorf("unknown or missing patch type after '+', must be one of %v", patchTypeList) + } + patchType := patchTypes[sub[4]] + + return targetName, patchType, nil, nil +} + +// createPatchSet creates a patchSet object, by splitting the given "data" by "\n---". +func createPatchSet(targetName string, patchType types.PatchType, data string) (*patchSet, error) { + var patches []string + + // Split the patches and convert them to JSON. + // Data that is already JSON will not cause an error. + buf := bytes.NewBuffer([]byte(data)) + reader := utilyaml.NewYAMLReader(bufio.NewReader(buf)) + for { + patch, err := reader.Read() + if err == io.EOF { + break + } else if err != nil { + return nil, errors.Wrapf(err, "could not split patches for data:\n%s\n", data) + } + + patch = bytes.TrimSpace(patch) + if len(patch) == 0 { + continue + } + + patchJSON, err := yaml.YAMLToJSON(patch) + if err != nil { + return nil, errors.Wrapf(err, "could not convert patch to JSON:\n%s\n", patch) + } + patches = append(patches, string(patchJSON)) + } + + return &patchSet{ + targetName: targetName, + patchType: patchType, + patches: patches, + }, nil +} + +// getPatchSetsFromPath walks a path, ignores sub-directories and non-patch files, and +// returns a list of patchFile objects. +func getPatchSetsFromPath(targetPath string, knownTargets []string, output io.Writer) ([]*patchSet, []string, []string, error) { + patchFiles := []string{} + ignoredFiles := []string{} + patchSets := []*patchSet{} + + // Check if targetPath is a directory. + info, err := os.Lstat(targetPath) + if err != nil { + goto return_path_error + } + if !info.IsDir() { + err = errors.New("not a directory") + goto return_path_error + } + + err = filepath.Walk(targetPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Sub-directories and "." are ignored. + if info.IsDir() { + return nil + } + + baseName := info.Name() + + // Parse the filename and retrieve the target and patch type + targetName, patchType, warn, err := parseFilename(baseName, knownTargets) + if err != nil { + return err + } + if warn != nil { + fmt.Fprintf(output, "[patches] Ignoring file %q: %v\n", baseName, warn) + ignoredFiles = append(ignoredFiles, baseName) + return nil + } + + // Read the patch file. + data, err := ioutil.ReadFile(path) + if err != nil { + return errors.Wrapf(err, "could not read the file %q", path) + } + + if len(data) == 0 { + fmt.Fprintf(output, "[patches] Ignoring empty file: %q\n", baseName) + ignoredFiles = append(ignoredFiles, baseName) + return nil + } + + // Create a patchSet object. + patchSet, err := createPatchSet(targetName, patchType, string(data)) + if err != nil { + return err + } + + patchFiles = append(patchFiles, baseName) + patchSets = append(patchSets, patchSet) + return nil + }) + +return_path_error: + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "could not list patch files for path %q", targetPath) + } + + return patchSets, patchFiles, ignoredFiles, nil +} diff --git a/cmd/kubeadm/app/util/patches/patches_test.go b/cmd/kubeadm/app/util/patches/patches_test.go new file mode 100644 index 00000000000..7d2e8f18a67 --- /dev/null +++ b/cmd/kubeadm/app/util/patches/patches_test.go @@ -0,0 +1,413 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package patches + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" +) + +var testKnownTargets = []string{ + "etcd", + "kube-apiserver", + "kube-controller-manager", + "kube-scheduler", +} + +const testDirPattern = "patch-files" + +func TestParseFilename(t *testing.T) { + tests := []struct { + name string + fileName string + expectedTargetName string + expectedPatchType types.PatchType + expectedWarning bool + expectedError bool + }{ + { + name: "valid: known target and patch type", + fileName: "etcd+merge.json", + expectedTargetName: "etcd", + expectedPatchType: types.MergePatchType, + }, + { + name: "valid: known target and default patch type", + fileName: "etcd0.yaml", + expectedTargetName: "etcd", + expectedPatchType: types.StrategicMergePatchType, + }, + { + name: "valid: known target and custom patch type", + fileName: "etcd0+merge.yaml", + expectedTargetName: "etcd", + expectedPatchType: types.MergePatchType, + }, + { + name: "invalid: unknown target", + fileName: "foo.yaml", + expectedWarning: true, + }, + { + name: "invalid: unknown extension", + fileName: "etcd.foo", + expectedWarning: true, + }, + { + name: "invalid: missing extension", + fileName: "etcd", + expectedWarning: true, + }, + { + name: "invalid: unknown patch type", + fileName: "etcd+foo.json", + expectedError: true, + }, + { + name: "invalid: missing patch type", + fileName: "etcd+.json", + expectedError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + targetName, patchType, warn, err := parseFilename(tc.fileName, testKnownTargets) + if (err != nil) != tc.expectedError { + t.Errorf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err) + } + if (warn != nil) != tc.expectedWarning { + t.Errorf("expected warning: %v, got: %v, warning: %v", tc.expectedWarning, warn != nil, warn) + } + if targetName != tc.expectedTargetName { + t.Errorf("expected target name: %v, got: %v", tc.expectedTargetName, targetName) + } + if patchType != tc.expectedPatchType { + t.Errorf("expected patch type: %v, got: %v", tc.expectedPatchType, patchType) + } + }) + } +} + +func TestCreatePatchSet(t *testing.T) { + tests := []struct { + name string + targetName string + patchType types.PatchType + expectedPatchSet *patchSet + data string + }{ + { + + name: "valid: YAML patches are separated and converted to JSON", + targetName: "etcd", + patchType: types.StrategicMergePatchType, + data: "foo: bar\n---\nfoo: baz\n", + expectedPatchSet: &patchSet{ + targetName: "etcd", + patchType: types.StrategicMergePatchType, + patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`}, + }, + }, + { + name: "valid: JSON patches are separated", + targetName: "etcd", + patchType: types.StrategicMergePatchType, + data: `{"foo":"bar"}` + "\n---\n" + `{"foo":"baz"}`, + expectedPatchSet: &patchSet{ + targetName: "etcd", + patchType: types.StrategicMergePatchType, + patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`}, + }, + }, + { + name: "valid: empty patches are ignored", + targetName: "etcd", + patchType: types.StrategicMergePatchType, + data: `{"foo":"bar"}` + "\n---\n ---\n" + `{"foo":"baz"}`, + expectedPatchSet: &patchSet{ + targetName: "etcd", + patchType: types.StrategicMergePatchType, + patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ps, _ := createPatchSet(tc.targetName, tc.patchType, tc.data) + if !reflect.DeepEqual(ps, tc.expectedPatchSet) { + t.Fatalf("expected patch set:\n%+v\ngot:\n%+v\n", tc.expectedPatchSet, ps) + } + }) + } +} + +func TestGetPatchSetsForPathMustBeDirectory(t *testing.T) { + tempFile, err := ioutil.TempFile("", "test-file") + if err != nil { + t.Errorf("error creating temporary file: %v", err) + } + defer os.Remove(tempFile.Name()) + + _, _, _, err = getPatchSetsFromPath(tempFile.Name(), testKnownTargets, ioutil.Discard) + if err == nil { + t.Fatalf("expected error for non-directory path %q", tempFile.Name()) + } +} + +func TestGetPatchSetsForPath(t *testing.T) { + const patchData = `{"foo":"bar"}` + + tests := []struct { + name string + filesToWrite []string + expectedPatchSets []*patchSet + expectedPatchFiles []string + expectedIgnoredFiles []string + expectedError bool + patchData string + }{ + { + name: "valid: patch files are sorted and non-patch files are ignored", + filesToWrite: []string{"kube-scheduler+merge.json", "kube-apiserver+json.yaml", "etcd.yaml", "foo", "bar.json"}, + patchData: patchData, + expectedPatchSets: []*patchSet{ + { + targetName: "etcd", + patchType: types.StrategicMergePatchType, + patches: []string{patchData}, + }, + { + targetName: "kube-apiserver", + patchType: types.JSONPatchType, + patches: []string{patchData}, + }, + { + targetName: "kube-scheduler", + patchType: types.MergePatchType, + patches: []string{patchData}, + }, + }, + expectedPatchFiles: []string{"etcd.yaml", "kube-apiserver+json.yaml", "kube-scheduler+merge.json"}, + expectedIgnoredFiles: []string{"bar.json", "foo"}, + }, + { + name: "valid: empty files are ignored", + patchData: "", + filesToWrite: []string{"kube-scheduler.json"}, + expectedPatchFiles: []string{}, + expectedIgnoredFiles: []string{"kube-scheduler.json"}, + expectedPatchSets: []*patchSet{}, + }, + { + name: "invalid: bad patch type in filename returns and error", + filesToWrite: []string{"kube-scheduler+foo.json"}, + expectedError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tempDir, err := ioutil.TempDir("", testDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + for _, file := range tc.filesToWrite { + filePath := filepath.Join(tempDir, file) + err := ioutil.WriteFile(filePath, []byte(tc.patchData), 0644) + if err != nil { + t.Fatalf("could not write temporary file %q", filePath) + } + } + + patchSets, patchFiles, ignoredFiles, err := getPatchSetsFromPath(tempDir, testKnownTargets, ioutil.Discard) + if (err != nil) != tc.expectedError { + t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err) + } + + if !reflect.DeepEqual(tc.expectedPatchFiles, patchFiles) { + t.Fatalf("expected patch files:\n%+v\ngot:\n%+v", tc.expectedPatchFiles, patchFiles) + } + if !reflect.DeepEqual(tc.expectedIgnoredFiles, ignoredFiles) { + t.Fatalf("expected ignored files:\n%+v\ngot:\n%+v", tc.expectedIgnoredFiles, ignoredFiles) + } + if !reflect.DeepEqual(tc.expectedPatchSets, patchSets) { + t.Fatalf("expected patch sets:\n%+v\ngot:\n%+v", tc.expectedPatchSets, patchSets) + } + }) + } +} + +func TestGetPatchManagerForPath(t *testing.T) { + type file struct { + name string + data string + } + + tests := []struct { + name string + files []*file + patchTarget *PatchTarget + expectedData []byte + expectedError bool + }{ + { + name: "valid: patch a kube-apiserver target using merge patch; json patch is applied first", + patchTarget: &PatchTarget{ + Name: "kube-apiserver", + StrategicMergePatchObject: v1.Pod{}, + Data: []byte("foo: bar\nbaz: qux\n"), + }, + expectedData: []byte(`{"baz":"qux","foo":"patched"}`), + files: []*file{ + { + name: "kube-apiserver+merge.yaml", + data: "foo: patched", + }, + { + name: "kube-apiserver+json.json", + data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`, + }, + }, + }, + { + name: "valid: kube-apiserver target is patched with json patch", + patchTarget: &PatchTarget{ + Name: "kube-apiserver", + StrategicMergePatchObject: v1.Pod{}, + Data: []byte("foo: bar\n"), + }, + expectedData: []byte(`{"foo":"zzz"}`), + files: []*file{ + { + name: "kube-apiserver+json.json", + data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`, + }, + }, + }, + { + name: "valid: kube-apiserver target is patched with strategic merge patch", + patchTarget: &PatchTarget{ + Name: "kube-apiserver", + StrategicMergePatchObject: v1.Pod{}, + Data: []byte("foo: bar\n"), + }, + expectedData: []byte(`{"foo":"zzz"}`), + files: []*file{ + { + name: "kube-apiserver+strategic.json", + data: `{"foo":"zzz"}`, + }, + }, + }, + { + name: "valid: etcd target is not changed because there are no patches for it", + patchTarget: &PatchTarget{ + Name: "etcd", + StrategicMergePatchObject: v1.Pod{}, + Data: []byte("foo: bar\n"), + }, + expectedData: []byte("foo: bar\n"), + files: []*file{ + { + name: "kube-apiserver+merge.yaml", + data: "foo: patched", + }, + }, + }, + { + name: "invalid: cannot patch etcd target due to malformed json patch", + patchTarget: &PatchTarget{ + Name: "etcd", + StrategicMergePatchObject: v1.Pod{}, + Data: []byte("foo: bar\n"), + }, + files: []*file{ + { + name: "etcd+json.json", + data: `{"foo":"zzz"}`, + }, + }, + expectedError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tempDir, err := ioutil.TempDir("", testDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + for _, file := range tc.files { + filePath := filepath.Join(tempDir, file.name) + err := ioutil.WriteFile(filePath, []byte(file.data), 0644) + if err != nil { + t.Fatalf("could not write temporary file %q", filePath) + } + } + + pm, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil) + if err != nil { + t.Fatal(err) + } + + err = pm.ApplyPatchesToTarget(tc.patchTarget) + if (err != nil) != tc.expectedError { + t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err) + } + if err != nil { + return + } + + if !bytes.Equal(tc.patchTarget.Data, tc.expectedData) { + t.Fatalf("expected result:\n%s\ngot:\n%s", tc.expectedData, tc.patchTarget.Data) + } + }) + } +} + +func TestGetPatchManagerForPathCache(t *testing.T) { + tempDir, err := ioutil.TempDir("", testDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + pmOld, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil) + if err != nil { + t.Fatal(err) + } + pmNew, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil) + if err != nil { + t.Fatal(err) + } + if pmOld != pmNew { + t.Logf("path %q was not cached, expected pointer: %p, got: %p", tempDir, pmOld, pmNew) + } +}