Repository: lyraproj/hiera
Pull request #22 (2019-06-11 15:26:24)

Add new "scope" lookup_key function provider.
This commit adds the new lookup_key provider "scope" which performs
lookups directly in the current variable scope.


Patch:
diff --git a/hiera/render.go b/hiera/render.go
new file mode 100644
index 0000000..fa3a530
--- /dev/null
+++ b/hiera/render.go
@@ -0,0 +1,45 @@
+package hiera
+
+import (
+	"encoding/json"
+	"io"
+
+	"github.com/lyraproj/pcore/px"
+	"github.com/lyraproj/pcore/types"
+	"github.com/lyraproj/pcore/utils"
+	"gopkg.in/yaml.v3"
+)
+
+func Render(c px.Context, renderAs string, value px.Value, out io.Writer) {
+	switch renderAs {
+	case `yaml`, `json`:
+		var v interface{}
+		if !value.Equals(px.Undef, nil) {
+			rf := c.Reflector().Reflect(value)
+			if rf.IsValid() && rf.CanInterface() {
+				v = rf.Interface()
+			} else {
+				v = value.String()
+			}
+		}
+		var bs []byte
+		var err error
+		if renderAs == `yaml` {
+			bs, err = yaml.Marshal(v)
+		} else {
+			bs, err = json.Marshal(v)
+		}
+		if err != nil {
+			panic(err)
+		}
+		utils.WriteString(out, string(bs))
+	case `binary`:
+		bi := types.CoerceTo(c, `lookup value`, types.DefaultBinaryType(), value).(*types.Binary)
+		_, err := out.Write(bi.Bytes())
+		if err != nil {
+			panic(err)
+		}
+	case `s`:
+		utils.Fprintln(out, value)
+	}
+}
diff --git a/hieraapi/api.go b/hieraapi/api.go
index 84fff99..b052601 100644
--- a/hieraapi/api.go
+++ b/hieraapi/api.go
@@ -17,6 +17,10 @@ const HieraConfigFileName = `Hiera::ConfigFileName`
 // HieraRoot and HieraConfigFileName will not have any effect.
 const HieraConfig = `Hiera::Config`
 
+// HieraScope is an option that can be used to pass a variable scope to Hiera. This scope is used
+// by the 'scope' lookup_key provider function and when doing variable interpolations
+const HieraScope = `Hiera::Scope`
+
 const KindDataDig = Kind(`data_dig`)
 const KindDataHash = Kind(`data_hash`)
 const KindLookupKey = Kind(`lookup_key`)
