-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Dynamically construct EC2 static routes
The EC2 frontend is composed of data routes and static routes. Static routes are those that serve child elements intead of dynamic data. They were previously manually defined which isn't particularly extensible. This change dynamically determines what static routes should exist based ont he dynamic paths. Signed-off-by: Chris Doherty <[email protected]>
- Loading branch information
1 parent
6a883d1
commit 20b45e8
Showing
10 changed files
with
263 additions
and
61 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
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,20 @@ | ||
package staticroute | ||
|
||
// unorderedSet is a utility data structure that behaves as a traditional unorderedSet. Its elements are unordered. | ||
type unorderedSet map[string]struct{} | ||
|
||
func newUnorderedSet() unorderedSet { | ||
return make(unorderedSet) | ||
} | ||
|
||
// Insert adds v to s. | ||
func (s unorderedSet) Insert(v string) { | ||
s[v] = struct{}{} | ||
} | ||
|
||
// Range iterates over the elements in s and calls fn for each element. | ||
func (s unorderedSet) Range(fn func(v string)) { | ||
for k := range s { | ||
fn(k) | ||
} | ||
} |
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,9 @@ | ||
package staticroute | ||
|
||
type sortableRoutes []Route | ||
|
||
func (r sortableRoutes) Len() int { return len(r) } | ||
func (r sortableRoutes) Swap(i, j int) { r[i], r[j] = r[j], r[i] } | ||
func (r sortableRoutes) Less(i, j int) bool { | ||
return r[i].Endpoint < r[j].Endpoint | ||
} |
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,86 @@ | ||
/* | ||
Package staticroute provides tools for building EC2 Instance Metadata static routes from | ||
the set of data endpoints. A data endpoint is an one that serves instance specific data. | ||
*/ | ||
package staticroute | ||
|
||
import ( | ||
"sort" | ||
"strings" | ||
) | ||
|
||
// Builder constructs a set of Route objects. Endpoints added via FromEndpoint will be result | ||
// in a static route for each level of endpoint nesting. The root route is always an empty string. | ||
// Endpoints that are descendable will be appended with a slash. For example, adding the endpoint | ||
// "/foo/bar/baz" will result in the following routes: | ||
// | ||
// "/foo/bar" -> baz | ||
// "/foo" -> bar/ | ||
// "" -> foo/ | ||
type Builder map[string]unorderedSet | ||
|
||
// NewBuilder returns a new Builder instance. | ||
func NewBuilder() Builder { | ||
return make(map[string]unorderedSet) | ||
} | ||
|
||
// FromEndpoint adds endpoint to b. endpoint should be of URL path form such as "/foo/bar". | ||
// FromEndpoint can be called multiple times. | ||
func (b Builder) FromEndpoint(endpoint string) { | ||
// Ensure our endpoint begins with a `/` so we can add to the root route for endpoint. | ||
if !strings.HasPrefix(endpoint, "/") { | ||
endpoint = "/" + endpoint | ||
} | ||
|
||
// Split the endpoint into its components so we can build the pieces we need. | ||
split := strings.Split(endpoint, "/") | ||
|
||
// Iterate over the components in reverse order so we can build parent paths for every | ||
// level of path nesting and track the child part. | ||
for i := len(split) - 1; i > 0; i-- { | ||
concat := strings.Join(split[:i], "/") | ||
if _, ok := b[concat]; !ok { | ||
b[concat] = newUnorderedSet() | ||
} | ||
b[concat].Insert(split[i]) | ||
} | ||
} | ||
|
||
// Build returns a slice of Route objects containing an Endpoint and its associated child | ||
// elements for the response body. The root route is identified by an empty string for the | ||
// Endpoint field of Route. | ||
func (b Builder) Build() []Route { | ||
var routes sortableRoutes | ||
|
||
for parent, children := range b { | ||
r := Route{Endpoint: parent} | ||
|
||
// Add children to the route prepending a slash for any child that is also a parent. | ||
children.Range(func(child string) { | ||
asParent := strings.Join([]string{parent, child}, "/") | ||
|
||
// If the child is also a parent, append a slash so the consumer knows it is a | ||
// descendable directory. | ||
if _, ok := b[asParent]; ok { | ||
child += "/" | ||
} | ||
|
||
r.Children = append(r.Children, child) | ||
}) | ||
|
||
sort.Strings(r.Children) | ||
|
||
routes = append(routes, r) | ||
} | ||
|
||
// Sort for determinism, no other reason. | ||
sort.Sort(routes) | ||
|
||
return routes | ||
} | ||
|
||
// Route is an endpoint and its associated child elements. | ||
type Route struct { | ||
Endpoint string | ||
Children []string | ||
} |
124 changes: 124 additions & 0 deletions
124
internal/frontend/ec2/internal/staticroute/staticroute_test.go
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,124 @@ | ||
package staticroute_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
. "github.com/tinkerbell/hegel/internal/frontend/ec2/internal/staticroute" | ||
) | ||
|
||
func TestBuilder(t *testing.T) { | ||
cases := []struct { | ||
Name string | ||
Endpoints []string | ||
Routes []Route | ||
}{ | ||
{ | ||
Name: "NoEndpoints", | ||
Endpoints: []string{}, | ||
Routes: nil, | ||
}, | ||
{ | ||
Name: "MissingLeadingSlash", | ||
Endpoints: []string{"foo/bar"}, | ||
Routes: []Route{ | ||
{ | ||
Endpoint: "", | ||
Children: []string{"foo/"}, | ||
}, | ||
{ | ||
Endpoint: "/foo", | ||
Children: []string{"bar"}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
Name: "SingleEndpoint", | ||
Endpoints: []string{"/foo/bar"}, | ||
Routes: []Route{ | ||
{ | ||
Endpoint: "", | ||
Children: []string{"foo/"}, | ||
}, | ||
{ | ||
Endpoint: "/foo", | ||
Children: []string{"bar"}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
Name: "NestedEndpoints", | ||
Endpoints: []string{"/foo/bar", "/foo/bar/baz"}, | ||
Routes: []Route{ | ||
{ | ||
Endpoint: "", | ||
Children: []string{"foo/"}, | ||
}, | ||
{ | ||
Endpoint: "/foo", | ||
Children: []string{"bar/"}, | ||
}, | ||
{ | ||
Endpoint: "/foo/bar", | ||
Children: []string{"baz"}, | ||
}, | ||
}, | ||
}, | ||
|
||
{ | ||
Name: "DeepNestedEndpoints", | ||
Endpoints: []string{"/foo/bar/baz/qux"}, | ||
Routes: []Route{ | ||
{ | ||
Endpoint: "", | ||
Children: []string{"foo/"}, | ||
}, | ||
{ | ||
Endpoint: "/foo", | ||
Children: []string{"bar/"}, | ||
}, | ||
{ | ||
Endpoint: "/foo/bar", | ||
Children: []string{"baz/"}, | ||
}, | ||
{ | ||
Endpoint: "/foo/bar/baz", | ||
Children: []string{"qux"}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
Name: "MultipleDifferentiatedEndpoints", | ||
Endpoints: []string{"/foo/bar", "/baz/qux"}, | ||
Routes: []Route{ | ||
{ | ||
Endpoint: "", | ||
Children: []string{"baz/", "foo/"}, | ||
}, | ||
{ | ||
Endpoint: "/baz", | ||
Children: []string{"qux"}, | ||
}, | ||
{ | ||
Endpoint: "/foo", | ||
Children: []string{"bar"}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tc := range cases { | ||
t.Run(tc.Name, func(t *testing.T) { | ||
builder := NewBuilder() | ||
for _, ep := range tc.Endpoints { | ||
builder.FromEndpoint(ep) | ||
} | ||
|
||
routes := builder.Build() | ||
|
||
if !cmp.Equal(tc.Routes, routes) { | ||
t.Fatalf("Unexpected routes: %s", cmp.Diff(tc.Routes, routes)) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.