mpdev/internal/resources/registry.go (166 lines of code) (raw):
// Copyright 2020 Google 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 resources
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"gonum.org/v1/gonum/graph/simple"
"gonum.org/v1/gonum/graph/topo"
"gopkg.in/yaml.v3"
"k8s.io/utils/exec"
)
// Registry stores references to all resources and can apply
// all resources in the registry
type Registry interface {
RegisterResource(resource Resource, workingDirectory string)
GetExecutor() exec.Interface
GetResource(reference Reference) Resource
ResolveFilePath(rs Resource, path string) (string, error)
Apply(dryRun bool) error
Test(dryRun bool) error
}
type registry struct {
refMap map[Reference]Resource
dirMap map[Reference]string
executor exec.Interface
}
// NewRegistry creates a registry that stores references to all resources
func NewRegistry(executor exec.Interface) Registry {
return ®istry{
refMap: map[Reference]Resource{},
dirMap: map[Reference]string{},
executor: executor,
}
}
func (r *registry) GetResource(reference Reference) Resource {
return r.refMap[reference]
}
func (r *registry) GetExecutor() exec.Interface {
return r.executor
}
// RegisterResource adds a resource to the registry
func (r *registry) RegisterResource(rs Resource, workingDirectory string) {
ref := rs.GetReference()
r.refMap[ref] = rs
r.dirMap[ref] = workingDirectory
}
// Apply invokes `Apply` on all resources in the registry.
func (r *registry) Apply(dryRun bool) error {
resources, err := r.topologicalSort()
if err != nil {
return err
}
for _, resource := range resources {
fmt.Printf("Starting to validate/create resource %+v\n", resource.GetReference())
applyErr := resource.Apply(r, dryRun)
if applyErr != nil {
applyErr := errors.Wrapf(applyErr, "Error in resource %+v\n", resource.GetReference())
// Accumulate errors if dryRun
if dryRun {
err = multierror.Append(applyErr, err)
} else {
return applyErr
}
}
}
fmt.Printf("all resources have been validated/created\n")
return err
}
// Apply invokes `Test` on all resources in the registry.
func (r *registry) Test(dryRun bool) error {
resources, err := r.topologicalSort()
if err != nil {
return err
}
for _, resource := range resources {
fmt.Printf("Starting to test resource %+v\n", resource.GetReference())
testErr := resource.Test(r, dryRun)
if testErr != nil {
testErr := errors.Wrapf(testErr, "Error in resource %+v\n", resource.GetReference())
// Accumulate errors if dryRun
if dryRun {
err = multierror.Append(testErr, err)
} else {
return testErr
}
}
}
fmt.Printf("all resources have been tested\n")
return err
}
func (r *registry) ResolveFilePath(rs Resource, path string) (string, error) {
if filepath.IsAbs(path) {
return path, nil
}
return filepath.Abs(filepath.Join(r.dirMap[rs.GetReference()], path))
}
// topologicalSort returns a list of resources such that each
// resource is after its dependencies in the list.
func (r *registry) topologicalSort() ([]Resource, error) {
dag := simple.NewDirectedGraph()
// Add resource references as nodes to graph
var refToID = map[Reference]int64{}
var idToRef = map[int64]Reference{}
for ref := range r.refMap {
n := dag.NewNode()
refToID[ref] = n.ID()
idToRef[n.ID()] = ref
dag.AddNode(n)
}
// Add dependencies as edges to graph
for ref, resource := range r.refMap {
to := dag.Node(refToID[ref])
for _, depRef := range resource.GetDependencies() {
depRes := r.GetResource(depRef)
if depRes == nil {
return []Resource{}, fmt.Errorf("resource not found with reference %+v", depRef)
}
from := dag.Node(refToID[depRef])
e := dag.NewEdge(from, to)
dag.SetEdge(e)
}
}
// Execute topological sort
nodes, err := topo.Sort(dag)
if err != nil {
return nil, err
}
// Convert node id to resource
resources := make([]Resource, 0, len(nodes))
for _, node := range nodes {
ref := idToRef[node.ID()]
resources = append(resources, r.refMap[ref])
}
return resources, err
}
// PopulateRegistryFromFiles registers resources declared in list of files
func PopulateRegistryFromFiles(r Registry, files []string) error {
for _, file := range files {
objs, err := decodeFile(file)
if err != nil {
return err
}
dir := filepath.Dir(file)
for _, obj := range objs {
resource, err := UnstructuredToResource(obj)
if err != nil {
return err
}
r.RegisterResource(resource, dir)
}
}
return nil
}
func decodeFile(file string) ([]Unstructured, error) {
var objs []Unstructured
var f *os.File
var err error
if file == "-" {
f = os.Stdin
} else {
f, err = os.Open(file)
if err != nil {
return objs, err
}
defer f.Close()
}
dec := yaml.NewDecoder(f)
for err == nil {
var m Unstructured
err = dec.Decode(&m)
if err == nil {
objs = append(objs, m)
}
}
if err != io.EOF {
return objs, errors.Wrap(err, "failed to parse yaml")
}
return objs, nil
}