diff --git a/cli/context/store/store.go b/cli/context/store/store.go index 4122e077927f..b36648ece7a6 100644 --- a/cli/context/store/store.go +++ b/cli/context/store/store.go @@ -192,7 +192,8 @@ func (s *ContextStore) ResetEndpointTLSMaterial(contextName string, endpointName } // ListTLSFiles returns the list of TLS files present for each endpoint in the -// context. +// context. Hidden files (such as ".DS_Store"), and the "Thumbs.db" and +// "desktop.ini" files created by Windows Explorer, are skipped. func (s *ContextStore) ListTLSFiles(name string) (map[string]EndpointFiles, error) { return s.tls.listContextData(name) } diff --git a/cli/context/store/store_test.go b/cli/context/store/store_test.go index 25bfd87c75c0..bec878d06245 100644 --- a/cli/context/store/store_test.go +++ b/cli/context/store/store_test.go @@ -57,6 +57,9 @@ func TestExportImport(t *testing.T) { }, }) assert.NilError(t, err) + // A .DS_Store left by Finder must not end up in the export. + err = os.WriteFile(filepath.Join(s.tls.endpointDir("source", "ep1"), ".DS_Store"), []byte("data"), 0o600) + assert.NilError(t, err) r := Export("source", s) defer r.Close() err = Import("dest", s, r) @@ -87,6 +90,8 @@ func TestExportImport(t *testing.T) { destData2, err := s.GetTLSData("dest", "ep1", "file2") assert.NilError(t, err) assert.DeepEqual(t, file2, destData2) + _, err = s.GetTLSData("dest", "ep1", ".DS_Store") + assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) } func TestRemove(t *testing.T) { diff --git a/cli/context/store/tlsstore.go b/cli/context/store/tlsstore.go index c1f5f8caa2cb..e956db820f23 100644 --- a/cli/context/store/tlsstore.go +++ b/cli/context/store/tlsstore.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/moby/sys/atomicwriter" ) @@ -81,7 +82,7 @@ func (s *tlsStore) listContextData(name string) (map[string]EndpointFiles, error } var files EndpointFiles for _, fs := range fss { - if !fs.IsDir() { + if !fs.IsDir() && !isIgnoredFile(fs.Name()) { files = append(files, fs.Name()) } } @@ -91,5 +92,15 @@ func (s *tlsStore) listContextData(name string) (map[string]EndpointFiles, error return r, nil } +// isIgnoredFile reports whether name should be skipped when listing TLS files. +// TLS files are never hidden, so all hidden files are skipped: ".DS_Store", +// "._*" AppleDouble files, and temporary files left by an interrupted write. +// Windows Explorer's "Thumbs.db" and "desktop.ini" are matched ignoring case. +func isIgnoredFile(name string) bool { + return strings.HasPrefix(name, ".") || + strings.EqualFold(name, "Thumbs.db") || + strings.EqualFold(name, "desktop.ini") +} + // EndpointFiles is a slice of strings representing file names type EndpointFiles []string diff --git a/cli/context/store/tlsstore_test.go b/cli/context/store/tlsstore_test.go index 799362d54e14..38d0707f14ca 100644 --- a/cli/context/store/tlsstore_test.go +++ b/cli/context/store/tlsstore_test.go @@ -1,6 +1,8 @@ package store import ( + "os" + "path/filepath" "testing" "github.com/containerd/errdefs" @@ -52,6 +54,11 @@ func TestTlsListAndBatchRemove(t *testing.T) { err := testee.createOrUpdate(contextName, name, file, []byte("data")) assert.NilError(t, err) } + // Files like these are created by Finder and Explorer, and must not be listed. + for _, file := range []string{".DS_Store", "Thumbs.db"} { + err := os.WriteFile(filepath.Join(testee.endpointDir(contextName, name), file), []byte("data"), 0o600) + assert.NilError(t, err) + } } resAll, err := testee.listContextData(contextName) @@ -70,3 +77,27 @@ func TestTlsListAndBatchRemove(t *testing.T) { assert.NilError(t, err) assert.DeepEqual(t, resEmpty, map[string]EndpointFiles{}) } + +func TestIsIgnoredFile(t *testing.T) { + tests := []struct { + fileName string + ignored bool + }{ + {fileName: "ca.pem"}, + {fileName: "cert.pem"}, + {fileName: "key.pem"}, + {fileName: "ca.crt"}, // unknown files must still produce a warning + {fileName: ".DS_Store", ignored: true}, + {fileName: "._ca.pem", ignored: true}, // AppleDouble file + {fileName: ".tmp-ca.pem1234567890", ignored: true}, // left behind by an interrupted write + {fileName: "Thumbs.db", ignored: true}, + {fileName: "thumbs.db", ignored: true}, + {fileName: "desktop.ini", ignored: true}, + {fileName: "Desktop.ini", ignored: true}, + } + for _, tc := range tests { + t.Run(tc.fileName, func(t *testing.T) { + assert.Equal(t, isIgnoredFile(tc.fileName), tc.ignored) + }) + } +}