Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
295d340
Initial plan
Copilot May 1, 2026
b5aad28
Register getGraph custom action for Radius.Core/applications
Copilot May 1, 2026
7dc9649
Move v20250801preview getGraph controller into v20250801preview/ subp…
Copilot May 1, 2026
096d077
Apply suggestions from code review
willtsai May 1, 2026
cf948d6
Add test for ComputeGraphResponse invalid environment ID path
Copilot May 1, 2026
1d411ed
Merge branch 'main' into copilot/fix-getgraph-route-404
willtsai May 4, 2026
8e9ec3b
Add test coverage for codecov-flagged lines in getgraph controllers
Copilot May 5, 2026
6995e57
Merge branch 'main' into copilot/fix-getgraph-route-404
willtsai May 5, 2026
9960231
Merge branch 'main' into copilot/fix-getgraph-route-404
willtsai May 6, 2026
0ebf1d7
Merge branch 'main' into copilot/fix-getgraph-route-404
willtsai May 6, 2026
5ab72de
Add tests for listAllResourcesByApplication and listAllResourcesByEnv…
Copilot May 6, 2026
e4d05d5
Merge branch 'main' into copilot/fix-getgraph-route-404
willtsai May 6, 2026
edba397
Merge branch 'main' into copilot/fix-getgraph-route-404
willtsai May 6, 2026
f695f20
Potential fix for pull request finding
willtsai May 6, 2026
778e064
Use resources.ParseResource with contextual error wrapping in Compute…
Copilot May 6, 2026
8452b9b
Merge branch 'main' into copilot/fix-getgraph-route-404
willtsai May 6, 2026
c228792
Merge branch 'main' into copilot/fix-getgraph-route-404
willtsai May 7, 2026
17a9244
Merge branch 'main' into copilot/fix-getgraph-route-404
willtsai May 7, 2026
6302849
Merge branch 'main' into copilot/fix-getgraph-route-404
willtsai May 8, 2026
e383e73
Rename environmentIDString parameter to environmentID in ComputeGraph…
Copilot May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 38 additions & 29 deletions pkg/corerp/frontend/controller/applications/getgraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package applications

import (
"context"
"fmt"
"net/http"

v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
Expand All @@ -36,6 +37,42 @@ const (
planeName = "local"
)

// ComputeGraphResponse computes the application graph for the given application and environment IDs and
// returns it wrapped in an OK rest.Response. It is shared by the Applications.Core and Radius.Core
// implementations of the getGraph custom action.
func ComputeGraphResponse(ctx context.Context, applicationID resources.ID, environmentID string, connection sdk.Connection) (rest.Response, error) {
// An application **MUST** have an environment id
parsedEnvironmentID, err := resources.ParseResource(environmentID)
if err != nil {
return nil, fmt.Errorf("failed to parse environment ID %q: %w", environmentID, err)
}

clientOptions := sdk.NewClientOptions(connection)

ucpApplicationsManagementClient := &clients.UCPApplicationsManagementClient{
RootScope: radiusPlane + planeName,
ClientOptions: clientOptions,
}

resourceTypes, err := ucpApplicationsManagementClient.ListAllResourceTypesNames(ctx, "local")
if err != nil {
return nil, err
}

applicationResources, err := listAllResourcesByApplication(ctx, applicationID, resourceTypes, clientOptions)
if err != nil {
return nil, err
}

environmentResources, err := listAllResourcesByEnvironment(ctx, parsedEnvironmentID, resourceTypes, clientOptions)
if err != nil {
return nil, err
}

graph := computeGraph(applicationResources, environmentResources)
return rest.NewOKResponse(graph), nil
}

var _ ctrl.Controller = (*GetGraph)(nil)

// GetGraph is the controller implementation to get application graph.
Expand Down Expand Up @@ -71,33 +108,5 @@ func (ctrl *GetGraph) Run(ctx context.Context, w http.ResponseWriter, req *http.
return rest.NewNotFoundResponse(sCtx.ResourceID), nil
}
// An application **MUST** have an environment id
environmentID, err := resources.Parse(applicationResource.Properties.Environment)
if err != nil {
return nil, err
}

clientOptions := sdk.NewClientOptions(ctrl.connection)

ucpApplicationsManagementClient := &clients.UCPApplicationsManagementClient{
RootScope: radiusPlane + planeName,
ClientOptions: clientOptions,
}

resourceTypes, err := ucpApplicationsManagementClient.ListAllResourceTypesNames(ctx, "local")
if err != nil {
return nil, err
}

applicationResources, err := listAllResourcesByApplication(ctx, applicationID, resourceTypes, clientOptions)
if err != nil {
return nil, err
}

environmentResources, err := listAllResourcesByEnvironment(ctx, environmentID, resourceTypes, clientOptions)
if err != nil {
return nil, err
}

graph := computeGraph(applicationResources, environmentResources)
return rest.NewOKResponse(graph), nil
return ComputeGraphResponse(ctx, applicationID, applicationResource.Properties.Environment, ctrl.connection)
}
207 changes: 207 additions & 0 deletions pkg/corerp/frontend/controller/applications/getgraph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@ package applications

