parser

package
v0.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 20, 2020 License: Apache-2.0 Imports: 18 Imported by: 6

README

TOSCA Parser

Optimized for speed via caching and concurrency. Parsing even very big and complex service templates is practically instantaneous, delayed at worst only by network and filesystem transfer.

Attempts to be as strictly compliant as possible, which is often challenging due to contradictions and unclarity in the TOSCA specification. Where ambiguous we adhere to the spirit of the spec, especially in regards to object-oriented polymorphism. Our prime directive is to ensure that an inherited node type does not break the contract of the base node type.

Coding Principles

There are over 60 different entity types in TOSCA 1.2. Writing custom code for each, even with reusable utility functions, would quickly become a maintenance nightmare, not only for fixing bugs but also for supporting future versions of TOSCA.

We have opted to combine utility functions with plenty of reflection. Reflection is used to solve generic, repeatable actions, such as reading data of various types, looking up names in the namespace, and inheriting fields from parent types. Generic code of this sort is tricky to get right, but once you do the entire domain is managed via simple annotations (field tags).

There is a cost to using such annotations, and indeed even the simple field tags are controversial within the Go community. The problem is that hidden, magical things happen that are not visible in the immediate vicinity of the code in front of you. You have to look elsewhere for the systems that read these tags and do things with them. The only way to reduce this cost is good documentation: make coders aware of these systems and what they do. We will consider this documentation to be a crucial component of the codebase.

Phase 1: Read

This first phase validates syntax. Higher level grammar is validated in subsequent phases.

  1. Read textual data from files and URLs
    • Files/URLs not found
    • I/O errors
    • Textual decoding errors
  2. Parse YAML to ARD
    • YAML parsing errors
  3. Parse ARD to TOSCA data structures, normalizing all short notations to their full form
    • Required fields not set
    • Fields set to wrong YAML type
    • Unsupported fields used
  4. Handle TOSCA imports recursively and concurrently
    • Import causes an endless loop

Phase 2: Namespaces

The goal of this phase is to validate names (ensure that they are not ambiguous), and to provide a mechanism for looking up names. We take into account the import namespace_prefix and support multiple names per entity, as is necessary for the normative types.

Recursively, starting at tips of the import hierarchy:

  1. Gather all names in the unit
  2. Apply the import's namespace_prefix if defined
  3. Set names in unit's namespace (every entity type has its own section)
  4. Merge namespace into parent unit's
    • Ambiguous names (per entity type section)

And then:

  1. Lookup fields from namespace
    • Name not found

Phase 3: Hierarchies

The entire goal of this phase is validation. The entities already have hierarchical information (their Parent field). But here we provide a hierarchy that is guaranteed to not have loops.

Recursively, starting at tips of the import hierarchy:

  1. Gather all TOSCA types in the unit
  2. Place types in hierarchy (every type has its own hierarchy)
    • Type's parent causes an endless loop
    • Type's parent is incomplete
  3. Merge hierarchy into parent unit's

Phase 4: Inheritance

From this phase we onward only deal with the root unit, not the imported units, because it already has all names and types merged in.

This phase is complex because the order of inheritance cannot be determined generally. Not only do types inherit from each other, but also definitions within the types are themselves typed, and those types have their own hierarchy. Thus, a type cannot be resolved before its definitions are, and they cannot be resolved before their types are, and their types' parents, etc.

This includes validating TOSCA's complex inheritance contract, which extends to embedded definitions beyond simple type inheritance. For example, a capability definition within a node type must have a capability type that is compatible with that defined at the parent node type.

Our solution to this complexity is to create a task graph. We will keep resolving independent tasks (those that do not have dependencies), which should in turn make more tasks independent, continuing until all tasks are resolved.

Note that in this phase we handle not only recursive type inheritance, but non-recursive definition inheritance. For example, an interface definition inherits data from the interface type.

  1. Copy over inherited fields from parent or definition type
    • Type's parent is incomplete (due to other problems reported in this phase)
  2. If we are overriding, make sure that we are not breaking the contract
    • Between value of field in parent and child

Incredibly, the TOSCA spec does not describe inheritance. It is non-trivial and not obvious. In Puccini we had to make our own implementation decisions. Unfortunately, other TOSCA parsers may very well handle inheritance differently, leading to incompatibilities. The fault lies entirely with the TOSCA spec.

For example, consider a capability definition within a node type. We inherit first from our parent node type, and only then our capability definition will inherit from its capability type (which may be a subtype of what our parent node type has). The first entity we come across in our traversal is considered to be the final value (for subequently found entities we will treat the field as "already assigned"). The bottom line is that the parent node type takes precedence over the capability type of the capability definition.

At the end of this phase the types are considered "complete" in that we should not need to access their parents for any data. All fields have been inherited.

Phase 5: Rendering

