Skip to content

Commit

Permalink
wip: stub structs and logic for a bundle workflow
Browse files Browse the repository at this point in the history
* resolve a dependency graph
* identify the order of execution
* still working on how to represent a workflow of bundles to execute in a way that we can abstract with a driver

Signed-off-by: Carolyn Van Slyck <me@carolynvanslyck.com>
  • Loading branch information
carolynvs committed Sep 1, 2022
1 parent dcbc21d commit 2bb25ad
Show file tree
Hide file tree
Showing 13 changed files with 944 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ require (
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.7.1
github.com/xeipuuv/gojsonschema v1.2.0
github.com/yourbasic/graph v0.0.0-20210606180040-8ecfec1c2869
go.mongodb.org/mongo-driver v1.7.1
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.29.0
go.opentelemetry.io/otel v1.7.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1578,6 +1578,8 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yourbasic/graph v0.0.0-20210606180040-8ecfec1c2869 h1:7v7L5lsfw4w8iqBBXETukHo4IPltmD+mWoLRYUmeGN8=
github.com/yourbasic/graph v0.0.0-20210606180040-8ecfec1c2869/go.mod h1:Rfzr+sqaDreiCaoQbFCu3sTXxeFq/9kXRuyOoSlGQHE=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down
234 changes: 234 additions & 0 deletions pkg/workflow/bundle_graph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package workflow

import (
"context"

"get.porter.sh/porter/pkg/cnab"
"get.porter.sh/porter/pkg/storage"
"get.porter.sh/porter/pkg/tracing"
"github.com/Masterminds/semver/v3"
"github.com/cnabio/cnab-go/bundle"
"github.com/yourbasic/graph"
"go.opentelemetry.io/otel/attribute"
)

type BundleGraph struct {
// map[node.key]nodeIndex
nodeKeys map[string]int
nodes []Node
// (DependencyV1 (unresolved), Bundle, Installation)
}

func NewBundleGraph() *BundleGraph {
return &BundleGraph{
nodeKeys: make(map[string]int),
}
}

// RegisterNode adds the specified node to the graph
// returning true if the node is already present.
func (g *BundleGraph) RegisterNode(node Node) bool {
_, exists := g.nodeKeys[node.GetKey()]
if !exists {
nodeIndex := len(g.nodes)
g.nodes = append(g.nodes, node)
g.nodeKeys[node.GetKey()] = nodeIndex
}
return exists
}

func (g *BundleGraph) Sort() ([]Node, bool) {
dag := graph.New(len(g.nodes))
for nodeIndex, node := range g.nodes {
for _, depKey := range node.GetRequires() {
depIndex, ok := g.nodeKeys[depKey]
if !ok {
panic("oops")
}
dag.Add(nodeIndex, depIndex)
}
}

indices, ok := graph.TopSort(dag)
if !ok {
return nil, false
}

// Reverse the sort so that items with no dependencies are listed first
count := len(indices)
results := make([]Node, count)
for i, nodeIndex := range indices {
results[count-i-1] = g.nodes[nodeIndex]
}
return results, true
}

func (g *BundleGraph) GetNode(key string) (Node, bool) {
if nodeIndex, ok := g.nodeKeys[key]; ok {
return g.nodes[nodeIndex], true
}
return nil, false
}

type Node interface {
GetRequires() []string
GetKey() string
}

var _ Node = BundleNode{}
var _ Node = InstallationNode{}

type BundleNode struct {
Key string
Reference cnab.BundleReference
Requires []string // TODO: we don't need to know this while resolving, find a less confusing way of storing this so it's clear who should set it
}

func (d BundleNode) GetKey() string {
return d.Key
}

func (d BundleNode) GetRequires() []string {
return d.Requires
}

type InstallationNode struct {
Key string
Namespace string
Name string
}

func (d InstallationNode) GetKey() string {
return d.Key
}

func (d InstallationNode) GetRequires() []string {
return nil
}

type Dependency struct {
Key string
DefaultBundle *BundleReferenceSelector
Interface *BundleInterfaceSelector
InstallationSelector *InstallationSelector
Requires []string
}

type BundleReferenceSelector struct {
Reference cnab.OCIReference
Version *semver.Constraints
}

func (s *BundleReferenceSelector) IsMatch(ctx context.Context, inst storage.Installation) bool {
log := tracing.LoggerFromContext(ctx)
log.Debug("Evaluating installation bundle definition")

if inst.Status.BundleReference == "" {
log.Debug("Installation does not match because it does not have an associated bundle")
return false
}

ref, err := cnab.ParseOCIReference(inst.Status.BundleReference)
if err != nil {
log.Warn("Could not evaluate installation because the BundleReference is invalid",
attribute.String("reference", inst.Status.BundleReference))
return false
}

// If no selector is defined, consider it a match
if s == nil {
return true
}

// If a version range is specified, ignore the version on the selector and apply the range
// otherwise match the tag or digest
if s.Version != nil {
if inst.Status.BundleVersion == "" {
log.Debug("Installation does not match because it does not have an associated bundle version")
return false
}

// First check that the repository is the same
gotRepo := ref.Repository()
wantRepo := s.Reference.Repository()
if gotRepo != wantRepo {
log.Warn("Installation does not match because the bundle repository is incorrect",
attribute.String("installation-bundle-repository", gotRepo),
attribute.String("dependency-bundle-repository", wantRepo),
)
return false
}

gotVersion, err := semver.NewVersion(inst.Status.BundleVersion)
if err != nil {
log.Warn("Installation does not match because the bundle version is invalid",
attribute.String("installation-bundle-version", inst.Status.BundleVersion),
)
return false
}

if s.Version.Check(gotVersion) {
log.Debug("Installation matches because the bundle version is in range",
attribute.String("installation-bundle-version", inst.Status.BundleVersion),
attribute.String("dependency-bundle-version", s.Version.String()),
)
return true
} else {
log.Debug("Installation does not match because the bundle version is incorrect",
attribute.String("installation-bundle-version", inst.Status.BundleVersion),
attribute.String("dependency-bundle-version", s.Version.String()),
)
return false
}
} else {
gotRef := ref.String()
wantRef := s.Reference.String()
if gotRef == wantRef {
log.Warn("Installation matches because the bundle reference is correct",
attribute.String("installation-bundle-reference", gotRef),
attribute.String("dependency-bundle-reference", wantRef),
)
return true
} else {
log.Warn("Installation does not match because the bundle reference is incorrect",
attribute.String("installation-bundle-reference", gotRef),
attribute.String("dependency-bundle-reference", wantRef),
)
return false
}
}
}

type InstallationSelector struct {
Bundle *BundleReferenceSelector
Interface *BundleInterfaceSelector
Labels map[string]string
Namespaces []string
}

func (s InstallationSelector) IsMatch(ctx context.Context, inst storage.Installation) bool {
// Skip checking labels and namespaces, those were used to query the set of
// installations that we are checking

bundleMatches := s.Bundle.IsMatch(ctx, inst)
if !bundleMatches {
return false
}

interfaceMatches := s.Interface.IsMatch(ctx, inst)
return interfaceMatches
}

// BundleInterfaceSelector defines how a bundle is going to be used.
// It is not the same as the bundle definition.
// It works like go interfaces where its defined by its consumer.
type BundleInterfaceSelector struct {
Parameters []bundle.Parameter
Credentials []bundle.Credential
Outputs []bundle.Output
}

func (s BundleInterfaceSelector) IsMatch(ctx context.Context, inst storage.Installation) bool {
// TODO: implement
return true
}
70 changes: 70 additions & 0 deletions pkg/workflow/bundle_graph_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package workflow

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/stretchr/testify/require"
)

