Repository: bitmaelum/bitmaelum-suite (go)

Issue:
resolver configuration
The resolver is now always a chain resolver, where it can happen that no resolvers are added to the chain. This results in a valid setup, but without an resolving.

Instead, use a configuration like this:

        config:
	        vault: 
		        path: ....
	        server:
		        allow_insecure: true
		        debug_http: false
	        # Define the default resolver. When not specified, it will default to "remote"
	        default_resolver: remote	
	        resolvers:
		        # This resolver will connect to the main resolver URL.
		        remote:
			        url: https://resolver.bitmaelum.com
		        # Uses a local SQLite database as resolver. Useful for tests only
		        sqlite:
			        path: ~/resolver.db
		        # Uses a local BoltDB database as resolver. Useful for tests only
		        boltdb:
			        path: ~/resolver-bolt.db
		        # Chain will try and resolve through multiple resolvers in the given order.
		        chain:
			        - remote
			        - sqlite
			        - boltdb

Pull request:
Resolver changes
Fixes #200 

- Changed the configuration into a more robust resolver configuration system
- allows to configure the resolvers
- setting a default_resolver that always works (or gives an error during startup)
- chaining is only done when asked, and will error when no entries are configured on the chain

Patch:
diff --git a/cmd/bm-send/main.go b/cmd/bm-send/main.go
index 1f28e76..3d4dc8e 100644
--- a/cmd/bm-send/main.go
+++ b/cmd/bm-send/main.go
@@ -90,8 +90,7 @@ func main() {
 	}
 
 	// Set resolve settings, as we don't use a client configuration file
-	config.Client.Resolver.Remote.Enabled = true
-	config.Client.Resolver.Remote.URL = opts.Resolver
+	config.Client.Resolvers.Remote.URL = opts.Resolver
 	if opts.Verbose {
 		config.Client.Server.DebugHTTP = true
 	}
diff --git a/internal/config/config.go b/internal/config/config.go
index 1170d5e..c2eb2b3 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -57,6 +57,13 @@ var (
 	LoadedBridgeConfigPath string
 )
 
+var loadedConfig string
+
+// IsLoaded will return true when the loaded configuration is the same as the config given
+func IsLoaded(config string) bool {
+	return config == loadedConfig
+}
+
 // LoadClientConfig loads client configuration from given path or panic if cannot load
 func LoadClientConfig(configPath string) {
 	err := LoadClientConfigOrPass(configPath)
diff --git a/internal/config/config_bridge.go b/internal/config/config_bridge.go
index ba73738..3f0b6e4 100644
--- a/internal/config/config_bridge.go
+++ b/internal/config/config_bridge.go
@@ -62,18 +62,17 @@ type BridgeConfig struct {
 		} `yaml:"imap"`
 	} `yaml:"server"`
 
-	Resolver struct {
-		Sqlite struct {
-			Enabled bool   `yaml:"enabled"`
-			Dsn     string `yaml:"dsn"`
-		} `yaml:"sqlite"`
-
+	DefaultResolver string `yaml:"default_resolver"`
+	Resolvers       struct {
 		Remote struct {
-			Enabled       bool   `yaml:"enabled"`
 			URL           string `yaml:"url"`
 			AllowInsecure bool   `yaml:"allow_insecure"`
 		} `yaml:"remote"`
-	}
+		Sqlite struct {
+			Path string `yaml:"path"`
+		} `yaml:"sqlite"`
+		Chain []string `yaml:"chain"`
+	} `yaml:"resolvers"`
 }
 
 // LoadConfig loads the client configuration from the given path
@@ -97,5 +96,9 @@ func (c *BridgeConfig) LoadConfig(r io.Reader) error {
 	c.Vault.Path, _ = homedir.Expand(c.Vault.Path)
 	c.Server.IMAP.Path, _ = homedir.Expand(c.Server.IMAP.Path)
 
+	c.Resolvers.Sqlite.Path, _ = homedir.Expand(c.Resolvers.Sqlite.Path)
+
+	loadedConfig = "bridge"
+
 	return nil
 }