import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"

v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
ctrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
"github.com/radius-project/radius/pkg/armrpc/rest"
"github.com/radius-project/radius/pkg/armrpc/rpctest"
"github.com/radius-project/radius/pkg/components/database"
"github.com/radius-project/radius/pkg/corerp/datamodel"
rpv1 "github.com/radius-project/radius/pkg/rp/v1"
"github.com/radius-project/radius/pkg/sdk"
"github.com/radius-project/radius/pkg/ucp/resources"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
Expand Down Expand Up @@ -67,3 +73,204 @@ func TestGetGraphRun_20231001Preview(t *testing.T) {
require.NoError(t, err)
})
}

func TestComputeGraphResponse_InvalidEnvironmentID(t *testing.T) {
conn, err := sdk.NewDirectConnection("http://localhost:9000/apis/api.ucp.dev/v1alpha3")
require.NoError(t, err)

applicationID, err := resources.Parse("/planes/radius/local/resourceGroups/default/providers/Applications.Core/applications/myapp")
require.NoError(t, err)

// An empty/invalid environment ID string must surface as a parse error from
// resources.Parse and not a panic or a successful response.
resp, err := ComputeGraphResponse(context.Background(), applicationID, "not-a-valid-resource-id", conn)
require.Error(t, err)
require.Nil(t, resp)
}

// TestComputeGraphResponse_UCPProvidersError covers the error-propagation path from
// ListAllResourceTypesNames (~lines 57-59) by returning a 500 from the UCP providers endpoint.
func TestComputeGraphResponse_UCPProvidersError(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/planes/radius/local/providers", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "boom", http.StatusInternalServerError)
})
server := httptest.NewServer(mux)
defer server.Close()

conn, err := sdk.NewDirectConnection(server.URL)
require.NoError(t, err)

applicationID, err := resources.Parse("/planes/radius/local/resourceGroups/default/providers/Applications.Core/applications/myapp")
require.NoError(t, err)

resp, err := ComputeGraphResponse(
context.Background(),
applicationID,
"/planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/myenv",
conn,
)
require.Error(t, err)
require.Nil(t, resp)
}

