Skip to content

Commit

Permalink
Merge pull request #199 from chrisdoherty4/feat/dynamically-build-sta…
Browse files Browse the repository at this point in the history
…tic-routes
  • Loading branch information
chrisdoherty4 authored Dec 2, 2022
2 parents 6a883d1 + 20b45e8 commit 148598b
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 148598b

Please sign in to comment.