cli/launchpad/runtime.go (144 lines of code) (raw):
// Package launchpad file runtime.go contains runtime support for reference
// tracking and output assembled object to represent parsed view of the org
// for later processing.
package launchpad
import (
"errors"
"fmt"
"io"
"log"
"sort"
"strings"
)
var (
errUndefinedReference = errors.New("undefined reference")
errConflictDefinition = errors.New("definition conflict")
errUnexpectedType = errors.New("unexpected type")
)
type resource struct {
yaml resourceHandler
inRefs []resourceHandler
}
type resourceMap map[string]*resource
func (rm resourceMap) getInit(rId string, yaml resourceHandler) (*resource, error) {
res, found := rm[rId]
if !found { // initialize resource was not encountered before
res = &resource{yaml: yaml} // yaml is possible be nil
rm[rId] = res
}
if yaml == nil {
return res, nil
}
if res.yaml.kind() == Organization {
// newer organization definition, pull sub-resources into current
o, ok := res.yaml.(*orgYAML)
if !ok {
return nil, errUnexpectedType
}
oNew, ok := yaml.(*orgYAML)
if !ok {
return nil, errUnexpectedType
}
return res, o.mergeFields(oNew)
}
if yaml != res.yaml {
log.Println("conflicting YAML definition detected on", yaml.resId())
return nil, errConflictDefinition
}
res.yaml = yaml
return res, nil
}
func (rm resourceMap) sortedResId() []string {
keys := make([]string, len(rm))
i := 0
for k := range rm {
keys[i] = k
i++
}
sort.Strings(keys)
return keys
}
// assembledOrg supports reference tracking and allows resources to be organized into an organization tree.
type assembledOrg struct {
resourceMap resourceMap // tracks seen resources and references.
org orgYAML // finalized view of all validated resources.
}
// newAssembledOrg creates and initializes an assembledOrg.
func newAssembledOrg() *assembledOrg {
ao := assembledOrg{}
ao.resourceMap = make(resourceMap)
ao.org.headerYAML = headerYAML{APIVersion: apiCFTv1alpha1, KindStr: Organization.String()}
return &ao
}
// String implements Stringer and generates a string representation.
func (ao *assembledOrg) String() string {
sb := strings.Builder{}
err := ao.dump(0, &sb)
if err != nil {
panic(err.Error())
}
return sb.String()
}
// assembleResourcesToOrg takes in resources and assembles into an organization.
func assembleResourcesToOrg(rs []resourceHandler) *assembledOrg {
ao := newAssembledOrg()
// discover resources in a DFS style
// initialize resource into resourceMap or update references if already exist.
for _, r := range rs {
if err := r.addToOrg(ao); err != nil {
log.Println("error validating YAML", err.Error())
panic(err.Error())
}
}
// assemble each discovered resource onto a finalized org
if err := ao.resolveReferences(); err != nil {
log.Println("unable to resolve referenceTracker between YAML resources:", err.Error())
panic(err.Error())
}
return ao
}
// registerResource registers a resource into resourceMap for later resolution.
//
// registerResource will init/update entry resourceMap with for resId as key. If
// the resource being registered (src) has a reference (dst) to another resource does
// not yet exist, it will also initialize the resourceMap for dst resource.
//
// If there are conflicting resources ID, silently pick the latest.
func (ao *assembledOrg) registerResource(src resourceHandler, dst *referenceYAML) error {
if _, err := ao.resourceMap.getInit(src.resId(), src); err != nil {
return err
}
if dst == nil { // no outgoing reference from src
return nil
}
// initialize a resource on the resource map, pending future definition on YAML
var dstYAML resourceHandler
if dst.TargetType() == Organization {
if ao.org.Spec.Id == "" {
ao.org.Spec.Id = dst.TargetId
} else if ao.org.Spec.Id != dst.TargetId {
log.Printf("fatal: org is identified as %s, cannot remap to %s\n", ao.org.Spec.Id, dst.TargetId)
return errConflictDefinition
}
dstYAML = &ao.org // allow future resolution to pick up finalized org directly
}
dstRes, err := ao.resourceMap.getInit(dst.resId(), dstYAML)
if err != nil {
return err
}
// update referenceTracker for references from src
dstRes.inRefs = append(dstRes.inRefs, src)
return nil
}
// resolveReferences loops through resourceMap to link up resource to sub resources.
func (ao *assembledOrg) resolveReferences() error {
for resId, res := range ao.resourceMap {
if res.yaml == nil {
// an item is initialized but the resourceHandler never provided
// only happen when this item is initialized via inbound reference(s)
log.Printf("fatal: reference to %s was not found\n", resId)
return errUndefinedReference
}
// each resource holds its own resolving logic
if err := res.yaml.resolveReferences(res.inRefs); err != nil {
return err
}
}
return nil
}
// dump writes resource's string representation into provided buffer.
func (ao *assembledOrg) dump(ind int, buff io.Writer) error {
indent := strings.Repeat(" ", ind)
_, err := fmt.Fprintf(buff, "%sResource Map [%d]:\n", indent, len(ao.resourceMap))
if err != nil {
return err
}
for _, resId := range ao.resourceMap.sortedResId() {
res := ao.resourceMap[resId]
var refs []string
for _, refRes := range res.inRefs {
refs = append(refs, refRes.resId())
}
sort.Strings(refs)
_, err = fmt.Fprintf(buff, "%s * %s <- [%s]\n", indent, resId, strings.Join(refs, ", "))
if err != nil {
return err
}
}
return ao.org.dump(ind, buff)
}