We call the act of applying a type to a template "rendering". ("Instantiation" is what happens next, when we turn a template into an instance, which is out of the scope of Puccini and indeed out of the scope of TOSCA.)

For entities that are "assignments" ("property assignments", "capability assignments", etc.) we do more than just validate: we also change the value, for example by applying defaults.

There are furthermore various custom validations per entity. One worth mentioning here is requirement validation. Specifically, we take into account the occurrences field, which limits the number of times a requirement may be assigned. The TOSCA spec mentions that the implied default occurrences is [1,1] (the TOSCA spec oddly says that in a range the upper bound must be greater than the lower bound, so that [1,1] is impossible: one of many contradictions we must resolve). However, we further assume that if occurrences is not specified then it is also intended for the requirement to be automatically assigned if not explicitly specified.

At the end of this phase the templates are considered "complete" in that we should not have to access their types for any data. All fields have been rendered.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func LookupFields

func LookupFields(entityPtr interface{}) bool

From "lookup" tags

func Parse

func Parse(url string, quirks []string, inputs map[string]interface{}) (*normal.ServiceTemplate, *problems.Problems, error)

func Render

func Render(entityPtr interface{}) tosca.EntityPtrs

From Renderable interface

func SetInputs

func SetInputs(entityPtr interface{}, inputs map[string]interface{})

From HasInputs interface

Types

type Context

type Context struct {
	Root            *Unit
	Quirks          []string
	Units           Units
	Parsing         sync.Map
	WaitGroup       sync.WaitGroup
	Locker          sync.Mutex
	NamespacesWork  *ContextualWork
	HierarchiesWork *ContextualWork
}

func NewContext

func NewContext(quirks []string) Context

func (*Context) AddHierarchies

func (self *Context) AddHierarchies()

func (*Context) AddNamespaces

func (self *Context) AddNamespaces()

func (*Context) AddUnit

func (self *Context) AddUnit(entityPtr interface{}, container *Unit, nameTransformer tosca.NameTransformer) *Unit

func (*Context) Gather

func (self *Context) Gather(pattern string) tosca.EntityPtrs

func (*Context) GetInheritTasks

func (self *Context) GetInheritTasks() Tasks

func (*Context) GetProblems

func (self *Context) GetProblems() *problems.Problems

func (*Context) LookupNames

func (self *Context) LookupNames()

func (*Context) PrintHierarchies

func (self *Context) PrintHierarchies(indent int)

func (*Context) PrintImports

func (self *Context) PrintImports(indent int)

func (*Context) PrintNamespaces

func (self *Context) PrintNamespaces(indent int)

func (*Context) ReadRoot

func (self *Context) ReadRoot(url urlpkg.URL) bool

func (*Context) Render

func (self *Context) Render() tosca.EntityPtrs

func (*Context) Traverse

func (self *Context) Traverse(phase string, traverse reflection.Traverser)

type ContextualWork

type ContextualWork struct {
	sync.Map
	Phase string
}

func NewContextualWork

func NewContextualWork(phase string) *ContextualWork

func (*ContextualWork) Start

func (self *ContextualWork) Start(context *tosca.Context) (Promise, bool)

type EntityWork

type EntityWork map[interface{}]bool

func (EntityWork) Start

func (self EntityWork) Start(phase string, entityPtr interface{}) bool

type Executor

type Executor func(task *Task)

type HasInputs

type HasInputs interface {
	SetInputs(map[string]interface{})
}

type InheritContext

type InheritContext struct {
	Tasks            Tasks
	TasksForEntities TasksForEntities
	InheritFields    InheritFields
}

func NewInheritContext

func NewInheritContext() *InheritContext

func (*InheritContext) GetDependencies

func (self *InheritContext) GetDependencies(entityPtr interface{}) map[interface{}]bool

func (*InheritContext) GetInheritTask

func (self *InheritContext) GetInheritTask(entityPtr interface{}) *Task

func (*InheritContext) NewExecutor

func (self *InheritContext) NewExecutor(entityPtr interface{}) Executor

type InheritField

type InheritField struct {
	Entity        reflect.Value
	FromEntityPtr interface{}
	Key           string
	Field         reflect.Value
	FromField     reflect.Value
}

func NewInheritFields

func NewInheritFields(entityPtr interface{}) []*InheritField

From "inherit" tags

func (*InheritField) Inherit

func (self *InheritField) Inherit()

func (*InheritField) InheritEntity

func (self *InheritField) InheritEntity()

Field is compatible with *interface{}

func (*InheritField) InheritStringsFromMap

func (self *InheritField) InheritStringsFromMap()

Field is *map[string]string

func (*InheritField) InheritStringsFromSlice

func (self *InheritField) InheritStringsFromSlice()

Field is *[]string

func (*InheritField) InheritStructsFromMap

func (self *InheritField) InheritStructsFromMap()

Field is compatible with map[string]*interface{}

func (*InheritField) InheritStructsFromSlice

