Skip to content

Commit

Permalink
Support routes.*.destination of gateway for App Graph (radius-project…
Browse files Browse the repository at this point in the history
…#7079)

# Description

This is to support `routes.*.destination` property for App graph. It
enables to show the connections between Gateway and the other resources
in App Graph

## Type of change

- This pull request is a minor refactor, code cleanup, test improvement,
or other maintenance task and doesn't change the functionality of Radius
(issue link optional).

Fixes: radius-project#7038

---------

Signed-off-by: Young Bu Park <[email protected]>
  • Loading branch information
youngbupark authored and willdavsmith committed Mar 4, 2024
1 parent f9d9f42 commit 666d4b1
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 70 deletions.
146 changes: 78 additions & 68 deletions pkg/corerp/frontend/controller/applications/graph_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,13 @@ var (

const (
connectionsPath = "/properties/connections"
routesPath = "/properties/routes"
portsPath = "/properties/container/ports"
)

// resolver is a function type to resolve appgraph connection.
type resolver func(any) (string, corerpv20231001preview.Direction, error)

// listAllResourcesByApplication takes a context, applicationID and clientOptions
// and returns a slice of GenericResources in the application and also an error if one occurs.
func listAllResourcesByApplication(ctx context.Context, applicationID resources.ID, clientOptions *policy.ClientOptions) ([]generated.GenericResource, error) {
Expand Down Expand Up @@ -248,8 +252,12 @@ func computeGraph(applicationName string, applicationResources []generated.Gener
applicationGraphResource.ProvisioningState = &state
}

connections := connectionsFromAPIData(resource, resources) // Outbound connections based on 'connections'
connections = append(connections, providesFromAPIData(resource)...) // Inbound connections based on 'provides'
// Resolve Outbound connections based on 'connections'.
connections := resolveConnections(resource, connectionsPath, connectionsResolver(resources))
// Resolve Outbound connections based on 'routes'.
connections = append(connections, resolveConnections(resource, routesPath, routesPathResolver(resources))...)
// Resolve Inbound connections based on 'provides'.
connections = append(connections, resolveConnections(resource, portsPath, providersResolver)...)

sort.Slice(connections, func(i, j int) bool {
return to.String(connections[i].ID) < to.String(connections[j].ID)
Expand Down Expand Up @@ -356,8 +364,8 @@ func computeGraph(applicationName string, applicationResources []generated.Gener
connectionsIn := connectionsByDestination[id]

entryConnections := make([]*corerpv20231001preview.ApplicationGraphConnection, len(connectionsIn))
for i, conn := range connectionsIn {
entryConnections[i] = &conn
for i := range connectionsIn {
entryConnections[i] = &connectionsIn[i]
}
entry.Connections = append(entry.Connections, entryConnections...)

Expand Down Expand Up @@ -459,16 +467,10 @@ func outputResourcesFromAPIData(resource generated.GenericResource) []*corerpv20
return entries
}

// connectionsFromAPIData resolves the outbound connections for a resource from the generic resource representation.
// For example if the resource is an 'Applications.Core/container' then this function can find it's connections
// to other resources like databases. Conversely if the resource is a database then this function
// will not find any connections (because they are inbound). Inbound connections are processed later.
func connectionsFromAPIData(resource generated.GenericResource, allResources []generated.GenericResource) []*corerpv20231001preview.ApplicationGraphConnection {
func resolveConnections(resource generated.GenericResource, jsonRefPath string, converter resolver) []*corerpv20231001preview.ApplicationGraphConnection {
// We need to access the connections in a weakly-typed way since the data type we're
// working with is a property bag.
//
// Any Radius resource type that supports connections uses the following property path to return them.
p, err := jsonpointer.New(connectionsPath)
p, err := jsonpointer.New(jsonRefPath)
if err != nil {
// This should never fail since we're hard-coding the path.
panic("parsing JSON pointer should not fail: " + err.Error())
Expand All @@ -480,8 +482,19 @@ func connectionsFromAPIData(resource generated.GenericResource, allResources []g
return []*corerpv20231001preview.ApplicationGraphConnection{}
}

connections, ok := raw.(map[string]any)
if !ok {
items := []any{}

// Convert array and map connection data to a slice of items.
switch conn := raw.(type) {
case []any:
items = conn
case map[string]any:
for _, v := range conn {
items = append(items, v)
}
}

if len(items) == 0 {
// Not a map of objects, this is fine.
return []*corerpv20231001preview.ApplicationGraphConnection{}
}
Expand All @@ -491,13 +504,9 @@ func connectionsFromAPIData(resource generated.GenericResource, allResources []g
//
// If we encounter an error processing this data, just skip "invalid" connection entry.
entries := []*corerpv20231001preview.ApplicationGraphConnection{}
for _, connection := range connections {
dir := corerpv20231001preview.DirectionOutbound
data := corerpv20231001preview.ConnectionProperties{}
err := toStronglyTypedData(connection, &data)
if err == nil {
sourceID, _ := findSourceResource(to.String(data.Source), allResources)

for _, item := range items {
sourceID, dir, err := converter(item)
if err == nil && sourceID != "" {
entries = append(entries, &corerpv20231001preview.ApplicationGraphConnection{
ID: to.Ptr(sourceID),
Direction: to.Ptr(dir),
Expand Down Expand Up @@ -547,61 +556,62 @@ func findSourceResource(source string, allResources []generated.GenericResource)
return orig, ErrInvalidSource
}

// providesFromAPIData is specifically to support HTTPRoute.
func providesFromAPIData(resource generated.GenericResource) []*corerpv20231001preview.ApplicationGraphConnection {
// Any Radius resource type that exposes a port uses the following property path to return them.
// The port may have a 'provides' attribute that specifies a httproute.
// This route should be parsed to find the connections between containers.
// For example, if container A provides a route and container B consumes it,
// then we have port.provides in container A and container.connection in container B.
// This gives us the connection: container A --> route R --> container B.
// Without parsing the 'provides' attribute, we would miss the connection between container A and route R.

p, err := jsonpointer.New(portsPath)
if err != nil {
// This should never fail since we're hard-coding the path.
panic("parsing JSON pointer should not fail: " + err.Error())
// connectionsResolver resolves the outbound connections for a resource from the generic resource representation.
// For example if the resource is an 'Applications.Core/container' then this function can find it's connections
// to other resources like databases. Conversely if the resource is a database then this function
// will not find any connections (because they are inbound). Inbound connections are processed later.
func connectionsResolver(resources []generated.GenericResource) resolver {
return func(item any) (string, corerpv20231001preview.Direction, error) {
data := &corerpv20231001preview.ConnectionProperties{}
err := toStronglyTypedData(item, data)
if err != nil {
return "", "", err
}
sourceID, err := findSourceResource(to.String(data.Source), resources)
if err != nil {
return "", "", err
}
return sourceID, corerpv20231001preview.DirectionOutbound, nil
}
}

raw, _, err := p.Get(&resource)
if err != nil {
// Not found, this is fine.
return []*corerpv20231001preview.ApplicationGraphConnection{}
// routesPathResolver resolves the outbound connections of Applications.Core/gateway resource.
func routesPathResolver(resources []generated.GenericResource) resolver {
return func(item any) (string, corerpv20231001preview.Direction, error) {
data := &corerpv20231001preview.GatewayRoute{}
err := toStronglyTypedData(item, data)
if err != nil {
return "", "", err
}
sourceID, err := findSourceResource(to.String(data.Destination), resources)
if err != nil {
return "", "", err
}
return sourceID, corerpv20231001preview.DirectionOutbound, nil
}
}

connections, ok := raw.(map[string]any)
if !ok {
// Not a map of objects, this is fine.
return []*corerpv20231001preview.ApplicationGraphConnection{}
// providersResolver is specifically to support HTTPRoute.
// Any Radius resource type that exposes a port uses the following property path to return them.
// The port may have a 'provides' attribute that specifies a httproute.
// This route should be parsed to find the connections between containers.
// For example, if container A provides a route and container B consumes it,
// then we have port.provides in container A and container.connection in container B.
// This gives us the connection: container A --> route R --> container B.
// Without parsing the 'provides' attribute, we would miss the connection between container A and route R.
func providersResolver(item any) (string, corerpv20231001preview.Direction, error) {
data := &corerpv20231001preview.ContainerPortProperties{}
err := toStronglyTypedData(item, data)
if err != nil {
return "", "", err
}

// The data is returned as a map of JSON objects. We need to convert each object from a map[string]any
// to the strongly-typed format we understand.
//
// If we encounter an error processing this data, just skip "invalid" connection entry.
entries := []*corerpv20231001preview.ApplicationGraphConnection{}
for _, connection := range connections {
dir := corerpv20231001preview.DirectionInbound
data := corerpv20231001preview.ContainerPortProperties{}
err := toStronglyTypedData(connection, &data)
if err == nil {
if to.String(data.Provides) == "" {
continue
}

entries = append(entries, &corerpv20231001preview.ApplicationGraphConnection{
ID: data.Provides,
Direction: to.Ptr(dir),
})
}
id := to.String(data.Provides)
if id == "" {
return "", "", nil
}

// Produce a stable output
sort.Slice(entries, func(i, j int) bool {
return to.String(entries[i].ID) < to.String(entries[j].ID)
})

return entries
return id, corerpv20231001preview.DirectionInbound, nil
}

// toStronglyTypedData uses JSON marshalling and unmarshalling to convert a weakly-typed
Expand Down
11 changes: 9 additions & 2 deletions pkg/corerp/frontend/controller/applications/graph_util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,14 @@ func Test_computeGraph(t *testing.T) {
expectedDataFile string
}{
{
name: "using httproute",
name: "using httproute without inbound resource",
applicationName: "myapp",
appResourceDataFile: "graph-app-httproute-in.json",
envResourceDataFile: "",
expectedDataFile: "graph-app-httproute-out.json",
},
{
name: "using httproute 2",
name: "using httproute with inbound resource",
applicationName: "myapp",
appResourceDataFile: "graph-app-httproute2-in.json",
envResourceDataFile: "",
Expand All @@ -154,6 +154,13 @@ func Test_computeGraph(t *testing.T) {
envResourceDataFile: "",
expectedDataFile: "graph-app-directroute-out.json",
},
{
name: "with gateway route",
applicationName: "myapp",
appResourceDataFile: "graph-app-gw-in.json",
envResourceDataFile: "",
expectedDataFile: "graph-app-gw-out.json",
},
}

for _, tt := range tests {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
[
{
"id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/gateways/httpgw",
"name": "httpgw",
"properties": {
"application": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/Applications/myapp",
"routes": [
{
"path": "/",
"destination": "http://frontend:8080"
},
{
"path": "/backendapi",
"destination": "http://backendapp:8080"
}
]
},
"type": "Applications.Core/containers"
},
{
"id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/frontend",
"name": "frontend",
"properties": {
"application": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/Applications/myapp",
"container": {
"image": "magpie:latest",
"readinessProbe": {
"kind": "httpGet",
"path": "/healthz",
"containerPort": 8080
},
"ports": {
"web": {
"port": 8080,
"protocol": "TCP"
}
}
},
"connections": {
"sql": {
"source": "http://backendapp:8080"
}
},
"provisioningState": "Succeeded",
"status": {
"outputResources": {
"id": "/some/thing/else",
"localId": "something"
}
}
},
"type": "Applications.Core/containers"
},
{
"id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp",
"name": "backendapp",
"properties": {
"application": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/Applications/myapp",
"container": {
"ports": {
"web": {
"port": 8080,
"protocol": "TCP"
}
}
},
"provisioningState": "Succeeded",
"status": {
"outputResources": {
"id": "/some/thing/else",
"localId": "something"
}
}
},
"type": "Applications.Core/containers"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[
{
"connections": [
{
"direction": "Outbound",
"id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp"
},
{
"direction": "Inbound",
"id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/gateways/httpgw"
}
],
"id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/frontend",
"name": "frontend",
"outputResources": [],
"provisioningState": "Succeeded",
"type": "Applications.Core/containers"
},
{
"connections": [
{
"direction": "Inbound",
"id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/frontend"
},
{
"direction": "Inbound",
"id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/gateways/httpgw"
}
],
"id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp",
"name": "backendapp",
"outputResources": [],
"provisioningState": "Succeeded",
"type": "Applications.Core/containers"
},
{
"connections": [
{
"direction": "Outbound",
"id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp"
},
{
"direction": "Outbound",
"id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/frontend"
}
],
"id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/gateways/httpgw",
"name": "httpgw",
"outputResources": [],
"provisioningState": "Succeeded",
"type": "Applications.Core/gateways"
}
]

0 comments on commit 666d4b1

Please sign in to comment.