func TestEngine_DependOnInstallation(t *testing.T) {
/*
A -> B (installation)
A -> C (bundle)
c.parameters.connstr <- B.outputs.connstr
*/

b := InstallationNode{Key: "b"}
c := BundleNode{
Key: "c",
Requires: []string{"b"},
}
a := BundleNode{
Key: "root",
Requires: []string{"b", "c"},
}

g := NewBundleGraph()
g.RegisterNode(a)
g.RegisterNode(b)
g.RegisterNode(c)
sortedNodes, ok := g.Sort()
require.True(t, ok, "graph should not be cyclic")

gotOrder := make([]string, len(sortedNodes))
for i, node := range sortedNodes {
gotOrder[i] = node.GetKey()
}
wantOrder := []string{
"b",
"c",
"root",
}
assert.Equal(t, wantOrder, gotOrder)
}

/*
✅ need to represent new dependency structure on an extended bundle wrapper
(put in cnab-go later)
need to read a bundle and make a BundleGraph
? how to handle a param that isn't a pure assignment, e.g. connstr: ${bundle.deps.VM.outputs.ip}:${bundle.deps.SVC.outputs.port}
? when are templates evaluated as the graph is executed (for simplicity, first draft no composition / templating)
need to resolve dependencies in the graph
* lookup against existing installations
* lookup against semver tags in registry
* lookup against bundle index? when would we look here? (i.e. preferred/registered implementations of interfaces)
need to turn the sorted nodes into an execution plan
execution plan needs:
* bundle to execute and the installation it will become
* parameters and credentials to pass
* sources:
root parameters/creds
installation outputs
need to write something that can run an execution plan
* knows how to grab sources and pass them into the bundle
*/
38 changes: 38 additions & 0 deletions pkg/workflow/default_bundle_resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package workflow

import (
"context"

"get.porter.sh/porter/pkg/porter"
)

var _ DependencyResolver = DefaultBundleResolver{}

// DefaultBundleResolver resolves the default bundle defined on the dependency.
type DefaultBundleResolver struct {
puller porter.BundleResolver
}

func (d DefaultBundleResolver) Resolve(ctx context.Context, dep Dependency) (Node, bool, error) {
if dep.DefaultBundle == nil {
return nil, false, nil
}

pullOpts := porter.BundlePullOptions{
Reference: dep.DefaultBundle.Reference.String(),
// todo: respect force pull and insecure registry
}
if err := pullOpts.Validate(); err != nil {
return nil, false, err
}
cb, err := d.puller.Resolve(ctx, pullOpts)
if err != nil {
// wrap not found error and indicate that we could resolve anything
return nil, false, err
}

return BundleNode{
Key: dep.Key,
Reference: cb.BundleReference,
}, true, nil
}
Loading

0 comments on commit 2bb25ad

Please sign in to comment.