-
Notifications
You must be signed in to change notification settings - Fork 97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support routes.*.destination of gateway for App Graph #7079
Changes from 5 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,6 +42,7 @@ var ( | |
|
||
const ( | ||
connectionsPath = "/properties/connections" | ||
routesPath = "/properties/routes" | ||
portsPath = "/properties/container/ports" | ||
) | ||
|
||
|
@@ -248,8 +249,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, connectionsConverter(resources)) | ||
// Resolve Outbound connections based on 'routes'. | ||
connections = append(connections, resolveConnections(resource, routesPath, routesPathConverter(resources))...) | ||
// Resolve Inbound connections based on 'provides'. | ||
connections = append(connections, resolveConnections(resource, portsPath, providersConverter)...) | ||
|
||
sort.Slice(connections, func(i, j int) bool { | ||
return to.String(connections[i].ID) < to.String(connections[j].ID) | ||
|
@@ -356,8 +361,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...) | ||
|
||
|
@@ -459,16 +464,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 func(any) (string, corerpv20231001preview.Direction, error)) []*corerpv20231001preview.ApplicationGraphConnection { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I refactored the code to simplify the code for resolving connections, routes, and provides instead of repeating the same code multiple times. |
||
// 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()) | ||
|
@@ -480,8 +479,17 @@ func connectionsFromAPIData(resource generated.GenericResource, allResources []g | |
return []*corerpv20231001preview.ApplicationGraphConnection{} | ||
} | ||
|
||
connections, ok := raw.(map[string]any) | ||
if !ok { | ||
items := []any{} | ||
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{} | ||
} | ||
|
@@ -491,13 +499,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), | ||
|
@@ -547,61 +551,61 @@ 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()) | ||
// connectionsConverter 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 connectionsConverter(resources []generated.GenericResource) func(any) (string, corerpv20231001preview.Direction, error) { | ||
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{} | ||
func routesPathConverter(resources []generated.GenericResource) func(any) (string, corerpv20231001preview.Direction, error) { | ||
return func(item any) (string, corerpv20231001preview.Direction, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't we have one function for all three and just pass in the resource type? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That way makes more complicated and each one has different logic for scanning resource. it is not worth. |
||
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{} | ||
// providersConverter 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 providersConverter(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 | ||
|
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 @@ | ||
[ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nithyatsu @rynowak Please review this appgraph output carefully. To me, the result makes sense. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks right to me 👍 |
||
{ | ||
"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" | ||
} | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the code bug.
&conn
refered to the same variable so that entryConnections array held the same pointer values.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh.
The good news is that they are fixing this: golang/go#60078
I wasn't able to find the link, but we can use the Go compiler to detect these cases.