From 20b45e8437e2a3527f6163e6f42961580d0a9eb8 Mon Sep 17 00:00:00 2001 From: Chris Doherty Date: Sat, 26 Nov 2022 12:24:50 -0600 Subject: [PATCH] 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 --- go.mod | 2 + go.sum | 6 +- internal/e2e/e2e_test.go | 2 +- internal/frontend/ec2/frontend.go | 23 ++-- internal/frontend/ec2/frontend_test.go | 6 +- .../frontend/ec2/internal/staticroute/set.go | 20 +++ .../ec2/internal/staticroute/sortable.go | 9 ++ .../ec2/internal/staticroute/staticroute.go | 86 ++++++++++++ .../internal/staticroute/staticroute_test.go | 124 ++++++++++++++++++ internal/frontend/ec2/routes.go | 46 +------ 10 files changed, 263 insertions(+), 61 deletions(-) create mode 100644 internal/frontend/ec2/internal/staticroute/set.go create mode 100644 internal/frontend/ec2/internal/staticroute/sortable.go create mode 100644 internal/frontend/ec2/internal/staticroute/staticroute.go create mode 100644 internal/frontend/ec2/internal/staticroute/staticroute_test.go diff --git a/go.mod b/go.mod index 37416f46..cc4fb465 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index ac079503..b0dc9a34 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= diff --git a/internal/e2e/e2e_test.go b/internal/e2e/e2e_test.go index b265327a..1fcacca1 100644 --- a/internal/e2e/e2e_test.go +++ b/internal/e2e/e2e_test.go @@ -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", diff --git a/internal/frontend/ec2/frontend.go b/internal/frontend/ec2/frontend.go index d3640811..e5554a15 100644 --- a/internal/frontend/ec2/frontend.go +++ b/internal/frontend/ec2/frontend.go @@ -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" @@ -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 { @@ -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) { @@ -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) } } diff --git a/internal/frontend/ec2/frontend_test.go b/internal/frontend/ec2/frontend_test.go index 78b62c3e..01fee026 100644 --- a/internal/frontend/ec2/frontend_test.go +++ b/internal/frontend/ec2/frontend_test.go @@ -238,7 +238,7 @@ func TestFrontendStaticEndpoints(t *testing.T) { Name: "Root", Endpoint: "/2009-04-04", Expect: `meta-data/ -user-data/`, +user-data`, }, { Name: "Metadata", @@ -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) } } diff --git a/internal/frontend/ec2/internal/staticroute/set.go b/internal/frontend/ec2/internal/staticroute/set.go new file mode 100644 index 00000000..483e7f56 --- /dev/null +++ b/internal/frontend/ec2/internal/staticroute/set.go @@ -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) + } +} diff --git a/internal/frontend/ec2/internal/staticroute/sortable.go b/internal/frontend/ec2/internal/staticroute/sortable.go new file mode 100644 index 00000000..92160cd8 --- /dev/null +++ b/internal/frontend/ec2/internal/staticroute/sortable.go @@ -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 +} diff --git a/internal/frontend/ec2/internal/staticroute/staticroute.go b/internal/frontend/ec2/internal/staticroute/staticroute.go new file mode 100644 index 00000000..b3e239c3 --- /dev/null +++ b/internal/frontend/ec2/internal/staticroute/staticroute.go @@ -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 +} diff --git a/internal/frontend/ec2/internal/staticroute/staticroute_test.go b/internal/frontend/ec2/internal/staticroute/staticroute_test.go new file mode 100644 index 00000000..9c14cd9b --- /dev/null +++ b/internal/frontend/ec2/internal/staticroute/staticroute_test.go @@ -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)) + } + }) + } +} diff --git a/internal/frontend/ec2/routes.go b/internal/frontend/ec2/routes.go index 9f47c0ba..51deabdc 100644 --- a/internal/frontend/ec2/routes.go +++ b/internal/frontend/ec2/routes.go @@ -6,51 +6,7 @@ package ec2 type filterFunc func(i Instance) string -var staticRoutes = []struct { - Endpoint string - ChildEndpoints []string -}{ - { - Endpoint: "/", - ChildEndpoints: []string{ - "user-data/", - "meta-data/", - }, - }, - { - Endpoint: "/meta-data", - ChildEndpoints: []string{ - "instance-id", - "hostname", - "local-hostname", - "iqn", - "plan", - "facility", - "tags", - "public-ipv4", - "public-ipv6", - "local-ipv4", - "public-keys", - "operating-system/", - }, - }, - { - Endpoint: "/meta-data/operating-system", - ChildEndpoints: []string{ - "slug", - "distro", - "version", - "image_tag", - "license_activation/", - }, - }, - { - Endpoint: "/meta-data/operating-system/license_activation", - ChildEndpoints: []string{"state"}, - }, -} - -var dynamicRoutes = []struct { +var dataRoutes = []struct { Endpoint string Filter filterFunc }{