I'm working on a small Go application that's basically a wrapper for various password stores (Ansible Vault, Hashicorp Vault, Chef Vault, etc). The idea is: In my various provisioning scripts, I can use my Go wrapper to grab secrets and if we decide to switch password stores behind the scenes, all of the interfaces don't need to be updated across my projects.
I'm trying to setup proper tests for this application, and in doing so, am trying to figure out the best way to to inject my dependencies.
For example, lets say the project is called secrets
. And one of my implementations is ansible
. And the ansible implementation needs its own parser
and needs to open its own connection
to the ansible vault, to retrieve the data.
So I might have the following:
package secrets
type PasswordStore interface {
GetKey(key string) (string, error)
}
func New(backend string, config map[string]interface{}) (PasswordStore, error) {
switch backend {
case "ansible":
return ansible.New(config)
default:
return nil, fmt.Errorf("Password store '%s' not supported.", backend)
}
}
package ansible
type Connection interface {
open() (string, error)
}
type Ansible struct {
connection Connection
contents map[string]string
}
func New(c map[string]interface{}) (*Ansible, error) {
conn, err := NewConnection(c["ansible_path"].(string))
if err != nil {
return nil, err
}
// open connection, parse, etc...
a := &Ansible{
connection: conn,
contents: parsedData,
}
return a, nil
}
So this seems nice because the secrets
package doesn't need knowledge of the ansible
package dependencies (connection), and the factory just new's up the instance with some config data. However, if I need to mock the connection
that Ansible receives, there doesn't seem to be a good way to do this (unless that config map had a connection option called mock
)
The other option is to abandon the factory, and just assemble all the dependencies from the secrets
package, like:
package secrets
type PasswordStore interface {
GetKey(key string) (string, error)
}
func New(backend string, config map[string]interface{}) (PasswordStore, error) {
switch backend {
case "ansible":
return ansible.New(AnsibleConnection{}, config)
default:
return nil, fmt.Errorf("Password store '%s' not supported.", backend)
}
}
package ansible
// same as before in this file, but with injected dependency ...
func New(connect Connection, c map[string]interface{}) (*Ansible, error) {
conn, err := connect.NewConnection(c["ansible_path"].(string))
if err != nil {
return nil, err
}
// open connection, parse, etc...
a := &Ansible{
connection: conn,
contents: parsedData,
}
return a, nil
}
Now the dependency is injected, but it seems like secrets
needs to have knowledge of every dependency for every implementation.
Is there a more logical way to structure this so that secrets
knows less? Or is it typical for the top level package to be orchestrating everything?