Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[27.x backport]cli/config/credentials: skip saving config-file if credentials didn't change #5569

Merged
merged 3 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions cli/config/credentials/file_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@ func NewFileStore(file store) Store {
return &fileStore{file: file}
}

// Erase removes the given credentials from the file store.
// Erase removes the given credentials from the file store.This function is
// idempotent and does not update the file if credentials did not change.
func (c *fileStore) Erase(serverAddress string) error {
if _, exists := c.file.GetAuthConfigs()[serverAddress]; !exists {
// nothing to do; no credentials found for the given serverAddress
return nil
}
delete(c.file.GetAuthConfigs(), serverAddress)
return c.file.Save()
}
Expand All @@ -52,9 +57,14 @@ func (c *fileStore) GetAll() (map[string]types.AuthConfig, error) {
return c.file.GetAuthConfigs(), nil
}

// Store saves the given credentials in the file store.
// Store saves the given credentials in the file store. This function is
// idempotent and does not update the file if credentials did not change.
func (c *fileStore) Store(authConfig types.AuthConfig) error {
authConfigs := c.file.GetAuthConfigs()
if oldAuthConfig, ok := authConfigs[authConfig.ServerAddress]; ok && oldAuthConfig == authConfig {
// Credentials didn't change, so skip updating the configuration file.
return nil
}
authConfigs[authConfig.ServerAddress] = authConfig
return c.file.Save()
}
Expand Down
93 changes: 83 additions & 10 deletions cli/config/credentials/file_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ import (

type fakeStore struct {
configs map[string]types.AuthConfig
saveFn func(*fakeStore) error
}

func (f *fakeStore) Save() error {
if f.saveFn != nil {
// Pass a reference to the fakeStore itself in case saveFn
// wants to access it.
return f.saveFn(f)
}
return nil
}

Expand All @@ -21,15 +27,82 @@ func (f *fakeStore) GetAuthConfigs() map[string]types.AuthConfig {
}

func (f *fakeStore) GetFilename() string {
return "/tmp/docker-fakestore"
return "no-config.json"
}

func newStore(auths map[string]types.AuthConfig) store {
return &fakeStore{configs: auths}
// TestFileStoreIdempotent verifies that the config-file isn't updated
// if nothing changed.
func TestFileStoreIdempotent(t *testing.T) {
var saveCount, expectedSaveCount int

s := NewFileStore(&fakeStore{
configs: map[string]types.AuthConfig{},
saveFn: func(*fakeStore) error {
saveCount++
return nil
},
})
authOne := types.AuthConfig{
Auth: "super_secret_token",
Email: "[email protected]",
ServerAddress: "https://example.com",
}
authTwo := types.AuthConfig{
Auth: "also_super_secret_token",
Email: "[email protected]",
ServerAddress: "https://other.example.com",
}

expectedSaveCount = 1
t.Run("store new credentials", func(t *testing.T) {
assert.NilError(t, s.Store(authOne))
retrievedAuth, err := s.Get(authOne.ServerAddress)
assert.NilError(t, err)
assert.Check(t, is.Equal(retrievedAuth, authOne))
assert.Check(t, is.Equal(saveCount, expectedSaveCount))
})
t.Run("store same credentials is a no-op", func(t *testing.T) {
assert.NilError(t, s.Store(authOne))
retrievedAuth, err := s.Get(authOne.ServerAddress)
assert.NilError(t, err)
assert.Check(t, is.Equal(retrievedAuth, authOne))
assert.Check(t, is.Equal(saveCount, expectedSaveCount), "should not have saved if nothing changed")
})
t.Run("store other credentials", func(t *testing.T) {
expectedSaveCount++
assert.NilError(t, s.Store(authTwo))
retrievedAuth, err := s.Get(authTwo.ServerAddress)
assert.NilError(t, err)
assert.Check(t, is.Equal(retrievedAuth, authTwo))
assert.Check(t, is.Equal(saveCount, expectedSaveCount))
})
t.Run("erase credentials", func(t *testing.T) {
expectedSaveCount++
assert.NilError(t, s.Erase(authOne.ServerAddress))
retrievedAuth, err := s.Get(authOne.ServerAddress)
assert.NilError(t, err)
assert.Check(t, is.Equal(retrievedAuth, types.AuthConfig{}))
assert.Check(t, is.Equal(saveCount, expectedSaveCount))
})
t.Run("erase non-existing credentials is a no-op", func(t *testing.T) {
assert.NilError(t, s.Erase(authOne.ServerAddress))
retrievedAuth, err := s.Get(authOne.ServerAddress)
assert.NilError(t, err)
assert.Check(t, is.Equal(retrievedAuth, types.AuthConfig{}))
assert.Check(t, is.Equal(saveCount, expectedSaveCount), "should not have saved if nothing changed")
})
t.Run("erase other credentials", func(t *testing.T) {
expectedSaveCount++
assert.NilError(t, s.Erase(authTwo.ServerAddress))
retrievedAuth, err := s.Get(authTwo.ServerAddress)
assert.NilError(t, err)
assert.Check(t, is.Equal(retrievedAuth, types.AuthConfig{}))
assert.Check(t, is.Equal(saveCount, expectedSaveCount))
})
}

func TestFileStoreAddCredentials(t *testing.T) {
f := newStore(make(map[string]types.AuthConfig))
f := &fakeStore{configs: map[string]types.AuthConfig{}}

s := NewFileStore(f)
auth := types.AuthConfig{
Expand All @@ -47,13 +120,13 @@ func TestFileStoreAddCredentials(t *testing.T) {
}

func TestFileStoreGet(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
f := &fakeStore{configs: map[string]types.AuthConfig{
"https://example.com": {
Auth: "super_secret_token",
Email: "[email protected]",
ServerAddress: "https://example.com",
},
})
}}

s := NewFileStore(f)
a, err := s.Get("https://example.com")
Expand All @@ -71,7 +144,7 @@ func TestFileStoreGet(t *testing.T) {
func TestFileStoreGetAll(t *testing.T) {
s1 := "https://example.com"
s2 := "https://example2.example.com"
f := newStore(map[string]types.AuthConfig{
f := &fakeStore{configs: map[string]types.AuthConfig{
s1: {
Auth: "super_secret_token",
Email: "[email protected]",
Expand All @@ -82,7 +155,7 @@ func TestFileStoreGetAll(t *testing.T) {
Email: "[email protected]",
ServerAddress: "https://example2.example.com",
},
})
}}

s := NewFileStore(f)
as, err := s.GetAll()
Expand All @@ -107,13 +180,13 @@ func TestFileStoreGetAll(t *testing.T) {
}

func TestFileStoreErase(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
f := &fakeStore{configs: map[string]types.AuthConfig{
"https://example.com": {
Auth: "super_secret_token",
Email: "[email protected]",
ServerAddress: "https://example.com",
},
})
}}

s := NewFileStore(f)
err := s.Erase("https://example.com")
Expand Down
32 changes: 16 additions & 16 deletions cli/config/credentials/native_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func mockCommandFn(args ...string) client.Program {
}

func TestNativeStoreAddCredentials(t *testing.T) {
f := newStore(make(map[string]types.AuthConfig))
f := &fakeStore{configs: map[string]types.AuthConfig{}}
s := &nativeStore{
programFunc: mockCommandFn,
fileStore: NewFileStore(f),
Expand All @@ -116,7 +116,7 @@ func TestNativeStoreAddCredentials(t *testing.T) {
}

func TestNativeStoreAddInvalidCredentials(t *testing.T) {
f := newStore(make(map[string]types.AuthConfig))
f := &fakeStore{configs: map[string]types.AuthConfig{}}
s := &nativeStore{
programFunc: mockCommandFn,
fileStore: NewFileStore(f),
Expand All @@ -132,11 +132,11 @@ func TestNativeStoreAddInvalidCredentials(t *testing.T) {
}

func TestNativeStoreGet(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress: {
Email: "[email protected]",
},
})
}}
s := &nativeStore{
programFunc: mockCommandFn,
fileStore: NewFileStore(f),
Expand All @@ -154,11 +154,11 @@ func TestNativeStoreGet(t *testing.T) {
}

func TestNativeStoreGetIdentityToken(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress2: {
Email: "[email protected]",
},
})
}}

s := &nativeStore{
programFunc: mockCommandFn,
Expand All @@ -176,11 +176,11 @@ func TestNativeStoreGetIdentityToken(t *testing.T) {
}

func TestNativeStoreGetAll(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress: {
Email: "[email protected]",
},
})
}}

s := &nativeStore{
programFunc: mockCommandFn,
Expand Down Expand Up @@ -217,11 +217,11 @@ func TestNativeStoreGetAll(t *testing.T) {
}

func TestNativeStoreGetMissingCredentials(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress: {
Email: "[email protected]",
},
})
}}

s := &nativeStore{
programFunc: mockCommandFn,
Expand All @@ -232,11 +232,11 @@ func TestNativeStoreGetMissingCredentials(t *testing.T) {
}

func TestNativeStoreGetInvalidAddress(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress: {
Email: "[email protected]",
},
})
}}

s := &nativeStore{
programFunc: mockCommandFn,
Expand All @@ -247,11 +247,11 @@ func TestNativeStoreGetInvalidAddress(t *testing.T) {
}

func TestNativeStoreErase(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress: {
Email: "[email protected]",
},
})
}}

s := &nativeStore{
programFunc: mockCommandFn,
Expand All @@ -263,11 +263,11 @@ func TestNativeStoreErase(t *testing.T) {
}

func TestNativeStoreEraseInvalidAddress(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
f := &fakeStore{configs: map[string]types.AuthConfig{
validServerAddress: {
Email: "[email protected]",
},
})
}}

s := &nativeStore{
programFunc: mockCommandFn,
Expand Down
Loading