From 63b8f8bf1eb003c1a7324769897bb53995a27c16 Mon Sep 17 00:00:00 2001 From: Young Bu Park Date: Thu, 25 Jan 2024 16:36:46 -0800 Subject: [PATCH 1/6] wip Signed-off-by: Young Bu Park --- .../applications/graph_util_test.go | 11 +++ .../applications/testdata/graph-app-gw-2.json | 36 +++++++++ .../testdata/graph-app-gw-in.json | 77 +++++++++++++++++++ .../testdata/graph-app-gw-out.json | 53 +++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-2.json create mode 100644 pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-in.json create mode 100644 pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-out.json diff --git a/pkg/corerp/frontend/controller/applications/graph_util_test.go b/pkg/corerp/frontend/controller/applications/graph_util_test.go index f45564c749..123be10d43 100644 --- a/pkg/corerp/frontend/controller/applications/graph_util_test.go +++ b/pkg/corerp/frontend/controller/applications/graph_util_test.go @@ -18,6 +18,8 @@ package applications import ( "context" + "encoding/json" + "fmt" "testing" "github.com/radius-project/radius/pkg/cli/clients_new/generated" @@ -154,6 +156,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 { @@ -173,6 +182,8 @@ func Test_computeGraph(t *testing.T) { testutil.MustUnmarshalFromFile(tt.expectedDataFile, &expected) got := computeGraph(tt.applicationName, appResource, envResource) + arr, _ := json.Marshal(got) + fmt.Println(string(arr)) require.ElementsMatch(t, expected, got.Resources) }) } diff --git a/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-2.json b/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-2.json new file mode 100644 index 0000000000..ee828e70e7 --- /dev/null +++ b/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-2.json @@ -0,0 +1,36 @@ +[ + { + "connections": [ + { + "direction": "Outbound", + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp" + } + ], + "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" + } + ], + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp", + "name": "backendapp", + "outputResources": [], + "provisioningState": "Succeeded", + "type": "Applications.Core/containers" + }, + { + "connections": [], + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/gateways/httpgw", + "name": "httpgw", + "outputResources": [], + "provisioningState": "Succeeded", + "type": "Applications.Core/gateways" + } +] \ No newline at end of file diff --git a/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-in.json b/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-in.json new file mode 100644 index 0000000000..c19897480f --- /dev/null +++ b/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-in.json @@ -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" + } +] diff --git a/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-out.json b/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-out.json new file mode 100644 index 0000000000..d81385f29d --- /dev/null +++ b/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-out.json @@ -0,0 +1,53 @@ +[ + { + "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" + }, + { + "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/containers/frontend" + } + ], + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp", + "name": "backendapp", + "outputResources": [], + "provisioningState": "Succeeded", + "type": "Applications.Core/containers" + } +] \ No newline at end of file From afbd67fe55c95bb2ed5a561200b849c232331e4a Mon Sep 17 00:00:00 2001 From: Young Bu Park Date: Thu, 25 Jan 2024 15:52:00 -0800 Subject: [PATCH 2/6] support gw Signed-off-by: Young Bu Park --- .../controller/applications/graph_util.go | 56 ++++++++++++++++++- .../applications/graph_util_test.go | 41 ++++++++------ 2 files changed, 78 insertions(+), 19 deletions(-) diff --git a/pkg/corerp/frontend/controller/applications/graph_util.go b/pkg/corerp/frontend/controller/applications/graph_util.go index 3cb78bdd97..50abebe7ea 100644 --- a/pkg/corerp/frontend/controller/applications/graph_util.go +++ b/pkg/corerp/frontend/controller/applications/graph_util.go @@ -42,6 +42,7 @@ var ( const ( connectionsPath = "/properties/connections" + routesPath = "/properties/routes" portsPath = "/properties/container/ports" ) @@ -248,8 +249,9 @@ 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' + connections := connectionsFromAPIData(resource, resources) // Outbound connections based on 'connections' + connections = append(connections, routesPathFromAPIData(resource, resources)...) // Outbound connections based on 'routes' + connections = append(connections, providesFromAPIData(resource)...) // Inbound connections based on 'provides' sort.Slice(connections, func(i, j int) bool { return to.String(connections[i].ID) < to.String(connections[j].ID) @@ -459,6 +461,56 @@ func outputResourcesFromAPIData(resource generated.GenericResource) []*corerpv20 return entries } +func routesPathFromAPIData(resource generated.GenericResource, allResources []generated.GenericResource) []*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(routesPath) + if err != nil { + // This should never fail since we're hard-coding the path. + panic("parsing JSON pointer should not fail: " + err.Error()) + } + + raw, _, err := p.Get(&resource) + if err != nil { + // Not found, this is fine. + return []*corerpv20231001preview.ApplicationGraphConnection{} + } + + routes, ok := raw.([]any) + if !ok { + // Not a map of objects, this is fine. + return []*corerpv20231001preview.ApplicationGraphConnection{} + } + + // 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 _, r := range routes { + dir := corerpv20231001preview.DirectionOutbound + data := &corerpv20231001preview.GatewayRoute{} + err := toStronglyTypedData(r, data) + if err == nil { + sourceID, _ := findSourceResource(to.String(data.Destination), allResources) + + entries = append(entries, &corerpv20231001preview.ApplicationGraphConnection{ + ID: to.Ptr(sourceID), + Direction: to.Ptr(dir), + }) + } + } + + // 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 +} + // 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 diff --git a/pkg/corerp/frontend/controller/applications/graph_util_test.go b/pkg/corerp/frontend/controller/applications/graph_util_test.go index 123be10d43..d7954e1779 100644 --- a/pkg/corerp/frontend/controller/applications/graph_util_test.go +++ b/pkg/corerp/frontend/controller/applications/graph_util_test.go @@ -135,26 +135,33 @@ func Test_computeGraph(t *testing.T) { envResourceDataFile string expectedDataFile string }{ + // { + // name: "using httproute", + // applicationName: "myapp", + // appResourceDataFile: "graph-app-httproute-in.json", + // envResourceDataFile: "", + // expectedDataFile: "graph-app-httproute-out.json", + // }, + // { + // name: "using httproute 2", + // applicationName: "myapp", + // appResourceDataFile: "graph-app-httproute2-in.json", + // envResourceDataFile: "", + // expectedDataFile: "graph-app-httproute2-out.json", + // }, + // { + // name: "direct route", + // applicationName: "myapp", + // appResourceDataFile: "graph-app-directroute-in.json", + // envResourceDataFile: "", + // expectedDataFile: "graph-app-directroute-out.json", + // }, { - name: "using httproute", + name: "with gateway", applicationName: "myapp", - appResourceDataFile: "graph-app-httproute-in.json", - envResourceDataFile: "", - expectedDataFile: "graph-app-httproute-out.json", - }, - { - name: "using httproute 2", - applicationName: "myapp", - appResourceDataFile: "graph-app-httproute2-in.json", - envResourceDataFile: "", - expectedDataFile: "graph-app-httproute2-out.json", - }, - { - name: "direct route", - applicationName: "myapp", - appResourceDataFile: "graph-app-directroute-in.json", + appResourceDataFile: "graph-app-gw-in.json", envResourceDataFile: "", - expectedDataFile: "graph-app-directroute-out.json", + expectedDataFile: "graph-app-gw-out.json", }, { name: "with gateway route", From 0acfc197e9b05a7b30be6fe72f5257fc4f50a337 Mon Sep 17 00:00:00 2001 From: Young Bu Park Date: Thu, 25 Jan 2024 20:28:31 -0800 Subject: [PATCH 3/6] support gw Signed-off-by: Young Bu Park --- .../controller/applications/graph_util.go | 176 +++++++----------- .../applications/graph_util_test.go | 45 ++--- .../applications/testdata/graph-app-gw-2.json | 36 ---- .../testdata/graph-app-gw-out.json | 30 +-- 4 files changed, 95 insertions(+), 192 deletions(-) delete mode 100644 pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-2.json diff --git a/pkg/corerp/frontend/controller/applications/graph_util.go b/pkg/corerp/frontend/controller/applications/graph_util.go index 50abebe7ea..bed924bec9 100644 --- a/pkg/corerp/frontend/controller/applications/graph_util.go +++ b/pkg/corerp/frontend/controller/applications/graph_util.go @@ -249,9 +249,12 @@ func computeGraph(applicationName string, applicationResources []generated.Gener applicationGraphResource.ProvisioningState = &state } - connections := connectionsFromAPIData(resource, resources) // Outbound connections based on 'connections' - connections = append(connections, routesPathFromAPIData(resource, resources)...) // Outbound connections based on 'routes' - connections = append(connections, providesFromAPIData(resource)...) // Inbound connections based on 'provides' + // Resolve Outbound connections based on 'connections'. + connections := resolveConnections(resource, connectionsPath, connectionsFromAPIData(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, providesFromAPIData)...) sort.Slice(connections, func(i, j int) bool { return to.String(connections[i].ID) < to.String(connections[j].ID) @@ -358,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...) @@ -461,12 +464,8 @@ func outputResourcesFromAPIData(resource generated.GenericResource) []*corerpv20 return entries } -func routesPathFromAPIData(resource generated.GenericResource, allResources []generated.GenericResource) []*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(routesPath) +func resolveConnections(resource generated.GenericResource, jsonRefPath string, converter func(any) (string, corerpv20231001preview.Direction, error)) []*corerpv20231001preview.ApplicationGraphConnection { + 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()) @@ -478,8 +477,17 @@ func routesPathFromAPIData(resource generated.GenericResource, allResources []ge return []*corerpv20231001preview.ApplicationGraphConnection{} } - routes, ok := raw.([]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{} } @@ -489,13 +497,9 @@ func routesPathFromAPIData(resource generated.GenericResource, allResources []ge // // If we encounter an error processing this data, just skip "invalid" connection entry. entries := []*corerpv20231001preview.ApplicationGraphConnection{} - for _, r := range routes { - dir := corerpv20231001preview.DirectionOutbound - data := &corerpv20231001preview.GatewayRoute{} - err := toStronglyTypedData(r, data) - if err == nil { - sourceID, _ := findSourceResource(to.String(data.Destination), 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), @@ -515,54 +519,34 @@ func routesPathFromAPIData(resource generated.GenericResource, allResources []ge // 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 { - // 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) - if err != nil { - // This should never fail since we're hard-coding the path. - panic("parsing JSON pointer should not fail: " + err.Error()) - } - - raw, _, err := p.Get(&resource) - if err != nil { - // Not found, this is fine. - return []*corerpv20231001preview.ApplicationGraphConnection{} - } - - connections, ok := raw.(map[string]any) - if !ok { - // Not a map of objects, this is fine. - return []*corerpv20231001preview.ApplicationGraphConnection{} +func connectionsFromAPIData(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 } +} - // 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.DirectionOutbound - data := corerpv20231001preview.ConnectionProperties{} - err := toStronglyTypedData(connection, &data) - if err == nil { - sourceID, _ := findSourceResource(to.String(data.Source), allResources) - - entries = append(entries, &corerpv20231001preview.ApplicationGraphConnection{ - ID: to.Ptr(sourceID), - Direction: to.Ptr(dir), - }) +func routesPathConverter(resources []generated.GenericResource) func(any) (string, corerpv20231001preview.Direction, error) { + 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 } - - // 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 } // findSourceResource looks up resource id by using source string by the following steps: @@ -600,60 +584,26 @@ func findSourceResource(source string, allResources []generated.GenericResource) } // 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) +// 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 providesFromAPIData(item any) (string, corerpv20231001preview.Direction, error) { + data := &corerpv20231001preview.ContainerPortProperties{} + err := toStronglyTypedData(item, data) if err != nil { - // This should never fail since we're hard-coding the path. - panic("parsing JSON pointer should not fail: " + err.Error()) - } - - raw, _, err := p.Get(&resource) - if err != nil { - // Not found, this is fine. - return []*corerpv20231001preview.ApplicationGraphConnection{} - } - - connections, ok := raw.(map[string]any) - if !ok { - // Not a map of objects, this is fine. - return []*corerpv20231001preview.ApplicationGraphConnection{} + 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 diff --git a/pkg/corerp/frontend/controller/applications/graph_util_test.go b/pkg/corerp/frontend/controller/applications/graph_util_test.go index d7954e1779..6997b9ba4f 100644 --- a/pkg/corerp/frontend/controller/applications/graph_util_test.go +++ b/pkg/corerp/frontend/controller/applications/graph_util_test.go @@ -18,8 +18,6 @@ package applications import ( "context" - "encoding/json" - "fmt" "testing" "github.com/radius-project/radius/pkg/cli/clients_new/generated" @@ -135,33 +133,26 @@ func Test_computeGraph(t *testing.T) { envResourceDataFile string expectedDataFile string }{ - // { - // name: "using httproute", - // applicationName: "myapp", - // appResourceDataFile: "graph-app-httproute-in.json", - // envResourceDataFile: "", - // expectedDataFile: "graph-app-httproute-out.json", - // }, - // { - // name: "using httproute 2", - // applicationName: "myapp", - // appResourceDataFile: "graph-app-httproute2-in.json", - // envResourceDataFile: "", - // expectedDataFile: "graph-app-httproute2-out.json", - // }, - // { - // name: "direct route", - // applicationName: "myapp", - // appResourceDataFile: "graph-app-directroute-in.json", - // envResourceDataFile: "", - // expectedDataFile: "graph-app-directroute-out.json", - // }, { - name: "with gateway", + name: "using httproute without inbound resource", applicationName: "myapp", - appResourceDataFile: "graph-app-gw-in.json", + appResourceDataFile: "graph-app-httproute-in.json", envResourceDataFile: "", - expectedDataFile: "graph-app-gw-out.json", + expectedDataFile: "graph-app-httproute-out.json", + }, + { + name: "using httproute with inbound resource", + applicationName: "myapp", + appResourceDataFile: "graph-app-httproute2-in.json", + envResourceDataFile: "", + expectedDataFile: "graph-app-httproute2-out.json", + }, + { + name: "direct route", + applicationName: "myapp", + appResourceDataFile: "graph-app-directroute-in.json", + envResourceDataFile: "", + expectedDataFile: "graph-app-directroute-out.json", }, { name: "with gateway route", @@ -189,8 +180,6 @@ func Test_computeGraph(t *testing.T) { testutil.MustUnmarshalFromFile(tt.expectedDataFile, &expected) got := computeGraph(tt.applicationName, appResource, envResource) - arr, _ := json.Marshal(got) - fmt.Println(string(arr)) require.ElementsMatch(t, expected, got.Resources) }) } diff --git a/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-2.json b/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-2.json deleted file mode 100644 index ee828e70e7..0000000000 --- a/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-2.json +++ /dev/null @@ -1,36 +0,0 @@ -[ - { - "connections": [ - { - "direction": "Outbound", - "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp" - } - ], - "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" - } - ], - "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp", - "name": "backendapp", - "outputResources": [], - "provisioningState": "Succeeded", - "type": "Applications.Core/containers" - }, - { - "connections": [], - "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/gateways/httpgw", - "name": "httpgw", - "outputResources": [], - "provisioningState": "Succeeded", - "type": "Applications.Core/gateways" - } -] \ No newline at end of file diff --git a/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-out.json b/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-out.json index d81385f29d..478b64fd78 100644 --- a/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-out.json +++ b/pkg/corerp/frontend/controller/applications/testdata/graph-app-gw-out.json @@ -6,29 +6,29 @@ "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp" }, { - "direction": "Outbound", - "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/gateways/httpgw", - "name": "httpgw", + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/frontend", + "name": "frontend", "outputResources": [], "provisioningState": "Succeeded", - "type": "Applications.Core/gateways" + "type": "Applications.Core/containers" }, { "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/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/frontend", - "name": "frontend", + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp", + "name": "backendapp", "outputResources": [], "provisioningState": "Succeeded", "type": "Applications.Core/containers" @@ -36,18 +36,18 @@ { "connections": [ { - "direction": "Inbound", - "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/frontend" + "direction": "Outbound", + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp" }, { - "direction": "Inbound", + "direction": "Outbound", "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/frontend" } ], - "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp", - "name": "backendapp", + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/gateways/httpgw", + "name": "httpgw", "outputResources": [], "provisioningState": "Succeeded", - "type": "Applications.Core/containers" + "type": "Applications.Core/gateways" } ] \ No newline at end of file From 9e9d0de4986d1ed69a6ffa363b55b8924c221528 Mon Sep 17 00:00:00 2001 From: Young Bu Park Date: Thu, 25 Jan 2024 20:33:02 -0800 Subject: [PATCH 4/6] clean up Signed-off-by: Young Bu Park --- .../controller/applications/graph_util.go | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/pkg/corerp/frontend/controller/applications/graph_util.go b/pkg/corerp/frontend/controller/applications/graph_util.go index bed924bec9..8d151be984 100644 --- a/pkg/corerp/frontend/controller/applications/graph_util.go +++ b/pkg/corerp/frontend/controller/applications/graph_util.go @@ -250,11 +250,11 @@ func computeGraph(applicationName string, applicationResources []generated.Gener } // Resolve Outbound connections based on 'connections'. - connections := resolveConnections(resource, connectionsPath, connectionsFromAPIData(resources)) + 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, providesFromAPIData)...) + 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) @@ -515,40 +515,6 @@ func resolveConnections(resource generated.GenericResource, jsonRefPath string, 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(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 - } -} - -func routesPathConverter(resources []generated.GenericResource) func(any) (string, corerpv20231001preview.Direction, error) { - 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 - } -} - // findSourceResource looks up resource id by using source string by the following steps: // 1. Immediately return the resource ID if the source is a valid resource ID. // 2. Parse the hostname from source and look up the hostname in the resource list if the source is a valid URL. @@ -583,7 +549,41 @@ func findSourceResource(source string, allResources []generated.GenericResource) return orig, ErrInvalidSource } -// providesFromAPIData is specifically to support HTTPRoute. +// 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 + } +} + +func routesPathConverter(resources []generated.GenericResource) func(any) (string, corerpv20231001preview.Direction, error) { + 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 + } +} + +// 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. @@ -591,7 +591,7 @@ func findSourceResource(source string, allResources []generated.GenericResource) // 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 providesFromAPIData(item any) (string, corerpv20231001preview.Direction, error) { +func providersConverter(item any) (string, corerpv20231001preview.Direction, error) { data := &corerpv20231001preview.ContainerPortProperties{} err := toStronglyTypedData(item, data) if err != nil { From c940f169f267bdf0932452f2e92ae3d673c12b73 Mon Sep 17 00:00:00 2001 From: Young Bu Park Date: Thu, 25 Jan 2024 20:35:10 -0800 Subject: [PATCH 5/6] Add comment Signed-off-by: Young Bu Park --- pkg/corerp/frontend/controller/applications/graph_util.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/corerp/frontend/controller/applications/graph_util.go b/pkg/corerp/frontend/controller/applications/graph_util.go index 8d151be984..f3f96e5dc8 100644 --- a/pkg/corerp/frontend/controller/applications/graph_util.go +++ b/pkg/corerp/frontend/controller/applications/graph_util.go @@ -465,6 +465,8 @@ func outputResourcesFromAPIData(resource generated.GenericResource) []*corerpv20 } func resolveConnections(resource generated.GenericResource, jsonRefPath string, converter func(any) (string, corerpv20231001preview.Direction, error)) []*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. p, err := jsonpointer.New(jsonRefPath) if err != nil { // This should never fail since we're hard-coding the path. From 15b2478e90091861271a073d097c5645460c0d2c Mon Sep 17 00:00:00 2001 From: Young Bu Park Date: Fri, 26 Jan 2024 09:08:24 -0800 Subject: [PATCH 6/6] more clean up Signed-off-by: Young Bu Park --- .../controller/applications/graph_util.go | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/pkg/corerp/frontend/controller/applications/graph_util.go b/pkg/corerp/frontend/controller/applications/graph_util.go index f3f96e5dc8..c4d984786b 100644 --- a/pkg/corerp/frontend/controller/applications/graph_util.go +++ b/pkg/corerp/frontend/controller/applications/graph_util.go @@ -46,6 +46,9 @@ const ( 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) { @@ -250,11 +253,11 @@ func computeGraph(applicationName string, applicationResources []generated.Gener } // Resolve Outbound connections based on 'connections'. - connections := resolveConnections(resource, connectionsPath, connectionsConverter(resources)) + connections := resolveConnections(resource, connectionsPath, connectionsResolver(resources)) // Resolve Outbound connections based on 'routes'. - connections = append(connections, resolveConnections(resource, routesPath, routesPathConverter(resources))...) + connections = append(connections, resolveConnections(resource, routesPath, routesPathResolver(resources))...) // Resolve Inbound connections based on 'provides'. - connections = append(connections, resolveConnections(resource, portsPath, providersConverter)...) + 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) @@ -464,7 +467,7 @@ func outputResourcesFromAPIData(resource generated.GenericResource) []*corerpv20 return entries } -func resolveConnections(resource generated.GenericResource, jsonRefPath string, converter func(any) (string, corerpv20231001preview.Direction, error)) []*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. p, err := jsonpointer.New(jsonRefPath) @@ -480,6 +483,8 @@ func resolveConnections(resource generated.GenericResource, jsonRefPath string, } items := []any{} + + // Convert array and map connection data to a slice of items. switch conn := raw.(type) { case []any: items = conn @@ -551,11 +556,11 @@ func findSourceResource(source string, allResources []generated.GenericResource) return orig, ErrInvalidSource } -// connectionsConverter resolves the outbound connections for a resource from the generic resource representation. +// 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 connectionsConverter(resources []generated.GenericResource) func(any) (string, corerpv20231001preview.Direction, error) { +func connectionsResolver(resources []generated.GenericResource) resolver { return func(item any) (string, corerpv20231001preview.Direction, error) { data := &corerpv20231001preview.ConnectionProperties{} err := toStronglyTypedData(item, data) @@ -570,7 +575,8 @@ func connectionsConverter(resources []generated.GenericResource) func(any) (stri } } -func routesPathConverter(resources []generated.GenericResource) func(any) (string, corerpv20231001preview.Direction, error) { +// 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) @@ -585,7 +591,7 @@ func routesPathConverter(resources []generated.GenericResource) func(any) (strin } } -// providersConverter is specifically to support HTTPRoute. +// 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. @@ -593,7 +599,7 @@ func routesPathConverter(resources []generated.GenericResource) func(any) (strin // 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) { +func providersResolver(item any) (string, corerpv20231001preview.Direction, error) { data := &corerpv20231001preview.ContainerPortProperties{} err := toStronglyTypedData(item, data) if err != nil {