diff --git a/internal/config/config_client.go b/internal/config/config_client.go
index e3f4420..c0cec7a 100644
--- a/internal/config/config_client.go
+++ b/internal/config/config_client.go
@@ -50,13 +50,17 @@ type ClientConfig struct {
 		DebugHTTP     bool `yaml:"debug_http"`
 	} `yaml:"server"`
 
-	Resolver struct {
+	DefaultResolver string `yaml:"default_resolver"`
+	Resolvers       struct {
 		Remote struct {
-			Enabled       bool   `yaml:"enabled"`
 			URL           string `yaml:"url"`
 			AllowInsecure bool   `yaml:"allow_insecure"`
 		} `yaml:"remote"`
-	} `yaml:"resolver"`
+		Sqlite struct {
+			Path string `yaml:"path"`
+		} `yaml:"sqlite"`
+		Chain []string `yaml:"chain"`
+	} `yaml:"resolvers"`
 }
 
 // LoadConfig loads the client configuration from the given path
@@ -78,6 +82,9 @@ func (c *ClientConfig) LoadConfig(r io.Reader) error {
 
 	// Expand homedirs in configuration
 	c.Vault.Path, _ = homedir.Expand(c.Vault.Path)
+	c.Resolvers.Sqlite.Path, _ = homedir.Expand(c.Resolvers.Sqlite.Path)
+
+	loadedConfig = "client"
 
 	return nil
 }
diff --git a/internal/config/config_server.go b/internal/config/config_server.go
index d518f04..a713f8b 100644
--- a/internal/config/config_server.go
+++ b/internal/config/config_server.go
@@ -99,18 +99,17 @@ type ServerConfig struct {
 		DatabasePath string `yaml:"database_path"`
 	} `yaml:"bolt"`
 
-	Resolver struct {
-		Sqlite struct {
-			Enabled bool   `yaml:"enabled"`
-			Dsn     string `yaml:"dsn"`
-		} `yaml:"sqlite"`
-
+	DefaultResolver string `yaml:"default_resolver"`
+	Resolvers       struct {
 		Remote struct {
-			Enabled       bool   `yaml:"enabled"`
 			URL           string `yaml:"url"`
 			AllowInsecure bool   `yaml:"allow_insecure"`
 		} `yaml:"remote"`
-	} `yaml:"resolver"`
+		Sqlite struct {
+			Path string `yaml:"path"`
+		} `yaml:"sqlite"`
+		Chain []string `yaml:"chain"`
+	} `yaml:"resolvers"`
 }
 
 // LoadConfig loads the server configuration from the given path
@@ -142,6 +141,9 @@ func (c *ServerConfig) LoadConfig(r io.Reader) error {
 	c.Server.KeyFile, _ = homedir.Expand(c.Server.KeyFile)
 	c.Server.RoutingFile, _ = homedir.Expand(c.Server.RoutingFile)
 	c.Bolt.DatabasePath, _ = homedir.Expand(c.Bolt.DatabasePath)
+	c.Resolvers.Sqlite.Path, _ = homedir.Expand(c.Resolvers.Sqlite.Path)
+
+	loadedConfig = "server"
 
 	return nil
 }
diff --git a/internal/config/config_templates.go b/internal/config/config_templates.go
index 21863d9..e60f2c1 100644
--- a/internal/config/config_templates.go
+++ b/internal/config/config_templates.go
@@ -36,14 +36,21 @@ config:
         debug_http: false
 
     # How can we resolve public keys and accounts
-    resolver:
+    default_resolver: remote
+    resolvers:
+        # Remote uses a remote URL call for resolving
         remote:
-            # Enable remote resolving
-            enabled: true
+            # URL to call
             url: "https://resolver.bitmaelum.com"
             # Allow insecure connections (to selfsigned certs)
             allow_insecure: false
