asteris-llc/converge

View on GitHub
render/renderer.go

Summary

Maintainability
A
2 hrs
Test Coverage
// 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
}