// TestComputeGraphResponse_ListByApplicationError covers lines 62-63: the error-propagation path
// from listAllResourcesByApplication when getAPIVersionForResourceType fails.
func TestComputeGraphResponse_ListByApplicationError(t *testing.T) {
mux := http.NewServeMux()
// Return a provider with one resource type so the list is non-empty.
mux.HandleFunc("/planes/radius/local/providers", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"value":[{"name":"Applications.Core","resourceTypes":{"containers":{"apiVersions":{},"defaultApiVersion":"2023-10-01-preview"}}}]}`))
})
// GetProviderSummary for the resource type returns a 500 so listAllResourcesByApplication fails.
mux.HandleFunc("/planes/radius/local/providers/Applications.Core", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "provider error", http.StatusInternalServerError)
})
server := httptest.NewServer(mux)
defer server.Close()

conn, err := sdk.NewDirectConnection(server.URL)
require.NoError(t, err)

applicationID, err := resources.Parse("/planes/radius/local/resourceGroups/app-rg/providers/Applications.Core/applications/myapp")
require.NoError(t, err)

resp, err := ComputeGraphResponse(
context.Background(),
applicationID,
"/planes/radius/local/resourceGroups/env-rg/providers/Applications.Core/environments/myenv",
conn,
)
require.Error(t, err)
require.Nil(t, resp)
}

// TestComputeGraphResponse_ListByEnvironmentError covers lines 67-68: the error-propagation path
// from listAllResourcesByEnvironment when the environment-scoped resource listing fails.
func TestComputeGraphResponse_ListByEnvironmentError(t *testing.T) {
mux := http.NewServeMux()
// Return a provider with one resource type.
mux.HandleFunc("/planes/radius/local/providers", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"value":[{"name":"Applications.Core","resourceTypes":{"containers":{"apiVersions":{"2023-10-01-preview":{}},"defaultApiVersion":"2023-10-01-preview"}}}]}`))
})
// GetProviderSummary succeeds with a valid API version.
mux.HandleFunc("/planes/radius/local/providers/Applications.Core", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"name":"Applications.Core","resourceTypes":{"containers":{"apiVersions":{"2023-10-01-preview":{}},"defaultApiVersion":"2023-10-01-preview"}}}`))
})
// Application-scoped listing succeeds (empty list).
mux.HandleFunc("/planes/radius/local/resourceGroups/app-rg/providers/Applications.Core/containers", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"value":[]}`))
})
// Environment-scoped listing fails.
mux.HandleFunc("/planes/radius/local/resourceGroups/env-rg/providers/Applications.Core/containers", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "env listing error", http.StatusInternalServerError)
})
server := httptest.NewServer(mux)
defer server.Close()

conn, err := sdk.NewDirectConnection(server.URL)
require.NoError(t, err)

applicationID, err := resources.Parse("/planes/radius/local/resourceGroups/app-rg/providers/Applications.Core/applications/myapp")
require.NoError(t, err)

resp, err := ComputeGraphResponse(
context.Background(),
applicationID,
"/planes/radius/local/resourceGroups/env-rg/providers/Applications.Core/environments/myenv",
conn,
)
require.Error(t, err)
require.Nil(t, resp)
}

// TestGetGraphRun_DatabaseError covers the error path from GetResource (line ~104) where the
// database client returns a non-NotFound error and the controller propagates it.
func TestGetGraphRun_DatabaseError(t *testing.T) {
mctrl := gomock.NewController(t)
defer mctrl.Finish()

databaseClient := database.NewMockClient(mctrl)
req, err := rpctest.NewHTTPRequestWithContent(
context.Background(),
v1.OperationPost.HTTPMethod(),
"http://localhost:8080/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Core/Applications/myapp/getGraph?api-version=2023-10-01-preview", nil)
require.NoError(t, err)

databaseClient.
EXPECT().
Get(gomock.Any(), gomock.Any()).
Return(nil, errors.New("boom"))

ctx := rpctest.NewARMRequestContext(req)
conn, err := sdk.NewDirectConnection("http://localhost:9000/apis/api.ucp.dev/v1alpha3")
require.NoError(t, err)

ctl, err := NewGetGraph(ctrl.Options{DatabaseClient: databaseClient}, conn)
require.NoError(t, err)

w := httptest.NewRecorder()
resp, err := ctl.Run(ctx, w, req)
require.Error(t, err)
require.Nil(t, resp)
}

