Skip to content
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
5 changes: 5 additions & 0 deletions store/errors.go
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
6 changes: 6 additions & 0 deletions store/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ replace github.com/docker/secrets-engine => ../

require (
github.com/docker/secrets-engine v0.0.0-00010101000000-000000000000
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a
github.com/keybase/go-keychain v0.0.1
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
golang.org/x/crypto v0.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
14 changes: 14 additions & 0 deletions store/go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a h1:K0EAzgzEQHW4Y5lxrmvPMltmlRDzlhLfGmots9EHUTI=
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a/go.mod h1:YPNKjjE7Ubp9dTbnWvsP3HT+hYnY6TfXzubYTBeUxc8=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
17 changes: 17 additions & 0 deletions store/keychain/README.md
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[*]()
}
```
52 changes: 52 additions & 0 deletions store/keychain/keychain.go
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
}
29 changes: 29 additions & 0 deletions store/keychain/keychain_dummy.go
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")
Copy link
Collaborator

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.

Copy link
Member Author

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.Store interface. The actual implementation has been done somewhere else. I just wanted to break up some of the PR.

}

// 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")
}
31 changes: 31 additions & 0 deletions store/mocks/mock_credential.go
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
}
65 changes: 65 additions & 0 deletions store/mocks/mock_store.go
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: m (at least in piñata we always name this m)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not so sure I understand why m is better than lock, sounds too obscure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

searching through containerd/containerd I see a lot of lock being used. Other variations are also used: mu, m, l and rwlock.

store map[store.ID]store.Secret
}

func (m *MockStore) init() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: s (if we'd go with renaming lock to m)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not have a new function instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new is a keyword in Go

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, I meant newSomething, eg newMockStore

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can rename it to tryInit instead. newSomething sounds like it will return a new object

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{}
2 changes: 1 addition & 1 deletion store/store.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package secrets
package store

import (
"context"
Expand Down
Loading