@@ -119,6 +123,9 @@ type Invocation interface {
 
 	WithSubLookup(key Key, f px.Producer) px.Value
 
+	// ExplainMode returns true if explain support is active
+	ExplainMode() bool
+
 	// ForConfig returns an Invocation that without explainer support
 	ForConfig() Invocation
 
diff --git a/internal/datadigprovider.go b/internal/datadigprovider.go
index 29daac9..f8f6c1e 100644
--- a/internal/datadigprovider.go
+++ b/internal/datadigprovider.go
@@ -18,9 +18,16 @@ type DataDigProvider struct {
 
 func (dh *DataDigProvider) UncheckedLookup(key hieraapi.Key, invocation hieraapi.Invocation, merge hieraapi.MergeStrategy) px.Value {
 	return invocation.WithDataProvider(dh, func() px.Value {
-		return merge.Lookup(dh.locations, invocation, func(location interface{}) px.Value {
-			return dh.invokeWithLocation(invocation, location.(hieraapi.Location), key)
-		})
+		switch len(dh.locations) {
+		case 0:
+			return dh.invokeWithLocation(invocation, nil, key)
+		case 1:
+			return dh.invokeWithLocation(invocation, dh.locations[0], key)
+		default:
+			return merge.Lookup(dh.locations, invocation, func(location interface{}) px.Value {
+				return dh.invokeWithLocation(invocation, location.(hieraapi.Location), key)
+			})
+		}
 	})
 }
 
diff --git a/internal/datahashprovider.go b/internal/datahashprovider.go
index 788c2ef..28c0910 100644
--- a/internal/datahashprovider.go
+++ b/internal/datahashprovider.go
@@ -21,9 +21,16 @@ type DataHashProvider struct {
 
 func (dh *DataHashProvider) UncheckedLookup(key hieraapi.Key, invocation hieraapi.Invocation, merge hieraapi.MergeStrategy) px.Value {
 	return invocation.WithDataProvider(dh, func() px.Value {
-		return merge.Lookup(dh.locations, invocation, func(location interface{}) px.Value {
-			return dh.invokeWithLocation(invocation, location.(hieraapi.Location), key.Root())
-		})
+		switch len(dh.locations) {
+		case 0:
+			return dh.invokeWithLocation(invocation, nil, key.Root())
+		case 1:
+			return dh.invokeWithLocation(invocation, dh.locations[0], key.Root())
+		default:
+			return merge.Lookup(dh.locations, invocation, func(location interface{}) px.Value {
+				return dh.invokeWithLocation(invocation, location.(hieraapi.Location), key.Root())
+			})
+		}
 	})
 }
 
diff --git a/internal/init.go b/internal/init.go
index 84ae99e..3948845 100644
--- a/internal/init.go
+++ b/internal/init.go
@@ -97,7 +97,7 @@ func Lookup2(
 		}
 	}
 
-	if defaultValue == nil {
+	if defaultValue == nil && !ic.ExplainMode() {
 		// nil (as opposed to UNDEF) means that no default was provided.
 		if len(names) == 1 {
 			panic(px.Error(hieraapi.NameNotFound, issue.H{`name`: names[0]}))
diff --git a/internal/invocation.go b/internal/invocation.go
index ed46a99..afe3a47 100644
--- a/internal/invocation.go
+++ b/internal/invocation.go
@@ -72,13 +72,43 @@ func InitContext(c px.Context, topProvider hieraapi.LookupKey, options map[strin
 	}
 }
 
+type nestedScope struct {
+	parentScope px.Keyed
+	scope       px.Keyed
+}
+
+func (ns *nestedScope) Get(key px.Value) (px.Value, bool) {
+	v, ok := ns.scope.Get(key)
+	if !ok {
+		v, ok = ns.parentScope.Get(key)
+	}
+	return v, ok
+}
+
 func NewInvocation(c px.Context, scope px.Keyed, explainer explain.Explainer) hieraapi.Invocation {
-	return &invocation{
+	options := globalOptions(c)
+	if gs, ok := options[hieraapi.HieraScope]; ok {
+		var globalScope px.Keyed
+		if globalScope, ok = gs.(px.Keyed); ok {
+			if scope != nil {
+				scope = &nestedScope{globalScope, scope}
+			} else {
+				scope = globalScope
+			}
+		}
+	}
+	if scope == nil {
+		scope = px.EmptyMap
+	}
+
+	ic := &invocation{
 		Context:    c,
 		nameStack:  []string{},
 		scope:      scope,
-		configPath: globalOptions(c)[hieraapi.HieraConfig].String(),
+		configPath: options[hieraapi.HieraConfig].String(),
 		explainer:  explainer}
+
+	return ic
 }
 
 func (ic *invocation) topProvider() hieraapi.LookupKey {
@@ -149,6 +179,10 @@ func (ic *invocation) Config() (conf hieraapi.ResolvedConfig) {
 	return
 }
 
+func (ic *invocation) ExplainMode() bool {
+	return ic.explainer != nil
+}
+
 func (ic *invocation) lookupViaCache(key hieraapi.Key, options map[string]px.Value) px.Value {
 	rootKey := key.Root()
 	if rootKey == `lookup_options` {
diff --git a/internal/lookupkeyprovider.go b/internal/lookupkeyprovider.go
index 59313d5..7b95156 100644
--- a/internal/lookupkeyprovider.go
+++ b/internal/lookupkeyprovider.go
@@ -20,9 +20,16 @@ type LookupKeyProvider struct {
 
 func (dh *LookupKeyProvider) UncheckedLookup(key hieraapi.Key, invocation hieraapi.Invocation, merge hieraapi.MergeStrategy) px.Value {
 	return invocation.WithDataProvider(dh, func() px.Value {
-		return merge.Lookup(dh.locations, invocation, func(location interface{}) px.Value {
-			return dh.invokeWithLocation(invocation, location.(hieraapi.Location), key.Root())
-		})
+		switch len(dh.locations) {
+		case 0:
+			return dh.invokeWithLocation(invocation, nil, key.Root())
+		case 1:
+			return dh.invokeWithLocation(invocation, dh.locations[0], key.Root())
+		default:
+			return merge.Lookup(dh.locations, invocation, func(location interface{}) px.Value {
+				return dh.invokeWithLocation(invocation, location.(hieraapi.Location), key.Root())
+			})
+		}
 	})
 }
 
@@ -62,9 +69,10 @@ func (dh *LookupKeyProvider) providerFunction(ic hieraapi.Invocation) (pf hieraa
 		n := dh.function.Name()
 		if n == `environment` {
 			dh.providerFunc = provider.Environment
-		}
-		// Load lookup provider function using the standard loader
-		if f, ok := px.Load(ic, px.NewTypedName(px.NsFunction, n)); ok {
+		} else if n == `scope` {
+			dh.providerFunc = provider.ScopeLookupKey
+		} else if f, ok := px.Load(ic, px.NewTypedName(px.NsFunction, n)); ok {
+			// Load lookup provider function using the standard loader
 			dh.providerFunc = func(pc hieraapi.ProviderContext, key string, options map[string]px.Value) px.Value {
 				defer catchNotFound()
 				return f.(px.Function).Call(ic, nil, []px.Value{pc, types.WrapString(key), px.Wrap(ic, options)}...)
diff --git a/lookup/lookup.go b/lookup/lookup.go
index 754d1d4..8f7a024 100644
--- a/lookup/lookup.go
+++ b/lookup/lookup.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"os"
 
@@ -18,9 +17,7 @@ import (
 	"github.com/lyraproj/issue/issue"
 	"github.com/lyraproj/pcore/px"
 	"github.com/lyraproj/pcore/types"
-	"github.com/lyraproj/pcore/utils"
 	"github.com/spf13/cobra"
-	yl "gopkg.in/yaml.v3"
 )
 
 var helpTemplate = `Description:
@@ -52,6 +49,7 @@ var (
 	logLevel       string
 	merge          string
 	facts          string
+	config         string
 	dflt           string
 	typ            string
 	renderAs       string
@@ -60,7 +58,7 @@ var (
 )
 
 func main() {
-	cmd := newCommnand()
+	cmd := newCommand()
 	err := cmd.Execute()
 	if err != nil {
 		fmt.Println(cmd.OutOrStderr(), err)
@@ -68,7 +66,7 @@ func main() {
 	}
 }
 
-func newCommnand() *cobra.Command {
+func newCommand() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:     "lookup <key> [<key> ...]",
 		Short:   `Lookup - Perform lookups in Hiera data storage`,
@@ -81,6 +79,7 @@ func newCommnand() *cobra.Command {
 	flags := cmd.Flags()
 	flags.StringVar(&logLevel, `loglevel`, `error`, `error/warn/info/debug`)
 	flags.StringVar(&merge, `merge`, `first`, `first/unique/hash/deep`)
+	flags.StringVar(&config, `config`, ``, `path to the hiera config file. Overrides <current directory>/hiera.yaml`)
 	flags.StringVar(&facts, `facts`, ``, `path to a JSON or YAML file that contains key-value mappings to become facts for this lookup`)
 	flags.StringVar(&dflt, `default`, ``, `a value to return if Hiera can't find a value in data`)
 	flags.StringVar(&typ, `type`, `Any`, `assert that the value has the specified type`)
@@ -104,6 +103,10 @@ func lookup(cmd *cobra.Command, args []string) {
 	configOptions := map[string]px.Value{
 		provider.LookupProvidersKey: types.WrapRuntime([]hieraapi.LookupKey{provider.ConfigLookupKey, provider.Environment})}
 
+	if config != `` {
+		configOptions[hieraapi.HieraConfig] = types.WrapString(config)
+	}
+
 	hiera.DoWithParent(context.Background(), provider.MuxLookupKey, configOptions, func(c px.Context) {
 		defer func() {
 			if r := recover(); r != nil {
@@ -169,38 +172,6 @@ func lookup(cmd *cobra.Command, args []string) {
 		if renderAs == `` {
 			renderAs = `yaml`
 		}
-
-		out := cmd.OutOrStdout()
-		switch renderAs {
-		case `yaml`, `json`:
-			var v interface{}
-			if !found.Equals(px.Undef, nil) {
-				rf := c.Reflector().Reflect(found)
-				if rf.IsValid() && rf.CanInterface() {
-					v = rf.Interface()
-				} else {
-					v = found.String()
-				}
-			}
-			var bs []byte
-			var err error
-			if renderAs == `yaml` {
-				bs, err = yl.Marshal(v)
-			} else {
-				bs, err = json.Marshal(v)
-			}
-			if err != nil {
-				panic(err)
-			}
-			utils.WriteString(out, string(bs))
-		case `binary`:
-			bi := types.CoerceTo(c, `lookup value`, types.DefaultBinaryType(), found).(*types.Binary)
-			_, err := out.Write(bi.Bytes())
-			if err != nil {
-				panic(err)
-			}
-		case `s`:
-			cmd.Println(found)
-		}
+		hiera.Render(c, renderAs, found, cmd.OutOrStdout())
 	})
 }
diff --git a/provider/scopelookupkey.go b/provider/scopelookupkey.go
new file mode 100644
index 0000000..9c56cf8
--- /dev/null
+++ b/provider/scopelookupkey.go
@@ -0,0 +1,15 @@
+package provider
+
+import (
+	"github.com/lyraproj/hiera/hieraapi"
+	"github.com/lyraproj/pcore/px"
+	"github.com/lyraproj/pcore/types"
+)
+
+// ScopeLookupKey is a function that performs a lookup in the current scope.
+func ScopeLookupKey(c hieraapi.ProviderContext, key string, _ map[string]px.Value) px.Value {
+	if v, ok := c.Invocation().Scope().Get(types.WrapString(key)); ok {
+		return v
+	}
+	return nil
+}


Test patch:
diff --git a/lookup/lookup_test.go b/lookup/lookup_test.go
index 53d2cc7..36a036e 100644
--- a/lookup/lookup_test.go
+++ b/lookup/lookup_test.go
@@ -66,6 +66,14 @@ func TestLookup_fact_interpolated_config(t *testing.T) {
 	})
 }
 
+func TestLookup_fact_directly(t *testing.T) {
+	inTestdata(func() {
+		result, err := executeLookup(`--facts`, `facts.yaml`, `--config`, `fact_directly.yaml`, `the_fact`)
+		require.NoError(t, err)
+		require.Equal(t, "value of the_fact\n", string(result))
+	})
+}
+
 func TestLookup_explain(t *testing.T) {
 	inTestdata(func() {
 		result, err := executeLookup(`--explain`, `--facts`, `facts.yaml`, `interpolate_ca`)
@@ -211,7 +219,7 @@ func inTestdata(f func()) {
 }
 
 func executeLookup(args ...string) (output []byte, err error) {
-	cmd := newCommnand()
+	cmd := newCommand()
 	buf := new(bytes.Buffer)
 	cmd.SetOutput(buf)
 	cmd.SetArgs(args)
diff --git a/lookup/testdata/fact_directly.yaml b/lookup/testdata/fact_directly.yaml
new file mode 100644
index 0000000..9e4022e
--- /dev/null
+++ b/lookup/testdata/fact_directly.yaml
@@ -0,0 +1,9 @@
+version: 5
+
+# Hierarchy that contains the "scope" lookup_key function. That function finds scope
+# variables directly
+hierarchy:
+  - name: Scope
+    lookup_key: scope
+  - name: Common
+    path: common.yaml
diff --git a/lookup/testdata/facts.yaml b/lookup/testdata/facts.yaml
index 0bdfea7..ec060ab 100644
--- a/lookup/testdata/facts.yaml
+++ b/lookup/testdata/facts.yaml
@@ -5,3 +5,5 @@ c:
   b: value of c.b
 
 data_file: by_fact
+
+the_fact: value of the_fact