-
Notifications
You must be signed in to change notification settings - Fork 212
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
wip: stub structs and logic for a bundle workflow
* 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 <[email protected]>
- Loading branch information
Showing
13 changed files
with
944 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.