// TestGetGraphRun_ComputeGraphSuccess covers the success path through the controller's Run into
// ComputeGraphResponse (line ~110 and the body of ComputeGraphResponse) by serving an empty UCP
// resource-providers list from an httptest server, which yields an empty graph.
func TestGetGraphRun_ComputeGraphSuccess(t *testing.T) {
mctrl := gomock.NewController(t)
defer mctrl.Finish()

mux := http.NewServeMux()
mux.HandleFunc("/planes/radius/local/providers", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"value":[]}`))
})
server := httptest.NewServer(mux)
defer server.Close()

const appIDStr = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Core/applications/myapp"
const envIDStr = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/radius-test-rg/providers/Applications.Core/environments/myenv"

databaseClient := database.NewMockClient(mctrl)
databaseClient.
EXPECT().
Get(gomock.Any(), gomock.Any()).
Return(rpctest.FakeStoreObject(&datamodel.Application{
BaseResource: v1.BaseResource{
TrackedResource: v1.TrackedResource{ID: appIDStr},
},
Properties: datamodel.ApplicationProperties{
BasicResourceProperties: rpv1.BasicResourceProperties{Environment: envIDStr},
},
}), nil)

req, err := rpctest.NewHTTPRequestWithContent(
context.Background(),
v1.OperationPost.HTTPMethod(),
"http://localhost:8080"+appIDStr+"/getGraph?api-version=2023-10-01-preview", nil)
require.NoError(t, err)

ctx := rpctest.NewARMRequestContext(req)
conn, err := sdk.NewDirectConnection(server.URL)
require.NoError(t, err)

ctl, err := NewGetGraph(ctrl.Options{DatabaseClient: databaseClient}, conn)
require.NoError(t, err)

w := httptest.NewRecorder()
resp, err := ctl.Run(ctx, w, req)
require.NoError(t, err)
_, ok := resp.(*rest.OKResponse)
require.True(t, ok, "expected an OK response, got %T", resp)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
Copyright 2023 The Radius Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v20250801preview

import (
"context"
"net/http"

v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
ctrl "github.com/radius-project/radius/pkg/armrpc/frontend/controller"
"github.com/radius-project/radius/pkg/armrpc/rest"
"github.com/radius-project/radius/pkg/corerp/datamodel"
"github.com/radius-project/radius/pkg/corerp/datamodel/converter"
app_ctrl "github.com/radius-project/radius/pkg/corerp/frontend/controller/applications"
"github.com/radius-project/radius/pkg/sdk"
)

var _ ctrl.Controller = (*GetGraphv20250801preview)(nil)

// GetGraphv20250801preview is the controller implementation to get the application graph for
// Radius.Core/applications resources.
type GetGraphv20250801preview struct {
ctrl.Operation[*datamodel.Application_v20250801preview, datamodel.Application_v20250801preview]
connection sdk.Connection
}

// NewGetGraphv20250801preview creates a new instance of the GetGraphv20250801preview controller.
func NewGetGraphv20250801preview(opts ctrl.Options, connection sdk.Connection) (ctrl.Controller, error) {
return &GetGraphv20250801preview{
ctrl.NewOperation(opts,
ctrl.ResourceOptions[datamodel.Application_v20250801preview]{
RequestConverter: converter.Application20250801DataModelFromVersioned,
ResponseConverter: converter.Application20250801DataModelToVersioned,
},
),
connection,
}, nil
}

// Run handles the getGraph custom action for Radius.Core/applications. It looks up the application,
// resolves its environment, lists application- and environment-scoped resources, and returns the
// computed application graph.
func (ctrl *GetGraphv20250801preview) Run(ctx context.Context, w http.ResponseWriter, req *http.Request) (rest.Response, error) {
sCtx := v1.ARMRequestContextFromContext(ctx)

// Request route for getGraph has the operation name as suffix which must be removed to get the resource id.
// route id format: /planes/radius/local/resourcegroups/default/providers/Radius.Core/applications/<app>/getGraph
applicationID := sCtx.ResourceID.Truncate()
applicationResource, _, err := ctrl.GetResource(ctx, applicationID)
if err != nil {
return nil, err
}
if applicationResource == nil {
return rest.NewNotFoundResponse(sCtx.ResourceID), nil
}

return app_ctrl.ComputeGraphResponse(ctx, applicationID, applicationResource.Properties.Environment, ctrl.connection)
}
Loading
Loading