-
Notifications
You must be signed in to change notification settings - Fork 1
store: add keychain package #25
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package store | ||
|
|
||
| import "github.com/docker/secrets-engine/pkg/secrets" | ||
|
|
||
| var ErrCredentialNotFound = secrets.ErrNotFound |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # Store Keychain | ||
|
|
||
| Keychain integrates with the OS keystore. It supports Linux, macOS and Windows | ||
| and can be used directly with `keychain.New`. | ||
|
|
||
| - Linux uses the [`org.freedesktop.secrets` API](https://www.freedesktop.org/wiki/Specifications/secret-storage-spec/secrets-api-0.1.html). | ||
| - macOS uses the [macOS Keychain services API](https://developer.apple.com/documentation/security/keychain-services). | ||
|
|
||
| ## Quickstart | ||
|
|
||
| ```go | ||
| import "github.com/docker/secrets-engine/store/keychain" | ||
|
|
||
| func main() { | ||
| kc, err := keychain.New[*]() | ||
| } | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package keychain | ||
|
|
||
| import ( | ||
| "errors" | ||
|
|
||
| "github.com/docker/secrets-engine/store" | ||
| ) | ||
|
|
||
| var ErrCollectionPathInvalid = errors.New("keychain collection path is invalid") | ||
|
|
||
| const ( | ||
| // the docker label is the default prefix on all keys stored by the keychain | ||
| // e.g. io.docker.Secrets:id(realm/app/username) | ||
| dockerSecretsLabel = "io.docker.Secrets" | ||
| ) | ||
|
|
||
| type keychainStore[T store.Secret] struct { | ||
| keyPrefix string | ||
| factory func() T | ||
| } | ||
|
|
||
| var _ store.Store = &keychainStore[store.Secret]{} | ||
|
|
||
| type Factory[T store.Secret] func() T | ||
|
|
||
| type Options[T store.Secret] func(*keychainStore[T]) error | ||
|
|
||
| func WithKeyPrefix[T store.Secret](prefix string) Options[T] { | ||
| return func(ks *keychainStore[T]) error { | ||
| if prefix == "" { | ||
| return errors.New("the prefix cannot be empty") | ||
| } | ||
| ks.keyPrefix = prefix | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| // New creates a new keychain store | ||
| // | ||
| // factory is a function used to instantiate new secrets of type T. | ||
| func New[T store.Secret](factory Factory[T], opts ...Options[T]) (store.Store, error) { | ||
| k := &keychainStore[T]{ | ||
| factory: factory, | ||
| keyPrefix: dockerSecretsLabel, | ||
| } | ||
| for _, o := range opts { | ||
| if err := o(k); err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
| return k, nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package keychain | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/docker/secrets-engine/store" | ||
| ) | ||
|
|
||
| var _ store.Store = &keychainStore[store.Secret]{} | ||
|
|
||
| // Erase implements secrets.Store. | ||
| func (k *keychainStore[T]) Delete(ctx context.Context, id store.ID) error { | ||
| panic("unimplemented") | ||
| } | ||
|
|
||
| // Get implements secrets.Store. | ||
| func (k *keychainStore[T]) Get(ctx context.Context, id store.ID) (store.Secret, error) { | ||
| panic("unimplemented") | ||
| } | ||
|
|
||
| // GetAll implements secrets.Store. | ||
| func (k *keychainStore[T]) GetAll(ctx context.Context) (map[store.ID]store.Secret, error) { | ||
| panic("unimplemented") | ||
| } | ||
|
|
||
| // Store implements secrets.Store. | ||
| func (k *keychainStore[T]) Save(ctx context.Context, id store.ID, secret store.Secret) error { | ||
| panic("unimplemented") | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package mocks | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "errors" | ||
|
|
||
| "github.com/docker/secrets-engine/store" | ||
| ) | ||
|
|
||
| type MockCredential struct { | ||
| Username string | ||
| Password string | ||
| } | ||
|
|
||
| var _ store.Secret = &MockCredential{} | ||
|
|
||
| // Marshal implements secrets.Secret. | ||
| func (m *MockCredential) Marshal() ([]byte, error) { | ||
| return []byte(m.Username + ":" + m.Password), nil | ||
| } | ||
|
|
||
| // Unmarshal implements secrets.Secret. | ||
| func (m *MockCredential) Unmarshal(data []byte) error { | ||
| items := bytes.Split(data, []byte(":")) | ||
| if len(items) != 2 { | ||
| return errors.New("failed to unmarshal data into mock credential type") | ||
| } | ||
| m.Username = string(items[0]) | ||
| m.Password = string(items[1]) | ||
| return nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package mocks | ||
|
|
||
| import ( | ||
| "context" | ||
| "maps" | ||
| "sync" | ||
|
|
||
| "github.com/docker/secrets-engine/store" | ||
| ) | ||
|
|
||
| type MockStore struct { | ||
| lock sync.RWMutex | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not so sure I understand why
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. searching through containerd/containerd I see a lot of |
||
| store map[store.ID]store.Secret | ||
| } | ||
|
|
||
| func (m *MockStore) init() { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not have a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right, I meant
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can rename it to |
||
| if m.store == nil { | ||
| m.store = make(map[store.ID]store.Secret) | ||
| } | ||
| } | ||
|
|
||
| // Delete implements Store. | ||
| func (m *MockStore) Delete(_ context.Context, id store.ID) error { | ||
| m.lock.Lock() | ||
| defer m.lock.Unlock() | ||
| m.init() | ||
|
|
||
| delete(m.store, id) | ||
| return nil | ||
| } | ||
|
|
||
| // Get implements Store. | ||
| func (m *MockStore) Get(_ context.Context, id store.ID) (store.Secret, error) { | ||
| m.lock.RLock() | ||
| defer m.lock.RUnlock() | ||
| m.init() | ||
|
|
||
| secret, exists := m.store[id] | ||
| if !exists { | ||
| return nil, store.ErrCredentialNotFound | ||
| } | ||
| return secret, nil | ||
| } | ||
|
|
||
| // GetAll implements Store. | ||
| func (m *MockStore) GetAll(_ context.Context) (map[store.ID]store.Secret, error) { | ||
| m.lock.RLock() | ||
| defer m.lock.RUnlock() | ||
| m.init() | ||
|
|
||
| // Return a copy of the store to avoid concurrent map read/write issues. | ||
| return maps.Clone(m.store), nil | ||
| } | ||
|
|
||
| // Save implements Store. | ||
| func (m *MockStore) Save(_ context.Context, id store.ID, secret store.Secret) error { | ||
| m.lock.Lock() | ||
| defer m.lock.Unlock() | ||
| m.init() | ||
|
|
||
| m.store[id] = secret | ||
| return nil | ||
| } | ||
|
|
||
| var _ store.Store = &MockStore{} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| package secrets | ||
| package store | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get the point of these. Is it just documentation purpose? Or do you tend to implement them later?
I don't see much value in the code as is, but maybe I'm missing something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's to satisfy the
store.Storeinterface. The actual implementation has been done somewhere else. I just wanted to break up some of the PR.