Skip to content

Commit 2bb25ad

Browse files
committed
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]>
1 parent dcbc21d commit 2bb25ad

13 files changed

+944
-0
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ require (
6060
github.com/spf13/viper v1.8.1
6161
github.com/stretchr/testify v1.7.1
6262
github.com/xeipuuv/gojsonschema v1.2.0
63+
github.com/yourbasic/graph v0.0.0-20210606180040-8ecfec1c2869
6364
go.mongodb.org/mongo-driver v1.7.1
6465
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.29.0
6566
go.opentelemetry.io/otel v1.7.0

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,8 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx
15781578
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
15791579
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
15801580
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
1581+
github.com/yourbasic/graph v0.0.0-20210606180040-8ecfec1c2869 h1:7v7L5lsfw4w8iqBBXETukHo4IPltmD+mWoLRYUmeGN8=
1582+
github.com/yourbasic/graph v0.0.0-20210606180040-8ecfec1c2869/go.mod h1:Rfzr+sqaDreiCaoQbFCu3sTXxeFq/9kXRuyOoSlGQHE=
15811583
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
15821584
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
15831585
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

pkg/workflow/bundle_graph.go

+234
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package workflow
2+
3+
import (
4+
"context"
5+
6+
"get.porter.sh/porter/pkg/cnab"
7+
"get.porter.sh/porter/pkg/storage"
8+
"get.porter.sh/porter/pkg/tracing"
9+
"github.com/Masterminds/semver/v3"
10+
"github.com/cnabio/cnab-go/bundle"
11+
"github.com/yourbasic/graph"
12+
"go.opentelemetry.io/otel/attribute"
13+
)
14+
15+
type BundleGraph struct {
16+
// map[node.key]nodeIndex
17+
nodeKeys map[string]int
18+
nodes []Node
19+
// (DependencyV1 (unresolved), Bundle, Installation)
20+
}
21+
22+
func NewBundleGraph() *BundleGraph {
23+
return &BundleGraph{
24+
nodeKeys: make(map[string]int),
25+
}
26+
}
27+
28+
// RegisterNode adds the specified node to the graph
29+
// returning true if the node is already present.
30+
func (g *BundleGraph) RegisterNode(node Node) bool {
31+
_, exists := g.nodeKeys[node.GetKey()]
32+
if !exists {
33+
nodeIndex := len(g.nodes)
34+
g.nodes = append(g.nodes, node)
35+
g.nodeKeys[node.GetKey()] = nodeIndex
36+
}
37+
return exists
38+
}
39+
40+
func (g *BundleGraph) Sort() ([]Node, bool) {
41+
dag := graph.New(len(g.nodes))
42+
for nodeIndex, node := range g.nodes {
43+
for _, depKey := range node.GetRequires() {
44+
depIndex, ok := g.nodeKeys[depKey]
45+
if !ok {
46+
panic("oops")
47+
}
48+
dag.Add(nodeIndex, depIndex)
49+
}
50+
}
51+
52+
indices, ok := graph.TopSort(dag)
53+
if !ok {
54+
return nil, false
55+
}
56+
57+
// Reverse the sort so that items with no dependencies are listed first
58+
count := len(indices)
59+
results := make([]Node, count)
60+
for i, nodeIndex := range indices {
61+
results[count-i-1] = g.nodes[nodeIndex]
62+
}
63+
return results, true
64+
}
65+
66+
func (g *BundleGraph) GetNode(key string) (Node, bool) {
67+
if nodeIndex, ok := g.nodeKeys[key]; ok {
68+
return g.nodes[nodeIndex], true
69+
}
70+
return nil, false
71+
}
72+
73+
type Node interface {
74+
GetRequires() []string
75+
GetKey() string
76+
}
77+
78+
var _ Node = BundleNode{}
79+
var _ Node = InstallationNode{}
80+
81+
type BundleNode struct {
82+
Key string
83+
Reference cnab.BundleReference
84+
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
85+
}
86+
87+
func (d BundleNode) GetKey() string {
88+
return d.Key
89+
}
90+
91+
func (d BundleNode) GetRequires() []string {
92+
return d.Requires
93+
}
94+
95+
type InstallationNode struct {
96+
Key string
97+
Namespace string
98+
Name string
99+
}
100+
101+
func (d InstallationNode) GetKey() string {
102+
return d.Key
103+
}
104+
105+
func (d InstallationNode) GetRequires() []string {
106+
return nil
107+
}
108+
109+
type Dependency struct {
110+
Key string
111+
DefaultBundle *BundleReferenceSelector
112+
Interface *BundleInterfaceSelector
113+
InstallationSelector *InstallationSelector
114+
Requires []string
115+
}
116+
117+
type BundleReferenceSelector struct {
118+
Reference cnab.OCIReference
119+
Version *semver.Constraints
120+
}
121+
122+
func (s *BundleReferenceSelector) IsMatch(ctx context.Context, inst storage.Installation) bool {
123+
log := tracing.LoggerFromContext(ctx)
124+
log.Debug("Evaluating installation bundle definition")
125+
126+
if inst.Status.BundleReference == "" {
127+
log.Debug("Installation does not match because it does not have an associated bundle")
128+
return false
129+
}
130+
131+
ref, err := cnab.ParseOCIReference(inst.Status.BundleReference)
132+
if err != nil {
133+
log.Warn("Could not evaluate installation because the BundleReference is invalid",
134+
attribute.String("reference", inst.Status.BundleReference))
135+
return false
136+
}
137+
138+
// If no selector is defined, consider it a match
139+
if s == nil {
140+
return true
141+
}
142+
143+
// If a version range is specified, ignore the version on the selector and apply the range
144+
// otherwise match the tag or digest
145+
if s.Version != nil {
146+
if inst.Status.BundleVersion == "" {
147+
log.Debug("Installation does not match because it does not have an associated bundle version")
148+
return false
149+
}
150+
151+
// First check that the repository is the same
152+
gotRepo := ref.Repository()
153+
wantRepo := s.Reference.Repository()
154+
if gotRepo != wantRepo {
155+
log.Warn("Installation does not match because the bundle repository is incorrect",
156+
attribute.String("installation-bundle-repository", gotRepo),
157+
attribute.String("dependency-bundle-repository", wantRepo),
158+
)
159+
return false
160+
}
161+
162+
gotVersion, err := semver.NewVersion(inst.Status.BundleVersion)
163+
if err != nil {
164+
log.Warn("Installation does not match because the bundle version is invalid",
165+
attribute.String("installation-bundle-version", inst.Status.BundleVersion),
166+
)
167+
return false
168+
}
169+
170+
if s.Version.Check(gotVersion) {
171+
log.Debug("Installation matches because the bundle version is in range",
172+
attribute.String("installation-bundle-version", inst.Status.BundleVersion),
173+
attribute.String("dependency-bundle-version", s.Version.String()),
174+
)
175+
return true
176+
} else {
177+
log.Debug("Installation does not match because the bundle version is incorrect",
178+
attribute.String("installation-bundle-version", inst.Status.BundleVersion),
179+
attribute.String("dependency-bundle-version", s.Version.String()),
180+
)
181+
return false
182+
}
183+
} else {
184+
gotRef := ref.String()
185+
wantRef := s.Reference.String()
186+
if gotRef == wantRef {
187+
log.Warn("Installation matches because the bundle reference is correct",
188+
attribute.String("installation-bundle-reference", gotRef),
189+
attribute.String("dependency-bundle-reference", wantRef),
190+
)
191+
return true
192+
} else {
193+
log.Warn("Installation does not match because the bundle reference is incorrect",
194+
attribute.String("installation-bundle-reference", gotRef),
195+
attribute.String("dependency-bundle-reference", wantRef),
196+
)
197+
return false
198+
}
199+
}
200+
}
201+
202+
type InstallationSelector struct {
203+
Bundle *BundleReferenceSelector
204+
Interface *BundleInterfaceSelector
205+
Labels map[string]string
206+
Namespaces []string
207+
}
208+
209+
func (s InstallationSelector) IsMatch(ctx context.Context, inst storage.Installation) bool {
210+
// Skip checking labels and namespaces, those were used to query the set of
211+
// installations that we are checking
212+
213+
bundleMatches := s.Bundle.IsMatch(ctx, inst)
214+
if !bundleMatches {
215+
return false
216+
}
217+
218+
interfaceMatches := s.Interface.IsMatch(ctx, inst)
219+
return interfaceMatches
220+
}
221+
222+
// BundleInterfaceSelector defines how a bundle is going to be used.
223+
// It is not the same as the bundle definition.
224+
// It works like go interfaces where its defined by its consumer.
225+
type BundleInterfaceSelector struct {
226+
Parameters []bundle.Parameter
227+
Credentials []bundle.Credential
228+
Outputs []bundle.Output
229+
}
230+
231+
func (s BundleInterfaceSelector) IsMatch(ctx context.Context, inst storage.Installation) bool {
232+
// TODO: implement
233+
return true
234+
}

pkg/workflow/bundle_graph_test.go

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package workflow
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestEngine_DependOnInstallation(t *testing.T) {
12+
/*
13+
A -> B (installation)
14+
A -> C (bundle)
15+
c.parameters.connstr <- B.outputs.connstr
16+
*/
17+
18+
b := InstallationNode{Key: "b"}
19+
c := BundleNode{
20+
Key: "c",
21+
Requires: []string{"b"},
22+
}
23+
a := BundleNode{
24+
Key: "root",
25+
Requires: []string{"b", "c"},
26+
}
27+
28+
g := NewBundleGraph()
29+
g.RegisterNode(a)
30+
g.RegisterNode(b)
31+
g.RegisterNode(c)
32+
sortedNodes, ok := g.Sort()
33+
require.True(t, ok, "graph should not be cyclic")
34+
35+
gotOrder := make([]string, len(sortedNodes))
36+
for i, node := range sortedNodes {
37+
gotOrder[i] = node.GetKey()
38+
}
39+
wantOrder := []string{
40+
"b",
41+
"c",
42+
"root",
43+
}
44+
assert.Equal(t, wantOrder, gotOrder)
45+
}
46+
47+
/*
48+
✅ need to represent new dependency structure on an extended bundle wrapper
49+
(put in cnab-go later)
50+
51+
need to read a bundle and make a BundleGraph
52+
? how to handle a param that isn't a pure assignment, e.g. connstr: ${bundle.deps.VM.outputs.ip}:${bundle.deps.SVC.outputs.port}
53+
? when are templates evaluated as the graph is executed (for simplicity, first draft no composition / templating)
54+
55+
need to resolve dependencies in the graph
56+
* lookup against existing installations
57+
* lookup against semver tags in registry
58+
* lookup against bundle index? when would we look here? (i.e. preferred/registered implementations of interfaces)
59+
60+
need to turn the sorted nodes into an execution plan
61+
execution plan needs:
62+
* bundle to execute and the installation it will become
63+
* parameters and credentials to pass
64+
* sources:
65+
root parameters/creds
66+
installation outputs
67+
68+
need to write something that can run an execution plan
69+
* knows how to grab sources and pass them into the bundle
70+
*/
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package workflow
2+
3+
import (
4+
"context"
5+
6+
"get.porter.sh/porter/pkg/porter"
7+
)
8+
9+
var _ DependencyResolver = DefaultBundleResolver{}
10+
11+
// DefaultBundleResolver resolves the default bundle defined on the dependency.
12+
type DefaultBundleResolver struct {
13+
puller porter.BundleResolver
14+
}
15+
16+
func (d DefaultBundleResolver) Resolve(ctx context.Context, dep Dependency) (Node, bool, error) {
17+
if dep.DefaultBundle == nil {
18+
return nil, false, nil
19+
}
20+
21+
pullOpts := porter.BundlePullOptions{
22+
Reference: dep.DefaultBundle.Reference.String(),
23+
// todo: respect force pull and insecure registry
24+
}
25+
if err := pullOpts.Validate(); err != nil {
26+
return nil, false, err
27+
}
28+
cb, err := d.puller.Resolve(ctx, pullOpts)
29+
if err != nil {
30+
// wrap not found error and indicate that we could resolve anything
31+
return nil, false, err
32+
}
33+
34+
return BundleNode{
35+
Key: dep.Key,
36+
Reference: cb.BundleReference,
37+
}, true, nil
38+
}

0 commit comments

Comments
 (0)