func (self *InheritField) InheritStructsFromSlice()

Field is compatible with []*interface{}

type InheritFields

type InheritFields map[interface{}][]*InheritField

func (InheritFields) Get

func (self InheritFields) Get(entityPtr interface{}) []*InheritField

Cache these, because we call twice for each entity

type LookupField

type LookupField struct {
	Types []reflect.Type
	Names []LookupName
}

type LookupName

type LookupName struct {
	Index int
	Name  string
	Found bool
}

type LookupProblems

type LookupProblems map[string]*LookupField

func (LookupProblems) AddType

func (self LookupProblems) AddType(key string, type_ reflect.Type)

func (LookupProblems) Field

func (self LookupProblems) Field(key string) *LookupField

func (LookupProblems) Report

func (self LookupProblems) Report(context *tosca.Context)

func (LookupProblems) SetFound

func (self LookupProblems) SetFound(key string, index int, name string, found bool)

type NoEntity

type NoEntity struct {
	Context *tosca.Context
}

func NewNoEntity

func NewNoEntity(toscaContext *tosca.Context) *NoEntity

func (*NoEntity) GetContext

func (self *NoEntity) GetContext() *tosca.Context

tosca.Contextual interface

type Promise

type Promise chan bool

func NewPromise

func NewPromise() Promise

func (Promise) Release

func (self Promise) Release()

func (Promise) Wait

func (self Promise) Wait()

type Renderable

type Renderable interface {
	Render()
}

type Task

type Task struct {
	Name         string
	Executor     Executor
	Parents      Tasks
	Dependencies Tasks
}

func NewTask

func NewTask(name string) *Task

func (*Task) AddDependency

func (self *Task) AddDependency(task *Task)

func (*Task) Done

func (self *Task) Done()

func (*Task) Execute

func (self *Task) Execute()

func (*Task) IsIndependent

func (self *Task) IsIndependent() bool

func (*Task) Print

func (self *Task) Print(indent int)

func (*Task) PrintDependencies

func (self *Task) PrintDependencies(indent int, treePrefix terminal.TreePrefix)

func (*Task) PrintDependency

func (self *Task) PrintDependency(indent int, treePrefix terminal.TreePrefix, last bool)

type TaskList

type TaskList []*Task

func (TaskList) Len

func (self TaskList) Len() int

func (TaskList) Less

func (self TaskList) Less(i, j int) bool

func (TaskList) Swap

func (self TaskList) Swap(i, j int)

type Tasks

type Tasks map[*Task]bool

func (Tasks) Add

func (self Tasks) Add(task *Task)

func (Tasks) Drain

func (self Tasks) Drain()

func (Tasks) FindIndependent

func (self Tasks) FindIndependent() (*Task, bool)

func (Tasks) Print

func (self Tasks) Print(indent int)

func (Tasks) Remove

func (self Tasks) Remove(task *Task)

func (Tasks) Validate

func (self Tasks) Validate() bool

type TasksForEntities

type TasksForEntities map[interface{}]*Task

type Unit

type Unit struct {
	EntityPtr       interface{}
	Container       *Unit
	Imports         Units
	NameTransformer tosca.NameTransformer
	Locker          sync.Mutex
}

func NewUnit

func NewUnit(entityPtr interface{}, container *Unit, nameTransformer tosca.NameTransformer) *Unit

func NewUnitNoEntity

func NewUnitNoEntity(toscaContext *tosca.Context, container *Unit, nameTransformer tosca.NameTransformer) *Unit

func (*Unit) AddImport

func (self *Unit) AddImport(import_ *Unit)

func (*Unit) GetContext

func (self *Unit) GetContext() *tosca.Context

func (*Unit) MergeHierarchies

func (self *Unit) MergeHierarchies(hierarchyContext tosca.HierarchyContext, work *ContextualWork)

func (*Unit) MergeNamespaces

func (self *Unit) MergeNamespaces(work *ContextualWork)

func (*Unit) PrintImports

func (self *Unit) PrintImports(indent int, treePrefix terminal.TreePrefix)

func (*Unit) PrintNode

func (self *Unit) PrintNode(indent int, treePrefix terminal.TreePrefix, last bool)

type Units

type Units []*Unit

func (Units) Len

func (self Units) Len() int

func (Units) Less

func (self Units) Less(i, j int) bool

func (Units) Swap

func (self Units) Swap(i, j int)

type YAMLError

type YAMLError struct {
	// contains filtered or unexported fields
}

func NewYAMLError

func NewYAMLError(err error) *YAMLError

func (*YAMLError) Error

func (self *YAMLError) Error() string

error interface

func (*YAMLError) Problem

func (self *YAMLError) Problem() (string, string, int, int)

tosca.problems.Problematic interface

func (*YAMLError) String

func (self *YAMLError) String() string

fmt.Stringer interface

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
JackTT - Gopher 🇻🇳