Repository: lyraproj/hiera (go)

Issue:
Options for lookup_key
Hi - I wanted to add a new lookup key provider (for Azure Key Vault). I've created the function in the provider package but the `options` map is empty when the function is called. Is this functionality implemented yet?

```go
func AzureKeyVaultLookupKey(c hieraapi.ProviderContext, key string, options map[string]px.Value) px.Value {
	vaultName, ok := options[`vault_name`]
	if !ok {
		panic(px.Error(hieraapi.MissingRequiredOption, issue.H{`option`: `vault_name`}))
	}
```

When running this always fails with Missing required provider option 'vault_name'

The hiera.yaml is

```yaml
---
version: 5
defaults:
  datadir: data
  data_hash: yaml_data
hierarchy:
  - name: secret
    lookup_key: azure_key_vault
    options:
      vault_name: xxx
```

Pull request description:
Ensure that hierarchy options are passed on to provider function
This commit changes how providers are constituted so that they now
contain the actual hierarchy entry. The options from the entry is then
passed on, possibly augmented with a "path" entry, in calls to the
actual provider function.

A small sample of how to add a provider function dynamically is included
in the lookup/lookup_test.go file.

Closes #25

Patch:
diff --git a/go.mod b/go.mod
index 08c27a1..e389de2 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ require (
 	github.com/bmatcuk/doublestar v1.1.1
 	github.com/hashicorp/go-hclog v0.9.0
 	github.com/lyraproj/issue v0.0.0-20190606092846-e082d6813d15
-	github.com/lyraproj/pcore v0.0.0-20190619162937-645af37a80ad
+	github.com/lyraproj/pcore v0.0.0-20190709110110-57c90466e307
 	github.com/spf13/cobra v0.0.4
 	github.com/stretchr/testify v1.3.0
 	gopkg.in/yaml.v3 v3.0.0-20190502103701-55513cacd4ae
diff --git a/go.sum b/go.sum
index 597dafd..4a5cf8e 100644
--- a/go.sum
+++ b/go.sum
@@ -23,6 +23,8 @@ github.com/lyraproj/issue v0.0.0-20190606092846-e082d6813d15 h1:e1uefKgfSdC7uaYG
 github.com/lyraproj/issue v0.0.0-20190606092846-e082d6813d15/go.mod h1:jqRRmAiVqjTehhncbKJPoC2AFoxxkritzzlfWioTa00=
 github.com/lyraproj/pcore v0.0.0-20190619162937-645af37a80ad h1:Smdt4D4MrveCvMVgQVQ7JAvMiY0+q5p/Nc/PKtQUv+E=
 github.com/lyraproj/pcore v0.0.0-20190619162937-645af37a80ad/go.mod h1:yqZNNXXw/2It5Jh2Vc+g93Qm+1q6EklXJ0t/xqNNBV8=
+github.com/lyraproj/pcore v0.0.0-20190709110110-57c90466e307 h1:Dr/NfFG/2mEgleyfsiLT+9j7olm5imQkynsDRpGL8Tk=
+github.com/lyraproj/pcore v0.0.0-20190709110110-57c90466e307/go.mod h1:yqZNNXXw/2It5Jh2Vc+g93Qm+1q6EklXJ0t/xqNNBV8=
 github.com/lyraproj/semver v0.0.0-20181213164306-02ecea2cd6a2 h1:vb4PbiMtIXdhsOUinkkcqZiASDIZzXRhSG4yvNfE0tg=
 github.com/lyraproj/semver v0.0.0-20181213164306-02ecea2cd6a2/go.mod h1:KOdZKnEBdDb2iGPUnHiKpk3M5cvv949xMyj8XPqaMF0=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
diff --git a/hieraapi/api.go b/hieraapi/api.go
index b052601..0b54e0b 100644
--- a/hieraapi/api.go
+++ b/hieraapi/api.go
@@ -40,6 +40,7 @@ type Function interface {
 type Entry interface {
 	Copy(Config) Entry
 	Options() px.OrderedMap
+	OptionsMap() map[string]px.Value
 	DataDir() string
 	Function() Function
 	Name() string
diff --git a/internal/config.go b/internal/config.go
index a68b210..23df2c1 100644
--- a/internal/config.go
+++ b/internal/config.go
@@ -39,6 +39,7 @@ type entry struct {
 	cfg       *hieraCfg
 	dataDir   string
 	options   px.OrderedMap
+	optsMap   map[string]px.Value
 	function  hieraapi.Function
 	name      string
 	locations []hieraapi.Location
@@ -48,6 +49,10 @@ func (e *entry) Options() px.OrderedMap {
 	return e.options
 }
 
+func (e *entry) OptionsMap() map[string]px.Value {
+	return e.optsMap
+}
+
 func (e *entry) DataDir() string {
 	return e.dataDir
 }
@@ -133,11 +138,13 @@ func (e *entry) Resolve(ic hieraapi.Invocation, defaults hieraapi.Entry) hieraap
 	if ce.options == nil {
 		if defaults != nil {
 			ce.options = defaults.Options()
+			ce.optsMap = defaults.OptionsMap()
 		}
 	} else if ce.options.Len() > 0 {
 		if o, oc := doInterpolate(ic, ce.options, false); oc {
 			ce.options = o.(*types.Hash)
 		}
+		ce.optsMap = ce.options.ToStringMap()
 	}
 
 	var dataRoot string
diff --git a/internal/datadigprovider.go b/internal/datadigprovider.go
index f8f6c1e..67b4df4 100644
--- a/internal/datadigprovider.go
+++ b/internal/datadigprovider.go
@@ -6,25 +6,24 @@ import (
 
 	"github.com/lyraproj/hiera/hieraapi"
 	"github.com/lyraproj/pcore/px"
-	"github.com/lyraproj/pcore/types"
 )
 
 type DataDigProvider struct {
-	function     hieraapi.Function
-	locations    []hieraapi.Location
-	providerFunc hieraapi.DataDig
-	hashes       *sync.Map
+	hierarchyEntry hieraapi.Entry
+	providerFunc   hieraapi.DataDig
+	hashes         *sync.Map
 }
 
 func (dh *DataDigProvider) UncheckedLookup(key hieraapi.Key, invocation hieraapi.Invocation, merge hieraapi.MergeStrategy) px.Value {
 	return invocation.WithDataProvider(dh, func() px.Value {
-		switch len(dh.locations) {
+		locations := dh.hierarchyEntry.Locations()
+		switch len(locations) {
 		case 0:
 			return dh.invokeWithLocation(invocation, nil, key)
 		case 1:
-			return dh.invokeWithLocation(invocation, dh.locations[0], key)
+			return dh.invokeWithLocation(invocation, locations[0], key)
 		default:
-			return merge.Lookup(dh.locations, invocation, func(location interface{}) px.Value {
+			return merge.Lookup(locations, invocation, func(location interface{}) px.Value {
 				return dh.invokeWithLocation(invocation, location.(hieraapi.Location), key)
 			})
 		}
@@ -50,12 +49,11 @@ func (dh *DataDigProvider) invokeWithLocation(invocation hieraapi.Invocation, lo
 
 func (dh *DataDigProvider) lookupKey(ic hieraapi.Invocation, location hieraapi.Location, key hieraapi.Key) px.Value {
 	cacheKey := ``
-	opts := NoOptions
+	opts := dh.hierarchyEntry.OptionsMap()
 	if location != nil {
 		cacheKey = location.Resolved()
-		opts = map[string]px.Value{`path`: types.WrapString(cacheKey)}
+		opts = optionsWithLocation(opts, cacheKey)
 	}
-
 	cache, _ := dh.hashes.LoadOrStore(cacheKey, &sync.Map{})
 	value := dh.providerFunction(ic)(newProviderContext(ic, cache.(*sync.Map)), key, opts)
 	if value != nil {
@@ -68,7 +66,7 @@ func (dh *DataDigProvider) lookupKey(ic hieraapi.Invocation, location hieraapi.L
 
 func (dh *DataDigProvider) providerFunction(ic hieraapi.Invocation) (pf hieraapi.DataDig) {
 	if dh.providerFunc == nil {
-		n := dh.function.Name()
+		n := dh.hierarchyEntry.Function().Name()
 		// Load lookup provider function using the standard loader
 		if f, ok := px.Load(ic, px.NewTypedName(px.NsFunction, n)); ok {
 			dh.providerFunc = func(pc hieraapi.ProviderContext, key hieraapi.Key, options map[string]px.Value) px.Value {
@@ -88,14 +86,9 @@ func (dh *DataDigProvider) providerFunction(ic hieraapi.Invocation) (pf hieraapi
 }
 
 func (dh *DataDigProvider) FullName() string {
-	return fmt.Sprintf(`data_dig function '%s'`, dh.function.Name())
+	return fmt.Sprintf(`data_dig function '%s'`, dh.hierarchyEntry.Function().Name())
 }
 
 func newDataDigProvider(he hieraapi.Entry) hieraapi.DataProvider {
-	ls := he.Locations()
-	return &DataDigProvider{
-		function:  he.Function(),
-		locations: ls,
-		hashes:    &sync.Map{},
-	}
+	return &DataDigProvider{hierarchyEntry: he, hashes: &sync.Map{}}
 }
diff --git a/internal/datahashprovider.go b/internal/datahashprovider.go
index 28c0910..0401e65 100644
--- a/internal/datahashprovider.go
+++ b/internal/datahashprovider.go
@@ -12,22 +12,22 @@ import (
 )
 
 type DataHashProvider struct {
-	function     hieraapi.Function
-	locations    []hieraapi.Location
-	providerFunc hieraapi.DataHash
-	hashes       map[string]px.OrderedMap
-	hashesLock   sync.RWMutex
+	hierarchyEntry hieraapi.Entry
+	providerFunc   hieraapi.DataHash
+	hashes         map[string]px.OrderedMap
+	hashesLock     sync.RWMutex
 }
 
 func (dh *DataHashProvider) UncheckedLookup(key hieraapi.Key, invocation hieraapi.Invocation, merge hieraapi.MergeStrategy) px.Value {
 	return invocation.WithDataProvider(dh, func() px.Value {
-		switch len(dh.locations) {
+		locations := dh.hierarchyEntry.Locations()
+		switch len(locations) {
 		case 0:
 			return dh.invokeWithLocation(invocation, nil, key.Root())
 		case 1:
-			return dh.invokeWithLocation(invocation, dh.locations[0], key.Root())
+			return dh.invokeWithLocation(invocation, locations[0], key.Root())
 		default:
-			return merge.Lookup(dh.locations, invocation, func(location interface{}) px.Value {
+			return merge.Lookup(locations, invocation, func(location interface{}) px.Value {
 				return dh.invokeWithLocation(invocation, location.(hieraapi.Location), key.Root())
 			})
 		}
@@ -77,7 +77,7 @@ func (dh *DataHashProvider) dataValue(ic hieraapi.Invocation, location hieraapi.
 
 func (dh *DataHashProvider) providerFunction(ic hieraapi.Invocation) (pf hieraapi.DataHash) {
 	if dh.providerFunc == nil {
-		n := dh.function.Name()
+		n := dh.hierarchyEntry.Function().Name()
 		if n == `yaml_data` {
 			// Shortcut. No need to go through pcore calling mechanism
 			dh.providerFunc = provider.YamlData
@@ -90,7 +90,7 @@ func (dh *DataHashProvider) providerFunction(ic hieraapi.Invocation) (pf hieraap
 		}
 
 		// Load lookup provider function using the standard loader
-		if f, ok := px.Load(ic, px.NewTypedName(px.NsFunction, dh.function.Name())); ok {
+		if f, ok := px.Load(ic, px.NewTypedName(px.NsFunction, n)); ok {
 			dh.providerFunc = func(pc hieraapi.ProviderContext, options map[string]px.Value) (value px.OrderedMap) {
 				value = px.EmptyMap
 				defer catchNotFound()
@@ -102,7 +102,7 @@ func (dh *DataHashProvider) providerFunction(ic hieraapi.Invocation) (pf hieraap
 			}
 		} else {
 			ic.ReportText(func() string {
-				return fmt.Sprintf(`unresolved function '%s'`, dh.function.Name())
+				return fmt.Sprintf(`unresolved function '%s'`, n)
 			})
 			dh.providerFunc = func(pc hieraapi.ProviderContext, options map[string]px.Value) px.OrderedMap {
 				return px.EmptyMap
@@ -114,10 +114,10 @@ func (dh *DataHashProvider) providerFunction(ic hieraapi.Invocation) (pf hieraap
 
 func (dh *DataHashProvider) dataHash(ic hieraapi.Invocation, location hieraapi.Location) (hash px.OrderedMap) {
 	key := ``
-	opts := NoOptions
+	opts := dh.hierarchyEntry.OptionsMap()
 	if location != nil {
 		key = location.Resolved()
-		opts = map[string]px.Value{`path`: types.WrapString(key)}
+		opts = optionsWithLocation(opts, key)
 	}
 
 	var ok bool
@@ -140,14 +140,23 @@ func (dh *DataHashProvider) dataHash(ic hieraapi.Invocation, location hieraapi.L
 }
 
 func (dh *DataHashProvider) FullName() string {
-	return fmt.Sprintf(`data_hash function '%s'`, dh.function.Name())
+	return fmt.Sprintf(`data_hash function '%s'`, dh.hierarchyEntry.Function().Name())
 }
 
 func newDataHashProvider(he hieraapi.Entry) hieraapi.DataProvider {
 	ls := he.Locations()
-	return &DataHashProvider{
-		function:  he.Function(),
-		locations: ls,
-		hashes:    make(map[string]px.OrderedMap, len(ls)),
+	return &DataHashProvider{hierarchyEntry: he, hashes: make(map[string]px.OrderedMap, len(ls))}
+}
+
+func optionsWithLocation(options map[string]px.Value, loc string) map[string]px.Value {
+	ov := types.WrapString(loc)
+	if len(options) == 0 {
+		return map[string]px.Value{`path`: ov}
+	}
+	newOpts := make(map[string]px.Value, len(options)+1)
+	for k, v := range options {
+		newOpts[k] = v
 	}
+	newOpts[`path`] = ov
+	return newOpts
 }
diff --git a/internal/lookupkeyprovider.go b/internal/lookupkeyprovider.go
index 7b95156..63e5366 100644
--- a/internal/lookupkeyprovider.go
+++ b/internal/lookupkeyprovider.go
@@ -12,21 +12,21 @@ import (
 )
 
 type LookupKeyProvider struct {
-	function     hieraapi.Function
-	locations    []hieraapi.Location
-	providerFunc hieraapi.LookupKey
-	hashes       *sync.Map
+	hierarchyEntry hieraapi.Entry
+	providerFunc   hieraapi.LookupKey
+	hashes         *sync.Map
 }
 
 func (dh *LookupKeyProvider) UncheckedLookup(key hieraapi.Key, invocation hieraapi.Invocation, merge hieraapi.MergeStrategy) px.Value {
 	return invocation.WithDataProvider(dh, func() px.Value {
-		switch len(dh.locations) {
+		locations := dh.hierarchyEntry.Locations()
+		switch len(locations) {
 		case 0:
 			return dh.invokeWithLocation(invocation, nil, key.Root())
 		case 1:
-			return dh.invokeWithLocation(invocation, dh.locations[0], key.Root())
+			return dh.invokeWithLocation(invocation, locations[0], key.Root())
 		default:
-			return merge.Lookup(dh.locations, invocation, func(location interface{}) px.Value {
+			return merge.Lookup(locations, invocation, func(location interface{}) px.Value {
 				return dh.invokeWithLocation(invocation, location.(hieraapi.Location), key.Root())
 			})
 		}
@@ -48,12 +48,11 @@ func (dh *LookupKeyProvider) invokeWithLocation(invocation hieraapi.Invocation,
 
 func (dh *LookupKeyProvider) lookupKey(ic hieraapi.Invocation, location hieraapi.Location, root string) px.Value {
 	key := ``
-	opts := NoOptions
+	opts := dh.hierarchyEntry.OptionsMap()
 	if location != nil {
 		key = location.Resolved()
-		opts = map[string]px.Value{`path`: types.WrapString(key)}
+		opts = optionsWithLocation(opts, key)
 	}
-
 	cache, _ := dh.hashes.LoadOrStore(key, &sync.Map{})
 	value := dh.providerFunction(ic)(newProviderContext(ic, cache.(*sync.Map)), root, opts)
 	if value != nil {
@@ -66,7 +65,7 @@ func (dh *LookupKeyProvider) lookupKey(ic hieraapi.Invocation, location hieraapi
 
 func (dh *LookupKeyProvider) providerFunction(ic hieraapi.Invocation) (pf hieraapi.LookupKey) {
 	if dh.providerFunc == nil {
-		n := dh.function.Name()
+		n := dh.hierarchyEntry.Function().Name()
 		if n == `environment` {
 			dh.providerFunc = provider.Environment
 		} else if n == `scope` {
@@ -90,14 +89,9 @@ func (dh *LookupKeyProvider) providerFunction(ic hieraapi.Invocation) (pf hieraa
 }
 
 func (dh *LookupKeyProvider) FullName() string {
-	return fmt.Sprintf(`lookup_key function '%s'`, dh.function.Name())
+	return fmt.Sprintf(`lookup_key function '%s'`, dh.hierarchyEntry.Function().Name())
 }
 
 func newLookupKeyProvider(he hieraapi.Entry) hieraapi.DataProvider {
-	ls := he.Locations()
-	return &LookupKeyProvider{
-		function:  he.Function(),
-		locations: ls,
-		hashes:    &sync.Map{},
-	}
+	return &LookupKeyProvider{hierarchyEntry: he, hashes: &sync.Map{}}
 }


Test patch:
diff --git a/lookup/lookup_test.go b/lookup/lookup_test.go
index 36a036e..8c939f5 100644
--- a/lookup/lookup_test.go
+++ b/lookup/lookup_test.go
@@ -5,6 +5,9 @@ import (
 	"os"
 	"testing"
 
+	"github.com/lyraproj/hiera/hieraapi"
+	"github.com/lyraproj/pcore/px"
+
 	"github.com/stretchr/testify/require"
 )
 
@@ -202,6 +205,36 @@ Searching for "hash"
 	})
 }
 
+func customLK(hc hieraapi.ProviderContext, key string, options map[string]px.Value) px.Value {
+	if v, ok := options[key]; ok {
+		return v
+	}
+	hc.NotFound()
+	return nil // Not reached
+}
+
+func init() {
+	px.NewGoFunction(`customLK`,
+		func(d px.Dispatch) {
+			d.Param(`Hiera::Context`)
+			d.Param(`String`)
+			d.Param(`Hash[String,Any]`)
+			d.Function(func(c px.Context, args []px.Value) px.Value {
+				return customLK(args[0].(hieraapi.ProviderContext), args[1].String(), args[2].(px.OrderedMap).ToStringMap())
+			})
+		},
+	)
+}
+
+func TestLookup_withCustomLK(t *testing.T) {
+
+	inTestdata(func() {
+		result, err := executeLookup(`--config`, `with_custom_provider.yaml`, `a`)
+		require.NoError(t, err)
+		require.Equal(t, "option a\n", string(result))
+	})
+}
+
 func inTestdata(f func()) {
 	cw, err := os.Getwd()
 	if err == nil {
diff --git a/lookup/testdata/with_custom_provider.yaml b/lookup/testdata/with_custom_provider.yaml
new file mode 100644
index 0000000..1c7f4e4
--- /dev/null
+++ b/lookup/testdata/with_custom_provider.yaml
@@ -0,0 +1,8 @@
+version: 5
+
+hierarchy:
+  - name: Custom
+    lookup_key: customLK
+    options:
+      a: option a
+      b: option b