context: avoid corrupt file writes

Write to a tempfile then move, so that if the
process dies mid-write it doesn't corrupt the store.

Also improve error messaging so that if a file does
get corrupted, the user has some hope of figuring
out which file is broken.

For background, see:
https://github.com/docker/for-win/issues/13180
https://github.com/docker/for-win/issues/12561

For a repro case, see:
https://github.com/nicks/contextstore-sandbox

Signed-off-by: Nick Santos <nick.santos@docker.com>
This commit is contained in:
Nick Santos 2023-02-20 19:51:57 -05:00
parent dfb36eaef8
commit c2487c2997
No known key found for this signature in database
GPG Key ID: DB427670C81C3850
3 changed files with 37 additions and 6 deletions

View File

@ -2,12 +2,14 @@ package store
import ( import (
"encoding/json" "encoding/json"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"sort" "sort"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/ioutils"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -35,7 +37,7 @@ func (s *metadataStore) createOrUpdate(meta Metadata) error {
if err != nil { if err != nil {
return err return err
} }
return os.WriteFile(filepath.Join(contextDir, metaFile), bytes, 0o644) return ioutils.AtomicWriteFile(filepath.Join(contextDir, metaFile), bytes, 0o644)
} }
func parseTypedOrMap(payload []byte, getter TypeGetter) (interface{}, error) { func parseTypedOrMap(payload []byte, getter TypeGetter) (interface{}, error) {
@ -65,7 +67,8 @@ func (s *metadataStore) get(name string) (Metadata, error) {
} }
func (s *metadataStore) getByID(id contextdir) (Metadata, error) { func (s *metadataStore) getByID(id contextdir) (Metadata, error) {
bytes, err := os.ReadFile(filepath.Join(s.contextDir(id), metaFile)) fileName := filepath.Join(s.contextDir(id), metaFile)
bytes, err := os.ReadFile(fileName)
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
return Metadata{}, errdefs.NotFound(errors.Wrap(err, "context not found")) return Metadata{}, errdefs.NotFound(errors.Wrap(err, "context not found"))
@ -77,15 +80,15 @@ func (s *metadataStore) getByID(id contextdir) (Metadata, error) {
Endpoints: make(map[string]interface{}), Endpoints: make(map[string]interface{}),
} }
if err := json.Unmarshal(bytes, &untyped); err != nil { if err := json.Unmarshal(bytes, &untyped); err != nil {
return Metadata{}, err return Metadata{}, fmt.Errorf("parsing %s: %v", fileName, err)
} }
r.Name = untyped.Name r.Name = untyped.Name
if r.Metadata, err = parseTypedOrMap(untyped.Metadata, s.config.contextType); err != nil { if r.Metadata, err = parseTypedOrMap(untyped.Metadata, s.config.contextType); err != nil {
return Metadata{}, err return Metadata{}, fmt.Errorf("parsing %s: %v", fileName, err)
} }
for k, v := range untyped.Endpoints { for k, v := range untyped.Endpoints {
if r.Endpoints[k], err = parseTypedOrMap(v, s.config.endpointTypes[k]); err != nil { if r.Endpoints[k], err = parseTypedOrMap(v, s.config.endpointTypes[k]); err != nil {
return Metadata{}, err return Metadata{}, fmt.Errorf("parsing %s: %v", fileName, err)
} }
} }
return r, err return r, err

View File

@ -7,9 +7,11 @@ import (
"bytes" "bytes"
"crypto/rand" "crypto/rand"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"os" "os"
"path" "path"
"path/filepath"
"testing" "testing"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
@ -230,3 +232,28 @@ func TestImportZipInvalid(t *testing.T) {
err = Import("zipInvalid", s, r) err = Import("zipInvalid", s, r)
assert.ErrorContains(t, err, "unexpected context file") assert.ErrorContains(t, err, "unexpected context file")
} }
func TestCorruptMetadata(t *testing.T) {
tempDir := t.TempDir()
s := New(tempDir, testCfg)
err := s.CreateOrUpdate(
Metadata{
Endpoints: map[string]interface{}{
"ep1": endpoint{Foo: "bar"},
},
Metadata: context{Bar: "baz"},
Name: "source",
})
assert.NilError(t, err)
// Simulate the meta.json file getting corrupted
// by some external process.
contextDir := s.meta.contextDir(contextdirOf("source"))
contextFile := filepath.Join(contextDir, metaFile)
err = os.WriteFile(contextFile, nil, 0o600)
assert.NilError(t, err)
// Assert that the error message gives the user some clue where to look.
_, err = s.GetMetadata("source")
assert.ErrorContains(t, err, fmt.Sprintf("parsing %s: unexpected end of JSON input", contextFile))
}

View File

@ -5,6 +5,7 @@ import (
"path/filepath" "path/filepath"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/ioutils"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -31,7 +32,7 @@ func (s *tlsStore) createOrUpdate(name, endpointName, filename string, data []by
if err := os.MkdirAll(endpointDir, 0o700); err != nil { if err := os.MkdirAll(endpointDir, 0o700); err != nil {
return err return err
} }
return os.WriteFile(filepath.Join(endpointDir, filename), data, 0o600) return ioutils.AtomicWriteFile(filepath.Join(endpointDir, filename), data, 0o600)
} }
func (s *tlsStore) getData(name, endpointName, filename string) ([]byte, error) { func (s *tlsStore) getData(name, endpointName, filename string) ([]byte, error) {