-
+        # Sqlite will resolve from a SQLite database. Used mostly for testing
+        sqlite:
+            path: ~/resolver.db
+        # Chain will resolve from multiple resolvers chained together		
+        chain:
+            - remote
+            - sqlite
 `
 
 const bridgeConfigTemplate string = `# BitMaelum Bridge Configuration Template. Edit for your own needs.
@@ -100,21 +107,22 @@ config:
             # Display IMAP communication between client and server
             debug: false
             
-    resolver:
-        # SQLite local resolver cache 
-        sqlite:
-            # Enable sqlite resolving
-            enabled: false
-            # Note: DSN currently does not support ~ homedir expansion
-            dsn: "file:/tmp/keyresolve.db"
-        # Remove resolver
-        remote:
-            # Enable remote resolving
-            enabled: true
-            # URL to the remote resolver
-            url: "https://resolver.bitmaelum.com"
-            # Allow insecure connections (to selfsigned certs)
-            allow_insecure: false
+        # How can we resolve public keys and accounts
+        default_resolver: remote
+        resolvers:
+            # Remote uses a remote URL call for resolving
+            remote:
+                # URL to call
+                url: "https://resolver.bitmaelum.com"
+                # Allow insecure connections (to selfsigned certs)
+                allow_insecure: false
+            # Sqlite will resolve from a SQLite database. Used mostly for testing
+            sqlite:
+                path: ~/resolver.db
+            # Chain will resolve from multiple resolvers chained together		
+            chain:
+                - remote
+                - sqlite
     
 `
 
@@ -229,21 +237,22 @@ config:
         # Redis Database Number (defaults to 0)
         db: 0
 
-    resolver:
-        # SQLite local resolver cache 
-        sqlite:
-            # Enable sqlite resolving
-            enabled: false
-            # Note: DSN currently does not support ~ homedir expansion
-            dsn: "file:/tmp/keyresolve.db"
-        # Remove resolver
+    # How can we resolve public keys and accounts
+    default_resolver: remote
+    resolvers:
+        # Remote uses a remote URL call for resolving
         remote:
-            # Enable remote resolving
-            enabled: true
-            # URL to the remote resolver
+            # URL to call
             url: "https://resolver.bitmaelum.com"
             # Allow insecure connections (to selfsigned certs)
             allow_insecure: false
+        # Sqlite will resolve from a SQLite database. Used mostly for testing
+        sqlite:
+            path: ~/resolver.db
+        # Chain will resolve from multiple resolvers chained together		
+        chain:
+            - remote
+            - sqlite
 
 `
 
