Skip to content

Commit

Permalink
Dynamically construct EC2 static routes
Browse files Browse the repository at this point in the history
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
chrisdoherty4 committed Dec 2, 2022
1 parent 6a883d1 commit 20b45e8
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 61 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ require (
sigs.k8s.io/controller-runtime v0.13.1
)

require github.com/kr/pretty v0.3.1 // indirect

require (
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -438,8 +438,9 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
Expand Down Expand Up @@ -585,8 +586,9 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rollbar/rollbar-go v1.4.2 h1:UzxjFgg9CFE0Vb3grGPpZHCnbKzNd8RYFtFHEKovauU=
github.com/rollbar/rollbar-go v1.4.2/go.mod h1:kLQ9gP3WCRGrvJmF0ueO3wK9xWocej8GRX98D8sa39w=
github.com/rollbar/rollbar-go/errors v0.0.0-20210929193720-32947096267e/go.mod h1:Ie0xEc1Cyj+T4XMO8s0Vf7pMfvSAAy1sb4AYc8aJsao=
Expand Down
2 changes: 1 addition & 1 deletion internal/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func TestHegel_EC2Frontend(t *testing.T) {
Name: "StaticRoute",
Endpoint: "/2009-04-04",
Expect: `meta-data/
user-data/`,
user-data`,
},
{
Name: "DynamicRoute",
Expand Down
23 changes: 13 additions & 10 deletions internal/frontend/ec2/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"context"
"errors"
"net/http"
"sort"
"strings"

"github.com/gin-gonic/gin"
"github.com/tinkerbell/hegel/internal/frontend/ec2/internal/staticroute"
"github.com/tinkerbell/hegel/internal/ginutil"
"github.com/tinkerbell/hegel/internal/http/httperror"
"github.com/tinkerbell/hegel/internal/http/request"
Expand Down Expand Up @@ -40,10 +40,11 @@ func New(client Client) Frontend {
//
// TODO(chrisdoherty4) Document unimplemented endpoints.
func (f Frontend) Configure(router gin.IRouter) {
// Setup the 2009-04-04 API path prefix.
// Setup the 2009-04-04 API path prefix and use a trailing slash route helper to patch
// equivalent trailing slash routes.
v20090404 := ginutil.TrailingSlashRouteHelper{IRouter: router.Group("/2009-04-04")}

dynamicEndpointBinder := func(router gin.IRouter, endpoint string, filter filterFunc) {
dataEndpointBinder := func(router gin.IRouter, endpoint string, filter filterFunc) {
router.GET(endpoint, func(ctx *gin.Context) {
instance, err := f.getInstance(ctx, ctx.Request)
if err != nil {
Expand All @@ -63,10 +64,15 @@ func (f Frontend) Configure(router gin.IRouter) {
})
}

// Create a static route builder that we can add all data routes to which are the basis for
// all static routes.
staticRoutes := staticroute.NewBuilder()

// Configure all dynamic routes. Dynamic routes are anything that requires retrieving a specific
// instance and returning data from it.
for _, route := range dynamicRoutes {
dynamicEndpointBinder(v20090404, route.Endpoint, route.Filter)
for _, r := range dataRoutes {
dataEndpointBinder(v20090404, r.Endpoint, r.Filter)
staticRoutes.FromEndpoint(r.Endpoint)
}

staticEndpointBinder := func(router gin.IRouter, endpoint string, childEndpoints []string) {
Expand All @@ -75,11 +81,8 @@ func (f Frontend) Configure(router gin.IRouter) {
})
}

for _, route := range staticRoutes {
children := make([]string, len(route.ChildEndpoints))
copy(children, route.ChildEndpoints)
sort.Strings(children)
staticEndpointBinder(v20090404, route.Endpoint, children)
for _, r := range staticRoutes.Build() {
staticEndpointBinder(v20090404, r.Endpoint, r.Children)
}
}

Expand Down
6 changes: 3 additions & 3 deletions internal/frontend/ec2/frontend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ func TestFrontendStaticEndpoints(t *testing.T) {
Name: "Root",
Endpoint: "/2009-04-04",
Expect: `meta-data/
user-data/`,
user-data`,
},
{
Name: "Metadata",
Expand Down Expand Up @@ -302,11 +302,11 @@ func validate(t *testing.T, router *gin.Engine, endpoint string, expect string)
router.ServeHTTP(w, r)

if w.Code != http.StatusOK {
t.Fatalf("Expected: 200; Received: %d", w.Code)
t.Fatalf("\nEndpoint=%s\nExpected status: 200; Received status: %d; ", endpoint, w.Code)
}

if w.Body.String() != expect {
t.Fatalf("Expected: %s;\nReceived: %s", expect, w.Body.String())
t.Fatalf("\nExpected: %s;\nReceived: %s;\n(Endpoint=%s)", expect, w.Body.String(), endpoint)
}
}

Expand Down
20 changes: 20 additions & 0 deletions internal/frontend/ec2/internal/staticroute/set.go
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)
}
}
9 changes: 9 additions & 0 deletions internal/frontend/ec2/internal/staticroute/sortable.go
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
}
86 changes: 86 additions & 0 deletions internal/frontend/ec2/internal/staticroute/staticroute.go
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 internal/frontend/ec2/internal/staticroute/staticroute_test.go
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))
}
})
}
}
Loading

0 comments on commit 20b45e8

Please sign in to comment.