render/renderer.go
// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package render
import (
"fmt"
"reflect"
log "github.com/Sirupsen/logrus"
"github.com/asteris-llc/converge/graph"
"github.com/asteris-llc/converge/parse"
"github.com/asteris-llc/converge/render/extensions"
"github.com/asteris-llc/converge/render/preprocessor"
"github.com/asteris-llc/converge/resource"
"github.com/asteris-llc/converge/resource/param"
"github.com/pkg/errors"
)
// ErrUnresolvable is returned by Render if the template string tries to resolve
// unaccesible node properties.
type ErrUnresolvable struct{}
func (ErrUnresolvable) Error() string { return "node is unresolvable" }
// ErrBadTemplate is returned by Render if the template string causes an error.
// It is likely to be returned in cases where a predicate function is rendered
// and does not result in a boolean value.
type ErrBadTemplate struct{ Err error }
func (e ErrBadTemplate) Error() string {
return fmt.Sprintf("%s: cannot execute template", e.Err)
}
// Renderer to be passed to preparers, which will render strings
type Renderer struct {
Graph func() *graph.Graph
ID string
DotValue resource.Value
DotValuePresent bool
resolverErr bool
Language *extensions.LanguageExtension
}
// GetID returns the ID of this renderer
func (r *Renderer) GetID() string {
return r.ID
}
// Value of this renderer
func (r *Renderer) Value() (value resource.Value, present bool) {
return r.DotValue, r.DotValuePresent
}
// Render a string with text/template
func (r *Renderer) Render(name, src string) (string, error) {
r.resolverErr = false
r.Language = r.Language.On("param", r.param)
r.Language = r.Language.On("paramList", r.paramList)
r.Language = r.Language.On("paramMap", r.paramMap)
r.Language = r.Language.On(extensions.RefFuncName, r.lookup)
out, err := r.Language.Render(r.DotValue, name, src)
if err != nil {
if r.resolverErr {
return "", ErrUnresolvable{}
}
return "", ErrBadTemplate{Err: err}
}
return out.String(), err
}
func getNearestAncestor(g *graph.Graph, id, node string) (string, bool) {
if graph.IsRoot(node) || node == "" {
return "", false
}
siblingID := graph.SiblingID(id, node)
val, ok := g.Get(siblingID)
if !ok {
return getNearestAncestor(g, graph.ParentID(id), node)
}
if elem, ok := val.Value().(*parse.Node); ok {
if elem.Kind() == "module" {
return "", false
}
}
return siblingID, true
}
func (r *Renderer) param(name string) (string, error) {
raw, err := r.paramRawValue(name)
if err != nil {
return "", err
}
return fmt.Sprintf("%v", raw), nil
}
func (r *Renderer) paramList(name string) ([]string, error) {
raw, err := r.paramRawValue(name)
if err != nil {
return nil, err
}
vals := reflect.ValueOf(raw)
if vals.Kind() != reflect.Slice {
return nil, fmt.Errorf("param is not a list, it is a %s (%s)", vals.Kind(), vals)
}
var out []string
for i := 0; i < vals.Len(); i++ {
val := vals.Index(i)
out = append(out, fmt.Sprintf("%v", val.Interface()))
}
return out, nil
}
func (r *Renderer) paramMap(name string) (map[string]interface{}, error) {
raw, err := r.paramRawValue(name)
if err != nil {
return nil, err
}
vals := reflect.ValueOf(raw)
if vals.Kind() != reflect.Map {
return nil, fmt.Errorf("param is not a map, it is a %s (%s)", vals.Kind(), vals)
}
out := map[string]interface{}{}
for _, key := range vals.MapKeys() {
k := fmt.Sprintf("%v", key)
out[k] = vals.MapIndex(key).Interface()
}
return out, nil
}
func (r *Renderer) paramRawValue(name string) (interface{}, error) {
ancestor, found := getNearestAncestor(r.Graph(), r.ID, "param."+name)
if !found {
return "", errors.New("param not found (no such ancestor)")
}
ancestorMeta, _ := r.Graph().Get(ancestor)
task, ok := resource.ResolveTask(ancestorMeta.Value())
if task == nil || !ok {
return "", errors.New("param not found")
}
// grab the value
param, ok := task.(*param.Param)
if !ok {
return nil, fmt.Errorf("task it not a param, but a %T", task)
}
return param.Val, nil
}
func (r *Renderer) lookup(name string) (string, error) {
g := r.Graph()
// fully-qualified graph name
fqgn := graph.SiblingID(r.ID, name)
vertexName, terms, found := preprocessor.VertexSplitTraverse(g,
name,
r.ID,
preprocessor.TraverseUntilModule,
make(map[string]struct{}),
)
if !validateLookup(g, r.ID, vertexName) {
return "", fmt.Errorf("%s cannot resolve inner-branch node at %s", r.ID, vertexName)
}
if !found {
return "", fmt.Errorf("%s does not resolve to a valid node", fqgn)
}
meta, ok := g.Get(vertexName)
if !ok {
return "", fmt.Errorf("%s is empty", vertexName)
}
if _, isThunk := meta.Value().(*PrepareThunk); isThunk {
log.WithField(
"proxy-reference",
vertexName,
).Warn(
fmt.Sprintf(
"%s: cannot resolve %s in node %s in prepare thunk",
r.ID,
vertexName, terms,
),
)
r.resolverErr = true
return "", ErrUnresolvable{}
}
if _, isPreparer := meta.Value().(*resource.Preparer); isPreparer {
log.WithField("proxy-reference", vertexName).Warn(fmt.Sprintf("%s: cannot resolve %s in node %s from preparer", r.ID, vertexName, terms))
r.resolverErr = true
return "", ErrUnresolvable{}
}
asTasker, ok := meta.Value().(resource.Tasker)
if !ok {
log.WithField("get-value", vertexName).Error(fmt.Sprintf("%s: lookup would address unevaluated field %s", r.ID, vertexName))
return "", errors.New("cannot lookup unevaluated field")
}
status := asTasker.GetStatus()
if status == nil {
log.WithField("status-reference", vertexName).Warn(r.ID + " no status for node " + vertexName)
r.resolverErr = true
return "", ErrUnresolvable{}
}
result, ok := status.ExportedFields()[terms]
if !ok {
var keys []string
for key := range status.ExportedFields() {
keys = append(keys, key)
}
innerTask, _ := asTasker.GetTask()
innerTask, _ = resource.ResolveTask(innerTask)
log.WithField("current-node", r.ID).Warn(fmt.Sprintf("%s is not one of the exported fields for type %T: %v at %s", terms, innerTask, keys, vertexName))
return "", ErrUnresolvable{}
}
return fmt.Sprintf("%v", result), nil
}
// validateLookup ensures that the lookup is valid and resolvable over cases of
// nesting and conditional evaluation. It restricts lookups such that a nested
// value may depend on an outer value, but an outer value may not depend on a
// nested value.
func validateLookup(g *graph.Graph, src, dst string) bool {
if g.AreSiblings(src, dst) {
return true
}
if g.IsNibling(src, dst) {
return false
}
return true
}