diff --git a/internal/container/resolver.go b/internal/container/resolver.go
index 2411400..5553a40 100644
--- a/internal/container/resolver.go
+++ b/internal/container/resolver.go
@@ -20,6 +20,7 @@
 package container
 
 import (
+	"errors"
 	"sync"
 
 	"github.com/bitmaelum/bitmaelum-suite/internal/config"
@@ -29,46 +30,139 @@ import (
 // We can have multiple resolvers to resolve a single address. We could resolve locally, remotely through
 // resolver-services, or through DHT. We chain them all together with the ChainRepository
 
+const defaultRemoteURL = "https://resolver.bitmaelum.com"
+
 var (
 	resolveOnce    sync.Once
 	resolveService *resolver.Service
 )
 
 func setupResolverService() (interface{}, error) {
+	var repo resolver.Repository
+	var err error
+
 	resolveOnce.Do(func() {
-		repo := resolver.NewChainRepository()
-		if config.Server.Resolver.Sqlite.Enabled {
-			r, err := getSQLiteRepository(config.Server.Resolver.Sqlite.Dsn)
-			if err == nil {
-				_ = repo.(*resolver.ChainRepository).Add(r)
-			}
+		switch config.Server.DefaultResolver {
+		default:
+			fallthrough
+		case "remote":
+			repo, err = getRemoteRepository()
+		case "sqlite":
+			repo, err = getSQLiteRepository()
+		case "chain":
+			repo, err = getChainRepository()
 		}
 
-		// We add either the client or the server resolver
-		if config.Client.Resolver.Remote.Enabled {
-			_ = repo.(*resolver.ChainRepository).Add(*getRemoteRepository(config.Client.Resolver.Remote.URL, config.Client.Server.DebugHTTP, config.Client.Resolver.Remote.AllowInsecure))
-		}
-		if config.Server.Resolver.Remote.Enabled {
-			_ = repo.(*resolver.ChainRepository).Add(*getRemoteRepository(config.Server.Resolver.Remote.URL, false, config.Server.Resolver.Remote.AllowInsecure))
-		}
-		if config.Bridge.Resolver.Remote.Enabled {
-			_ = repo.(*resolver.ChainRepository).Add(*getRemoteRepository(config.Bridge.Resolver.Remote.URL, false, config.Bridge.Resolver.Remote.AllowInsecure))
+		if err != nil {
+			return
 		}
 
 		resolveService = resolver.KeyRetrievalService(repo)
 	})
 
+	// No correct resolver found, default to remote resolver
+	if resolveService == nil {
+		repo, err = getRemoteRepository()
+		if err != nil {
+			return nil, err
+		}
+
+		resolveService = resolver.KeyRetrievalService(repo)
+	}
+
 	return resolveService, nil
 }
 
-func getRemoteRepository(url string, debug, allowInsecure bool) *resolver.Repository {
-	repo := resolver.NewRemoteRepository(url, debug, allowInsecure)
-	return &repo
+func getRemoteRepository() (resolver.Repository, error) {
+	var (
+		url           string
+		debug         bool
+		allowInsecure bool
+	)
+
+	if config.IsLoaded("client") {
+		url = config.Client.Resolvers.Remote.URL
+		debug = config.Client.Server.DebugHTTP
+		allowInsecure = config.Client.Resolvers.Remote.AllowInsecure
+	}
+	if config.IsLoaded("server") {
+		url = config.Server.Resolvers.Remote.URL
+		debug = false
+		allowInsecure = config.Server.Resolvers.Remote.AllowInsecure
+	}
+	if config.IsLoaded("bridge") {
+		url = config.Bridge.Resolvers.Remote.URL
+		debug = false
+		allowInsecure = config.Bridge.Resolvers.Remote.AllowInsecure
+	}
+
+	// Set default URL if none is given
+	if url == "" {
+		url = defaultRemoteURL
+	}
+
+	return resolver.NewRemoteRepository(url, debug, allowInsecure), nil
+}
+
+func getSQLiteRepository() (resolver.Repository, error) {
+	var path string
+
+	if config.IsLoaded("client") {
+		path = config.Client.Resolvers.Sqlite.Path
+	}
+	if config.IsLoaded("server") {
+		path = config.Server.Resolvers.Sqlite.Path
+	}
+	if config.IsLoaded("bridge") {
+		path = config.Bridge.Resolvers.Sqlite.Path
+	}
+
+	return resolver.NewSqliteRepository(path)
 }
 
-func getSQLiteRepository(dsn string) (resolver.Repository, error) {
-	repo, err := resolver.NewSqliteRepository(dsn)
-	return repo, err
+func getChainRepository() (resolver.Repository, error) {
+	repo := resolver.NewChainRepository()
+
+	var resolvers []string
+	if config.IsLoaded("client") {
+		resolvers = config.Client.Resolvers.Chain
+	}
+	if config.IsLoaded("server") {
+		resolvers = config.Server.Resolvers.Chain
+	}
+	if config.IsLoaded("bridge") {
+		resolvers = config.Bridge.Resolvers.Chain
+	}
+
+	var (
+		chainedRepo resolver.Repository
+		err         error
+	)
+
+	idx := 0
+	for _, resolverName := range resolvers {
+		switch resolverName {
+		default:
+			fallthrough
+		case "remote":
+			chainedRepo, err = getRemoteRepository()
+		case "sqlite":
+			chainedRepo, err = getSQLiteRepository()
+		}
+
+		if err != nil {
+			return nil, err
+		}
+
+		_ = repo.(*resolver.ChainRepository).Add(chainedRepo)
+		idx++
+	}
+
+	if idx == 0 {
+		return nil, errors.New("the chain repo is empty")
+	}
+
+	return repo, nil
 }
 
 func init() {


Test patch:
diff --git a/internal/config/config_templates_test.go b/internal/config/config_templates_test.go
index 8ba878b..ef5f2aa 100644
--- a/internal/config/config_templates_test.go
+++ b/internal/config/config_templates_test.go
@@ -41,10 +41,10 @@ func TestTemplates(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NotEmpty(t, buf.String())
 
-	assert.Empty(t, Client.Resolver.Remote.URL)
+	assert.Empty(t, Client.Resolvers.Remote.URL)
 	err = Client.LoadConfig(&buf)
 	assert.NoError(t, err)
-	assert.Equal(t, "https://resolver.bitmaelum.com", Client.Resolver.Remote.URL)
+	assert.Equal(t, "https://resolver.bitmaelum.com", Client.Resolvers.Remote.URL)
 
 	err = GenerateBridgeConfig(&buf)
 	assert.NoError(t, err)
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 47a0ded..0f6fec0 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -53,44 +53,44 @@ func TestClientConfig(t *testing.T) {
 	assert.NoError(t, err)
 	_ = f.Close()
 
-	Client.Resolver.Remote.URL = ""
+	Client.Resolvers.Remote.URL = ""
 	err = LoadClientConfigOrPass(testingClientConfigPath)
 	assert.NoError(t, err)
-	assert.Equal(t, testingResolverRemoteURL, Client.Resolver.Remote.URL)
+	assert.Equal(t, testingResolverRemoteURL, Client.Resolvers.Remote.URL)
 
-	Client.Resolver.Remote.URL = ""
+	Client.Resolvers.Remote.URL = ""
 	err = LoadClientConfigOrPass(testingNotExistsFilePath)
 	assert.Error(t, err)
-	assert.Equal(t, "", Client.Resolver.Remote.URL)
+	assert.Equal(t, "", Client.Resolvers.Remote.URL)
 
-	Client.Resolver.Remote.URL = ""
+	Client.Resolvers.Remote.URL = ""
 	err = LoadClientConfigOrPass(testingClientConfigPath)
 	assert.NoError(t, err)
-	assert.Equal(t, testingResolverRemoteURL, Client.Resolver.Remote.URL)
+	assert.Equal(t, testingResolverRemoteURL, Client.Resolvers.Remote.URL)
 
 	// Read from searchpath
 	if runtime.GOOS != "windows" {
 		// This test fails on windows. It expects the file to be on the searchpath, but it isn't because
 		// the searchpath for windows is different. However, we expect a regular path like /etc/bitmaelum/*.yml
 		// for this test to succeed.
-		Client.Resolver.Remote.URL = ""
+		Client.Resolvers.Remote.URL = ""
 		err = LoadClientConfigOrPass("")
 		assert.NoError(t, err)
-		assert.Equal(t, testingResolverRemoteURL, Client.Resolver.Remote.URL)
+		assert.Equal(t, testingResolverRemoteURL, Client.Resolvers.Remote.URL)
 	}
 
 	// Read from non-existing env
-	Client.Resolver.Remote.URL = ""
+	Client.Resolvers.Remote.URL = ""
 	_ = os.Setenv("BITMAELUM_CLIENT_CONFIG", "/etc/does/not/exist.yml")
 	err = LoadClientConfigOrPass("")
 	assert.Error(t, err)
 
 	// Read from existing env
-	Client.Resolver.Remote.URL = ""
+	Client.Resolvers.Remote.URL = ""
 	_ = os.Setenv("BITMAELUM_CLIENT_CONFIG", testingClientConfigPath)
 	err = LoadClientConfigOrPass("")
 	assert.NoError(t, err)
-	assert.Equal(t, testingResolverRemoteURL, Client.Resolver.Remote.URL)
+	assert.Equal(t, testingResolverRemoteURL, Client.Resolvers.Remote.URL)
 }
 
 func TestServerConfig(t *testing.T) {