From adb8db1f8882b1b69c1dc52d6c786ca7eadd1a50 Mon Sep 17 00:00:00 2001 From: Young Bu Park Date: Thu, 25 Jan 2024 10:51:26 -0800 Subject: [PATCH] Support direct route connection in AppGraph (#7072) # Description * Fixed the possible nil panicking issues with `to` package * Handled http connection source without httproute * Refactored test code ## Type of change - This pull request fixes a bug in Radius and has an approved issue (issue link required). - This pull request adds or changes features of Radius and has an approved issue (issue link required). Fixes: #7004 #6937 --------- Signed-off-by: Young Bu Park --- .../controller/applications/graph_util.go | 111 +++++--- .../applications/graph_util_test.go | 254 ++++++------------ .../testdata/graph-app-directroute-in.json | 45 ++++ .../testdata/graph-app-directroute-out.json | 23 ++ .../testdata/graph-app-httproute-in.json | 55 ++++ .../testdata/graph-app-httproute-out.json | 47 ++++ test/testutil/util.go | 11 + 7 files changed, 334 insertions(+), 212 deletions(-) create mode 100644 pkg/corerp/frontend/controller/applications/testdata/graph-app-directroute-in.json create mode 100644 pkg/corerp/frontend/controller/applications/testdata/graph-app-directroute-out.json create mode 100644 pkg/corerp/frontend/controller/applications/testdata/graph-app-httproute-in.json create mode 100644 pkg/corerp/frontend/controller/applications/testdata/graph-app-httproute-out.json diff --git a/pkg/corerp/frontend/controller/applications/graph_util.go b/pkg/corerp/frontend/controller/applications/graph_util.go index d5dad12bbb4..6c5d1bae835 100644 --- a/pkg/corerp/frontend/controller/applications/graph_util.go +++ b/pkg/corerp/frontend/controller/applications/graph_util.go @@ -19,6 +19,8 @@ package applications import ( "context" "encoding/json" + "errors" + "net/url" "sort" "strings" @@ -29,9 +31,15 @@ import ( aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials" "github.com/radius-project/radius/pkg/cli/clients_new/generated" corerpv20231001preview "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" + "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/pkg/ucp/resources" ) +var ( + // ErrInvalidSource reprents the error when the source is not a valid resource ID or URL. + ErrInvalidSource = errors.New("source is not a valid resource ID or URL") +) + const ( connectionsPath = "/properties/connections" portsPath = "/properties/container/ports" @@ -208,11 +216,11 @@ func computeGraph(applicationName string, applicationResources []generated.Gener resources = append(resources, resource) // Application-scoped resources are by definition "in" the application - resourcesByIDInApplication[*resource.ID] = true + resourcesByIDInApplication[to.String(resource.ID)] = true } for _, resource := range environmentResources { - _, found := resourcesByIDInApplication[*resource.ID] + _, found := resourcesByIDInApplication[to.String(resource.ID)] if found { // Appears in both application and environment lists, avoid duplicates. continue @@ -221,7 +229,7 @@ func computeGraph(applicationName string, applicationResources []generated.Gener // This is an environment-scoped resource. We need to process the connections // to determine if it's part of the application. resources = append(resources, resource) - resourcesByIDInApplication[*resource.ID] = false + resourcesByIDInApplication[to.String(resource.ID)] = false } // Next we need to process each entry in the resources list and build up the application graph. @@ -240,11 +248,11 @@ func computeGraph(applicationName string, applicationResources []generated.Gener applicationGraphResource.ProvisioningState = &state } - connections := connectionsFromAPIData(resource) // Outbound connections based on 'connections' + connections := connectionsFromAPIData(resource, resources) // Outbound connections based on 'connections' connections = append(connections, providesFromAPIData(resource)...) // Inbound connections based on 'provides' sort.Slice(connections, func(i, j int) bool { - return *connections[i].ID < *connections[j].ID + return to.String(connections[i].ID) < to.String(connections[j].ID) }) applicationGraphResource.Connections = connections @@ -275,8 +283,8 @@ func computeGraph(applicationName string, applicationResources []generated.Gener // First process add resources we *know* are in the application to the queue. As we explore the graph we'll // visit resources outside the application if necessary. for _, entry := range applicationGraphResourcesByID { - if resourcesByIDInApplication[*entry.ID] { - queue = append(queue, *entry.ID) + if resourcesByIDInApplication[to.String(entry.ID)] { + queue = append(queue, to.String(entry.ID)) } } @@ -357,7 +365,7 @@ func computeGraph(applicationName string, applicationResources []generated.Gener // Print connections in stable order. sort.Slice(entry.Connections, func(i, j int) bool { // Connections are guaranteed to have a name. - return *entry.Connections[i].ID < *entry.Connections[j].ID + return to.String(entry.Connections[i].ID) < to.String(entry.Connections[j].ID) }) graph.Resources = append(graph.Resources, &entry) @@ -372,29 +380,21 @@ func applicationGraphResourceFromID(id string) *corerpv20231001preview.Applicati return nil // Invalid resource ID, skip } - appName := application.Name() - appType := application.Type() - provisioningState := string(v1.ProvisioningStateSucceeded) - - return &corerpv20231001preview.ApplicationGraphResource{ID: &id, - Name: &appName, - Type: &appType, - ProvisioningState: &provisioningState, + return &corerpv20231001preview.ApplicationGraphResource{ + ID: to.Ptr(id), + Name: to.Ptr(application.Name()), + Type: to.Ptr(application.Type()), + ProvisioningState: to.Ptr(string(v1.ProvisioningStateSucceeded)), } - } // outputResourceEntryFromID creates a outputResourceEntry from a resource ID. func outputResourceEntryFromID(id resources.ID) corerpv20231001preview.ApplicationGraphOutputResource { - orID := id.String() - orName := id.Name() - orType := id.Type() - entry := corerpv20231001preview.ApplicationGraphOutputResource{ID: &orID, - Name: &orName, - Type: &orType, + return corerpv20231001preview.ApplicationGraphOutputResource{ + ID: to.Ptr(id.String()), + Name: to.Ptr(id.Name()), + Type: to.Ptr(id.Type()), } - - return entry } // outputResourcesFromAPIData processes the generic resource representation returned by the Radius API @@ -447,13 +447,13 @@ func outputResourcesFromAPIData(resource generated.GenericResource) []*corerpv20 // Produce a stable output sort.Slice(entries, func(i, j int) bool { - if entries[i].Type != entries[j].Type { - return *entries[i].Type < *entries[j].Type + if to.String(entries[i].Type) != to.String(entries[j].Type) { + return to.String(entries[i].Type) < to.String(entries[j].Type) } - if entries[i].Name != entries[j].Name { - return *entries[i].Name < *entries[j].Name + if to.String(entries[i].Name) != to.String(entries[j].Name) { + return to.String(entries[i].Name) < to.String(entries[j].Name) } - return *entries[i].ID < *entries[j].ID + return to.String(entries[i].ID) < to.String(entries[j].ID) }) @@ -464,7 +464,7 @@ func outputResourcesFromAPIData(resource generated.GenericResource) []*corerpv20 // 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) []*corerpv20231001preview.ApplicationGraphConnection { +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. // @@ -497,21 +497,57 @@ func connectionsFromAPIData(resource generated.GenericResource) []*corerpv202310 data := corerpv20231001preview.ConnectionProperties{} err := toStronglyTypedData(connection, &data) if err == nil { + sourceID, _ := findSourceResource(to.String(data.Source), allResources) + entries = append(entries, &corerpv20231001preview.ApplicationGraphConnection{ - ID: data.Source, - Direction: &dir, + ID: to.Ptr(sourceID), + Direction: to.Ptr(dir), }) } } // Produce a stable output sort.Slice(entries, func(i, j int) bool { - return *entries[i].ID < *entries[j].ID + 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: +// 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. +// 3. Otherwise, return the original source string with error. +func findSourceResource(source string, allResources []generated.GenericResource) (string, error) { + // 1. Return the resource id if the source is a valid resource ID + id, err := resources.Parse(source) + if err == nil && id.IsResource() { + return id.String(), nil + } + + // 2. Parse hostname from source and look up hostname in resource list. + orig := source + + // Add "//" to source to enable url.Parse to parse source correctly if the scheme is not given. + if !strings.Contains(source, "//") { + source = "//" + source + } + + sourceURL, err := url.Parse(source) + if err == nil { + // Linear search resource name in resource list. + for _, resource := range allResources { + if to.String(resource.Name) == sourceURL.Hostname() { + return to.String(resource.ID), nil + } + } + // Fall back to original source string if not found. + } + + // 3. Return the original source string with false boolean value. + 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. @@ -550,19 +586,20 @@ func providesFromAPIData(resource generated.GenericResource) []*corerpv20231001p data := corerpv20231001preview.ContainerPortProperties{} err := toStronglyTypedData(connection, &data) if err == nil { - if *data.Provides == "" { + if to.String(data.Provides) == "" { continue } + entries = append(entries, &corerpv20231001preview.ApplicationGraphConnection{ ID: data.Provides, - Direction: &dir, + Direction: to.Ptr(dir), }) } } // Produce a stable output sort.Slice(entries, func(i, j int) bool { - return *entries[i].ID < *entries[j].ID + return to.String(entries[i].ID) < to.String(entries[j].ID) }) return entries diff --git a/pkg/corerp/frontend/controller/applications/graph_util_test.go b/pkg/corerp/frontend/controller/applications/graph_util_test.go index dcddf2c99bb..de94c21dabc 100644 --- a/pkg/corerp/frontend/controller/applications/graph_util_test.go +++ b/pkg/corerp/frontend/controller/applications/graph_util_test.go @@ -18,11 +18,12 @@ package applications import ( "context" - "sort" "testing" "github.com/radius-project/radius/pkg/cli/clients_new/generated" corerpv20231001preview "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" + "github.com/radius-project/radius/test/testutil" + "github.com/stretchr/testify/require" ) func Test_isResourceInEnvironment(t *testing.T) { @@ -69,9 +70,7 @@ func Test_isResourceInEnvironment(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := isResourceInEnvironment(tt.args.ctx, tt.args.resource, tt.args.environmentName); got != tt.want { - t.Errorf("isResourceInEnvironment() = %v, want %v", got, tt.want) - } + require.Equal(t, tt.want, isResourceInEnvironment(tt.args.ctx, tt.args.resource, tt.args.environmentName)) }) } } @@ -120,199 +119,104 @@ func Test_isResourceInApplication(t *testing.T) { } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := isResourceInApplication(tt.args.ctx, tt.args.resource, tt.args.applicationName); got != tt.want { - t.Errorf("isResourceInApplication() = %v, want %v", got, tt.want) - } + require.Equal(t, tt.want, isResourceInApplication(tt.args.ctx, tt.args.resource, tt.args.applicationName)) }) } } func Test_computeGraph(t *testing.T) { + tests := []struct { + name string + applicationName string + appResourceDataFile string + envResourceDataFile string + expectedDataFile string + }{ + { + name: "using httproute", + applicationName: "myapp", + appResourceDataFile: "graph-app-httproute-in.json", + envResourceDataFile: "", + expectedDataFile: "graph-app-httproute-out.json", + }, + { + name: "direct route", + applicationName: "myapp", + appResourceDataFile: "graph-app-directroute-in.json", + envResourceDataFile: "", + expectedDataFile: "graph-app-directroute-out.json", + }, + } - sqlRteID := "/planes/radius/local/resourcegroups/default/providers/Applications.Core/httpRoutes/sql-rte" - sqlRteType := "Applications.Core/httpRoutes" - sqlRteName := "sql-rte" + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + appResource := []generated.GenericResource{} + envResource := []generated.GenericResource{} - sqlAppCntrID := "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/sql-app-ctnr" - sqlAppCntrName := "sql-app-ctnr" - sqlAppCntrType := "Applications.Core/containers" + if tt.appResourceDataFile != "" { + testutil.MustUnmarshalFromFile(tt.appResourceDataFile, &appResource) + } - sqlCntrID := "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/sql-ctnr" - sqlCntrName := "sql-ctnr" - sqlCntrType := "Applications.Core/containers" + if tt.envResourceDataFile != "" { + testutil.MustUnmarshalFromFile(tt.envResourceDataFile, &envResource) + } - sqlDbID := "/planes/radius/local/resourcegroups/default/providers/Applications.Datastores/sqlDatabases/sql-db" - sqlDbName := "sql-db" - sqlDbType := "Applications.Datastores/sqlDatabases" + expected := []*corerpv20231001preview.ApplicationGraphResource{} + testutil.MustUnmarshalFromFile(tt.expectedDataFile, &expected) - provisioningStateSuccess := "Succeeded" - dirInbound := corerpv20231001preview.DirectionInbound - dirOutbound := corerpv20231001preview.DirectionOutbound + got := computeGraph(tt.applicationName, appResource, envResource) + require.ElementsMatch(t, expected, got.Resources) + }) + } +} - expected := []*corerpv20231001preview.ApplicationGraphResource{ - { - ID: &sqlRteID, - Name: &sqlRteName, - Type: &sqlRteType, - ProvisioningState: &provisioningStateSuccess, - OutputResources: []*corerpv20231001preview.ApplicationGraphOutputResource{}, - Connections: []*corerpv20231001preview.ApplicationGraphConnection{ - { - ID: &sqlCntrID, - Direction: &dirInbound, - }, - }, - }, +func TestFindSourceResource(t *testing.T) { + tests := []struct { + name string + source string + resourceDataFile string + + parsedSource string + wantErr error + }{ { - ID: &sqlCntrID, - Name: &sqlCntrName, - Type: &sqlCntrType, - ProvisioningState: &provisioningStateSuccess, - OutputResources: []*corerpv20231001preview.ApplicationGraphOutputResource{}, - Connections: []*corerpv20231001preview.ApplicationGraphConnection{ - { - Direction: &dirOutbound, - ID: &sqlRteID, - }, - }, + name: "valid source ID", + source: "/planes/radius/local/resourcegroups/default/providers/Applications.Datastores/sqlDatabases/sql-db", + resourceDataFile: "graph-app-directroute-in.json", + parsedSource: "/planes/radius/local/resourcegroups/default/providers/Applications.Datastores/sqlDatabases/sql-db", + wantErr: nil, }, { - ID: &sqlDbID, - Name: &sqlDbName, - Type: &sqlDbType, - ProvisioningState: &provisioningStateSuccess, - OutputResources: []*corerpv20231001preview.ApplicationGraphOutputResource{}, + name: "invalid source", + source: "invalid", + resourceDataFile: "graph-app-directroute-in.json", + parsedSource: "invalid", + wantErr: ErrInvalidSource, }, { - ID: &sqlAppCntrID, - Name: &sqlAppCntrName, - Type: &sqlAppCntrType, - ProvisioningState: &provisioningStateSuccess, - OutputResources: []*corerpv20231001preview.ApplicationGraphOutputResource{}, - Connections: []*corerpv20231001preview.ApplicationGraphConnection{ - { - Direction: &dirInbound, - ID: &sqlDbID, - }, - }, + name: "direct route without scheme", + source: "backendapp:8080", + resourceDataFile: "graph-app-directroute-in.json", + parsedSource: "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp", + wantErr: nil, }, - } - - // sort the expected result - sort.Slice(expected, func(i, j int) bool { - return *expected[i].ID < *expected[j].ID - }) - - type args struct { - applicationName string - applicationResources []generated.GenericResource - environmentResources []generated.GenericResource - } - tests := []struct { - name string - args args - want *corerpv20231001preview.ApplicationGraphResponse - }{ { - name: "compute graph", - args: args{ - applicationName: "myapp", - applicationResources: []generated.GenericResource{ - { - ID: &sqlRteID, - Properties: map[string]interface{}{ - "application": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/Applications/myapp", - "provisioningState": "Succeeded", - }, - Name: &sqlRteName, - Type: &sqlRteType, - }, - { - ID: &sqlAppCntrID, - Properties: map[string]interface{}{ - "connections": map[string]interface{}{ - "sql": map[string]interface{}{ - "source": "/planes/radius/local/resourcegroups/default/providers/Applications.Datastores/sqlDatabases/sql-db", - }, - }, - "application": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/Applications/myapp", - "provisioningState": "Succeeded", - "status": map[string]interface{}{ - "outputResources": map[string]interface{}{ - "localId": "something", - "id": "/some/thing/else", - }, - }, - }, - Name: &sqlAppCntrName, - Type: &sqlAppCntrType, - }, - { - ID: &sqlCntrID, - Properties: map[string]interface{}{ - "container": map[string]interface{}{ - "ports": map[string]interface{}{ - "web": map[string]interface{}{ - "port": 8080, - "protocol": "TCP", - "provides": sqlRteID, - }, - }, - }, - "application": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/Applications/myapp", - "provisioningState": "Succeeded", - "status": map[string]interface{}{ - "outputResources": map[string]interface{}{ - "localId": "something", - "id": "/some/thing/else", - }, - }, - }, - Name: &sqlCntrName, - Type: &sqlCntrType, - }, - }, - environmentResources: []generated.GenericResource{}, - }, - want: &corerpv20231001preview.ApplicationGraphResponse{ - Resources: expected, - }, + name: "direct route with scheme", + source: "http://backendapp:8080", + resourceDataFile: "graph-app-directroute-in.json", + parsedSource: "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp", + wantErr: nil, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := computeGraph(tt.args.applicationName, tt.args.applicationResources, tt.args.environmentResources) - // sort the result - sort.Slice(got.Resources, func(i, j int) bool { - return *got.Resources[i].ID < *got.Resources[j].ID - }) - - for i := range got.Resources { - gotResource := got.Resources[i] - wantResource := tt.want.Resources[i] - if *gotResource.ID != *wantResource.ID || *gotResource.Name != *wantResource.Name || *gotResource.Type != *wantResource.Type || *gotResource.ProvisioningState != *wantResource.ProvisioningState { - t.Errorf("computeGraph() = %v, want %v", *got.Resources[i], *tt.want.Resources[i]) - } - //sort connections - sort.Slice(gotResource.Connections, func(i, j int) bool { - return *gotResource.Connections[i].ID < *gotResource.Connections[j].ID - }) - sort.Slice(wantResource.Connections, func(i, j int) bool { - return *wantResource.Connections[i].ID < *wantResource.Connections[j].ID - }) - //iterate through connections and compare - for j := range gotResource.Connections { - gotConnection := gotResource.Connections[j] - wantConnection := wantResource.Connections[j] - if *gotConnection.ID != *wantConnection.ID || *gotConnection.Direction != *wantConnection.Direction { - t.Errorf("computeGraph() = %v, want %v", *gotResource.Connections[j], *wantResource.Connections[j]) - } - } - } - + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + resources := []generated.GenericResource{} + testutil.MustUnmarshalFromFile(tc.resourceDataFile, &resources) + parsedSource, err := findSourceResource(tc.source, resources) + require.Equal(t, tc.parsedSource, parsedSource) + require.ErrorIs(t, err, tc.wantErr) }) } } diff --git a/pkg/corerp/frontend/controller/applications/testdata/graph-app-directroute-in.json b/pkg/corerp/frontend/controller/applications/testdata/graph-app-directroute-in.json new file mode 100644 index 00000000000..0c86907ac9e --- /dev/null +++ b/pkg/corerp/frontend/controller/applications/testdata/graph-app-directroute-in.json @@ -0,0 +1,45 @@ +[ + { + "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", + "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-directroute-out.json b/pkg/corerp/frontend/controller/applications/testdata/graph-app-directroute-out.json new file mode 100644 index 00000000000..ed65c15b5c5 --- /dev/null +++ b/pkg/corerp/frontend/controller/applications/testdata/graph-app-directroute-out.json @@ -0,0 +1,23 @@ +[ + { + "connections": [ + { + "direction": "Inbound", + "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": [], + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/backendapp", + "name": "backendapp", + "outputResources": [], + "provisioningState": "Succeeded", + "type": "Applications.Core/containers" + } +] diff --git a/pkg/corerp/frontend/controller/applications/testdata/graph-app-httproute-in.json b/pkg/corerp/frontend/controller/applications/testdata/graph-app-httproute-in.json new file mode 100644 index 00000000000..917de30ace2 --- /dev/null +++ b/pkg/corerp/frontend/controller/applications/testdata/graph-app-httproute-in.json @@ -0,0 +1,55 @@ +[ + { + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/httpRoutes/sql-rte", + "name": "sql-rte", + "properties": { + "application": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/Applications/myapp", + "provisioningState": "Succeeded" + }, + "type": "Applications.Core/httpRoutes" + }, + { + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/sql-app-ctnr", + "name": "sql-app-ctnr", + "properties": { + "application": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/Applications/myapp", + "connections": { + "sql": { + "source": "/planes/radius/local/resourcegroups/default/providers/Applications.Datastores/sqlDatabases/sql-db" + } + }, + "provisioningState": "Succeeded", + "status": { + "outputResources": { + "id": "/some/thing/else", + "localId": "something" + } + } + }, + "type": "Applications.Core/containers" + }, + { + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/sql-ctnr", + "name": "sql-ctnr", + "properties": { + "application": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/Applications/myapp", + "container": { + "ports": { + "web": { + "port": 8080, + "protocol": "TCP", + "provides": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/httpRoutes/sql-rte" + } + } + }, + "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-httproute-out.json b/pkg/corerp/frontend/controller/applications/testdata/graph-app-httproute-out.json new file mode 100644 index 00000000000..e34c4ec30e8 --- /dev/null +++ b/pkg/corerp/frontend/controller/applications/testdata/graph-app-httproute-out.json @@ -0,0 +1,47 @@ +[ + { + "connections": [ + { + "direction": "Outbound", + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/httpRoutes/sql-rte" + } + ], + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/sql-ctnr", + "name": "sql-ctnr", + "outputResources": [], + "provisioningState": "Succeeded", + "type": "Applications.Core/containers" + }, + { + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Datastores/sqlDatabases/sql-db", + "name": "sql-db", + "provisioningState": "Succeeded", + "type": "Applications.Datastores/sqlDatabases" + }, + { + "connections": [ + { + "direction": "Inbound", + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/sql-ctnr" + } + ], + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/httpRoutes/sql-rte", + "name": "sql-rte", + "outputResources": [], + "provisioningState": "Succeeded", + "type": "Applications.Core/httpRoutes" + }, + { + "connections": [ + { + "direction": "Inbound", + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Datastores/sqlDatabases/sql-db" + } + ], + "id": "/planes/radius/local/resourcegroups/default/providers/Applications.Core/containers/sql-app-ctnr", + "name": "sql-app-ctnr", + "outputResources": [], + "provisioningState": "Succeeded", + "type": "Applications.Core/containers" + } +] diff --git a/test/testutil/util.go b/test/testutil/util.go index 01ed6931b11..32d162a595c 100644 --- a/test/testutil/util.go +++ b/test/testutil/util.go @@ -17,6 +17,7 @@ limitations under the License. package testutil import ( + "bytes" "encoding/json" "os" "path" @@ -32,6 +33,16 @@ func MustGetTestData[T any](file string) *T { return &data } +// MustUnmarshalFromFile reads testdata and unmarshals it to the given type, panicking if an error occurs. +func MustUnmarshalFromFile(file string, out any) { + dec := json.NewDecoder(bytes.NewReader(ReadFixture(file))) + dec.DisallowUnknownFields() + err := dec.Decode(out) + if err != nil { + panic(err) + } +} + // ReadFixture reads testdata fixtures, panicking if an error occurs. // // The prefix `./testdata/` is automatically added to the filename. Tests can 'escape' the testdata directory by