mirror of https://github.com/docker/cli.git
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:
parent
dfb36eaef8
commit
c2487c2997
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue